[
  {
    "path": ".cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\n# Increase default stack size to avoid running out of stack\n# space in debug builds. The size matches Linux's default.\nrustflags = [\"-C\", \"link-arg=/STACK:8000000\"]\n[target.aarch64-pc-windows-msvc]\n# Increase default stack size to avoid running out of stack\n# space in debug builds. The size matches Linux's default.\nrustflags = [\"-C\", \"link-arg=/STACK:8000000\"]"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: qarmin # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve app\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Bug Description**\n\n**Steps to reproduce:**\n<!-- Please describe what you expected to see and what you saw instead. Also include screenshots or screencasts if needed. -->\n\n**Terminal output** (optional):\n\n```\n<!--\nAdd terminal output only if needed - if there are some errors or warnings or you have performance/freeze issues.  \nVery helpful in this situation will be logs from czkawka run with RUST_LOG environment variable set e.g. \n`RUST_LOG=debug ./czkawka` or `flatpak run --env=RUST_LOG=debug com.github.qarmin.czkawka` if you use flatpak, which will print more detailed info about executed function.\n-->\n\n<details>\n<summary>Debug log</summary>\n\n# UNCOMMENT DETAILS AND PUT LOGS HERE\n\n</details>\n```\n\n**System**\n\n<!-- OS and Czkawka/Krokiet version and other OS info - you can copy it from the logs if you run the app from a terminal or locate the log files manually\n(Linux: `/home/username/.cache/czkawka`,\nmacOS: `/Users/Username/Library/Caches/pl.Qarmin.Czkawka`,\nWindows: `C:\\Users\\Username\\AppData\\Local\\Qarmin\\Czkawka\\cache`).\nNote: the exact path depends on the installation method(you can open config/cache path from gui). -->\n<!-- Example of logs: -->\n<!-- Czkawka gtk version: 11.0.1, debug mode, rust 1.92.0 (2025-06-23), os Ubuntu 25.4.0 (x86_64 64-bit), 24 cpu/threads, features(1): [fast_image_resize], app cpu version: x86-64-v3 (AVX2) or x86-64-v4 (AVX-512), os cpu version: x86-64-v4 (AVX-512) -->\n<!-- Config folder set to \"/home/rafal/.config/czkawka\" and cache folder set to \"/home/rafal/.cache/czkawka\" -->\n<!-- Czkawka Gui - used thread number: 24, gtk version 4.18.5 -->\n\n<!-- Please do not report feature request unique for Gtk Czkawka gui, because it is in maintenance mode. -->\n\n- Czkawka/Krokiet version: <!--  e.g. 11.0.1 cli/gui -->\n- OS version: <!--  e.g. Ubuntu 22.04, Windows 11, Mac 15.1 ARM -->\n- Installation method: <!-- e.g. github binaries, flatpak, msys2 -->\n\n<!-- If you use flatpak, please include the result of `flatpak info com.github.qarmin.czkawka`. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Feature Description**\n...\n"
  },
  {
    "path": ".github/workflows/android.yml",
    "content": "name: 🤖 Android APK\n\non:\n  push:\n    branches: [ master, main ]\n  pull_request:\n    branches: [ master, main ]\n  workflow_dispatch:\n\njobs:\n  build-apk:\n    name: Build Android APK\n    runs-on: ubuntu-latest\n    env:\n      NDK_VERSION: 26.3.11579264\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Java (required by Android SDK)\n        uses: actions/setup-java@v4\n        with:\n          distribution: temurin\n          java-version: '17'\n\n      - name: Set up Android SDK\n        uses: android-actions/setup-android@v3\n\n      - name: Cache Android SDK\n        uses: actions/cache@v4\n        with:\n          path: |\n            ${{ env.ANDROID_SDK_ROOT }}/cmdline-tools\n            ${{ env.ANDROID_SDK_ROOT }}/ndk\n            ${{ env.ANDROID_SDK_ROOT }}/platforms\n            ${{ env.ANDROID_SDK_ROOT }}/platform-tools\n            ${{ env.ANDROID_SDK_ROOT }}/build-tools\n          key: ${{ runner.os }}-android-sdk-${{ env.NDK_VERSION }}\n          restore-keys: |\n            ${{ runner.os }}-android-sdk-\n\n      - name: Install Android NDK and tools\n        run: |\n          rustup target add aarch64-linux-android\n          cargo install cargo-apk || true\n\n          yes | sdkmanager --install \"ndk;${NDK_VERSION}\"\n          echo \"ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/${NDK_VERSION}\" >> $GITHUB_ENV\n\n      - name: Cache Cargo\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-android-v1\n          restore-keys: |\n            ${{ runner.os }}-cargo-\n\n      - name: Generate keystores\n        run: |\n          sudo apt update || true\n          sudo apt install -y just\n\n          just gen_keystores\n\n      - name: Build APK (release)\n        if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}\n        run: |\n          echo \"VERS=release\" >> $GITHUB_ENV\n          cargo apk build -p cedinia --lib --release\n          mv target/release/apk/cedinia.apk cedinia.apk\n\n      - name: Build APK (debug)\n        if: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }}\n        run: |\n          echo \"VERS=debug\" >> $GITHUB_ENV\n          cargo apk build -p cedinia --lib\n          mv target/debug/apk/cedinia.apk cedinia.apk\n\n      - name: Upload APK artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: cedinia-${{ env.VERS }}\n          path: cedinia.apk\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            cedinia.apk\n          token: ${{ secrets.PAT_REPOSITORY }}"
  },
  {
    "path": ".github/workflows/linux.yml",
    "content": "name: 🐧 Linux\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: '0 0 * * 2'\n\nenv:\n  CARGO_TERM_COLOR: always\n  CZKAWKA_OFFICIAL_BUILD: ${{ vars.CZKAWKA_OFFICIAL_BUILD }}\n\njobs:\n  linux-all:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-22.04, ubuntu-22.04-arm]\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup env\n        run: |\n          ARCHNAME=$([ \"${{ runner.arch }}\" = \"ARM64\" ] && echo arm64 || echo x86_64)\n          echo \"ARCHNAME=$ARCHNAME\" >> $GITHUB_ENV\n\n      - name: Install basic libraries\n        run: sudo apt update || true; sudo apt install libheif-dev libraw-dev ffmpeg libgtk-4-dev p7zip-full -y\n\n      - name: Setup rust version\n        run: rustup default 1.92.0\n\n      - name: Build Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          sed -i 's/#lto = /lto = /g' Cargo.toml\n          sed -i 's/#codegen-units /codegen-units /g' Cargo.toml\n          \n          echo \"VERS=release\" >> $GITHUB_ENV\n          \n          cargo build --release\n          mv target/release/czkawka_cli linux_czkawka_cli_${{ env.ARCHNAME }}\n          mv target/release/czkawka_gui linux_czkawka_gui_${{ env.ARCHNAME }}\n          mv target/release/krokiet linux_krokiet_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_opengl,winit_software\"\n          mv target/release/krokiet linux_krokiet_skia_opengl_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software\"\n          mv target/release/krokiet linux_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"femtovg_wgpu\"\n          mv target/release/krokiet linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu\"\n          mv target/release/krokiet linux_krokiet_all_backends_${{ env.ARCHNAME }}\n\n      # Fast CI profile, to avoid out of disk space errors\n      # I doubt that anyone would use debug builds from here, because they are slow and contains not final changes\n      - name: Build Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          \n          echo \"VERS=debug\" >> $GITHUB_ENV\n          \n          cargo build --profile fastci\n          mv target/fastci/czkawka_cli linux_czkawka_cli_${{ env.ARCHNAME }}\n          mv target/fastci/czkawka_gui linux_czkawka_gui_${{ env.ARCHNAME }}\n          mv target/fastci/krokiet linux_krokiet_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"winit_skia_opengl,winit_software\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_skia_opengl_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"femtovg_wgpu\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_all_backends_${{ env.ARCHNAME }}\n\n      - name: Pack with 7z\n        run: |\n          # 7z -mx=3 in rust files, takes 40% less space but is 2x slower than zip -mx=1\n          # 7z -mx=3 is 8x faster than 7z -mx=5, but generates 20% bigger \n          # So looks that -mx=3 is the best option\n          time 7z a -t7z -mx=3 czkawka_all.7z \\\n            linux_czkawka_cli_${{ env.ARCHNAME }} \\\n            linux_czkawka_gui_${{ env.ARCHNAME }} \\\n            linux_krokiet_${{ env.ARCHNAME }} \\\n            linux_krokiet_skia_opengl_${{ env.ARCHNAME }} \\\n            linux_krokiet_skia_vulkan_${{ env.ARCHNAME }} \\\n            linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} \\\n            linux_krokiet_all_backends_${{ env.ARCHNAME }}\n\n      - name: Store\n        uses: actions/upload-artifact@v4\n        with:\n          name: all-${{ runner.os }}-${{ runner.arch }}-${{ env.VERS }}\n          path: |\n            czkawka_all.7z\n\n      - name: Release\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            linux_czkawka_cli_${{ env.ARCHNAME }}\n            linux_czkawka_gui_${{ env.ARCHNAME }}\n            linux_krokiet_${{ env.ARCHNAME }}\n            linux_krokiet_skia_opengl_${{ env.ARCHNAME }}\n            linux_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n            linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n            linux_krokiet_all_backends_${{ env.ARCHNAME }}\n          token: ${{ secrets.PAT_REPOSITORY }}\n\n  # Some dependencies requires ubuntu 24.04\n  linux-all-extra:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-24.04, ubuntu-24.04-arm]\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup env\n        run: |\n          ARCHNAME=$([ \"${{ runner.arch }}\" = \"ARM64\" ] && echo arm64 || echo x86_64)\n          echo \"ARCHNAME=$ARCHNAME\" >> $GITHUB_ENV\n\n      - name: Install basic libraries\n        run: sudo apt update || true; sudo apt install libheif-dev libraw-dev ffmpeg libgtk-4-dev p7zip-full -y\n\n      - name: Setup rust version\n        run: rustup default 1.92.0\n\n      - name: Build Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          sed -i 's/#lto = /lto = /g' Cargo.toml\n          sed -i 's/#codegen-units /codegen-units /g' Cargo.toml\n          \n          echo \"VERS=release\" >> $GITHUB_ENV\n          \n          cargo build --release --features \"heif,libraw\"\n          mv target/release/czkawka_cli linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }}\n          mv target/release/czkawka_gui linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }}\n          mv target/release/krokiet linux_krokiet_heif_raw_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_opengl,winit_software,heif,libraw\"\n          mv target/release/krokiet linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software,heif,libraw\"\n          mv target/release/krokiet linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"femtovg_wgpu,heif,libraw\"\n          mv target/release/krokiet linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libraw\"\n          mv target/release/krokiet linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }}\n\n      # Fast CI profile, to avoid out of disk space errors\n      # I doubt that anyone would use debug builds from here, because they are slow and contains not final changes\n      - name: Build Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          \n          echo \"VERS=debug\" >> $GITHUB_ENV\n          \n          cargo build --features \"heif,libraw\" --profile fastci\n          mv target/fastci/czkawka_cli linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }}\n          mv target/fastci/czkawka_gui linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }}\n          mv target/fastci/krokiet linux_krokiet_heif_raw_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"winit_skia_opengl,winit_software,heif,libraw\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software,heif,libraw\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"femtovg_wgpu,heif,libraw\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libraw\" --profile fastci\n          mv target/fastci/krokiet linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }}\n\n      - name: Pack with 7z\n        run: |\n          # 7z -mx=3 in rust files, takes 40% less space but is 2x slower than zip -mx=1\n          # 7z -mx=3 is 8x faster than 7z -mx=5, but generates 20% bigger \n          # So looks that -mx=3 is the best option\n          time 7z a -t7z -mx=3 czkawka_all.7z \\\n            linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }} \\\n            linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }} \\\n            linux_krokiet_heif_raw_${{ env.ARCHNAME }} \\\n            linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }} \\\n            linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }} \\\n            linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }} \\\n            linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }}\n\n      - name: Store\n        uses: actions/upload-artifact@v4\n        with:\n          name: all-${{ runner.os }}-${{ runner.arch }}-${{ env.VERS }}-heif-libraw\n          path: |\n            czkawka_all.7z\n\n      - name: Release\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }}\n            linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }}\n            linux_krokiet_heif_raw_${{ env.ARCHNAME }}\n            linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }}\n            linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }}\n            linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }}\n            linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }}\n          token: ${{ secrets.PAT_REPOSITORY }}\n\n  ### MUSL CLI and Krokiet Release and Debug\n  # GUI not works with MUSL :(\n  # https://github.com/slint-ui/slint/issues/7586\n  # https://github.com/rust-windowing/winit/issues/1818\n  linux-cli-musl:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install basic libraries\n        run: |\n          sudo apt update || true; sudo apt install musl-tools -y\n\n      - name: Setup rust version\n        run: |\n          rustup default 1.92.0\n          rustup target add x86_64-unknown-linux-musl\n\n      - name: Build Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          sed -i 's/#lto = /lto = /g' Cargo.toml\n          sed -i 's/#codegen-units /codegen-units /g' Cargo.toml\n          cargo build --release --bin czkawka_cli --target x86_64-unknown-linux-musl\n          \n          mv target/x86_64-unknown-linux-musl/release/czkawka_cli linux_czkawka_cli_musl\n\n      - name: Build Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          cargo build --bin czkawka_cli --target x86_64-unknown-linux-musl\n          \n          mv target/x86_64-unknown-linux-musl/debug/czkawka_cli linux_czkawka_cli_musl\n\n      - name: Store Linux CLI\n        uses: actions/upload-artifact@v4\n        with:\n          name: czkawka_cli-${{ runner.os }}-musl\n          path: |\n            linux_czkawka_cli_musl\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            linux_czkawka_cli_musl\n          token: ${{ secrets.PAT_REPOSITORY }}\n\n  ### Below, builds that do not produce artifacts\n\n  ### 32 bit CLI and Krokiet Release and Debug - TODO test also gtk gui but it requires gtk4:i386 and also would be good to test libraw and heif\n  linux-all-debug-32bit:\n    if: ${{ github.ref != 'refs/heads/master' }}\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install basic libraries\n        run: |\n          sudo apt update || true\n          sudo apt install gcc-multilib -y\n\n      - name: Setup rust version and target\n        run: |\n          rustup default 1.92.0\n          rustup target add i686-unknown-linux-gnu\n\n      - name: Build Debug for 32-bit\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          cargo build --target i686-unknown-linux-gnu --bin czkawka_cli --bin krokiet\n          mv target/i686-unknown-linux-gnu/debug/czkawka_cli linux_czkawka_cli_32bit\n          mv target/i686-unknown-linux-gnu/debug/krokiet linux_krokiet_32bit\n\n      - name: Store\n        uses: actions/upload-artifact@v4\n        with:\n          name: all-32bit-${{ runner.os }}-${{ runner.arch }}-debug\n          path: |\n            linux_czkawka_cli_32bit\n            linux_krokiet_32bit\n\n  linux-stability:\n    if: ${{ github.ref == 'refs/heads/master' }} # Runs only in master, because it is really time consuming\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install basic libraries\n        run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev -y\n\n      - name: Setup rust version\n        run: rustup default 1.92.0\n\n      - name: Build packages\n        run: |\n          rm -rf target || true\n          cargo build --features \"heif,libraw\"\n          mv target/debug/czkawka_cli czkawka_cli_debug_1\n          mv target/debug/czkawka_gui czkawka_gui_debug_1\n          mv target/debug/krokiet krokiet_debug_1\n          \n          rm -rf target || true\n          cargo build --release --features \"heif,libraw\"\n          mv target/release/czkawka_cli czkawka_cli_release_1\n          mv target/release/czkawka_gui czkawka_gui_release_1\n          mv target/release/krokiet krokiet_release_1\n          \n          rm -rf target || true\n          cargo build --features \"heif,libraw\"\n          mv target/debug/czkawka_cli czkawka_cli_debug_2\n          mv target/debug/czkawka_gui czkawka_gui_debug_2\n          mv target/debug/krokiet krokiet_debug_2\n          \n          rm -rf target || true\n          cargo build --release --features \"heif,libraw\"\n          mv target/release/czkawka_cli czkawka_cli_release_2\n          mv target/release/czkawka_gui czkawka_gui_release_2\n          mv target/release/krokiet krokiet_release_2\n          \n          bash misc/compare_files.sh\n\n  linux-tests:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install basic libraries\n        run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev -y\n\n      - name: Setup rust version\n        run: rustup default 1.92.0\n\n      - name: Test\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          xvfb-run cargo test\n\n  linux-regression-tests-on-minimal-rust-version:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install basic libraries\n        run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev ffmpeg -y\n\n      - name: Setup rust version\n        run: rustup default 1.92.0\n\n      - name: Build test version\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          cargo build --profile test --bin czkawka_cli\n\n      - name: Linux Regression Test\n        run: |\n          wget -q https://github.com/qarmin/czkawka/releases/download/6.0.0/TestFiles.zip\n          cd ci_tester\n          cargo build --release\n          cd ..\n\n          ci_tester/target/release/ci_tester target/debug/czkawka_cli\n\n  android:\n    if: ${{ github.ref == 'refs/heads/master' }}\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup rust version and target\n        run: |\n          rustup default 1.92.0\n          rustup target add aarch64-linux-android\n\n      - name: Check for Android\n        run: |\n          cd czkawka_core\n          cargo check --target aarch64-linux-android --features \"blake_pure\"\n"
  },
  {
    "path": ".github/workflows/mac.yml",
    "content": "name: 🍎 MacOS\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: '0 0 * * 2'\n\nenv:\n  CARGO_TERM_COLOR: always\n  CZKAWKA_OFFICIAL_BUILD: ${{ vars.CZKAWKA_OFFICIAL_BUILD }}\n\njobs:\n  macos:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [macos-latest, macos-15-intel]\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup env\n        run: |\n          ARCHNAME=$([ \"${{ runner.arch }}\" = \"ARM64\" ] && echo arm64 || echo x86_64)\n          echo \"ARCHNAME=$ARCHNAME\" >> $GITHUB_ENV\n\n      - name: Setup rust version\n        run: rustup default 1.92.0\n\n      - name: Install Homebrew\n        run: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\n      - name: Install GTK4\n        run: |\n          brew link --overwrite python@3.13\n          brew install gtk4 libheif libavif dav1d || true\n          # brew link --overwrite python@3.13\n\n      - name: Build Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          set -e\n          sed -i '' 's/#lto = \"thin\"/lto = \"thin\"/g' Cargo.toml\n          sed -i '' 's/#codegen-units /codegen-units /g' Cargo.toml\n          \n          echo \"VERS=release\" >> $GITHUB_ENV\n          \n          export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix)/lib\n          \n          cargo build --release\n          mv target/release/czkawka_cli mac_czkawka_cli_${{ env.ARCHNAME }}\n          mv target/release/czkawka_gui mac_czkawka_gui_${{ env.ARCHNAME }}\n          mv target/release/krokiet mac_krokiet_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software\"\n          mv target/release/krokiet mac_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"femtovg_wgpu\"\n          mv target/release/krokiet mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu\"\n          mv target/release/krokiet mac_krokiet_all_backends_${{ env.ARCHNAME }}\n          \n          cargo build --release --features \"heif,libavif\"\n          mv target/release/czkawka_cli mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }}\n          mv target/release/czkawka_gui mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }}\n          mv target/release/krokiet mac_krokiet_heif_avif_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software,heif,libavif\"\n          mv target/release/krokiet mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"femtovg_wgpu,heif,libavif\"\n          mv target/release/krokiet mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --release --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libavif\"\n          mv target/release/krokiet mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }}\n\n      - name: Build Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          set -e\n          sed -i '' 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i '' 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          \n          echo \"VERS=debug\" >> $GITHUB_ENV\n          \n          export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix)/lib\n          \n          cargo build --profile fastci\n          mv target/fastci/czkawka_cli mac_czkawka_cli_${{ env.ARCHNAME }}\n          mv target/fastci/czkawka_gui mac_czkawka_gui_${{ env.ARCHNAME }}\n          mv target/fastci/krokiet mac_krokiet_${{ env.ARCHNAME }}\n          cargo build --profile fastci --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software\"\n          mv target/fastci/krokiet mac_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n          cargo build --profile fastci --bin krokiet --no-default-features --features \"femtovg_wgpu\"\n          mv target/fastci/krokiet mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --profile fastci --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu\"\n          mv target/fastci/krokiet mac_krokiet_all_backends_${{ env.ARCHNAME }}\n          \n          cargo build --profile fastci --features \"heif,libavif\"\n          mv target/fastci/czkawka_cli mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }}\n          mv target/fastci/czkawka_gui mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }}\n          mv target/fastci/krokiet mac_krokiet_heif_avif_${{ env.ARCHNAME }}\n          cargo build --profile fastci --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software,heif,libavif\"\n          mv target/fastci/krokiet mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }}\n          cargo build --profile fastci --bin krokiet --no-default-features --features \"femtovg_wgpu,heif,libavif\"\n          mv target/fastci/krokiet mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }}\n          cargo build --profile fastci --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libavif\"\n          mv target/fastci/krokiet mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }}\n\n      - name: Store MacOS\n        uses: actions/upload-artifact@v4\n        with:\n          name: all-${{ runner.os }}-${{ runner.arch }}-${{ env.VERS }}\n          path: |\n            mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }}\n            mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }}\n            mac_krokiet_heif_avif_${{ env.ARCHNAME }}\n            mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }}\n            mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }}\n            mac_czkawka_cli_${{ env.ARCHNAME }}\n            mac_czkawka_gui_${{ env.ARCHNAME }}\n            mac_krokiet_${{ env.ARCHNAME }}\n            mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }}\n            mac_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n            mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n            mac_krokiet_all_backends_${{ env.ARCHNAME }}\n\n      - name: Release\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }}\n            mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }}\n            mac_krokiet_heif_avif_${{ env.ARCHNAME }}\n            mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }}\n            mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }}\n            mac_czkawka_cli_${{ env.ARCHNAME }}\n            mac_czkawka_gui_${{ env.ARCHNAME }}\n            mac_krokiet_${{ env.ARCHNAME }}\n            mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }}\n            mac_krokiet_skia_vulkan_${{ env.ARCHNAME }}\n            mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }}\n            mac_krokiet_all_backends_${{ env.ARCHNAME }}\n          token: ${{ secrets.PAT_REPOSITORY }}\n"
  },
  {
    "path": ".github/workflows/quality.yml",
    "content": "name: 🧹 Quality\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: '0 0 * * 2'\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  quality:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install Gtk 4\n        run: sudo apt update || true; sudo apt install -y libgtk-4-dev libraw-dev libheif-dev libavif-dev libdav1d-dev libasound2-dev -y\n\n      - name: Setup rust version\n        run: |\n          rustup default 1.92.0\n          rustup component add rustfmt\n          rustup component add clippy\n\n      - name: Disable optimizations\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n\n      - name: Check the format\n        run: cargo fmt --all -- --check\n\n      - name: Run clippy\n        run: |\n          cargo clippy --all-targets --all-features -- -D warnings\n          cargo clippy --all-targets -- -D warnings\n\n      - name: Check tools\n        run: |\n          cd misc/test_image_perf\n          cargo check\n          cd ../../\n          \n          cd misc/test_read_perf\n          cargo check\n          cd ../../\n"
  },
  {
    "path": ".github/workflows/windows.yml",
    "content": "name: 🏁 Windows\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: '0 0 * * 2'\n\nenv:\n  CARGO_TERM_COLOR: always\n  CZKAWKA_OFFICIAL_BUILD: ${{ vars.CZKAWKA_OFFICIAL_BUILD }}\n\njobs:\n  krokiet-compiled-on-linux:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install dependencies\n        run: |\n          sudo apt update || true\n          sudo apt install -y mingw-w64 mingw-w64-x86-64-dev wget wget2 curl wine || true\n\n      - name: Setup rust version\n        run: |\n          rustup default 1.92.0\n          rustup target add x86_64-pc-windows-gnu\n\n      - name: Download rcedit\n        run: |\n          curl -L https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -o rcedit-x64.exe\n\n      - name: Compile Krokiet Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          sed -i 's/#lto = /lto = /g' Cargo.toml\n          sed -i 's/#codegen-units /codegen-units /g' Cargo.toml\n          cargo build --release --target x86_64-pc-windows-gnu --bin krokiet\n          mv target/x86_64-pc-windows-gnu/release/krokiet.exe windows_krokiet_on_linux.exe\n          \n          export WINEPREFIX=$(mktemp -d)\n          wine rcedit-x64.exe windows_krokiet_on_linux.exe --set-icon krokiet/icons/krokiet_logo_flag.ico\n\n      - name: Compile Krokiet Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          sed -i 's/^\\(\\[profile\\.dev\\.package.*\\)/#\\1/' Cargo.toml\n          sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml\n          cargo build --target x86_64-pc-windows-gnu --bin krokiet\n          mv target/x86_64-pc-windows-gnu/debug/krokiet.exe windows_krokiet_on_linux.exe\n          \n          export WINEPREFIX=$(mktemp -d)\n          wine rcedit-x64.exe windows_krokiet_on_linux.exe --set-icon krokiet/icons/krokiet_logo_flag.ico\n\n      - name: Pack with 7z\n        run: |\n          time 7z a -t7z -mx=3 czkawka_all.7z \\\n            windows_krokiet_on_linux.exe\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: krokiet-windows-on-linux-${{ github.sha }}\n          path: |\n            czkawka_all.7z\n          if-no-files-found: error\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            windows_krokiet_on_linux.exe\n          token: ${{ secrets.PAT_REPOSITORY }}\n\n  # Skia not provides support for gnu toolchain, which is easy to cross-compile - https://github.com/rust-skia/rust-skia/issues/345\n  # So need to compile krokiet on msvc Windows\n  krokiet-compiled-on-windows:\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup rust version\n        run: |\n          rustup default 1.92.0\n\n      - name: Download rcedit\n        run: |\n          curl -L https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -o rcedit-x64.exe\n\n      - name: Compile Krokiet Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          powershell -Command \"(Get-Content Cargo.toml) -replace '#lto = ', 'lto = ' | Set-Content Cargo.toml; (Get-Content Cargo.toml) -replace '#codegen-units ', 'codegen-units ' | Set-Content Cargo.toml\"\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_opengl,winit_software\"\n          mv target/release/krokiet.exe windows_krokiet_on_windows_skia_opengl.exe\n          cargo build --release --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software\"\n          mv target/release/krokiet.exe windows_krokiet_on_windows_skia_vulkan.exe\n          cargo build --release --bin krokiet --no-default-features --features \"femtovg_wgpu\"\n          mv target/release/krokiet.exe windows_krokiet_on_windows_femtovg_wgpu.exe\n          cargo build --release --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu\"\n          mv target/release/krokiet.exe windows_krokiet_on_windows_all_backends.exe\n          Get-ChildItem windows_krokiet_on_windows_*.exe | ForEach-Object { ./rcedit-x64.exe $_.Name --set-icon krokiet/icons/krokiet_logo_flag.ico }\n\n      - name: Compile Krokiet Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          (Get-Content Cargo.toml) -replace '#lto = ','lto = ' -replace '#codegen-units ','codegen-units ' -replace '^\\[profile\\.dev\\.package','#\\[profile.dev.package' -replace '^opt-level = 3 # OPT PACKAGES','#opt-level = 3 # OPT PACKAGES' | Set-Content Cargo.toml\n          \n          cargo build --bin krokiet --no-default-features --features \"winit_skia_opengl,winit_software\"\n          mv target/debug/krokiet.exe windows_krokiet_on_windows_skia_opengl.exe\n          cargo build --bin krokiet --no-default-features --features \"winit_skia_vulkan,winit_software\"\n          mv target/debug/krokiet.exe windows_krokiet_on_windows_skia_vulkan.exe\n          cargo build --bin krokiet --no-default-features --features \"femtovg_wgpu\"\n          mv target/debug/krokiet.exe windows_krokiet_on_windows_femtovg_wgpu.exe\n          cargo build --bin krokiet --no-default-features --features \"winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu\"\n          mv target/debug/krokiet.exe windows_krokiet_on_windows_all_backends.exe\n          \n          Get-ChildItem windows_krokiet_on_windows_*.exe | ForEach-Object { ./rcedit-x64.exe $_.Name --set-icon krokiet/icons/krokiet_logo_flag.ico }\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: krokiet-windows-on-windows-${{ github.sha }}\n          path: |\n            windows_krokiet_on_windows_skia_opengl.exe\n            windows_krokiet_on_windows_skia_vulkan.exe\n            windows_krokiet_on_windows_femtovg_wgpu.exe\n            windows_krokiet_on_windows_all_backends.exe\n          if-no-files-found: error\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            windows_krokiet_on_windows_skia_opengl.exe\n            windows_krokiet_on_windows_skia_vulkan.exe\n            windows_krokiet_on_windows_femtovg_wgpu.exe\n            windows_krokiet_on_windows_all_backends.exe\n          token: ${{ secrets.PAT_REPOSITORY }}\n\n  container_4_12:\n    runs-on: ubuntu-latest\n    container:\n      image: ghcr.io/mglolenstine/gtk4-cross:gtk-4.12\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install additional dependencies\n        # gio is for the build script\n        run: |\n          dnf install curl wget2 wget unzip mingw64-bzip2.noarch mingw64-poppler mingw64-poppler-glib mingw32-python3 rust-gio-devel adwaita-icon-theme wine -y && dnf clean all -y\n          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n          source \"$HOME/.cargo/env\"\n          rustup default 1.92.0\n          rustup target add x86_64-pc-windows-gnu\n          \n          mkdir -p package\n          curl -L https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -o rcedit-x64.exe\n\n      - name: Cross compile for Windows - Release\n        if: ${{ github.ref == 'refs/heads/master' }}\n        run: |\n          source \"$HOME/.cargo/env\"\n          export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/share/pkgconfig:$MINGW_PREFIX/lib/pkgconfig/:/usr/x86_64-w64-mingw32/lib/pkgconfig/\n          cargo build --target=x86_64-pc-windows-gnu --release --locked\n          cp target/x86_64-pc-windows-gnu/release/czkawka_gui.exe package/\n          cp target/x86_64-pc-windows-gnu/release/czkawka_cli.exe package/\n          \n          export WINEPREFIX=$(mktemp -d)\n          wine rcedit-x64.exe package/czkawka_gui.exe --set-icon czkawka_gui/icons/icon.ico\n\n      - name: Cross compile for Windows - Debug\n        if: ${{ github.ref != 'refs/heads/master' }}\n        run: |\n          source \"$HOME/.cargo/env\"\n          export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/share/pkgconfig:$MINGW_PREFIX/lib/pkgconfig/:/usr/x86_64-w64-mingw32/lib/pkgconfig/\n          cargo build --target=x86_64-pc-windows-gnu --locked --profile fastci\n          cp target/x86_64-pc-windows-gnu/fastci/czkawka_gui.exe package/\n          cp target/x86_64-pc-windows-gnu/fastci/czkawka_cli.exe package/\n          \n          export WINEPREFIX=$(mktemp -d)\n          wine rcedit-x64.exe package/czkawka_gui.exe --set-icon czkawka_gui/icons/icon.ico\n\n      - name: Package\n        run: |\n          #!/bin/bash\n          set -euo pipefail\n          cp -t package $(pds -vv -f package/*.exe)\n          # Add gdbus which is recommended on Windows (why?)\n          cp $MINGW_PREFIX/bin/gdbus.exe package\n          # Handle the glib schema compilation as well\n          glib-compile-schemas $MINGW_PREFIX/share/glib-2.0/schemas/\n          mkdir -p package/share/glib-2.0/schemas/\n          cp -T $MINGW_PREFIX/share/glib-2.0/schemas/gschemas.compiled package/share/glib-2.0/schemas/gschemas.compiled\n          # Pixbuf stuff, in order to get SVGs (scalable icons) to load\n          mkdir -p package/lib/gdk-pixbuf-2.0\n          cp -rT $MINGW_PREFIX/lib/gdk-pixbuf-2.0 package/lib/gdk-pixbuf-2.0\n          cp -f -t package $(pds -vv -f $MINGW_PREFIX/lib/gdk-pixbuf-2.0/2.10.0/loaders/*)\n          find package -iname \"*.dll\" -or -iname \"*.exe\" -type f -exec mingw-strip {} +\n\n          cd package/share\n          wget2 https://github.com/qarmin/czkawka/files/10832192/gtk4_theme.zip\n          unzip gtk4_theme.zip\n          rm gtk4_theme.zip\n          cd ../..\n          \n          wget2 https://github.com/qarmin/Automated-Fuzzer/releases/download/test/libGL.zip\n          unzip libGL.zip\n          mv libEGL.dll package/\n          mv libGLESv2.dll package/\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: czkawka-windows-${{ github.sha }}-4.12\n          path: |\n            ./package\n          if-no-files-found: error\n\n      - name: Prepare files to release\n        run: |\n          cd package\n          zip -r ../windows_czkawka_gui_gtk_412.zip .\n          cd ..\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }}\n        with:\n          tag_name: \"Nightly\"\n          files: |\n            windows_czkawka_gui_gtk_412.zip\n          token: ${{ secrets.PAT_REPOSITORY }}\n\n\n\n  windows-tests:\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup rust version\n        run: |\n          rustup default 1.92.0\n\n      # Both additional features and gtk gui are non-testable due really complicated setup of environment on Windows\n      - name: Test\n        run: |\n          cargo test -p czkawka_core -p krokiet"
  },
  {
    "path": ".gitignore",
    "content": "/target\n.idea/\n*.iml\n*~\n*#\nresults*.txt\nTestSuite*\n*.snap\nflatpak/\n*.zip\n*.zst\n*.profraw\n*.profdata\n/lcov_report*\n/report\nci_tester/target\nci_tester/Cargo.lock\nkrokiet/Cargo.lock\nkrokiet/target\n*.json\n*.mm_profdata\nperf.data\nperf.data.old\nkrokiet/ui/test.slint\n*.html\nmisc/*/*.lock\nmisc/*/target/\nmisc/*/.idea\nbenchmarks\nTestFiles\n*.txt\n.venv\ncharts*\n*__pycache__*/\nresult\n.direnv\ncedinia/android/keystore/*.keystore\ncedinia/docs"
  },
  {
    "path": ".mailmap",
    "content": "TheEvilSkeleton <theevilskeleton@riseup.net> Proprietary Chrome-chan <theevilskeleton@riseup.net>\nRafał Mikrut <mikrutrafal@protonmail.com> <mikrutrafal54@gmail.com> <41945903+qarmin@users.noreply.github.com>\n"
  },
  {
    "path": ".rustfmt.toml",
    "content": "newline_style = \"Unix\"\nmax_width = 180\n\n# Enable only with nightly channel via - cargo +nightly fmt\nimports_granularity = \"Module\"\ngroup_imports = \"StdExternalCrate\""
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\n    \"czkawka_core\",\n    \"czkawka_cli\",\n    \"czkawka_gui\",\n    \"krokiet\",\n    \"cedinia\"\n]\nexclude = [\n    \"misc/test_read_perf\",\n    \"misc/test_image_perf\",\n    \"misc/test_compilation_speed_size\",\n    \"ci_tester\",\n]\nresolver = \"3\"\n\n# android-activity 0.6.0 panics when ANativeActivity_onCreate is called a second\n# time (Activity recreation) because ndk_context::initialize_android_context\n# asserts previous.is_none().  The fix (OnceLock, init once with Application ref)\n# is merged to main but not yet released.  Remove this patch once 0.6.1+ ships.\n# Affects only android-activity ^0.6 (cedinia); Slint's ^0.5 dependency is unaffected.\n[patch.crates-io]\nandroid-activity = { git = \"https://github.com/rust-mobile/android-activity\", rev = \"43de2770b91b1b8ff870f00551f89f04062216cc\" }\n\n[profile.release]\n# panic = \"unwind\" in opposite to \"abort\", allows to catch panic!()\n# Since Czkawka parse different types of files with few libraries, it is possible\n# that some files will cause crash, so at this moment I don't recommend to use \"abort\"\n# until you are ready to occasional crashes\npanic = \"unwind\"\n\n# Should find more panics, that now are hidden from user - in long term it should decrease bugs in app\n# It may cause some crashes, that are not handled via panic::catch_unwind, so feel free to disable it, if you want\noverflow-checks = true\n\n# LTO setting is disabled by default, because release mode is usually needed to develop app and compilation with LTO would take a lot of time\n# But it is used to optimize release builds(and probably also in CI, where time is not so important as in local development)\n# Fat lto, generates a lot smaller executable than thin lto\n# Also using codegen-units = 1, to generate smaller binaries\n#lto = \"fat\"\n#codegen-units = 1\n\n# Optimize all dependencies except application/workspaces, even in debug builds to get reasonable performance e.g. when opening images\n[profile.dev.package.\"*\"] # OPT PACKAGES\nopt-level = 3 # OPT PACKAGES\n\n[profile.fast_release]\ninherits = \"release\"\nincremental = true\noverflow-checks = true\ndebug = false\nstrip = true\n\n[profile.test]\ndebug-assertions = true # Forces to crash when there is duplicated item in cli\noverflow-checks = true\nopt-level = 3\n\n# Fast compilation and small binary size\n[profile.fastci]\ninherits = \"dev\"\nstrip = \"symbols\"\ndebug = false\nlto = \"off\"\n\n[profile.rdebug]\ninherits = \"release\"\ndebug = \"full\"\nstrip = \"none\"\n\n# Unsafe profile, just to check, how fast and small app could be \n[profile.fastest]\ninherits = \"release\"\npanic = \"abort\"\nlto = \"fat\"\nstrip = \"symbols\"\ncodegen-units = 1\nopt-level = 3\ndebug = false\n\n[workspace.lints]\nclippy.unreachable = \"allow\" # This is a legitimate use case in most places\nclippy.enum_variant_names = \"allow\" # Not always is possible to use different names\nclippy.too_many_arguments = \"allow\" # Sometimes such functions are needed\nclippy.type_complexity = \"allow\" # Sometimes such types are needed\nclippy.collapsible_else_if = \"allow\" # Sometimes it is more readable\nclippy.iter_on_single_items = \"allow\" # Allows to extend slice items, without needing to converting it when number of items change\nclippy.needless_range_loop = \"allow\" # Sometimes it is more readable\n\nclippy.doc_broken_link = \"warn\"\nclippy.ip_constant = \"warn\"\nclippy.unnecessary_semicolon = \"warn\"\nclippy.trivially_copy_pass_by_ref = \"warn\"\nclippy.indexing_slicing = \"warn\"\nclippy.non_std_lazy_statics = \"warn\"\nclippy.undocumented_unsafe_blocks = \"warn\"\nclippy.manual_midpoint = \"warn\"\nclippy.ignore_without_reason = \"warn\"\nclippy.elidable_lifetime_names = \"warn\"\n#clippy.duration_suboptimal_units = \"warn\"\n#clippy.decimal_bitwise_operands = \"warn\"\n\nclippy.allow_attributes = \"warn\"\nclippy.assertions_on_result_states = \"warn\"\nclippy.bool_to_int_with_if = \"warn\"\nclippy.branches_sharing_code = \"warn\"\nclippy.collection_is_never_read = \"warn\"\nclippy.dbg_macro = \"warn\"\nclippy.debug_assert_with_mut_call = \"warn\"\nclippy.empty_enum_variants_with_brackets = \"warn\"\nclippy.enum_glob_use = \"warn\"\nclippy.equatable_if_let = \"warn\"\nclippy.error_impl_error = \"warn\"\nclippy.explicit_into_iter_loop = \"warn\"\nclippy.explicit_iter_loop = \"warn\"\nclippy.expl_impl_clone_on_copy = \"warn\"\nclippy.fallible_impl_from = \"warn\"\nclippy.filter_map_next = \"warn\"\nclippy.flat_map_option = \"warn\"\nclippy.float_cmp = \"warn\"\nclippy.from_iter_instead_of_collect = \"warn\"\nclippy.ignored_unit_patterns = \"warn\"\nclippy.implicit_clone = \"warn\"\nclippy.index_refutable_slice = \"warn\"\nclippy.invalid_upcast_comparisons = \"warn\"\nclippy.iter_filter_is_ok = \"warn\"\nclippy.iter_filter_is_some = \"warn\"\nclippy.iter_on_empty_collections = \"warn\"\nclippy.iter_with_drain = \"warn\"\nclippy.large_stack_arrays = \"warn\"\nclippy.large_types_passed_by_value = \"warn\"\nclippy.literal_string_with_formatting_args = \"warn\"\nclippy.lossy_float_literal = \"warn\"\nclippy.macro_use_imports = \"warn\"\nclippy.manual_assert = \"warn\"\nclippy.manual_instant_elapsed = \"warn\"\nclippy.manual_is_variant_and = \"warn\"\nclippy.manual_let_else = \"warn\"\nclippy.manual_ok_or = \"warn\"\nclippy.map_unwrap_or = \"warn\"\nclippy.match_bool = \"warn\"\nclippy.match_same_arms = \"warn\"\nclippy.match_wildcard_for_single_variants = \"warn\"\nclippy.mutex_atomic = \"warn\"\nclippy.mutex_integer = \"warn\"\nclippy.mut_mut = \"warn\"\nclippy.needless_bitwise_bool = \"warn\"\nclippy.needless_collect = \"warn\"\nclippy.needless_continue = \"warn\"\nclippy.needless_for_each = \"warn\"\nclippy.needless_pass_by_ref_mut = \"warn\"\nclippy.needless_pass_by_value = \"warn\"\nclippy.needless_raw_strings = \"warn\"\nclippy.nonstandard_macro_braces = \"warn\"\nclippy.option_as_ref_cloned = \"warn\"\nclippy.pathbuf_init_then_push = \"warn\"\nclippy.path_buf_push_overwrite = \"warn\"\nclippy.print_stderr = \"warn\"\nclippy.print_stdout = \"warn\"\nclippy.pub_underscore_fields = \"warn\"\nclippy.question_mark = \"warn\"\nclippy.range_minus_one = \"warn\"\nclippy.range_plus_one = \"warn\"\nclippy.redundant_clone = \"warn\"\nclippy.redundant_else = \"warn\"\nclippy.ref_binding_to_reference = \"warn\"\nclippy.ref_option_ref = \"warn\"\nclippy.same_functions_in_if_condition = \"warn\"\nclippy.semicolon_if_nothing_returned = \"warn\"\nclippy.set_contains_or_insert = \"warn\"\nclippy.stable_sort_primitive = \"warn\"\nclippy.string_add_assign = \"warn\"\nclippy.string_slice = \"warn\"\nclippy.suspicious_operation_groupings = \"warn\"\nclippy.suspicious_xor_used_as_pow = \"warn\"\nclippy.todo = \"warn\"\nclippy.trait_duplication_in_bounds = \"warn\"\nclippy.trivial_regex = \"warn\"\nclippy.type_repetition_in_bounds = \"warn\"\nclippy.unimplemented = \"warn\"\nclippy.uninlined_format_args = \"warn\"\nclippy.unnecessary_box_returns = \"warn\"\nclippy.unnecessary_join = \"warn\"\nclippy.unnecessary_wraps = \"warn\"\nclippy.unnested_or_patterns = \"warn\"\nclippy.unused_async = \"warn\"\nclippy.unused_result_ok = \"warn\"\nclippy.unused_rounding = \"warn\"\nclippy.unused_self = \"warn\"\nclippy.unwrap_used = \"warn\"\nclippy.used_underscore_binding = \"warn\"\nclippy.useless_let_if_seq = \"warn\"\nclippy.use_self = \"warn\"\nclippy.verbose_file_reads = \"warn\"\nclippy.wildcard_imports = \"warn\"\n\n\n"
  },
  {
    "path": "Changelog.md",
    "content": "## Version 11.0.1 - 20.02.2026r\n### Core\n- Fixed issue with excluded folders not working on Windows - [#1808](https://github.com/qarmin/czkawka/pull/1808)\n\n### GTK GUI\n- Added missing Duration column to the similar videos tab, fixing a panic that occurred after video analysis - [#1793](https://github.com/qarmin/czkawka/pull/1793)\n- Removed warning log message shown for non-existing excluded directories - [#1795](https://github.com/qarmin/czkawka/pull/1795)\n- Fixed panic occurring when double-clicking a folder from the included or excluded directories list - [#1799](https://github.com/qarmin/czkawka/pull/1799)\n- Updated to stable gtk4-rs 0.11 - [#1808](https://github.com/qarmin/czkawka/pull/1808)\n\n### Krokiet\n- Increased default maximum file size limit - [#1808](https://github.com/qarmin/czkawka/pull/1808)\n\n## Prebuilt binaries\n- Added new Krokiet wgpu binaries\n- Added new all-in-one Krokiet binaries with all backends included\n\n## CI\n- Added Windows CI job running `cargo test`\n\n## Version 11.0.0 - 14.02.2026r\n\n### Breaking changes\n#### Users\n- The Czkawka GUI config file was migrated from a custom, broken format to JSON. All settings must be configured again. The old TXT file is not removed and can be used as a reference.\n- In broken files mode, file type is no longer stored in cache. Existing cache files are incompatible with this version and will be automatically regenerated.\n- The `Similarity Preset` enum in similar images mode was replaced with an integer argument `Max Difference` in range 0-40.\n- HEIF images are now rotated only once instead of twice. Existing cache may contain incorrectly rotated images and should be regenerated by removing cache(but this requires manual intervention).\n\n#### Devs\n- Public API functions were slightly adjusted to avoid unnecessary cloning and referencing of copyable types.\n- `Similarity` variables were renamed to `Difference`.\n- Applications must call `register_image_decoding_hooks();` at startup to enable reading HEIF and JXL images.\n\n### Core\n- In similar images mode and previews, extension validation was removed in most cases - [#1623](https://github.com/qarmin/czkawka/pull/1623)\n- Build-time and runtime versions of Musl and Glibc are now printed to logs - [#1604](https://github.com/qarmin/czkawka/pull/1604/files)\n- Destination file removal during symlinking is now delayed to prevent data loss in case of failure - [#1672](https://github.com/qarmin/czkawka/pull/1672)\n- Fixed invalid path canonicalization on Windows - [#1604](https://github.com/qarmin/czkawka/pull/1604/files)\n- Comparison results are now deterministic - [#1654](https://github.com/qarmin/czkawka/pull/1654)\n- Built-in JPEG previews are now read from RAW images when available - [#1655](https://github.com/qarmin/czkawka/pull/1655)\n- Fixed silent panics when the logger could not write to the terminal - [#1658](https://github.com/qarmin/czkawka/pull/1658)\n- Commit hash is now included in logs - [#1672](https://github.com/qarmin/czkawka/pull/1672)\n- Improved and fixed logic for grouping similar images by similarity level - [#1685](https://github.com/qarmin/czkawka/pull/1685)\n- Added scan time measurement - [#1674](https://github.com/qarmin/czkawka/pull/1674), [#1685](https://github.com/qarmin/czkawka/pull/1685)\n- Added support for detecting broken video files in the broken files tool, via external ffmpeg and ffprobe - [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Added new video optimizer mode to reencode videos with more efficient codecs and crop black/static bars, via external ffmpeg and ffprobe - [#1726](https://github.com/qarmin/czkawka/pull/1726), [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Added new exif remover mode to remove selected EXIF tags from files - [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Added new bad names mode to find and rename files with problematic names, e.g. non-ASCII characters or uppercase extensions - [#1754](https://github.com/qarmin/czkawka/pull/1754)\n- Added ability to scan individual files, not only folders - [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Limited supported image size to 2000 MP - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Automatic cleanup of outdated entries now runs at most once per week - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Added a function to manually remove outdated entries from cache files - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Added video property information, bitrate, codec, FPS, dimensions, duration for similar videos tool - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Fixed double rotation of HEIF images - [#1783](https://github.com/qarmin/czkawka/pull/1783)\n- Fixed incorrect handling of some HEIF images by using built-in libheif-rs decoding methods - [#1783](https://github.com/qarmin/czkawka/pull/1783)\n\n### CLI\n- Enabled colored terminal output by default, can be disabled via feature flag - [#1672](https://github.com/qarmin/czkawka/pull/1672)\n- Fixed a regression where results were not printed to the terminal - [#1672](https://github.com/qarmin/czkawka/pull/1672)\n- Added `dry_run` and `move_to_trash` options to most of tools - [#1685](https://github.com/qarmin/czkawka/pull/1685)\n- Fixed unbound `--excluded-extensions` option - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Added new modes: video optimizer, exif remover, and bad names - [#1760](https://github.com/qarmin/czkawka/pull/1760)\n\n### GTK GUI\n- Restored the sort button and fixed crashes related to sorting - [#1623](https://github.com/qarmin/czkawka/pull/1623)\n- Configuration now uses JSON format instead of a custom one - [#1623](https://github.com/qarmin/czkawka/pull/1623)\n- Added multithreaded creation of hard links, symbolic links, and file removal - [#1672](https://github.com/qarmin/czkawka/pull/1672)\n- Fixed a GTK regression that caused image previews to appear extremely small - [#1658](https://github.com/qarmin/czkawka/pull/1658)\n- Added a button to easily swap between compared images - [#1658](https://github.com/qarmin/czkawka/pull/1658)\n- Performed refactoring to evaluate possible migration to GTK 5, currently not very feasible - [#1658](https://github.com/qarmin/czkawka/pull/1658)\n- Fixed sorting by size in big files mode - [#1691](https://github.com/qarmin/czkawka/pull/1691)\n- Fixed freezes caused by an invalid function declaration in gtk4-rs - [#1691](https://github.com/qarmin/czkawka/pull/1691)\n- Added an About popup informing that Krokiet is the successor application - [#1718](https://github.com/qarmin/czkawka/pull/1718)\n- Added `--cache` and `--config` CLI options to open cache and config paths - [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Added shortest and longest path selection modes - [#1738](https://github.com/qarmin/czkawka/pull/1738)\n\n### Krokiet\n- Added a new logo - [#1726](https://github.com/qarmin/czkawka/pull/1726)\n- Added video thumbnails, single and grid view - [#1714](https://github.com/qarmin/czkawka/pull/1714)\n- Displayed cache, thumbnails, and logs size in settings - [#1714](https://github.com/qarmin/czkawka/pull/1714)\n- Added sorting by clicking column headers - [#1718](https://github.com/qarmin/czkawka/pull/1718)\n- Introduced a default limit of 500 message lines to prevent freezes caused by slow TextEdit performance - [#1718](https://github.com/qarmin/czkawka/pull/1718)\n- Slightly increased font sizes to improve readability - [#1726](https://github.com/qarmin/czkawka/pull/1726)\n- Added runtime application scaling, with some limitations - [#1726](https://github.com/qarmin/czkawka/pull/1726)\n- Cleared messages in the bottom panel when a new scan starts - [#1726](https://github.com/qarmin/czkawka/pull/1726)\n- Fixed a crash when clicking previous results while a new scan was in progress - [#1726](https://github.com/qarmin/czkawka/pull/1726)\n- Changed default behavior to move files to trash instead of permanently deleting them - [#1726](https://github.com/qarmin/czkawka/pull/1726)\n- Added a notification dialog when the application cannot be opened - [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Added `--cache` and `--config` CLI options to open cache and config paths - [#1745](https://github.com/qarmin/czkawka/pull/1745)\n- Added new modes: video optimizer, exif remover, and bad names - [#1726](https://github.com/qarmin/czkawka/pull/1726), [#1745](https://github.com/qarmin/czkawka/pull/1745), [#1754](https://github.com/qarmin/czkawka/pull/1754)\n- Modification dates are now displayed in local time instead of UTC - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Added a new menu option to manually remove outdated cache entries - [#1748](https://github.com/qarmin/czkawka/pull/1748)\n- Added an optional scan completion sound, hidden behind the `audio` feature flag - [#1754](https://github.com/qarmin/czkawka/pull/1754)\n- Fixed an issue where sort options were not updating due to multiple invalid signal connections - [#1760](https://github.com/qarmin/czkawka/pull/1760)\n- Added support for creating hard links and symbolic links - [#1760](https://github.com/qarmin/czkawka/pull/1760)\n- Added shortest and longest path selection modes - [#1738](https://github.com/qarmin/czkawka/pull/1738)\n- Fixed crashes caused by selection cache desynchronization - [#1783](https://github.com/qarmin/czkawka/pull/1783)\n\n### External\n- Wine 10.20 includes a bugfix that resolves crashes when opening file dialogs in Czkawka GUI - [Wine 49987 issue](https://bugs.winehq.org/show_bug.cgi?id=49987)\n\n### Prebuilt binaries\n- Krokiet Windows binaries with the Skia backend are now available, this only works with MSVC build and requires Visual C++ Redistributable\n- Intel Mac binaries are now built with the latest available macOS version, currently 15\n- Windows prebuilt binaries now bundle libEGL and libGLES, which fixes issues running GTK 4.12 builds on some systems, GTK 4.6 builds are no longer provided\n- Krokiet macOS OpenGL binaries are deprecated due to outdated and broken Apple OpenGL drivers, Skia Vulkan binaries are now provided and recommended\n- Some Linux binaries are now built on Ubuntu 24.04 to support a newer libheif-rs with improvements, including reading images with pixel formats other than rgb8\n- Windows binaries now use an 8 MB stack size to match Linux, fixing stack overflows in debug builds\n- Windows binaries now include built-in icons\n\n## Version 10.0.0 - 18.08.2025r\n### Breaking changes\n#### Users\n- Some languages now have unified names in Crowdin (e.g. `es` → `es-ES`). The GUI may not find them and will fall back to the default language.\n- Cache files now use memory limits and are incompatible with previous versions.\n- Cli image filter argument changed from `faussian` to `gaussian`\n\n#### Devs\n- `stop_flag` is now required argument in most of the core functions\n- Visibility of some core functions has been reduced to `pub(crate)`\n- The modules in czkawka_core have been split and reorganized a bit - imports need to be adjusted, although the actual behavior and item names should not be changed too much\n\n### Core\n- Replaced `println`/`eprintln` with logging functions - [#1478](https://github.com/qarmin/czkawka/pull/1478)\n- Slightly improved cache loading and saving speed - [#1478](https://github.com/qarmin/czkawka/pull/1478)\n- Messages and panics are now also logged to a file (can be disabled by setting the `DISABLE_FILE_LOGGING` environment variable) - [#1508](https://github.com/qarmin/czkawka/pull/1508)\n- Added a 8GB memory limit when loading or saving cache to avoid out-of-memory crashes with broken cache files - [#1508](https://github.com/qarmin/czkawka/pull/1508)\n- Czkawka binaries are now reproducible - [#1565](https://github.com/qarmin/czkawka/pull/1565)\n- Added protection against deleting a folder that is no longer empty since the scan - [#1566](https://github.com/qarmin/czkawka/pull/1566)\n- Replaced `pdf-rs` with the more popular `lopdf` library, which also has fewer dependencies - [#1566](https://github.com/qarmin/czkawka/pull/1566)\n- Replaced `imagepipe` + `rawloader` with `rawler` which is still supported and faster to decode raw files - [#1572](https://github.com/qarmin/czkawka/pull/1572)\n- Added more configuration options in video finder - [#1578](https://github.com/qarmin/czkawka/pull/1578)\n- `fast_image_resize` feature is removed and `image_hasher/fast_resize_unstable` is enabled unconditionally - [#1586](https://github.com/qarmin/czkawka/pull/1586)\n\n### CLI\n- Improved logic for deleting files and added progress bar for this operation - [#1571](https://github.com/qarmin/czkawka/pull/1571)\n\n### GTK GUI\n- New icons - less visually appealing, but created by me and released under a truly free CC BY license - [#1478](https://github.com/qarmin/czkawka/pull/1478)\n- Fixed crash when removing outdated cache - [#1508](https://github.com/qarmin/czkawka/pull/1508)\n- Fixed missing file and folder names for similar videos in reference folders - [#1520](https://github.com/qarmin/czkawka/pull/1520)\n- Fixed crashes when the SVG pixbuf loader is not available - [#1565](https://github.com/qarmin/czkawka/pull/1565)\n- Fixed using custom select on referenced folders - [#1581](https://github.com/qarmin/czkawka/pull/1581)\n\n### Krokiet\n- Added the ability to select multiple items with mouse and keyboard - [#1478](https://github.com/qarmin/czkawka/pull/1478)\n- Added sort button - [#1501](https://github.com/qarmin/czkawka/pull/1501)\n- Window size is now remembered - [#1508](https://github.com/qarmin/czkawka/pull/1508)\n- Added translations - [#1508](https://github.com/qarmin/czkawka/pull/1508), [#1513](https://github.com/qarmin/czkawka/pull/1513)\n- Improved popup styling - [#1520](https://github.com/qarmin/czkawka/pull/1520)\n- Dark and light themes can now be switched at runtime - [#1520](https://github.com/qarmin/czkawka/pull/1520)\n- Changed icon color to white for dark theme to improve visibility - [#1520](https://github.com/qarmin/czkawka/pull/1520)\n- Added the ability to hide text on buttons - [#1520](https://github.com/qarmin/czkawka/pull/1520)\n- Multithreaded removing, moving, and renaming of files - [#1565](https://github.com/qarmin/czkawka/pull/1565)\n- Files that fail to be removed, renamed, or moved are no longer deleted from the results list - [#1565](https://github.com/qarmin/czkawka/pull/1565)\n- Progress information is shown when removing, renaming, or moving files, with the ability to stop the process - [#1565](https://github.com/qarmin/czkawka/pull/1565)\n- Folders to scan can be now set via cli e.g. `krokiet /home/rafal` - for more info see `krokiet --help` - [#1566](https://github.com/qarmin/czkawka/pull/1566)\n- Improved appearance of bottom directories panel - [#1569](https://github.com/qarmin/czkawka/pull/1569)\n- Some buttons, are disabled, when there is no files selected - [#1586](https://github.com/qarmin/czkawka/pull/1586)\n- Added info about the number of items selected to delete - [#1589](https://github.com/qarmin/czkawka/pull/1589)\n- Limit image preview to max 1024 width/height, to speedup preview loading and fixing crash in software renderer - [#1590](https://github.com/qarmin/czkawka/pull/1590) \n\n### External\n- There is a new unofficial Tauri-based frontend for Czkawka - [Czkawka Tauri](https://github.com/shixinhuang99/czkawka-tauri)\n- Czkawka 8.0.0 is now available in Debian Sid - [Cli](https://packages.debian.org/sid/czkawka-cli)/[Gui Gtk](https://packages.debian.org/sid/czkawka-gui) \n\n### CI\n- Compilation for 32-bit targets is now checked in CI\n- Czkawka binaries are now checked for reproducibility in CI\n\n### Prebuilt binaries\n- AppImage binaries are no longer provided due to random bugs (not present in other packaging formats) and minimal added value compared to prebuilt Linux binaries or Flatpak\n- HEIF Mac binaries are now provided\n- CI now builds Linux binaries on Ubuntu 22.04 instead of 20.04(github removed 20.04 images)\n- `musl` builds of `czkawka_cli` are now provided instead of `eyra` builds (slightly easier to maintain). GUI builds are not included due to limitations of `musl` and `eyra` :(\n- Prebuilt Windows console binaries are no longer provided - logs are now saved to a file, which is easier to read than terminal output\n- Skia opengl and vulkan backends are provided for Krokiet on Linux(no binaries on Windows, because don't know how to replace `sed`)\n- Prebuilt binaries are now build with `lto fat` instead `lto thin` and `codegen-units=1` to greatly reduce binary size(~25% smaller binaries)\n\n## Version 9.0.0 - 16.03.2025r\n\n## Breaking changes\n\n- Video, Duplicate (smaller prehash size), and Image cache (EXIF orientation + faster resize implementation) are incompatible with previous versions and need to be regenerated.\n\n### Core\n- Automatically rotating all images based on their EXIF orientation - [#1368](https://github.com/qarmin/czkawka/pull/1368)\n- Fixed a crash caused by negative time values on some operating systems - [#1369](https://github.com/qarmin/czkawka/pull/1369)\n- Updated `vid_dup_finder`; it can now detect similar videos shorter than 30 seconds - [#1425](https://github.com/qarmin/czkawka/pull/1425)\n- Added support for more JXL image formats (using a built-in JXL → image-rs converter) - [#1425](https://github.com/qarmin/czkawka/pull/1425)\n- Improved duplicate file detection by using a larger, reusable buffer for file reading - [#1425](https://github.com/qarmin/czkawka/pull/1425)\n- Added an option for significantly faster image resizing to speed up image hashing - [#1458](https://github.com/qarmin/czkawka/pull/1458)\n- Logs now include information about the operating system and compiled app features(only x86_64 versions) - [#1458](https://github.com/qarmin/czkawka/pull/1458)\n- Added size progress tracking in certain modes - [#1458](https://github.com/qarmin/czkawka/pull/1458), [#1464](https://github.com/qarmin/czkawka/pull/1464)\n- Ability to stop hash calculations for large files mid-process - [#1458](https://github.com/qarmin/czkawka/pull/1458)\n- Implemented multithreading to speed up filtering of hard links - [#1458](https://github.com/qarmin/czkawka/pull/1458)\n- Reduced prehash read file size to a maximum of 4 KB - [#1458](https://github.com/qarmin/czkawka/pull/1458)\n- Fixed a slowdown at the end of scans when searching for duplicates on systems with a high number of CPU cores - [#1460](https://github.com/qarmin/czkawka/pull/1460)\n- Improved scan cancellation speed when collecting files to check - [#1460](https://github.com/qarmin/czkawka/pull/1460)\n- Added support for configuring config/cache paths using the `CZKAWKA_CONFIG_PATH` and `CZKAWKA_CACHE_PATH` environment variables - [#1464](https://github.com/qarmin/czkawka/pull/1464)\n- Fixed a crash in debug mode when checking broken files named `.mp3` - [#1464](https://github.com/qarmin/czkawka/pull/1464)\n- Catching panics from symphonia crashes in broken files mode - [#1466](https://github.com/qarmin/czkawka/pull/1466)\n- Printing a warning, when using `panic=abort`(that may speed up app and cause occasional crashes) - [#1466](https://github.com/qarmin/czkawka/pull/1466)\n\n### Krokiet\n- Changed the default tab to \"Duplicate Files\" - [#1368](https://github.com/qarmin/czkawka/pull/1368)\n\n### GTK GUI\n- Added a window icon in Wayland - [#1400](https://github.com/qarmin/czkawka/pull/1400)\n- Disabled the broken sort button - [#1400](https://github.com/qarmin/czkawka/pull/1400)\n\n### CLI\n- Added `-N` and `-M` flags to suppress printing results/warnings to the console - [#1464](https://github.com/qarmin/czkawka/pull/1464)\n- Fixed an issue where messages were not cleared at the end of a scan - [#1464](https://github.com/qarmin/czkawka/pull/1464)\n- Ability to disable cache via `-H` flag(useful for benchmarking) - [#1466](https://github.com/qarmin/czkawka/pull/1466)\n\n### Prebuild-binaries\n- This release is last version, that supports Ubuntu 20.04 - github actions drops this OS in its runners\n- Linux and Mac binaries now are provided with two options x86_64 and arm64\n- Arm linux builds needs at least Ubuntu 24.04\n- Gtk 4.12 is used to build windows gtk gui instead gtk 4.10\n- Dropping support for snap builds - too much time-consuming to maintain and testing(also it is broken currently)\n- Removed native windows build krokiet version - now it is available only cross-compiled version from linux(should not be any difference) \n\n## Version 8.0.0 - 11.10.2024r\n\n### Breaking changes\n\n- Due to the removal image_type from image struct, old cache files are incompatible with new version and should be regenerated from scratch(it uses new name)\n- Some CLI arguments could change short name, due fixing ambiguous names\n\n### Known regressions\n\n- Slint 1.8 which Krokiet uses requires femtovg 0.9.2 which broke font rendering - https://github.com/slint-ui/slint/issues/6298\n\n### CI\n\n- Providing nightly builds - [#1360](https://github.com/qarmin/czkawka/pull/1360) - https://github.com/qarmin/czkawka/releases/tag/Nightly\n- Added finding duplicated options in CLI - [#1364](https://github.com/qarmin/czkawka/pull/1364)\n\n### Core\n\n- Removed some unnecessary panics - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Simplified usage of structures when sending/receiving progress information - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Added Median hash algorithm - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Fixed compilation with Rust >=1.80 - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Extracted tool input parameters, that helped to find not used parameters - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Added new mod to find similar music only in groups with similar title tag - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Printing to file/console no longer uses two backslashes in windows paths - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Fixed panic when failed to decode raw picture - [#1355](https://github.com/qarmin/czkawka/pull/1355)\n- Remove useless saving/loading cache when there is no files to check - [#1358](https://github.com/qarmin/czkawka/pull/1358)\n- Filtering hard links on windows - [#1316](https://github.com/qarmin/czkawka/pull/1316)\n- Added jxl support - [#1358](https://github.com/qarmin/czkawka/pull/1358)\n- Added avif support(via external C library, not enabled by default) - [#1358](https://github.com/qarmin/czkawka/pull/1358)\n- Integer overflow are enabled by default(prepare for reporting bugs, slower performance and general unstability) - [#1358](https://github.com/qarmin/czkawka/pull/1358)\n- Fixed crash when loading invalid image cache - [#1230](https://github.com/qarmin/czkawka/pull/1230)\n\n### Krokiet\n\n- Fixed invalid default hash size in similar images - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Fixed and added more input parameters to the application - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Fixed problem with loading invalid preset - [#1226](https://github.com/qarmin/czkawka/pull/1226)\n- Fixed crash when using 8 hash size with small similarity - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Disabling buttons when no files were found - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Changed way to close/open panel at bottom - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Modify logo a little - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Avoid errors when trying to load preview of not supported file - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Added ability to show preview of referenced folders - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Enable selecting with space and jumping over entries with arrows and opening with enter - [#1359](https://github.com/qarmin/czkawka/pull/1359)\n- Added button to rename files with invalid extension - [#1364](https://github.com/qarmin/czkawka/pull/1364)\n\n### GTK GUI\n\n- Fixed and added more input parameters to the application - [#1355](https://github.com/qarmin/czkawka/pull/1355)\n- Added option to use external libraries instead gtk pixbuf loader for previews - [#1358](https://github.com/qarmin/czkawka/pull/1358)\n- Using static runtime with zstd compression in appimage - [#1350](https://github.com/qarmin/czkawka/pull/1355)\n- Restoring flatpak builds - [#1275](https://github.com/qarmin/czkawka/pull/1275)\n- [External] Mac homebrew version of app - https://formulae.brew.sh/formula/czkawka\n\n### CLI\n\n- Added options to find/remove images by size - [#1255](https://github.com/qarmin/czkawka/pull/1255)\n- Fixed and added more input parameters to the application - [#1354](https://github.com/qarmin/czkawka/pull/1354)\n- Fixed crash when stopping scan multiple times - [#1355](https://github.com/qarmin/czkawka/pull/1355)\n- Print results also in debug build - [#1355](https://github.com/qarmin/czkawka/pull/1355)\n- Added support for selecting reference directories - [#1364](https://github.com/qarmin/czkawka/pull/1364)\n\n## Version 7.0.0 - 19.02.2024r\n\n### BREAKING CHANGES\n\n- Reducing size of cache files, made old cache files incompatible with new version\n- `-C` in CLI now saves as compact json\n\n### GTK GUI\n\n- Added drag&drop support for included/excluded folders - [#1106](https://github.com/qarmin/czkawka/pull/1106)\n- Added information where are saved scan results - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n\n### CLI\n\n- Providing full static rust binary with [Eyra](https://github.com/sunfishcode/eyra) - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Fixed duplicated `-c` argument, now saving as compact json is handled via `-C` - [#1153](https://github.com/qarmin/czkawka/pull/1153)\n- Added scan progress bar - [#1183](https://github.com/qarmin/czkawka/pull/1183)\n- Clean and safe cancelling of scan - [#1183](https://github.com/qarmin/czkawka/pull/1183)\n- Unification of CLI arguments - [#1183](https://github.com/qarmin/czkawka/pull/1183)\n- Hardlink support for similar images/videos - [#1201](https://github.com/qarmin/czkawka/pull/1201)\n\n### Krokiet GUI\n\n- Initial release of new gui - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n\n### Core\n\n- Using normal crossbeam channels instead of asyncio tokio channel - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Fixed tool type when using progress of empty directories - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Fixed missing json support when saving size and name duplicate results - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Fix cross-compiled debug windows build - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Added bigger stack size by default(fixes stack overflow in some musl apps) - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Added optional libraw dependency(better single-core performance and support more raw files) - [#1102](https://github.com/qarmin/czkawka/pull/1102)\n- Speedup checking for wildcards and fix invalid recognizing long excluded items - [#1152](https://github.com/qarmin/czkawka/pull/1152)\n- Big speedup when searching for empty folders(especially with multithreading + cached FS schema) - [#1152](https://github.com/qarmin/czkawka/pull/1152)\n- Collecting files for scan can be a lot of faster due lazy file metadata gathering - [#1152](https://github.com/qarmin/czkawka/pull/1152)\n- Fixed recognizing not accessible folders as non-empty - [#1152](https://github.com/qarmin/czkawka/pull/1152)\n- Unifying code for collecting files to scan - [#1159](https://github.com/qarmin/czkawka/pull/1159)\n- Decrease memory usage when collecting files by removing unused fields in custom file entries structs - [#1159](https://github.com/qarmin/czkawka/pull/1159)\n- Decrease a little size of cache by few percents and improve loading/saving speed - [#1159](https://github.com/qarmin/czkawka/pull/1159)\n- Added ability to remove from scan files with excluded extensions - [#1184](https://github.com/qarmin/czkawka/pull/1102)\n- Fixed not showing in similar images results, files with same hashes when using reference folders - [#1184](https://github.com/qarmin/czkawka/pull/1102)\n- Optimize release binaries with LTO(~25/50% smaller, ~5/10% faster) - [#1184](https://github.com/qarmin/czkawka/pull/1102)\n\n## Version 6.1.0 - 15.10.2023r\n\n- BREAKING CHANGE - Changed cache saving method, deduplicated, optimized and simplified procedure(all files needs to be hashed again) - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1086](https://github.com/qarmin/czkawka/pull/1086)\n- Remove up to 340ms of delay when waiting for results - [#1070](https://github.com/qarmin/czkawka/pull/1070)\n- Added logger with useful info when debugging app (level can be adjusted via e.g. `RUST_LOG=debug` env) - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1070](https://github.com/qarmin/czkawka/pull/1070)\n- Core code cleanup - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1070](https://github.com/qarmin/czkawka/pull/1070), [#1082](https://github.com/qarmin/czkawka/pull/1082)\n- Updated list of bad extensions and support for finding invalid jar files - [#1070](https://github.com/qarmin/czkawka/pull/1070)\n- More default excluded items on Windows(like pagefile) - [#1074](https://github.com/qarmin/czkawka/pull/1074)\n- Unified printing/saving method to files/terminal and fixed some differences/bugs - [#1082](https://github.com/qarmin/czkawka/pull/1082)\n- Uses fun_time library to print how much functions take time - [#1082](https://github.com/qarmin/czkawka/pull/1082)\n- Added exporting results into json file format - [#1083](https://github.com/qarmin/czkawka/pull/1083)\n- Added new test/regression suite for CI - [#1083](https://github.com/qarmin/czkawka/pull/1083)\n- Added ability to use relative paths - [#1083](https://github.com/qarmin/czkawka/pull/1083)\n- Allowed removing similar images/videos/music from cli - [#1087](https://github.com/qarmin/czkawka/pull/1087)\n- Added info about saving/loading items to cache in duplicate and music mode - [#1091](https://github.com/qarmin/czkawka/pull/1091)\n- Fixed number of files to check in duplicate mode - [#1091](https://github.com/qarmin/czkawka/pull/1091)\n- Added support for qoi image format(without preview yet) - [e92a](https://github.com/qarmin/czkawka/commit/e92a8a65de9bd1250be482dbce06959125554849)\n- Fixed stability problem, that could remove invalid file in CLI - [#1083](https://github.com/qarmin/czkawka/pull/1083)\n- Fix Windows gui crashes by using gtk 4.6 instead 4.8 or 4.10 - [#992](https://github.com/qarmin/czkawka/pull/992)\n- Fixed printing info about duplicated music files - [#1016](https://github.com/qarmin/czkawka/pull/1016)\n- Fixed printing info about duplicated video files - [#1017](https://github.com/qarmin/czkawka/pull/1017)\n\n## Version 6.0.0 - 11.06.2023r\n\n- Add finding similar audio files by content - [#970](https://github.com/qarmin/czkawka/pull/970)\n- Allow to find duplicates by name/size at once - [#956](https://github.com/qarmin/czkawka/pull/956)\n- Fix, simplify and speed up finding similar images - [#983](https://github.com/qarmin/czkawka/pull/956)\n- Fixed bug when cache for music tags not worked - [#970](https://github.com/qarmin/czkawka/pull/970)\n- Allow to set number of threads from CLI - [#972](https://github.com/qarmin/czkawka/pull/972)\n- Fix problem with invalid item sorting in bad extensions mode - [#972](https://github.com/qarmin/czkawka/pull/972)\n- Big refactor/cleaning of code - [#956](https://github.com/qarmin/czkawka/pull/956)/[#970](https://github.com/qarmin/czkawka/pull/970)/[#972](https://github.com/qarmin/czkawka/pull/972)\n- Use builtin gtk webp loader for previews - [#923](https://github.com/qarmin/czkawka/pull/923)\n- Fixed docker build - [#947](https://github.com/qarmin/czkawka/pull/947)\n- Restore snap builds broken since GTk 4 port - [#965](https://github.com/qarmin/czkawka/pull/947)\n- Instruction how to build native ARM64 binaries on Mac - [#945](https://github.com/qarmin/czkawka/pull/945)/[#971](https://github.com/qarmin/czkawka/pull/971)\n\n## Version 5.1.0 - 19.02.2023r\n\n- Added sort button - [#894](https://github.com/qarmin/czkawka/pull/894)\n- Allow to set number of thread used to scan - [#839](https://github.com/qarmin/czkawka/pull/839)\n- Faster similar images comparing with reference folders - [#826](https://github.com/qarmin/czkawka/pull/826)\n- Update to clap 4 - [#878](https://github.com/qarmin/czkawka/pull/878)\n- Use FileChooserNative instead FileChooserDialog - [#894](https://github.com/qarmin/czkawka/pull/894)\n- Fix invalid music tags in music files when using reference folders - [#894](https://github.com/qarmin/czkawka/pull/894)\n- Updated pdf dependency(a lot of less amount of broken pdf false positives) - [#894](https://github.com/qarmin/czkawka/pull/894)\n- Changed strange PDF error message - \"Try at\" - [#894](https://github.com/qarmin/czkawka/pull/894)\n- Treat extensions Mp4 and m4v as identical - [#834](https://github.com/qarmin/czkawka/pull/834)\n- Improve thumbnail quality - [#895](https://github.com/qarmin/czkawka/pull/895)\n- Verify if hardlinking works, and if not, disable button with proper message - [#881](https://github.com/qarmin/czkawka/pull/881)\n- Apply some pydantic clippy lints on project - [#901](https://github.com/qarmin/czkawka/pull/901)\n\n## Version 5.0.2 - 30.08.2022r\n\n- Fixed problem with missing some similar images when using similarity > 0 - [#799](https://github.com/qarmin/czkawka/pull/799)\n- Prebuilt Linux binaries are compiled without heif support - [24b](https://github.com/qarmin/czkawka/commit/24b64a32c65904c506b54270f0977ccbe5098cc8)\n- Similar videos stops to proceed video after certain amount of time(fixes freezes) - [#815](https://github.com/qarmin/czkawka/pull/815)\n- Add --version argument for czkawka_cli - [#806](https://github.com/qarmin/czkawka/pull/806)\n- Rewrite a little nonsense message about minimal file size - [#807](https://github.com/qarmin/czkawka/pull/807)\n\n## Version 5.0.1 - 03.08.2022r\n\n- Fixed problem with removing ending slash with empty disk window path - [975](https://github.com/qarmin/czkawka/commit/97563a7b2a70fb5fcf6463f28069e6ea3b0ff5c2)\n- Added to CLI bad extensions mode - [#795](https://github.com/qarmin/czkawka/pull/795)\n- Restore default sorting method in CLI where finding biggest files - [5d7](https://github.com/qarmin/czkawka/commit/5d79dc7ccfee6d5426e37c4e6a860fa555c5927a)\n- Added tests to CI - [#791](https://github.com/qarmin/czkawka/pull/791)\n- Show error message when all directories are set as reference folders - [#795](https://github.com/qarmin/czkawka/pull/795)\n- Added more info about new requirements on Linux - [#795](https://github.com/qarmin/czkawka/pull/795)\n\n## Version 5.0.0 - 28.07.2022r\n\n- GUI ported to use GTK 4 - [#466](https://github.com/qarmin/czkawka/pull/466)\n- Use multithreading and improved algorithm to compare image hashes - [#762](https://github.com/qarmin/czkawka/pull/762)\n- Resize preview with window - [#466](https://github.com/qarmin/czkawka/pull/466)\n- Fix removing only one item from list view - [#466](https://github.com/qarmin/czkawka/pull/466)\n- Fix showing help command in duplicate CLI mode - [#720](https://github.com/qarmin/czkawka/pull/720)\n- Fix freeze when not choosing any tag in similar music mode - [#732](https://github.com/qarmin/czkawka/pull/732)\n- Fix preview of files with non-lowercase extensions - [#694](https://github.com/qarmin/czkawka/pull/694)\n- Read more tags from music files - [#705](https://github.com/qarmin/czkawka/pull/705)\n- Improve checking for invalid extensions - [#705](https://github.com/qarmin/czkawka/pull/705), [#747](https://github.com/qarmin/czkawka/pull/747), [#749](https://github.com/qarmin/czkawka/pull/749)\n- Support for finding invalid PDF files - [#705](https://github.com/qarmin/czkawka/pull/705)\n- Re-enable checking for broken music files(`libasound.so.2` no longer needed) - [#705](https://github.com/qarmin/czkawka/pull/705)\n- Fix disabled ui when using invalid settings in similar music - [#740](https://github.com/qarmin/czkawka/pull/740)\n- Speedup searching for invalid extensions - [#740](https://github.com/qarmin/czkawka/pull/740)\n- Support for finding the smallest files - [#741](https://github.com/qarmin/czkawka/pull/741)\n- Improved Windows CI - [#749](https://github.com/qarmin/czkawka/pull/749)\n- Ability to check for broken files by types - [#749](https://github.com/qarmin/czkawka/pull/749)\n- Add heif and Webp files support - [#750](https://github.com/qarmin/czkawka/pull/750)\n- Use in CLI Clap library instead StructOpt - [#759](https://github.com/qarmin/czkawka/pull/759)\n- Multiple directories can be added via Manual Add button - [#782](https://github.com/qarmin/czkawka/pull/782)\n- Option to exclude files from other filesystems in GUI(Linux) - [#776](https://github.com/qarmin/czkawka/pull/776)\n\n## Version 4.1.0 - 24.04.2022r\n\n- New mode - finding files whose content not match with their extension - [#678](https://github.com/qarmin/czkawka/pull/678)\n- Builtin icons - no more invalid, theme/OS dependent icons - [#659](https://github.com/qarmin/czkawka/pull/659)\n- Big(usually 2x) speedup of showing previews of images(both previews in scan and compare window) - [#660](https://github.com/qarmin/czkawka/pull/660)\n- Fix selecting records by custom selection popup - [#632](https://github.com/qarmin/czkawka/pull/632)\n- Support more tags when comparing music files - [#590](https://github.com/qarmin/czkawka/pull/590)\n- Fix not proper selecting path - [#656](https://github.com/qarmin/czkawka/pull/656)\n- No more popups during scan for similar videos on Windows - [#656](https://github.com/qarmin/czkawka/pull/656) - external change [4056](https://github.com/Farmadupe/ffmpeg_cmdline_utils/commit/405687514f9d9e8984cbe2547c53e85b71e08b27)\n- Custom selecting is now case-insensitive by default - [#657](https://github.com/qarmin/czkawka/pull/657)\n- Better approximate comparison of tags - [#641](https://github.com/qarmin/czkawka/pull/641)\n- Fix search problem due accumulated stop events - [#623](https://github.com/qarmin/czkawka/pull/623)\n- Option to ignore other filesystems in Unix OS(for now only in CLI) - [#673](https://github.com/qarmin/czkawka/pull/673)\n- Fix file hardlinking on Windows - [#668](https://github.com/qarmin/czkawka/pull/668)\n- Support for case-insensitive name grouping of files - [#669](https://github.com/qarmin/czkawka/pull/669)\n- Directories for search GUI can be passed by CLI - [#677](https://github.com/qarmin/czkawka/pull/677)\n- Prevent from getting non respond app notification from display servers - [#625](https://github.com/qarmin/czkawka/pull/625)\n\n## Version 4.0.0 - 20.01.2022r\n\n- Multithreading support for collecting files to check(2/3x speedup on 4 thread processor and SSD) - [#502](https://github.com/qarmin/czkawka/pull/502), [#504](https://github.com/qarmin/czkawka/pull/504)\n- Add multiple translations - Polish, Italian, French, German, Russian ... - [#469](https://github.com/qarmin/czkawka/pull/469), [#508](https://github.com/qarmin/czkawka/pull/508), [5be](https://github.com/qarmin/czkawka/commit/5be801e76395855f07ab1da43cdbb8bd0b843834)\n- Add support for finding similar videos - [#460](https://github.com/qarmin/czkawka/pull/460)\n- GUI code refactoring and search code unification - [#462](https://github.com/qarmin/czkawka/pull/462), [#531](https://github.com/qarmin/czkawka/pull/531)\n- Fixed crash when trying to hard/symlink 0 files - [#462](https://github.com/qarmin/czkawka/pull/462)\n- GTK 4 compatibility improvements for future change of toolkit - [#467](https://github.com/qarmin/czkawka/pull/467), [#468](https://github.com/qarmin/czkawka/pull/468), [#473](https://github.com/qarmin/czkawka/pull/473), [#474](https://github.com/qarmin/czkawka/pull/474), [#503](https://github.com/qarmin/czkawka/pull/503), [#505](https://github.com/qarmin/czkawka/pull/505)\n- Change minimal supported OS to Ubuntu 20.04(needed by GTK) - [#468](https://github.com/qarmin/czkawka/pull/468)\n- Increased performance by avoiding creating unnecessary image previews - [#468](https://github.com/qarmin/czkawka/pull/468)\n- Improved performance due caching hash of broken/not supported images/videos = [#471](https://github.com/qarmin/czkawka/pull/471)\n- Option to not remove cache from non-existent files(e.g. from unplugged pendrive) - [#472](https://github.com/qarmin/czkawka/pull/472)\n- Add multiple tooltips with helpful messages - [#472](https://github.com/qarmin/czkawka/pull/472)\n- Allow caching prehash - [#477](https://github.com/qarmin/czkawka/pull/477)\n- Improve custom selecting of records(allows to use Rust regex) - [#489](https://github.com/qarmin/czkawka/pull/478)\n- Remove support for finding zeroed files - [#461](https://github.com/qarmin/czkawka/pull/461)\n- Remove HashMB mode - [#476](https://github.com/qarmin/czkawka/pull/476)\n- Approximate comparison of music - [#483](https://github.com/qarmin/czkawka/pull/483)\n- Enable column sorting for simple treeview - [#487](https://github.com/qarmin/czkawka/pull/487)\n- Allow hiding upper panel - [#491](https://github.com/qarmin/czkawka/pull/491)\n- Make UI take less space - [#500](https://github.com/qarmin/czkawka/pull/500)\n- Add support for raw images(NEF, CR2, KDC...) - [#532](https://github.com/qarmin/czkawka/pull/532)\n- Image compare performance and usability improvements - [#529](https://github.com/qarmin/czkawka/pull/529), [#528](https://github.com/qarmin/czkawka/pull/528), [#530](https://github.com/qarmin/czkawka/pull/530), [#525](https://github.com/qarmin/czkawka/pull/525)\n- Reorganize(unify) saving/loading data from file - [#524](https://github.com/qarmin/czkawka/pull/524)\n- Add \"reference folders\" - [#516](https://github.com/qarmin/czkawka/pull/516)\n- Add cache for similar music files - [#558](https://github.com/qarmin/czkawka/pull/558)\n\n## Version 3.3.1 - 22.11.2021r\n\n- Fix crash when moving buttons [#457](https://github.com/qarmin/czkawka/pull/457)\n- Hide move button at start [c9ca230](https://github.com/qarmin/czkawka/commit/c9ca230dfd05e2166b2d68683b091cfd45037edd)\n\n## Version 3.3.0 - 20.11.2021r\n\n- Select files by pressing space key [#415](https://github.com/qarmin/czkawka/pull/415)\n- Add additional info to printed errors [#446](https://github.com/qarmin/czkawka/pull/446)\n- Add support for multiple image filters, hashes and sizes in similar images tool [#447](https://github.com/qarmin/czkawka/pull/447), [#448](https://github.com/qarmin/czkawka/pull/448)\n- Button to move files/folders to provided location [#449](https://github.com/qarmin/czkawka/pull/449)\n- Add non-clickable button to fix white theme [#450](https://github.com/qarmin/czkawka/pull/450)\n- Fixed freeze when opening in same thread file/folder [#448](https://github.com/qarmin/czkawka/pull/448)\n- Tool to check performance of different image filters and hash types and sizes [#447](https://github.com/qarmin/czkawka/pull/447)\n- Add scheduled CI and pin it to support Rust 1.53.0 [7bb](https://github.com/qarmin/czkawka/commit/7bbdf742739a513b80d0cc06ba61dfafec976b23), [#431](https://github.com/qarmin/czkawka/pull/431)\n- Update snap file to use builtin rust plugin and update gnome extension [8f2](https://github.com/qarmin/czkawka/commit/8f232285e5c34bee6d5da8e1453d7f40a0ffd08d)\n- Disable from checking in similar images `webp`, `gif`, `bmp`, `ico` extension which caused crashes [#445](https://github.com/qarmin/czkawka/pull/446), [49e](https://github.com/qarmin/czkawka/commit/49effca169adb57b33f666757966d43b244319cc)\n\n## Version 3.2.0 - 07.08.2021r\n\n- Use checkbox instead selection to select files [#392](https://github.com/qarmin/czkawka/pull/392)\n- Re-enable hardlink on windows - [#410](https://github.com/qarmin/czkawka/pull/410)\n- Fix symlink and hardlink creating - [#409](https://github.com/qarmin/czkawka/pull/409)\n- Add image preview to duplicate finder [#408](https://github.com/qarmin/czkawka/pull/408)\n- Add setting maximum file size [#407](https://github.com/qarmin/czkawka/pull/407)\n- Add new grouping algorithm to similar images [#405](https://github.com/qarmin/czkawka/pull/405)\n- Update to Rust 1.54 [#400](https://github.com/qarmin/czkawka/pull/400)\n- Add webp support to similar images [#396](https://github.com/qarmin/czkawka/pull/396)\n- Use GtkScale instead radio buttons for similarity [#397](https://github.com/qarmin/czkawka/pull/397)\n- Update all dependencies [#405](https://github.com/qarmin/czkawka/pull/405), [#395](https://github.com/qarmin/czkawka/pull/395)\n- Split UI into multiple files [#391](https://github.com/qarmin/czkawka/pull/391)\n- Update to gtk-rs 0.14 [#383](https://github.com/qarmin/czkawka/pull/383)\n- Fix bug with moving windows [#361](https://github.com/qarmin/czkawka/pull/361)\n- Generate Minimal Appimage [#339](https://github.com/qarmin/czkawka/pull/339)\n\n## Version 3.1.0 - 09.05.2021r\n\n- Clean README, by moving instructions to different files - [9aea6e9b](https://github.com/qarmin/czkawka/commit/9aea6e9b1ef5ac1e56ccd008e7456b80401179d0)\n- Fix excluded items on Windows - [#324](https://github.com/qarmin/czkawka/pull/324)\n- Center windows and add missing settings icon - [#323](https://github.com/qarmin/czkawka/pull/323)\n- Sort cache - [#322](https://github.com/qarmin/czkawka/pull/322)\n- Add desktop file to Snap - [018d5bebb](https://github.com/qarmin/czkawka/commit/018d5bebb0b297ba35529b03b8e2e68eb0a9b474), [ade2a756e2](https://github.com/qarmin/czkawka/commit/ade2a756e29c5ce5739d6268fcab7e76f59ed5f6)\n- Customize minimum file size of cached records - [#321](https://github.com/qarmin/czkawka/pull/321)\n- Update benchmarks - [2044b9185](https://github.com/qarmin/czkawka/commit/2044b91852fea89dfaf10dc1ab79c1d00e9e0c12)\n- Rearrange Instruction - [8e7ac4a2d7f5b0](https://github.com/qarmin/czkawka/commit/8e7ac4a2d7f5b0beba2552581fb3a0d19c2efeb5)\n- Add info that Czkawka and Bleachbit are not alternatives to each other - [30602a486](https://github.com/qarmin/czkawka/commit/30602a486f6ade6f9b7b91a73708225b4f4c2a7d)\n- Fix crashes with too small message queue - [#316](https://github.com/qarmin/czkawka/pull/316)\n- Fix a little unsorted results - [#304](https://github.com/qarmin/czkawka/pull/304)\n- Fix Appimage(external bug) - [#299](https://github.com/qarmin/czkawka/issues/299)\n- Fix error with saving results of name duplicates - [#307](https://github.com/qarmin/czkawka/pull/307)\n- Update to Rust 1.5.1 - [#302](https://github.com/qarmin/czkawka/pull/302)\n\n## Version 3.0.0 - 11.03.2021r\n\n- Option to not ignore hardlinks - [#273](https://github.com/qarmin/czkawka/pull/273)\n- Hardlink support for GUI - [#276](https://github.com/qarmin/czkawka/pull/276)\n- New settings window - [#262](https://github.com/qarmin/czkawka/pull/262)\n- Unify file removing - [#278](https://github.com/qarmin/czkawka/pull/278)\n- Dryrun in duplicates CLI - [#277](https://github.com/qarmin/czkawka/pull/277)\n- Option to turn off cache - [#263](https://github.com/qarmin/czkawka/pull/263)\n- Update Image dependency and fix crashes - [#270](https://github.com/qarmin/czkawka/pull/270), [e3aca69](https://github.com/qarmin/czkawka/commit/e3aca69499966499413e4b7cd4d1037bec6a5d68)\n- Add confirmation dialog when trying to remove all files in group - [#281](https://github.com/qarmin/czkawka/pull/281)\n- Add confirmation dialog when removing files with delete key - [#282](https://github.com/qarmin/czkawka/pull/282)\n- Open file when clicking at the Enter button - [#285](https://github.com/qarmin/czkawka/pull/285)\n- Allow to put files to trash instead fully remove them - [#284](https://github.com/qarmin/czkawka/pull/284)\n\n## Version 2.4.0 - 22.02.2021r\n\n- Add about dialog - [#226](https://github.com/qarmin/czkawka/pull/226)\n- Remove checking for ico in similar images - [#227](https://github.com/qarmin/czkawka/pull/227)\n- Change progress dialog to progress window - [#229](https://github.com/qarmin/czkawka/pull/229)\n- Restore snap confinement - [#218](https://github.com/qarmin/czkawka/pull/218), [8dcb718](https://github.com/qarmin/czkawka/commit/8dcb7188434e1c1728368642e17ccec29a4b372d)\n- Add support for CRC32 and XXH3 hash - [#243](https://github.com/qarmin/czkawka/pull/243)\n- Add delete method to replace duplicate files with hard links - [#236](https://github.com/qarmin/czkawka/pull/236)\n- Add checking for broken music opt-in - [#249](https://github.com/qarmin/czkawka/pull/249)\n- Allow to save to file similar images results - [10156ccfd3](https://github.com/qarmin/czkawka/commit/10156ccfd3ba880d26d4bbad1e025b0050d7753b)\n- Keep original file if replacing duplicate with hardlink fails - [#256](https://github.com/qarmin/czkawka/pull/256)\n- Fix Windows theme - [#265](https://github.com/qarmin/czkawka/pull/265)\n- Windows taskbar progress support - [#264](https://github.com/qarmin/czkawka/pull/264)\n- Ignore duplicates if those are hard links - [#234](https://github.com/qarmin/czkawka/pull/234)\n- Support the hash type parameter in the CLI - [#267](https://github.com/qarmin/czkawka/pull/267)\n- Use one implementation for all hash calculations - [#268](https://github.com/qarmin/czkawka/pull/268)\n- Disable for now broken tga and gif files - [#270](https://github.com/qarmin/czkawka/pull/270)\n\n## Version 2.3.2 - 21.01.2021r\n\n- Add support for moving selection by keyboard to update similar image preview [#223](https://github.com/qarmin/czkawka/pull/223)\n\nThis version is only needed to test flatpak build\n\n## Version 2.3.1 - 20.01.2021r\n\n- Added flatpak support - [#203](https://github.com/qarmin/czkawka/pull/203)\n- Spell fixes - [#222](https://github.com/qarmin/czkawka/pull/222), [#219](https://github.com/qarmin/czkawka/pull/219)\n\n## Version 2.3.0 - 15.01.2021r\n\n- Add cache for duplicate finder - [#205](https://github.com/qarmin/czkawka/pull/205)\n- Add cache for broken files - [#204](https://github.com/qarmin/czkawka/pull/204)\n- Decrease ram usage - [#212](https://github.com/qarmin/czkawka/pull/212)\n- Add support for finding broken zip and audio files - [#210](https://github.com/qarmin/czkawka/pull/210)\n- Sort Results by path where it is possible - [#211](https://github.com/qarmin/czkawka/pull/211)\n- Add missing popover info for invalid symlinks - [#209](https://github.com/qarmin/czkawka/pull/209)\n- Use the oldest available OS in Linux and Mac CI and the newest on Windows - [#206](https://github.com/qarmin/czkawka/pull/206)\n- Add broken files support - [#202](https://github.com/qarmin/czkawka/pull/202)\n- Remove save workaround and fix crashes when loading/saving cache - [#200](https://github.com/qarmin/czkawka/pull/200)\n- Fix error when closing dialog progress by X - [#199](https://github.com/qarmin/czkawka/pull/199)\n\n## Version 2.2.0 - 11.01.2021r\n\n- Adds Mac GUI - [#160](https://github.com/qarmin/czkawka/pull/160)\n- Use master gtk plugin again - [#179](https://github.com/qarmin/czkawka/pull/179)\n- Only show preview when 1 image is selected - [#183](https://github.com/qarmin/czkawka/pull/183)\n- Add buffered write/read - [#186](https://github.com/qarmin/czkawka/pull/186)\n- Fix included/excluded files which contains commas - [#195](https://github.com/qarmin/czkawka/pull/195)\n- Move image cache to cache from config dir - [#197](https://github.com/qarmin/czkawka/pull/197)\n- Reorganize GUI Code(no visible changes) - [#184](https://github.com/qarmin/czkawka/pull/184), [#184](https://github.com/qarmin/czkawka/pull/184), [#189](https://github.com/qarmin/czkawka/pull/189), [#190](https://github.com/qarmin/czkawka/pull/190), [#194](https://github.com/qarmin/czkawka/pull/194)\n\n## Version 2.1.0 - 31.12.2020r\n\n- Hide preview when deleting images or symlinking it - [#167](https://github.com/qarmin/czkawka/pull/167)\n- Add manual adding of directories - [#165](https://github.com/qarmin/czkawka/pull/165), [#168](https://github.com/qarmin/czkawka/pull/168)\n- Add resizable top panel - [#164](https://github.com/qarmin/czkawka/pull/164)\n- Add support for delete button - [#159](https://github.com/qarmin/czkawka/pull/159)\n- Allow to select multiple entries in File Chooser - [#154](https://github.com/qarmin/czkawka/pull/154)\n- Add cache support for similar images - [#139](https://github.com/qarmin/czkawka/pull/139)\n- Add selecting images with its size - [#138](https://github.com/qarmin/czkawka/pull/138)\n- Modernize popovers code and simplify later changes - [#137](https://github.com/qarmin/czkawka/pull/137)\n\n## Version 2.0.0 - 23.12.2020r\n\n- Add Snap support - [ee3d4](https://github.com/qarmin/czkawka/commit/ee3d450552cd0c37a114b05c557ff9381ef92466)\n- Select longer names by default - [#113](https://github.com/qarmin/czkawka/pull/113)\n- Add setting for deletion confirmation dialog - [#114](https://github.com/qarmin/czkawka/pull/114)\n- Add button to hide/show text view errors - [#115](https://github.com/qarmin/czkawka/pull/115)\n- Remove console window in Windows - [#116](https://github.com/qarmin/czkawka/pull/116)\n- Add custom selection/unselection - [#117](https://github.com/qarmin/czkawka/pull/117)\n- Add Image preview to similar images - [#118](https://github.com/qarmin/czkawka/pull/118)\n- Remove orbtk frontend - [#119](https://github.com/qarmin/czkawka/pull/119)\n- Update Icon - [#120](https://github.com/qarmin/czkawka/pull/120)\n- Add setting button to disable/enable previews(enabled by default) - [#121](https://github.com/qarmin/czkawka/pull/121)\n- Add button to enable/disable in settings text view errors - [#122](https://github.com/qarmin/czkawka/pull/122)\n- Add support for symbolic links - [#123](https://github.com/qarmin/czkawka/pull/123)\n- Add support for checking for invalid symlinks - [#124](https://github.com/qarmin/czkawka/pull/124)\n- Add new windows dark theme - [#125](https://github.com/qarmin/czkawka/pull/125)\n- Fix appimage crash by adding PNG version of icon - [#126](https://github.com/qarmin/czkawka/pull/126)\n- Split symlink path to two path and file name - [#127](https://github.com/qarmin/czkawka/pull/127)\n- Add option to open folders by double right click - [#128](https://github.com/qarmin/czkawka/pull/128)\n- Add minimal similarity level - [#129](https://github.com/qarmin/czkawka/pull/129)\n- Show errors in image previewer when failed to generate it - [#130](https://github.com/qarmin/czkawka/pull/130)\n- Added instruction - [58e6221a](https://github.com/qarmin/czkawka/commit/58e6221a0e02d17d07c71152f56b948f616751a8), [598aec345e](https://github.com/qarmin/czkawka/commit/598aec345e9f5ac199fc3d642c0699d5228100a6), [afaa402b](https://github.com/qarmin/czkawka/commit/afaa402b31526aa8e6b47f3670bc62b26ad9f60f)\n\n## Version 1.5.1 - 08.12.2020r\n\n- Fix errors in progress bar caused by dividing by 0 - [#109](https://github.com/qarmin/czkawka/pull/109)\n- Add option to save file, store settings and load them - [#108](https://github.com/qarmin/czkawka/pull/108)\n- Center dialog to current window - [a04](https://github.com/qarmin/czkawka/commit/a047380dbe8aa4d04f9c482364469e21d231fab2)\n\n## Version 1.5.0 - 02.12.2020r\n\n- Added progress bar - [#106](https://github.com/qarmin/czkawka/pull/106)\n- Removed unused buttons - [#107](https://github.com/qarmin/czkawka/pull/107)\n\n## Version 1.4.0 - 09.11.2020r\n\n- Multithreading Support to most modules - [#98](https://github.com/qarmin/czkawka/pull/98) [#99](https://github.com/qarmin/czkawka/pull/99) [#100](https://github.com/qarmin/czkawka/pull/100) [#101](https://github.com/qarmin/czkawka/pull/101)\n- Simplify GUI code [#96](https://github.com/qarmin/czkawka/pull/96)\n- Group similar images - [#97](https://github.com/qarmin/czkawka/pull/97)\n- Add select buttons to each type of mode - [#102](https://github.com/qarmin/czkawka/pull/102)\n- Fix GUI behavior in GUI when deleting similar image - [#103](https://github.com/qarmin/czkawka/pull/103)\n- Add new similarity level - [#104](https://github.com/qarmin/czkawka/pull/104)\n\n## Version 1.3.0 - 02.11.2020r\n\n- Appimage support - [#77](https://github.com/qarmin/czkawka/pull/77)\n- Removed warnings about non-existed excluded directories - [#79](https://github.com/qarmin/czkawka/pull/79)\n- Updated README - [8ec](https://github.com/qarmin/czkawka/commit/8ecde0fc9adb3e6cedf432c4ba749e698b645a7a)\n- Added pre hash support(speedup for searching big duplicates) - [#83](https://github.com/qarmin/czkawka/pull/83)\n- Support for searching duplicates by file name - [#84](https://github.com/qarmin/czkawka/pull/84)\n- Added support for checking for zeroed file - [#88](https://github.com/qarmin/czkawka/pull/88)\n- Refactored GUI code to faster and safer changing/adding code - [#89](https://github.com/qarmin/czkawka/pull/89)\n- Added some missing options to CLI in some modes - [#90](https://github.com/qarmin/czkawka/pull/90)\n- Implemented finding duplicates by music tags - [#95](https://github.com/qarmin/czkawka/pull/95)\n\n## Version 1.2.1 - 17.10.2020r\n\n- Make image similarity search significantly faster. [#72](https://github.com/qarmin/czkawka/pull/72)\n- Improve similar images GUI a little and add sorting to Similarity Enum [#73](https://github.com/qarmin/czkawka/pull/73)\n- Improve deleting files in Similar files in GUI [#75](https://github.com/qarmin/czkawka/pull/75)\n\n## Version 1.2.0 - 15.10.2020r\n\n- Replace String with PathBuf for paths [#59](https://github.com/qarmin/czkawka/pull/59)\n- Add test suite to PR [#65](https://github.com/qarmin/czkawka/pull/65)\n- Support for finding similar images to CLI [#66](https://github.com/qarmin/czkawka/pull/66)\n- Fix grammar-related errors and Ponglish expressions [#62](https://github.com/qarmin/czkawka/pull/62), [#63](https://github.com/qarmin/czkawka/pull/63)\n- Don't delete by default files in duplicate finder in CLI - [23f203](https://github.com/qarmin/czkawka/commit/23f203a061e254275c95ca23ca4f1a78bd941f02)\n- Support for finding similar images to GUI [#69](https://github.com/qarmin/czkawka/pull/69)\n- Add support for opening files/folders from GUI with double-click [#70](https://github.com/qarmin/czkawka/pull/70)\n\n## Version 1.1.0 - 10.10.2020r\n\n- Windows support [#58](https://github.com/qarmin/czkawka/pull/58)\n- Improve code quality/Simplify codebase [#52](https://github.com/qarmin/czkawka/pull/52)\n- Fixed skipping some correct results in specific situations [#52](https://github.com/qarmin/czkawka/pull/52#discussion_r502613895)\n- Added support for searching in other thread [#51](https://github.com/qarmin/czkawka/pull/51)\n- Divide CI across files [#48](https://github.com/qarmin/czkawka/pull/48)\n- Added ability to stop task from GUI [#55](https://github.com/qarmin/czkawka/pull/55)\n- Fixed removing directories which contains only empty directories from GUI [#57](https://github.com/qarmin/czkawka/pull/57)\n\n## Version 1.0.1 - 06.10.2020r\n\n- Replaced default argument parser with StructOpt [#37](https://github.com/qarmin/czkawka/pull/37)\n- Added all(except macOS GTK build) builds to CI where can be freely downloaded [#41](https://github.com/qarmin/czkawka/pull/41) [#39](https://github.com/qarmin/czkawka/pull/39)\n- App can be downloaded also from Arch AUR and Cargo [#36](https://github.com/qarmin/czkawka/pull/36)\n- Fixed crash with invalid file modification date [#33](https://github.com/qarmin/czkawka/issues/33)\n- Upper tabs can hide and show when this is necessary [#38](https://github.com/qarmin/czkawka/pull/38)\n- Fixed crash when file/folder name have non Unicode character [#44](https://github.com/qarmin/czkawka/issues/44)\n- Added support for finding similar pictures in GUI [#69](https://github.com/qarmin/czkawka/issues/69)\n\n## Version 1.0.0 - 02.10.2020r\n\n- Added confirmation dialog to delete button\n- Updated Readme\n- Tested a lot app, so I think that it version 1.0.0 can be freely released\n\n## Version 0.1.4 - 01.10.2020r\n\n- Fixes -f default argument\n- Added save button to GUI\n- Cleaned a little code\n- Deleting files and folders i GUI\n- Support for all notebooks items in GUI\n- Support for deleting and adding directories to search and to exclude in GUI\n- Support for light themes in GUI\n- Changed SystemTime to u64 from EPOCH_TIME\n- Selective selecting of rows duplicate finder in GUI\n- Changed minimum version of GTK to 3.22\n- Added save system to GUI\n- Added Big, Temporary and Empty folders finder to GUI\n\n## Version 0.1.3 - 27.09.2020r\n\n- Big code refactoring - now is a lot of easier create new modules and maintain old ones\n- Added finding empty files\n- Added new option to find duplicates by checking hash max 1MB of file\n- Added support for finding temporary folder finder\n- Improved README\n- Simplify CLI help and improve it\n\n## Version 0.1.2 - 26.09.2020r\n\n- Add basic search empty folders in GTK GUI\n- Remember place where button are placed\n- Read and parse more values from GUI\n- Print errors/warnings/messages to text field in GUI\n- Add upper notebook with included, excluded directories, items and extensions\n- Improve a little GUI\n- Add version argument which print version e.g. `czkawka_gui --version`\n- Simple Empty folder support in GUI\n- The biggest files support in CLI\n\n## Version 0.1.1 - 20.09.2020r\n\n- Added images to readme\n- Better GTK buttons and glade file\n- Basic search in GTK\n- Cleaned core from println\n- Core functions doesn't use now process::exit(everything is done with help of messages/errors/warnings)\n- Added support for non-recursive search\n- Improved finding number and size of duplicated files\n- Saving results to file\n- Print how much data was read by duplicate finder(debug only)\n- Added GitHub CI\n- Only debug build prints debug information's\n- Clean code\n- Add basic idea config to misc folder\n\n## Version 0.1.0 - 07.09.2020r\n\n- Initial Version\n- Duplicate file finder\n- Empty folder finder\n- Very WIP Orbtk GUI frontend\n- Basic GTK Frontend(without any logic)\n- CLI\n\n## Initial commit - 26.08.2020r\n"
  },
  {
    "path": "LICENSE_CC_BY_4_ICONS",
    "content": "All icons, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).\n\nCopyright (c) 2020 [jannuary](https://github.com/jannuary)\n- data/icons/com.github.qarmin.czkawka.Devel.svg\n- data/icons/com.github.qarmin.czkawka.svg\n- data/icons/com.github.qarmin.czkawka-symbolic.svg\n\nCopyright (c) 2020-2026 Rafał Mikrut\n- data/icons/io.github.qarmin.krokiet.svg\n\nLicense: CC-BY-4.0\n Creative Commons Attribution 4.0 International Public License\n .\n By exercising the Licensed Rights (defined below), You accept and agree\n to be bound by the terms and conditions of this Creative Commons\n Attribution 4.0 International Public License (\"Public\n License\"). To the extent this Public License may be interpreted as a\n contract, You are granted the Licensed Rights in consideration of Your\n acceptance of these terms and conditions, and the Licensor grants You\n such rights in consideration of benefits the Licensor receives from\n making the Licensed Material available under these terms and\n conditions.\n .\n Section 1 -- Definitions.\n .\n a. Adapted Material means material subject to Copyright and Similar\n Rights that is derived from or based upon the Licensed Material\n and in which the Licensed Material is translated, altered,\n arranged, transformed, or otherwise modified in a manner requiring\n permission under the Copyright and Similar Rights held by the\n Licensor. For purposes of this Public License, where the Licensed\n Material is a musical work, performance, or sound recording,\n Adapted Material is always produced where the Licensed Material is\n synched in timed relation with a moving image.\n .\n b. Adapter's License means the license You apply to Your Copyright\n and Similar Rights in Your contributions to Adapted Material in\n accordance with the terms and conditions of this Public License.\n .\n c. Copyright and Similar Rights means copyright and/or similar rights\n closely related to copyright including, without limitation,\n performance, broadcast, sound recording, and Sui Generis Database\n Rights, without regard to how the rights are labeled or\n categorized. For purposes of this Public License, the rights\n specified in Section 2(b)(1)-(2) are not Copyright and Similar\n Rights.\n .\n d. Effective Technological Measures means those measures that, in the\n absence of proper authority, may not be circumvented under laws\n fulfilling obligations under Article 11 of the WIPO Copyright\n Treaty adopted on December 20, 1996, and/or similar international\n agreements.\n .\n e. Exceptions and Limitations means fair use, fair dealing, and/or\n any other exception or limitation to Copyright and Similar Rights\n that applies to Your use of the Licensed Material.\n .\n f. Licensed Material means the artistic or literary work, database,\n or other material to which the Licensor applied this Public\n License.\n .\n g. Licensed Rights means the rights granted to You subject to the\n terms and conditions of this Public License, which are limited to\n all Copyright and Similar Rights that apply to Your use of the\n Licensed Material and that the Licensor has authority to license.\n .\n h. Licensor means the individual(s) or entity(ies) granting rights\n under this Public License.\n .\n i. Share means to provide material to the public by any means or\n process that requires permission under the Licensed Rights, such\n as reproduction, public display, public performance, distribution,\n dissemination, communication, or importation, and to make material\n available to the public including in ways that members of the\n public may access the material from a place and at a time\n individually chosen by them.\n .\n j. Sui Generis Database Rights means rights other than copyright\n resulting from Directive 96/9/EC of the European Parliament and of\n the Council of 11 March 1996 on the legal protection of databases,\n as amended and/or succeeded, as well as other essentially\n equivalent rights anywhere in the world.\n .\n k. You means the individual or entity exercising the Licensed Rights\n under this Public License. Your has a corresponding meaning.\n .\n Section 2 -- Scope.\n .\n a. License grant.\n .\n 1. Subject to the terms and conditions of this Public License,\n the Licensor hereby grants You a worldwide, royalty-free,\n non-sublicensable, non-exclusive, irrevocable license to\n exercise the Licensed Rights in the Licensed Material to:\n .\n a. reproduce and Share the Licensed Material, in whole or\n in part; and\n .\n b. produce, reproduce, and Share Adapted Material.\n .\n 2. Exceptions and Limitations. For the avoidance of doubt, where\n Exceptions and Limitations apply to Your use, this Public\n License does not apply, and You do not need to comply with\n its terms and conditions.\n .\n 3. Term. The term of this Public License is specified in Section\n 6(a).\n .\n 4. Media and formats; technical modifications allowed. The\n Licensor authorizes You to exercise the Licensed Rights in\n all media and formats whether now known or hereafter created,\n and to make technical modifications necessary to do so. The\n Licensor waives and/or agrees not to assert any right or\n authority to forbid You from making technical modifications\n necessary to exercise the Licensed Rights, including\n technical modifications necessary to circumvent Effective\n Technological Measures. For purposes of this Public License,\n simply making modifications authorized by this Section 2(a)\n (4) never produces Adapted Material.\n .\n 5. Downstream recipients.\n .\n a. Offer from the Licensor -- Licensed Material. Every\n recipient of the Licensed Material automatically\n receives an offer from the Licensor to exercise the\n Licensed Rights under the terms and conditions of this\n Public License.\n .\n b. No downstream restrictions. You may not offer or impose\n any additional or different terms or conditions on, or\n apply any Effective Technological Measures to, the\n Licensed Material if doing so restricts exercise of the\n Licensed Rights by any recipient of the Licensed\n Material.\n .\n 6. No endorsement. Nothing in this Public License constitutes or\n may be construed as permission to assert or imply that You\n are, or that Your use of the Licensed Material is, connected\n with, or sponsored, endorsed, or granted official status by,\n the Licensor or others designated to receive attribution as\n provided in Section 3(a)(1)(A)(i).\n .\n b. Other rights.\n .\n 1. Moral rights, such as the right of integrity, are not\n licensed under this Public License, nor are publicity,\n privacy, and/or other similar personality rights; however, to\n the extent possible, the Licensor waives and/or agrees not to\n assert any such rights held by the Licensor to the limited\n extent necessary to allow You to exercise the Licensed\n Rights, but not otherwise.\n .\n 2. Patent and trademark rights are not licensed under this\n Public License.\n .\n 3. To the extent possible, the Licensor waives any right to\n collect royalties from You for the exercise of the Licensed\n Rights, whether directly or through a collecting society\n under any voluntary or waivable statutory or compulsory\n licensing scheme. In all other cases the Licensor expressly\n reserves any right to collect such royalties.\n .\n Section 3 -- License Conditions.\n .\n Your exercise of the Licensed Rights is expressly made subject to the\n following conditions.\n .\n a. Attribution.\n .\n 1. If You Share the Licensed Material (including in modified\n form), You must:\n .\n a. retain the following if it is supplied by the Licensor\n with the Licensed Material:\n .\n i. identification of the creator(s) of the Licensed\n Material and any others designated to receive\n attribution, in any reasonable manner requested by\n the Licensor (including by pseudonym if\n designated);\n .\n ii. a copyright notice;\n .\n iii. a notice that refers to this Public License;\n .\n iv. a notice that refers to the disclaimer of\n warranties;\n .\n v. a URI or hyperlink to the Licensed Material to the\n extent reasonably practicable;\n .\n b. indicate if You modified the Licensed Material and\n retain an indication of any previous modifications; and\n .\n c. indicate the Licensed Material is licensed under this\n Public License, and include the text of, or the URI or\n hyperlink to, this Public License.\n .\n 2. You may satisfy the conditions in Section 3(a)(1) in any\n reasonable manner based on the medium, means, and context in\n which You Share the Licensed Material. For example, it may be\n reasonable to satisfy the conditions by providing a URI or\n hyperlink to a resource that includes the required\n information.\n .\n 3. If requested by the Licensor, You must remove any of the\n information required by Section 3(a)(1)(A) to the extent\n reasonably practicable.\n .\n 4. If You Share Adapted Material You produce, the Adapter's\n License You apply must not prevent recipients of the Adapted\n Material from complying with this Public License.\n .\n Section 4 -- Sui Generis Database Rights.\n .\n Where the Licensed Rights include Sui Generis Database Rights that\n apply to Your use of the Licensed Material:\n .\n a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n to extract, reuse, reproduce, and Share all or a substantial\n portion of the contents of the database;\n .\n b. if You include all or a substantial portion of the database\n contents in a database in which You have Sui Generis Database\n Rights, then the database in which You have Sui Generis Database\n Rights (but not its individual contents) is Adapted Material; and\n .\n c. You must comply with the conditions in Section 3(a) if You Share\n all or a substantial portion of the contents of the database.\n .\n For the avoidance of doubt, this Section 4 supplements and does not\n replace Your obligations under this Public License where the Licensed\n Rights include other Copyright and Similar Rights.\n .\n Section 5 -- Disclaimer of Warranties and Limitation of Liability.\n .\n a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n .\n b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n .\n c. The disclaimer of warranties and limitation of liability provided\n above shall be interpreted in a manner that, to the extent\n possible, most closely approximates an absolute disclaimer and\n waiver of all liability.\n .\n Section 6 -- Term and Termination.\n .\n a. This Public License applies for the term of the Copyright and\n Similar Rights licensed here. However, if You fail to comply with\n this Public License, then Your rights under this Public License\n terminate automatically.\n .\n b. Where Your right to use the Licensed Material has terminated under\n Section 6(a), it reinstates:\n .\n 1. automatically as of the date the violation is cured, provided\n it is cured within 30 days of Your discovery of the\n violation; or\n .\n 2. upon express reinstatement by the Licensor.\n .\n For the avoidance of doubt, this Section 6(b) does not affect any\n right the Licensor may have to seek remedies for Your violations\n of this Public License.\n .\n c. For the avoidance of doubt, the Licensor may also offer the\n Licensed Material under separate terms or conditions or stop\n distributing the Licensed Material at any time; however, doing so\n will not terminate this Public License.\n .\n d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n License.\n .\n Section 7 -- Other Terms and Conditions.\n .\n a. The Licensor shall not be bound by any additional or different\n terms or conditions communicated by You unless expressly agreed.\n .\n b. Any arrangements, understandings, or agreements regarding the\n Licensed Material not stated herein are separate from and\n independent of the terms and conditions of this Public License.\n .\n Section 8 -- Interpretation.\n .\n a. For the avoidance of doubt, this Public License does not, and\n shall not be interpreted to, reduce, limit, restrict, or impose\n conditions on any use of the Licensed Material that could lawfully\n be made without permission under this Public License.\n .\n b. To the extent possible, if any provision of this Public License is\n deemed unenforceable, it shall be automatically reformed to the\n minimum extent necessary to make it enforceable. If the provision\n cannot be reformed, it shall be severed from this Public License\n without affecting the enforceability of the remaining terms and\n conditions.\n .\n c. No term or condition of this Public License will be waived and no\n failure to comply consented to unless expressly agreed to by the\n Licensor.\n .\n d. Nothing in this Public License constitutes or may be interpreted\n as a limitation upon, or waiver of, any privileges and immunities\n that apply to the Licensor or You, including from the legal\n processes of any jurisdiction or authority."
  },
  {
    "path": "LICENSE_MIT_EVERYTHING_OUTSIDE_ANY_CARGO_APP_LIBRARY",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\"><img src=\"https://github.com/user-attachments/assets/f5e4b290-d001-4cf4-9f52-dab65a30e441\" alt=\"krokiet_logo\" width=\"600\" /></div>\n     \n**Krokiet** ((IPA: [ˈkrɔcɛt]), \"croquette\" in Polish) new generation GUI frontend, simple, multiplatform, fast and free app to remove unnecessary files from your computer.\n\n\n<div align=\"center\"><img src=\"https://user-images.githubusercontent.com/41945903/102616149-66490400-4137-11eb-9cd6-813b2b070834.png\" alt=\"czkawka_logo\" width=\"600\" /></div>\n\n**Czkawka** (_tch•kav•ka_ (IPA: [ˈʧ̑kafka]), \"hiccup\" in Polish) older gtk4 GUI frontend, superseded by Krokiet, but still receiving bugfix updates.\n\n## Features\n\n- **Written in memory-safe Rust** - almost 100% unsafe code free\n- **Amazingly fast** - due multithreading and efficient algorithms\n- **Free, Open Source without any ads**\n- **Multiplatform** - runs on Linux, Windows, macOS, FreeBSD, x86, ARM, RISC-V and even Android\n- **Cache support** - second and further scans should be much faster than the first one\n- **Easy to run, easy to compile** - minimal runtime and build dependencies, portable version available\n- **CLI frontend** - for easy automation\n- **GUI frontend** - uses Slint or GTK 4 frameworks\n- **Core library** - allows to reuse functionality in other apps\n- **Android app** - experimental touch-friendly frontend for Android devices\n- **No spying** - Czkawka does not have access to the Internet, nor does it collect any user information or statistics\n- **Multilingual** - support multiple languages like Polish, English or Italian\n- **Multiple tools to use**:\n    - **Duplicates** - Finds duplicates based on file name, size or hash\n    - **Empty Folders** - Finds empty folders with the help of an advanced algorithm\n    - **Big Files** - Finds the provided number of the biggest files in given location\n    - **Empty Files** - Looks for empty files across the drive\n    - **Temporary Files** - Finds temporary files\n    - **Similar Images** - Finds images which are not exactly the same (different resolution, watermarks)\n    - **Similar Videos** - Looks for visually similar videos\n    - **Same Music** - Searches for similar music by tags or by reading content and comparing it\n    - **Invalid Symbolic Links** - Shows symbolic links which point to non-existent files/directories\n    - **Broken Files** - Finds files that are invalid or corrupted\n    - **Bad Extensions** - Lists files whose content not match with their extension\n    - **Exif Remover** - Removes Exif metadata from various file types\n    - **Video Optimizer** - Crops from static parts and converts videos to more efficient formats\n    - **Bad Names** - Finds files with names that may be not wanted (e.g., containing special characters)\n\n![Krokiet](https://github.com/user-attachments/assets/3cc7ec6a-3d6a-42cb-9d33-4b0f0c547af6)\n\n![Czkawka](https://github.com/user-attachments/assets/b0409515-1bec-4e13-8fac-7bdfa15f5848)\n\nChangelog about each version can be found in [CHANGELOG.md](Changelog.md).\n\nNew releases can be found in [Github releases](https://github.com/qarmin/czkawka/releases) and nightly builds also in [Nightly releases](https://github.com/qarmin/czkawka/releases/tag/Nightly)\n\nYou can read more about the 11.0.0 release, its new features, and the issues that were fixed in the following articles:\n- English article – https://medium.com/@qarmin/czkawka-krokiet-11-0-0f6cea385934\n- Polish article – https://medium.com/@qarmin/czkawka-krokiet-11-0-c95ee35eccc2\n\n## Usage, installation, compilation, requirements, license\n\nEach tool uses different technologies, so you can find instructions for each of them in the appropriate file:\n\n- [Krokiet GUI (Slint frontend)](krokiet/README.md)</br>\n- [Czkawka GUI (GTK frontend)](czkawka_gui/README.md)</br>\n- [Czkawka CLI](czkawka_cli/README.md)</br>\n- [Czkawka Core](czkawka_core/README.md)</br>\n- [Cedinia](cedinia/README.md)</br>\n\n## Comparison to other tools\n\nIn this comparison remember, that even if app have same features they may work different(e.g. one app may have more\noptions to choose than other).\n\n|                           |   Krokiet   |     Czkawka      | FSlint |     DupeGuru      |  Bleachbit  |\n|:-------------------------:|:-----------:|:----------------:|:------:|:-----------------:|:-----------:|\n|         Language          |    Rust     |       Rust       | Python |   Python/Obj-C    |   Python    |\n|  Framework base language  |    Rust     |        C         |   C    | C/C++/Obj-C/Swift |      C      |\n|         Framework         |    Slint    |      GTK 4       | PyGTK2 | Qt 5 (PyQt)/Cocoa |   PyGTK3    |\n|            OS             | Lin,Mac,Win |   Lin,Mac,Win    |  Lin   |    Lin,Mac,Win    | Lin,Mac,Win |\n|     Duplicate finder      |      ✔      |        ✔         |   ✔    |         ✔         |             |\n|        Empty files        |      ✔      |        ✔         |   ✔    |                   |             |\n|       Empty folders       |      ✔      |        ✔         |   ✔    |                   |             |\n|      Temporary files      |      ✔      |        ✔         |   ✔    |                   |      ✔      |\n|         Big files         |      ✔      |        ✔         |        |                   |             |\n|      Similar images       |      ✔      |        ✔         |        |         ✔         |             |\n|      Similar videos       |      ✔      |        ✔         |        |                   |             |\n|  Music duplicates(tags)   |      ✔      |        ✔         |        |         ✔         |             |\n| Music duplicates(content) |      ✔      |        ✔         |        |                   |             |\n|     Invalid symlinks      |      ✔      |        ✔         |   ✔    |                   |             |\n|       Broken files        |      ✔      |        ✔         |        |                   |             |\n| Invalid names/extensions  |      ✔      |        ✔         |   ✔    |                   |             |\n|       Exif cleaner        |      ✔      |                  |        |                   |             |\n|      Video optimizer      |      ✔      |                  |        |                   |             |\n|         Bad Names         |      ✔      |                  |        |                   |             |\n|      Names conflict       |             |                  |   ✔    |                   |             |\n|    Installed packages     |             |                  |   ✔    |                   |             |\n|          Bad ID           |             |                  |   ✔    |                   |             |\n|   Non stripped binaries   |             |                  |   ✔    |                   |             |\n|   Redundant whitespace    |             |                  |   ✔    |                   |             |\n|     Overwriting files     |             |                  |   ✔    |                   |      ✔      |\n|     Portable version      |      ✔      |        ✔         |        |                   |      ✔      |\n|    Multiple languages     |      ✔      |        ✔         |   ✔    |         ✔         |      ✔      |\n|       Cache support       |      ✔      |        ✔         |        |         ✔         |             |\n|   In active development   |     Yes     | Yes<sup>**</sup> |   No   |  No<sup>*</sup>   |     Yes     |\n\n<p><sup>*</sup> Few small commits added recently and last version released in 2023</p> \n<p><sup>**</sup> Czkawka GTK is in maintenance mode receiving only bugfixes</p>\n\n## Other apps\n\nThere are many similar applications to Czkawka on the Internet, which do some things better and some things worse:\n\n### GUI\n\n- [DupeGuru](https://github.com/arsenetar/dupeguru) - Many options to customize; great photo compare tool\n- [FSlint](https://github.com/pixelb/fslint) - A little outdated, but still have some tools not available in Czkawka\n- [AntiDupl.NET](https://github.com/ermig1979/AntiDupl) - Shows a lot of metadata of compared images\n- [Video Duplicate Finder](https://github.com/0x90d/videoduplicatefinder) - Finds similar videos(surprising, isn't it)\n\n### CLI\n\nDue to limited time, the biggest emphasis is on the GUI version so if you are looking for really good and feature-packed\nconsole apps, then take a look at these:\n\n- [Fclones](https://github.com/pkolaczk/fclones) - One of the fastest tools to find duplicates; it is written also in\n  Rust\n- [Rmlint](https://github.com/sahib/rmlint) - Nice console interface and also is feature packed\n- [RdFind](https://github.com/pauldreik/rdfind) - Fast, but written in C++ ¯\\\\\\_(ツ)\\_/¯\n\n\n## Projects using Czkawka\n\nCzkawka exposes its common functionality through a crate called **`czkawka_core`**, which can be reused by other projects.\n\nIt is written in Rust and is used by all Czkawka frontends (`czkawka_gui`, `czkawka_cli`, `krokiet`, `cedinia`).\n\nIt is also used by external projects, such as:\n\n- **Czkawka Tauri** - https://github.com/shixinhuang99/czkawka-tauri - A Tauri-based GUI frontend for Czkawka.\n- **page-dewarp** – https://github.com/lmmx/page-dewarp - A library for dewarping document images using a cubic sheet model.\n\nBindings are also available for:\n\n- **Python** – https://pypi.org/project/czkawka/\n\nSome projects work as wrappers around `czkawka_cli`. Without directly depending on `czkawka_core`, they allow simple scanning and retrieving results in JSON format:\n\n- **Schluckauf** – https://github.com/fadykuzman/schluckauf\n\n## Thanks\n\nBig thanks to Pádraig Brady, creator of fantastic FSlint, because without his work I wouldn't create this tool.\n\nThanks also to all the people who contributed to the project in every possible way\n\nAlso, I really appreciate work of people that create crates on which Czkawka is based and for that I try to report bugs to make it even better.\n\n## How to help?\n\n- **Creating issues** - Mainly related to bugs, oddly behaving functionality, etc. As you can see from the issue tracker, there are plenty of ideas for new features, but most of them are either difficult to implement or not aligned with the vision of the project, which evolves slightly over time.\n- **Creating pull requests** - Bug fixes are of course very welcome. Regarding new features, it is best to consult with me before implementing them to confirm they align with the project vision.\n- **Updating translations** - The project uses the Crowdin platform, where translations can be created and updated. In the case of a new release and missing translations, I use machine translation, which is often inaccurate, so updating translations is highly appreciated.\n- **Creating packages for various platforms** - Due to the difficulties related to adding and maintaining support for each new platform, such as learning package formats like deb or rpm, creating installers and packages, I decided to mainly focus on providing prebuilt binaries. However, having the project available in distribution repositories or in projects such as Chocolatey, Homebrew or Winget would be beneficial for users who prefer centralized repositories.\n- **Creating articles, videos, tutorials, etc.** - Any material that helps people better understand this program and its capabilities is welcome.\n- **Recommending it to friends, family, coworkers, etc.** - This is probably the simplest way to help the project become even more popular, which gives me motivation to continue developing the program. Here are a few example ways to naturally mention this program in a regular conversation:\n\n**S** - Someone  \n**Y** - You  \n\n### Situation 1:\n\n- **S** - Hey Anon, I have a lot of junk on my disk, what should I do?\n- **Y** - Download Krokiet/Czkawka. They are completely free and works on almost every system.\n- **S** - Thanks man!\n\n### Situation 2:\n\n- **S** - I am so thirsty...\n- **Y** - Have you heard about Krokiet/Czkawka?\n- **S** - Wait, what?\n- **Y** - Krokiet and Czkawka, in case you did not know, let you clean unnecessary files from your disk. They are completely free...\n- **S** - That is nice, but I am thirsty...\n- **Y** - ...they work on Windows, Linux and macOS, and some people even port them to FreeBSD and Android...\n\n\n## AI Policy\nThe vast majority of the code in this project was written by me(qarmin), without using AI. However, as AI tools have improved and can significantly simplify development and reduce boilerplate, I see no reason to forbid their use.\n\nThat said, every pull request, whether created with AI or not, must meet proper quality standards. The author must be able to clearly explain what the code does, without relying on AI for that explanation. I manually review every PR and test each change, so the risk of incorrect code slipping through is low. Still, to avoid wasting time, please refrain from submitting AI Slop PRs.\n\n## Officially Supported Projects\nOnly this repository, [prebuild-binaries](https://github.com/qarmin/czkawka/releases), projects on [crates.io](https://crates.io/crates/czkawka_gui) and [flathub](https://flathub.org/apps/com.github.qarmin.czkawka) are directly maintained by me.  \n\nCzkawka does not have an official website, so do not trust any sites that claim to be the official one.  \n\nIf you use packages from unofficial sources, make sure they are safe.\n\n## License\n\nThe entire code in this repository is licensed under the [MIT](https://mit-license.org/) license.\n\nAll images and audio files are licensed under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license.\n\nThe Czkawka GTK GUI and CLI applications are licensed under the [MIT](https://mit-license.org/) license, while the Krokiet/Cedinia(due Slint license requirements) are licensed under the [GPL-3.0-only](https://www.gnu.org/licenses/gpl-3.0.en.html) license.\n\n## Donations\n\nIf you are using the app, I would appreciate a donation for its further development, which can be\ndone [here](https://github.com/sponsors/qarmin).\n\n"
  },
  {
    "path": "cedinia/Cargo.toml",
    "content": "[package]\nname = \"cedinia\"\nversion = \"11.0.1\"\nauthors = [\"Rafał Mikrut <mikrutrafal@protonmail.com>\"]\nedition = \"2024\"\nrust-version = \"1.92.0\"\ndescription = \"Android touch-friendly GUI for Czkawka Core – named after the Battle of Cedynia (972 AD)\"\nlicense = \"GPL-3.0-only\"\nhomepage = \"https://github.com/qarmin/czkawka\"\nrepository = \"https://github.com/qarmin/czkawka\"\nbuild = \"build.rs\"\n\n# Android requires a cdylib crate type\n[lib]\nname = \"cedinia\"\ncrate-type = [\"cdylib\", \"rlib\"]\n\n\n[dependencies]\nczkawka_core = { version = \"11.0.1\", path = \"../czkawka_core\" }\ncrossbeam-channel = \"0.5\"\nlog = \"0.4\"\nhumansize = \"2.1\"\nfiletime = \"0.2\"\nserde = \"1.0\"\nserde_json = \"1.0\"\nimage = { version = \"0.25\" }\nfast_image_resize = { version = \"6.0.0\", features = [\"image\"] }\n\nslint = { version = \"1.15.0\", default-features = false, features = [\n    \"std\",\n    \"compat-1-2\",\n] }\n\n# Translations\ni18n-embed = { version = \"0.16\", features = [\"fluent-system\", \"desktop-requester\"] }\ni18n-embed-fl = \"0.10\"\nrust-embed = { version = \"8.5\", features = [\"debug-embed\"] }\n\n\n[target.'cfg(target_os = \"android\")'.dependencies]\nslint = { version = \"1.15.0\", default-features = false, features = [\n    \"std\",\n    \"compat-1-2\",\n    \"backend-android-activity-06\",\n] }\nandroid-activity = { version = \"0.6\", features = [\"native-activity\"] }\n# JNI bindings needed by file_picker_android.rs\njni = { version = \"0.22.1\", default-features = false }\n# Log to Android logcat\nandroid_logger = \"0.15.1\"\n\n[target.'cfg(not(target_os = \"android\"))'.dependencies]\nrfd = { version = \"0.17\", default-features = false, features = [\"xdg-portal\"] }\ntrash = \"5.2.5\"\nslint = { version = \"1.15.0\", default-features = false, features = [\n    \"std\",\n    \"compat-1-2\",\n    \"backend-winit\",\n    \"renderer-winit-femtovg\",\n    \"renderer-winit-software\",\n] }\n\n[build-dependencies]\nslint-build = \"1.15\"\n# Used by build.rs to compile Java helpers to DEX when cross-compiling for Android.\n# The build.rs itself guards on TARGET containing \"android\" so this is a no-op on desktop.\nandroid-build = \"0.1.2\"\n\n[features]\ndefault = []\n\n# ── Android packaging metadata (used by cargo-apk) ───────────────────────────\n[package.metadata.android]\npackage = \"io.github.qarmin.cedinia\"\nlabel = \"Cedinia\"\nresources = \"res\"\n\n[package.metadata.android.sdk]\nmin_sdk_version = 21\ntarget_sdk_version = 34\n\n[package.metadata.android.application]\nlabel = \"@string/app_name\"\nicon = \"@mipmap/ic_launcher\"\n\n# Use our NativeActivity subclass so that onActivityResult is intercepted\n# and forwarded to CediniaFilePicker / the Rust JNI callback.\n[package.metadata.android.application.activity]\nname = \"android.app.NativeActivity\"\n# Prevent Android from destroying and recreating the Activity on common\n# configuration changes (nav-bar inset, keyboard, rotation, locale, …).\n# Without this, setSystemUiVisibility(0) in setupNavBar() causes a\n# screenSize change which Android defers until foreground → causes the\n# \"every-other-time restart\" pattern.\nconfig_changes = \"orientation|keyboardHidden|screenSize|smallestScreenSize|navigation|uiMode|screenLayout|locale|density|fontScale\"\n\n# Full external storage access (needed to scan /sdcard root on Android 11+)\n[[package.metadata.android.uses_permission]]\nname = \"android.permission.MANAGE_EXTERNAL_STORAGE\"\n\n[[package.metadata.android.uses_permission]]\nname = \"android.permission.READ_EXTERNAL_STORAGE\"\n\n[[package.metadata.android.uses_permission]]\nname = \"android.permission.WRITE_EXTERNAL_STORAGE\"\n\n[package.metadata.android.signing.dev]\npath = \"android/keystore/debug.keystore\"\nkeystore_password = \"123456\"\n\n[package.metadata.android.signing.release]\npath = \"android/keystore/release.keystore\"\nkeystore_password = \"123456\"\n\n[lints]\nworkspace = true"
  },
  {
    "path": "cedinia/LICENSE_CC_BY_4_ICONS",
    "content": "Icons\nicons/*\nres/*\nui/icons/*\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nLicense: CC-BY-4.0\n Creative Commons Attribution 4.0 International Public License\n .\n By exercising the Licensed Rights (defined below), You accept and agree\n to be bound by the terms and conditions of this Creative Commons\n Attribution 4.0 International Public License (\"Public\n License\"). To the extent this Public License may be interpreted as a\n contract, You are granted the Licensed Rights in consideration of Your\n acceptance of these terms and conditions, and the Licensor grants You\n such rights in consideration of benefits the Licensor receives from\n making the Licensed Material available under these terms and\n conditions.\n .\n Section 1 -- Definitions.\n .\n a. Adapted Material means material subject to Copyright and Similar\n Rights that is derived from or based upon the Licensed Material\n and in which the Licensed Material is translated, altered,\n arranged, transformed, or otherwise modified in a manner requiring\n permission under the Copyright and Similar Rights held by the\n Licensor. For purposes of this Public License, where the Licensed\n Material is a musical work, performance, or sound recording,\n Adapted Material is always produced where the Licensed Material is\n synched in timed relation with a moving image.\n .\n b. Adapter's License means the license You apply to Your Copyright\n and Similar Rights in Your contributions to Adapted Material in\n accordance with the terms and conditions of this Public License.\n .\n c. Copyright and Similar Rights means copyright and/or similar rights\n closely related to copyright including, without limitation,\n performance, broadcast, sound recording, and Sui Generis Database\n Rights, without regard to how the rights are labeled or\n categorized. For purposes of this Public License, the rights\n specified in Section 2(b)(1)-(2) are not Copyright and Similar\n Rights.\n .\n d. Effective Technological Measures means those measures that, in the\n absence of proper authority, may not be circumvented under laws\n fulfilling obligations under Article 11 of the WIPO Copyright\n Treaty adopted on December 20, 1996, and/or similar international\n agreements.\n .\n e. Exceptions and Limitations means fair use, fair dealing, and/or\n any other exception or limitation to Copyright and Similar Rights\n that applies to Your use of the Licensed Material.\n .\n f. Licensed Material means the artistic or literary work, database,\n or other material to which the Licensor applied this Public\n License.\n .\n g. Licensed Rights means the rights granted to You subject to the\n terms and conditions of this Public License, which are limited to\n all Copyright and Similar Rights that apply to Your use of the\n Licensed Material and that the Licensor has authority to license.\n .\n h. Licensor means the individual(s) or entity(ies) granting rights\n under this Public License.\n .\n i. Share means to provide material to the public by any means or\n process that requires permission under the Licensed Rights, such\n as reproduction, public display, public performance, distribution,\n dissemination, communication, or importation, and to make material\n available to the public including in ways that members of the\n public may access the material from a place and at a time\n individually chosen by them.\n .\n j. Sui Generis Database Rights means rights other than copyright\n resulting from Directive 96/9/EC of the European Parliament and of\n the Council of 11 March 1996 on the legal protection of databases,\n as amended and/or succeeded, as well as other essentially\n equivalent rights anywhere in the world.\n .\n k. You means the individual or entity exercising the Licensed Rights\n under this Public License. Your has a corresponding meaning.\n .\n Section 2 -- Scope.\n .\n a. License grant.\n .\n 1. Subject to the terms and conditions of this Public License,\n the Licensor hereby grants You a worldwide, royalty-free,\n non-sublicensable, non-exclusive, irrevocable license to\n exercise the Licensed Rights in the Licensed Material to:\n .\n a. reproduce and Share the Licensed Material, in whole or\n in part; and\n .\n b. produce, reproduce, and Share Adapted Material.\n .\n 2. Exceptions and Limitations. For the avoidance of doubt, where\n Exceptions and Limitations apply to Your use, this Public\n License does not apply, and You do not need to comply with\n its terms and conditions.\n .\n 3. Term. The term of this Public License is specified in Section\n 6(a).\n .\n 4. Media and formats; technical modifications allowed. The\n Licensor authorizes You to exercise the Licensed Rights in\n all media and formats whether now known or hereafter created,\n and to make technical modifications necessary to do so. The\n Licensor waives and/or agrees not to assert any right or\n authority to forbid You from making technical modifications\n necessary to exercise the Licensed Rights, including\n technical modifications necessary to circumvent Effective\n Technological Measures. For purposes of this Public License,\n simply making modifications authorized by this Section 2(a)\n (4) never produces Adapted Material.\n .\n 5. Downstream recipients.\n .\n a. Offer from the Licensor -- Licensed Material. Every\n recipient of the Licensed Material automatically\n receives an offer from the Licensor to exercise the\n Licensed Rights under the terms and conditions of this\n Public License.\n .\n b. No downstream restrictions. You may not offer or impose\n any additional or different terms or conditions on, or\n apply any Effective Technological Measures to, the\n Licensed Material if doing so restricts exercise of the\n Licensed Rights by any recipient of the Licensed\n Material.\n .\n 6. No endorsement. Nothing in this Public License constitutes or\n may be construed as permission to assert or imply that You\n are, or that Your use of the Licensed Material is, connected\n with, or sponsored, endorsed, or granted official status by,\n the Licensor or others designated to receive attribution as\n provided in Section 3(a)(1)(A)(i).\n .\n b. Other rights.\n .\n 1. Moral rights, such as the right of integrity, are not\n licensed under this Public License, nor are publicity,\n privacy, and/or other similar personality rights; however, to\n the extent possible, the Licensor waives and/or agrees not to\n assert any such rights held by the Licensor to the limited\n extent necessary to allow You to exercise the Licensed\n Rights, but not otherwise.\n .\n 2. Patent and trademark rights are not licensed under this\n Public License.\n .\n 3. To the extent possible, the Licensor waives any right to\n collect royalties from You for the exercise of the Licensed\n Rights, whether directly or through a collecting society\n under any voluntary or waivable statutory or compulsory\n licensing scheme. In all other cases the Licensor expressly\n reserves any right to collect such royalties.\n .\n Section 3 -- License Conditions.\n .\n Your exercise of the Licensed Rights is expressly made subject to the\n following conditions.\n .\n a. Attribution.\n .\n 1. If You Share the Licensed Material (including in modified\n form), You must:\n .\n a. retain the following if it is supplied by the Licensor\n with the Licensed Material:\n .\n i. identification of the creator(s) of the Licensed\n Material and any others designated to receive\n attribution, in any reasonable manner requested by\n the Licensor (including by pseudonym if\n designated);\n .\n ii. a copyright notice;\n .\n iii. a notice that refers to this Public License;\n .\n iv. a notice that refers to the disclaimer of\n warranties;\n .\n v. a URI or hyperlink to the Licensed Material to the\n extent reasonably practicable;\n .\n b. indicate if You modified the Licensed Material and\n retain an indication of any previous modifications; and\n .\n c. indicate the Licensed Material is licensed under this\n Public License, and include the text of, or the URI or\n hyperlink to, this Public License.\n .\n 2. You may satisfy the conditions in Section 3(a)(1) in any\n reasonable manner based on the medium, means, and context in\n which You Share the Licensed Material. For example, it may be\n reasonable to satisfy the conditions by providing a URI or\n hyperlink to a resource that includes the required\n information.\n .\n 3. If requested by the Licensor, You must remove any of the\n information required by Section 3(a)(1)(A) to the extent\n reasonably practicable.\n .\n 4. If You Share Adapted Material You produce, the Adapter's\n License You apply must not prevent recipients of the Adapted\n Material from complying with this Public License.\n .\n Section 4 -- Sui Generis Database Rights.\n .\n Where the Licensed Rights include Sui Generis Database Rights that\n apply to Your use of the Licensed Material:\n .\n a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n to extract, reuse, reproduce, and Share all or a substantial\n portion of the contents of the database;\n .\n b. if You include all or a substantial portion of the database\n contents in a database in which You have Sui Generis Database\n Rights, then the database in which You have Sui Generis Database\n Rights (but not its individual contents) is Adapted Material; and\n .\n c. You must comply with the conditions in Section 3(a) if You Share\n all or a substantial portion of the contents of the database.\n .\n For the avoidance of doubt, this Section 4 supplements and does not\n replace Your obligations under this Public License where the Licensed\n Rights include other Copyright and Similar Rights.\n .\n Section 5 -- Disclaimer of Warranties and Limitation of Liability.\n .\n a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n .\n b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n .\n c. The disclaimer of warranties and limitation of liability provided\n above shall be interpreted in a manner that, to the extent\n possible, most closely approximates an absolute disclaimer and\n waiver of all liability.\n .\n Section 6 -- Term and Termination.\n .\n a. This Public License applies for the term of the Copyright and\n Similar Rights licensed here. However, if You fail to comply with\n this Public License, then Your rights under this Public License\n terminate automatically.\n .\n b. Where Your right to use the Licensed Material has terminated under\n Section 6(a), it reinstates:\n .\n 1. automatically as of the date the violation is cured, provided\n it is cured within 30 days of Your discovery of the\n violation; or\n .\n 2. upon express reinstatement by the Licensor.\n .\n For the avoidance of doubt, this Section 6(b) does not affect any\n right the Licensor may have to seek remedies for Your violations\n of this Public License.\n .\n c. For the avoidance of doubt, the Licensor may also offer the\n Licensed Material under separate terms or conditions or stop\n distributing the Licensed Material at any time; however, doing so\n will not terminate this Public License.\n .\n d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n License.\n .\n Section 7 -- Other Terms and Conditions.\n .\n a. The Licensor shall not be bound by any additional or different\n terms or conditions communicated by You unless expressly agreed.\n .\n b. Any arrangements, understandings, or agreements regarding the\n Licensed Material not stated herein are separate from and\n independent of the terms and conditions of this Public License.\n .\n Section 8 -- Interpretation.\n .\n a. For the avoidance of doubt, this Public License does not, and\n shall not be interpreted to, reduce, limit, restrict, or impose\n conditions on any use of the Licensed Material that could lawfully\n be made without permission under this Public License.\n .\n b. To the extent possible, if any provision of this Public License is\n deemed unenforceable, it shall be automatically reformed to the\n minimum extent necessary to make it enforceable. If the provision\n cannot be reformed, it shall be severed from this Public License\n without affecting the enforceability of the remaining terms and\n conditions.\n .\n c. No term or condition of this Public License will be waived and no\n failure to comply consented to unless expressly agreed to by the\n Licensor.\n .\n d. Nothing in this Public License constitutes or may be interpreted\n as a limitation upon, or waiver of, any privileges and immunities\n that apply to the Licensor or You, including from the legal\n processes of any jurisdiction or authority."
  },
  {
    "path": "cedinia/LICENSE_GPL_APP",
    "content": "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n\nCopyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble\n\nThe GNU General Public License is a free, copyleft license for software and other kinds of works.\n\nThe licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.\n\nSome devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\nTERMS AND CONDITIONS\n\n0. Definitions.\n\n“This License” refers to version 3 of the GNU General Public License.\n\n“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.\n\n“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.\n\nTo “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.\n\nA “covered work” means either the unmodified Program or a work based on the Program.\n\nTo “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.\n\nTo “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.\n\n1. Source Code.\nThe “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.\n\nA “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.\n\nThe “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.\n\nThe “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.\n\nThe Corresponding Source for a work in source code form is that same work.\n\n2. Basic Permissions.\nAll rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\nNo covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.\n\nWhen you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.\n\n4. Conveying Verbatim Copies.\nYou may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\nYou may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:\n\n     a) The work must carry prominent notices stating that you modified it, and giving a relevant date.\n\n     b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.\n\n     c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.\n\n     d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.\n\nA compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.\n\n6. Conveying Non-Source Forms.\nYou may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:\n\n     a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.\n\n     b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.\n\n     c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.\n\n     d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.\n\n     e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.\n\nA “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.\n\n“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.\n\nIf you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).\n\nThe requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.\n\n7. Additional Terms.\n“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:\n\n     a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or\n\n     b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or\n\n     c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or\n\n     d) Limiting the use for publicity purposes of names of licensors or authors of the material; or\n\n     e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or\n\n     f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.\n\nAll other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.\n\n8. Termination.\nYou may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).\n\nHowever, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.\n\nTermination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.\n\n9. Acceptance Not Required for Having Copies.\nYou are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\nEach time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.\n\nAn “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.\n\n11. Patents.\nA “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.\n\nA contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.\n\nIn the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.\n\nA patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\nIf conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License.\nNotwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.\n\n14. Revised Versions of this License.\nThe Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.\n\nLater license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.\n\n15. Disclaimer of Warranty.\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\nIf the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.\n\n     <one line to give the program's name and a brief idea of what it does.>\n     Copyright (C) <year>  <name of author>\n\n     This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\n     This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.\n\n     You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:\n\n     Krokiet Copyright (C) 2024  Rafał Mikrut\n     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n     This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.\n\nYou should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.\n\nThe GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "cedinia/LICENSE_MIT_CODE",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "cedinia/README.md",
    "content": "# Cedinia\n\nCedinia is an experimental Android touch friendly GUI frontend for Czkawka Core, built with Slint. \n\nThe name refers to the Battle of Cedynia in 972, a victory significant to the early Polish state.\n\n## Installation\nProbably the easiest way is to install it from prebuilt APKs, which can be found in the releases section.\n\n## Compilation/Setup\nQuite complicated - look for now at the CI or TMP_INSTALL.md for basic and not fully detailed instructions.\n\n## AI usage\nBecause this project goes into parts of Android that I am not familiar with, I used a lot of AI assistance during development. I reviewed and guided every step myself, but AI helped speed things up. \n\nAI is a tool, like any other tool such as IntelliSense, debuggers, or documentation search, it can be used well or poorly. I simply try to use the available tools responsibly to build good software. \n\nI do not expect this project to become anything serious. It will most likely remain a small side experiment. If that changes, I will review all AI generated code and rewrite it where necessary so it works properly and meets my standards."
  },
  {
    "path": "cedinia/TMP_INSTALL.md",
    "content": "\n```shell\nexport JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64   # must be JDK 17, see note above\nexport ANDROID_HOME=$HOME/android-sdk\nexport ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.3.11579264\nexport ANDROID_BUILD_TOOLS_VERSION=35.0.0              # set to 35 if you use JDK 21+\nexport PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin\nexport PATH=$PATH:$ANDROID_HOME/platform-tools\n\n# Ubuntu / Debian - install Java (if you don't have it already)\nsudo apt install openjdk-17-jdk\n\n# Desktop (Rust) build/run\ncargo build -p cedinia\ncargo run -p cedinia\n\n# Linux example – Android command-line tools setup (adjust paths to taste)\nANDROID_HOME=$HOME/android-sdk\nmkdir -p $ANDROID_HOME/cmdline-tools\ncd $ANDROID_HOME/cmdline-tools\n# Download from https://developer.android.com/studio#command-tools, e.g. https://dl.google.com/android/repository/commandlinetools-linux-14742923_latest.zip\n# unzip commandlinetools-linux-*.zip\n# mv cmdline-tools latest\n\n# Accept licences & install required packages\n$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses\n$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \\\n    \"platform-tools\" \\\n    \"platforms;android-30\" \\\n    \"platforms;android-34\" \\\n    \"build-tools;34.0.0\" \\\n    \"build-tools;35.0.0\" \\\n    \"ndk;26.3.11579264\"\n\n# Reload the shell (if you added the exports to ~/.bashrc or ~/.zshrc)\nsource ~/.bashrc\n\n# Install cargo-apk or cargo-xbuild\ncargo install cargo-apk\n# Or for an alternative toolchain\ncargo install xbuild\n\n# Add Android Rust targets\nrustup target add \\\n    aarch64-linux-android \\\n    armv7-linux-androideabi \\\n    x86_64-linux-android \\\n    i686-linux-android\n\n# Build the APK\n# Debug APK\ncargo apk build -p cedinia --lib\n\n# Release APK (requires a signing key)\ncargo apk build -p cedinia --lib --release\n\n# Check connected devices\nadb devices\n\n# Install and launch on a connected device\nadb install -r target/debug/apk/cedinia.apk\nadb shell am start -n io.github.qarmin.cedinia/android.app.NativeActivity\n\n# One-liner: build → install → launch\ncargo apk build -p cedinia --lib && \\\n  adb install -r target/debug/apk/cedinia.apk && \\\n  adb shell am start -n io.github.qarmin.cedinia/android.app.NativeActivity\n\n# Debugging: Live logs (Rust stdout/stderr, panics)\nadb logcat -s RustStdoutStderr:V\n\n# Full logcat filtered to Cedinia\nadb logcat -s RustStdoutStderr:V io.github.qarmin.cedinia:V AndroidRuntime:E\n\n# Crash / panic backtrace: set RUST_BACKTRACE=1 before building\nRUST_BACKTRACE=1 cargo apk build -p cedinia --lib\n# Then check adb logcat -s RustStdoutStderr:V\n\n# Uninstall\nadb uninstall io.github.qarmin.cedinia\n```"
  },
  {
    "path": "cedinia/build.rs",
    "content": "use std::env;\nuse std::path::PathBuf;\n\nfn main() {\n    slint_build::compile_with_config(\"ui/main_window.slint\", slint_build::CompilerConfiguration::new().with_style(\"material\".into()))\n        .expect(\"Unable to compile Slint UI files for Cedinia\");\n\n    // Compile Java helper classes to DEX when building for Android.\n    // This follows the same pattern used by the Slint android-activity backend.\n    if env::var(\"TARGET\").unwrap_or_default().contains(\"android\") {\n        compile_android_java();\n    }\n}\n\nfn compile_android_java() {\n    use android_build::{Dexer, JavaBuild};\n\n    let java_files = [\"java/CediniaActivity.java\", \"java/CediniaFilePicker.java\"];\n\n    let out_dir: PathBuf = env::var_os(\"OUT_DIR\").expect(\"OUT_DIR environment variable not set\").into();\n    let out_class_dir = out_dir.join(\"java_classes\");\n\n    if out_class_dir.try_exists().unwrap_or(false) {\n        let _ = std::fs::remove_dir_all(&out_class_dir);\n    }\n    std::fs::create_dir_all(&out_class_dir).unwrap_or_else(|e| panic!(\"Cannot create output directory {out_class_dir:?} - {e}\"));\n\n    let android_jar = android_build::android_jar(None).expect(\"No Android platform SDK found; install SDK\");\n\n    let release_mode = env::var(\"PROFILE\").as_ref().map(|s| s.as_str()) == Ok(\"release\");\n\n    // Compile all Java sources to .class files.\n    let mut build = JavaBuild::new();\n    for f in &java_files {\n        build.file(f);\n    }\n    let status = build\n        .class_path(&android_jar)\n        .classes_out_dir(&out_class_dir)\n        .java_source_version(8)\n        .java_target_version(8)\n        .debug_info(android_build::DebugInfo {\n            line_numbers: !release_mode,\n            variables: !release_mode,\n            source_files: !release_mode,\n        })\n        .command()\n        .unwrap_or_else(|e| panic!(\"Could not generate javac command: {e}\"))\n        .args([\"-encoding\", \"UTF-8\"])\n        .output()\n        .unwrap_or_else(|e| panic!(\"Could not run javac: {e}\"));\n\n    assert!(status.status.success(), \"Java compilation failed:\\n{}\", String::from_utf8_lossy(&status.stderr));\n\n    // Convert .class files to a single classes.dex.\n    let dex_out = Dexer::new()\n        .android_jar(&android_jar)\n        .class_path(&out_class_dir)\n        .collect_classes(&out_class_dir)\n        .expect(\"Failed to collect compiled Java classes for DEX conversion\")\n        .release(release_mode)\n        .android_min_api(21)\n        .out_dir(&out_dir)\n        .command()\n        .unwrap_or_else(|e| panic!(\"Could not generate D8 command: {e}\"))\n        .output()\n        .unwrap_or_else(|e| panic!(\"Error running D8: {e}\"));\n\n    assert!(dex_out.status.success(), \"Dex conversion failed:\\n{}\", String::from_utf8_lossy(&dex_out.stderr));\n\n    for f in &java_files {\n        println!(\"cargo:rerun-if-changed={f}\");\n    }\n}\n"
  },
  {
    "path": "cedinia/i18n/en/cedinia.ftl",
    "content": "# Cedinia – English (fallback)\n\n# App / top bar titles\napp_name = Cedinia\ntool_duplicate_files = Duplicates\ntool_empty_folders = Empty Folders\ntool_similar_images = Similar Images\ntool_empty_files = Empty Files\ntool_temporary_files = Temporary Files\ntool_big_files = Biggest Files\ntool_broken_files = Broken Files\ntool_bad_extensions = Bad Extensions\ntool_same_music = Music Duplicates\ntool_bad_names = Bad Names\ntool_exif_remover = EXIF Data\ntool_directories = Directories\ntool_settings = Settings\n\n# Home screen tool card descriptions\nhome_dup_description = Find files with the same content\nhome_empty_folders_description = Directories without content\nhome_similar_images_description = Find visually similar photos\nhome_empty_files_description = Files with zero size\nhome_temp_files_description = Temporary and cached files\nhome_big_files_description = Biggest/Smallest files on disk\nhome_broken_files_description = PDF, audio, images, archives\nhome_bad_extensions_description = Files with invalid extension\nhome_same_music_description = Similar audio files by tags\nhome_bad_names_description = Files with problematic characters in name\nhome_exif_description = Images with EXIF metadata\n\n# Results list\nscanning = Scanning in progress...\nstopping = Stopping...\nno_results = No results\npress_start = Press START to scan\nselect_label = Sel.\ndeselect_label = Desel.\nlist_label = List\ngallery_label = Gal.\n\n# Selection popup\nselection_popup_title = Select\nselect_all = Select all\nselect_except_one = Select all except one\nselect_except_largest = Select all except largest\nselect_except_smallest = Select all except smallest\nselect_largest = Select largest\nselect_smallest = Select smallest\nselect_except_highest_res = Select all except highest resolution\nselect_except_lowest_res = Select all except lowest resolution\nselect_highest_res = Select highest resolution\nselect_lowest_res = Select lowest resolution\ninvert_selection = Invert selection\nclose = Close\n\n# Deselection popup\ndeselection_popup_title = Deselect\ndeselect_all = Deselect all\ndeselect_except_one = Deselect all except one\n\n# Confirm popup\ncancel = Cancel\ndelete = Delete\nrename = Rename\n\n# Delete errors popup\ndelete_errors_title = Failed to delete some files:\nok = OK\n\n# Stopping overlay\nstopping_overlay_title = ■ Stopping\nstopping_overlay_body = Finishing current scan…\\nPlease wait.\n\n# Permission popup\npermission_title = 🔒 File Access\npermission_body = To scan files, the app needs access to device storage. Without this permission, scanning will not be possible.\ngrant = Grant\nno_permission_scan_warning = No file access – grant permission to scan\n\n# Settings screen tabs\nsettings_tab_general = General\nsettings_tab_tools = Tools\nsettings_tab_diagnostics = Info\n\n# Settings — General tab\nsettings_use_cache = Use cache\nsettings_use_cache_desc = Speeds up subsequent scans (hash/images)\nsettings_ignore_hidden = Ignore hidden files\nsettings_ignore_hidden_desc = Files and folders starting with '.'\nsettings_scan_label = SCAN\nsettings_filters_label = FILTERS (some tools)\nsettings_min_file_size = Min. file size\nsettings_max_file_size = Max. file size\nsettings_language = Language\nsettings_language_restart = Requires app restart\nsettings_common_label = COMMON SETTINGS\nsettings_excluded_items = EXCLUDED ITEMS (glob patterns, comma separated)\nsettings_excluded_items_placeholder = e.g. *.tmp, */.git/*, */node_modules/*\nsettings_allowed_extensions = ALLOWED EXTENSIONS (empty = all)\nsettings_allowed_extensions_placeholder = e.g. jpg, png, mp4\nsettings_excluded_extensions = EXCLUDED EXTENSIONS\nsettings_excluded_extensions_placeholder = e.g. bak, tmp, log\n\n# Settings — Tools section labels\nsettings_duplicates_header = DUPLICATES\nsettings_check_method_label = COMPARISON METHOD\nsettings_check_method = Method\nsettings_hash_type_label = HASH TYPE\nsettings_hash_type = Hash type\nsettings_hash_type_desc = Blake3 – fastest; CRC32/xxH3 – alternatives\nsettings_similar_images_header = SIMILAR IMAGES\nsettings_similarity_preset = Similarity threshold\nsettings_similarity_desc = Very High = only near-identical\nsettings_hash_size = Hash size\nsettings_hash_size_desc = Larger = more accurate, slower\nsettings_hash_alg = Hash algorithm\nsettings_image_filter = Resize filter\nsettings_ignore_same_size = Ignore images with the same dimensions\nsettings_big_files_header = BIGGEST FILES\nsettings_search_mode = Search mode\nsettings_file_count = File count\nsettings_same_music_header = MUSIC DUPLICATES\nsettings_music_check_method = Comparison mode\nsettings_music_compare_tags_label = COMPARED TAGS\nsettings_music_title = Title\nsettings_music_artist = Artist\nsettings_music_year = Year\nsettings_music_length = Length\nsettings_music_genre = Genre\nsettings_music_bitrate = Bitrate\nsettings_music_approx = Approximate tag comparison\nsettings_broken_files_header = BROKEN FILES\nsettings_broken_files_types_label = CHECKED TYPES\nsettings_broken_audio = Audio\nsettings_broken_pdf = PDF\nsettings_broken_archive = Archive\nsettings_broken_image = Image\nsettings_bad_names_header = BAD NAMES\nsettings_bad_names_checks_label = CHECKS\nsettings_bad_names_uppercase_ext = Uppercase extension\nsettings_bad_names_emoji = Emoji in name\nsettings_bad_names_space = Spaces at start/end\nsettings_bad_names_non_ascii = Non-ASCII characters\nsettings_bad_names_duplicated = Repeated characters\n\n# Settings — Diagnostics tab\ndiagnostics_header = DIAGNOSTICS\ndiagnostics_thumbnails = Thumbnail cache\ndiagnostics_app_cache = App cache\ndiagnostics_refresh = Refresh\ndiagnostics_clear_thumbnails = Clear thumbnails\ndiagnostics_clear_cache = Clear cache\ndiagnostics_collect_test = Scan test\ndiagnostics_collect_test_desc = Scans each volume recursively\ndiagnostics_collect_test_run = Run\ndiagnostics_collect_test_stop = Stop\nabout_repo = Repository\nabout_translate = Translations\nabout_donate = Support\n\n# Collect-test result popup\ncollect_test_title = 📊 Test results\ncollect_test_volumes = 💾 Volumes:\ncollect_test_folders = 📁 Folders:\ncollect_test_files = 📄 Files:\ncollect_test_time = ⏱ Time:\ncollect_test_ms = \" ms\"\n\n# Directories screen\ndirectories_include_header = Directories to scan\ndirectories_exclude_header = Excluded directories\ndirectories_add = + Add\nno_paths = No paths – add below\ndirectories_volume_header = Volumes\ndirectories_volume_refresh = Refresh\ndirectories_volume_add = Add\n\n# Bottom navigation\nnav_home = Start\nnav_dirs = Directories\nnav_settings = Settings\n\n# Status messages set from Rust\nstatus_ready = Ready\nstatus_stopped = Stopped\nstatus_no_results = No results\nstatus_deleted_selected = Deleted selected\nstatus_deleted_with_errors = Deleted with errors\nscan_not_started = Scan not started\nfound_items_prefix = Found\nfound_items_suffix = items\ndeleted_items_prefix = Deleted\ndeleted_items_suffix = items\ndeleted_errors_suffix = errors\nrenamed_prefix = Renamed\nrenamed_files_suffix = files\nrenamed_errors_suffix = errors\ncleaned_exif_prefix = Cleaned EXIF from\ncleaned_exif_suffix = files\ncleaned_exif_errors_suffix = errors\nand_more_prefix = …and\nand_more_suffix = more\n\n# Gallery / delete popups\ngallery_delete_button = Delete\ngallery_back = Back\ngallery_confirm_delete = Yes, delete\ndeleting_files = Deleting files…\nstop = Stop\nfiles_suffix = files\nscanning_fallback = Scanning…\napp_subtitle = In honour of the Battle of Cedynia (972 CE)\napp_license = Frontend for Czkawka Core  •  GPL-3.0\nabout_app_label = ABOUT\ncache_label = CACHE\n\n"
  },
  {
    "path": "cedinia/i18n/pl/cedinia.ftl",
    "content": "# Cedinia – Polski (Polish)\n\n# App / top bar titles\napp_name = Cedinia\ntool_duplicate_files = Duplikaty\ntool_empty_folders = Puste foldery\ntool_similar_images = Podobne obrazy\ntool_empty_files = Puste pliki\ntool_temporary_files = Pliki tymczasowe\ntool_big_files = Największe pliki\ntool_broken_files = Uszkodzone pliki\ntool_bad_extensions = Złe rozszerzenia\ntool_same_music = Duplikaty muzyki\ntool_bad_names = Złe nazwy\ntool_exif_remover = Dane EXIF\ntool_directories = Katalogi\ntool_settings = Ustawienia\n\n# Home screen tool card descriptions\nhome_dup_description = Znajdź pliki o tej samej zawartości\nhome_empty_folders_description = Katalogi bez zawartości\nhome_similar_images_description = Znajdź wizualnie podobne zdjęcia\nhome_empty_files_description = Pliki o zerowym rozmiarze\nhome_temp_files_description = Tymczasowe i cache'owane pliki\nhome_big_files_description = 50 największych plików na dysku\nhome_broken_files_description = Pliki PDF, audio, obrazy, archiwa\nhome_bad_extensions_description = Pliki z nieprawidłowym rozszerzeniem\nhome_same_music_description = Podobne pliki audio wg tagów\nhome_bad_names_description = Pliki z problematycznymi znakami w nazwie\nhome_exif_description = Obrazy z metadanymi EXIF\n\n# Results list\nscanning = Skanowanie w toku...\nstopping = Zatrzymywanie...\nno_results = Brak wynikow\npress_start = Nacisnij START aby skanowac\nselect_label = Zaz.\ndeselect_label = Odzn.\nlist_label = Lista\ngallery_label = Gal.\n\n# Selection popup\nselection_popup_title = Zaznaczanie\nselect_all = Zaznacz wszystko\nselect_except_one = Zaznacz poza jednym\nselect_except_largest = Zaznacz poza największym\nselect_except_smallest = Zaznacz poza najmniejszym\nselect_largest = Zaznacz największy\nselect_smallest = Zaznacz najmniejszy\nselect_except_highest_res = Zaznacz poza największą rozdzielczością\nselect_except_lowest_res = Zaznacz poza najmniejszą rozdzielczością\nselect_highest_res = Zaznacz największą rozdzielczość\nselect_lowest_res = Zaznacz najmniejszą rozdzielczość\ninvert_selection = Odwroc zaznaczenie\nclose = Zamknij\n\n# Deselection popup\ndeselection_popup_title = Odznaczanie\ndeselect_all = Odznacz wszystko\ndeselect_except_one = Odznacz poza jednym\n\n# Confirm popup\ncancel = Anuluj\ndelete = Usuń\nrename = Zmień nazwę\n\n# Delete errors popup\ndelete_errors_title = Nie udalo sie usunac niektorych plikow:\nok = OK\n\n# Stopping overlay\nstopping_overlay_title = ■ Zatrzymywanie\nstopping_overlay_body = Kończenie bieżącego skanu…\\nProszę czekać.\n\n# Permission popup\npermission_title = 🔒 Dostęp do plików\npermission_body = Aby skanować pliki, aplikacja potrzebuje dostępu do pamięci urządzenia. Bez tego uprawnienia skanowanie nie będzie możliwe.\ngrant = Przyznaj\nno_permission_scan_warning = Brak uprawnień do plików – przyznaj dostęp aby skanować\n\n# Settings screen tabs\nsettings_tab_general = Ogólne\nsettings_tab_tools = Narzędzia\nsettings_tab_diagnostics = Info\n\n# Settings — General tab\nsettings_use_cache = Użyj pamięci podręcznej\nsettings_use_cache_desc = Przyspiesza kolejne skany (hash/obrazy)\nsettings_ignore_hidden = Ignoruj ukryte pliki\nsettings_ignore_hidden_desc = Pliki i foldery zaczynające się od '.'\nsettings_scan_label = SKANOWANIE\nsettings_filters_label = FILTRY (niektóre narzędzia)\nsettings_min_file_size = Min. rozmiar pliku\nsettings_max_file_size = Maks. rozmiar pliku\nsettings_language = Język\nsettings_excluded_items = WYKLUCZONE ELEMENTY (wzorce glob, oddzielone przecinkiem)\nsettings_excluded_items_placeholder = np. *.tmp, */.git/*, */node_modules/*\nsettings_allowed_extensions = DOZWOLONE ROZSZERZENIA (puste = wszystkie)\nsettings_allowed_extensions_placeholder = np. jpg, png, mp4\nsettings_excluded_extensions = WYKLUCZONE ROZSZERZENIA\nsettings_excluded_extensions_placeholder = np. bak, tmp, log\n\n# Settings — Tools section labels\nsettings_duplicates_header = DUPLIKATY\nsettings_check_method_label = METODA PORÓWNANIA\nsettings_check_method = Metoda\nsettings_hash_type_label = TYP HASHA\nsettings_hash_type = Typ hasha\nsettings_similar_images_header = PODOBNE OBRAZY\nsettings_similarity_preset = Próg podobieństwa\nsettings_hash_size = Rozmiar hasha\nsettings_hash_alg = Algorytm hasha\nsettings_image_filter = Filtr zmiany rozmiaru\nsettings_ignore_same_size = Ignoruj obrazy o tych samych wymiarach\nsettings_big_files_header = NAJWIĘKSZE PLIKI\nsettings_search_mode = Tryb wyszukiwania\nsettings_file_count = Liczba plików\nsettings_same_music_header = DUPLIKATY MUZYKI\nsettings_music_check_method = Tryb porównania\nsettings_music_compare_tags_label = PORÓWNYWANE TAGI\nsettings_music_title = Tytuł\nsettings_music_artist = Artysta\nsettings_music_year = Rok\nsettings_music_length = Długość\nsettings_music_genre = Gatunek\nsettings_music_bitrate = Przepływność\nsettings_music_approx = Przybliżone porównywanie tagów\nsettings_broken_files_header = USZKODZONE PLIKI\nsettings_broken_files_types_label = SPRAWDZANE TYPY\nsettings_broken_audio = Dźwięk\nsettings_broken_pdf = PDF\nsettings_broken_archive = Archiwum\nsettings_broken_image = Obraz\nsettings_bad_names_header = ZŁE NAZWY\nsettings_bad_names_checks_label = SPRAWDZENIA\nsettings_bad_names_uppercase_ext = Wielkie litery w rozszerzeniu\nsettings_bad_names_emoji = Emoji w nazwie\nsettings_bad_names_space = Spacje na początku/końcu\nsettings_bad_names_non_ascii = Znaki spoza ASCII\nsettings_bad_names_duplicated = Powtarzające się znaki\n\n# Settings — Diagnostics tab\ndiagnostics_header = DIAGNOSTYKA\ndiagnostics_thumbnails = Pamięć miniatur\ndiagnostics_app_cache = Pamięć aplikacji\ndiagnostics_refresh = Odśwież\ndiagnostics_clear_thumbnails = Wyczyść miniatury\ndiagnostics_clear_cache = Wyczyść cache\ndiagnostics_collect_test = Test skanowania\ndiagnostics_collect_test_run = Uruchom\ndiagnostics_collect_test_stop = Stop\nabout_repo = Repozytorium\nabout_translate = Tłumaczenia\nabout_donate = Wesprzyj\n\n# Collect-test result popup\ncollect_test_title = 📊 Wyniki testu\ncollect_test_volumes = 💾 Woluminy:\ncollect_test_folders = 📁 Foldery:\ncollect_test_files = 📄 Pliki:\ncollect_test_time = ⏱ Czas:\ncollect_test_ms = \" ms\"\n\n# Directories screen\ndirectories_include_header = Katalogi do skanowania\ndirectories_exclude_header = Katalogi wykluczone\ndirectories_add = + Dodaj\ndirectories_volume_header = Woluminy\ndirectories_volume_refresh = Odśwież\ndirectories_volume_add = Dodaj\n\n# Bottom navigation\nnav_home = Start\nnav_dirs = Katalogi\nnav_settings = Ustawienia\n\n# Status messages set from Rust\nstatus_ready = Gotowy / Ready\nstatus_stopped = Zatrzymano\nstatus_no_results = Brak wyników\nstatus_deleted_selected = Usunięto zaznaczone\nstatus_deleted_with_errors = Usunięto z błędami\nscan_not_started = Nie uruchomiono skanu\nfound_items_prefix = Znaleziono\nfound_items_suffix = elementów\ndeleted_items_prefix = Usunięto\ndeleted_items_suffix = elementów\ndeleted_errors_suffix = błędów\nrenamed_prefix = Zmieniono nazwy\nrenamed_files_suffix = plików\nrenamed_errors_suffix = błędów\ncleaned_exif_prefix = Wyczyszczono EXIF z\ncleaned_exif_suffix = plików\ncleaned_exif_errors_suffix = błędów\nand_more_prefix = …i\nand_more_suffix = więcej\n\n"
  },
  {
    "path": "cedinia/i18n.toml",
    "content": "fallback_language = \"en\"\n\n[fluent]\nassets_dir = \"i18n\"\n\n"
  },
  {
    "path": "cedinia/java/CediniaActivity.java",
    "content": "// CediniaActivity.java\n// Thin NativeActivity subclass that intercepts onActivityResult and forwards\n// it to the CediniaFilePicker helper, which then calls back into Rust via JNI.\n//\n// Usage: set android:name=\".CediniaActivity\" in the manifest activity element.\n// cargo-apk injects the package name; we declare the package-less class here\n// because the DEX is loaded dynamically (via InMemoryDexClassLoader) and the\n// real package name is resolved at load time by the Rust side.\n\nimport android.app.NativeActivity;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.WindowManager;\n\npublic class CediniaActivity extends NativeActivity {\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        // Prevent fullscreen mode from hiding the system navigation bar.\n        // NativeActivity (and some GPU renderers) may set FLAG_FULLSCREEN;\n        // FLAG_FORCE_NOT_FULLSCREEN overrides it before super.onCreate runs.\n        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);\n        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    public void onWindowFocusChanged(boolean hasFocus) {\n        super.onWindowFocusChanged(hasFocus);\n        if (hasFocus) {\n            // Reset system UI flags so status bar and nav bar stay visible.\n            // Passing 0 clears SYSTEM_UI_FLAG_HIDE_NAVIGATION,\n            // SYSTEM_UI_FLAG_FULLSCREEN, SYSTEM_UI_FLAG_IMMERSIVE, etc.\n            getWindow().getDecorView().setSystemUiVisibility(0);\n        }\n    }\n\n    @Override\n    public void onBackPressed() {\n        // Move the task to the background instead of finishing the Activity.\n        // This mirrors the Home-button behaviour: tapping the launcher icon\n        // will resume the existing instance rather than recreating it from\n        // scratch (which is the default NativeActivity behaviour when back\n        // is pressed on the root activity).\n        moveTaskToBack(true);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        CediniaFilePicker.handleActivityResult(this, requestCode, resultCode, data);\n    }\n}\n"
  },
  {
    "path": "cedinia/java/CediniaFilePicker.java",
    "content": "// CediniaFilePicker.java\n// Launches the Android Storage Access Framework folder picker and calls back into\n// Rust once the user makes a selection.\n//\n// For NativeActivity (no ComponentActivity): shows an AlertDialog with an EditText\n// so the user can type/paste a path.  The SAF picker via startActivityForResult is\n// also attempted if the manifest activity is CediniaActivity.\n\nimport android.app.Activity;\nimport android.app.AlertDialog;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.provider.DocumentsContract;\nimport android.provider.Settings;\nimport android.util.Log;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.LinearLayout;\n\npublic class CediniaFilePicker {\n    private static final String TAG = \"CediniaFilePicker\";\n\n    // Request codes used with startActivityForResult (CediniaActivity path)\n    static final int REQ_INCLUDE = 0x4345_0001;\n    static final int REQ_EXCLUDE = 0x4345_0002;\n\n    // ── permission helpers ────────────────────────────────────────────────\n\n    /**\n     * Returns true if the app currently has broad file read access.\n     * Android 11+: checks MANAGE_EXTERNAL_STORAGE.\n     * Android 6-10: checks READ_EXTERNAL_STORAGE.\n     * Below Android 6: always true.\n     */\n    public static boolean hasStoragePermission(Activity activity) {\n        if (Build.VERSION.SDK_INT >= 30) {\n            // Android 11+ – MANAGE_EXTERNAL_STORAGE\n            return Environment.isExternalStorageManager();\n        } else if (Build.VERSION.SDK_INT >= 23) {\n            // Android 6-10 – runtime READ_EXTERNAL_STORAGE\n            return activity.checkSelfPermission(\"android.permission.READ_EXTERNAL_STORAGE\")\n                    == PackageManager.PERMISSION_GRANTED;\n        } else {\n            return true;\n        }\n    }\n\n    /**\n     * Opens the appropriate system UI to grant storage permission:\n     * - Android 11+: Settings > Special app access > All files access\n     * - Android 6-10: requestPermissions for READ/WRITE_EXTERNAL_STORAGE\n     */\n    public static void requestStoragePermission(final Activity activity) {\n        Log.i(TAG, \"requestStoragePermission: API=\" + Build.VERSION.SDK_INT);\n        if (Build.VERSION.SDK_INT >= 30) {\n            // Must send user to the special settings screen for MANAGE_EXTERNAL_STORAGE\n            activity.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    try {\n                        Intent intent = new Intent(\n                                Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,\n                                Uri.parse(\"package:\" + activity.getPackageName()));\n                        activity.startActivity(intent);\n                    } catch (Exception e) {\n                        // Fallback: open the general \"all files\" settings page\n                        Log.w(TAG, \"requestStoragePermission: direct intent failed, opening generic: \" + e);\n                        try {\n                            activity.startActivity(\n                                    new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION));\n                        } catch (Exception e2) {\n                            Log.e(TAG, \"requestStoragePermission: fallback also failed: \" + e2);\n                        }\n                    }\n                }\n            });\n        } else if (Build.VERSION.SDK_INT >= 23) {\n            activity.requestPermissions(new String[]{\n                    \"android.permission.READ_EXTERNAL_STORAGE\",\n                    \"android.permission.WRITE_EXTERNAL_STORAGE\"\n            }, 0x4345_0010);\n        }\n        // Below API 23: permissions are granted at install time, nothing to do\n    }\n\n    // ── nav bar visibility ────────────────────────────────────────────────\n\n    /**\n     * Clears immersive / hide-navigation flags so the system nav bar (Back,\n     * Home) stays visible.  Re-applies the clear whenever Slint's renderer\n     * would hide it again.  Call once after Slint has been initialised.\n     */\n    public static void setupNavBar(final Activity activity) {\n        activity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                final View decorView = activity.getWindow().getDecorView();\n                decorView.setSystemUiVisibility(0);\n                decorView.setOnSystemUiVisibilityChangeListener(\n                    new View.OnSystemUiVisibilityChangeListener() {\n                        @Override\n                        public void onSystemUiVisibilityChange(int visibility) {\n                            if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0) {\n                                decorView.setSystemUiVisibility(0);\n                            }\n                        }\n                    }\n                );\n                Log.i(TAG, \"setupNavBar: nav bar listener registered\");\n            }\n        });\n    }\n\n    // ── called from Rust ──────────────────────────────────────────────────\n\n    /** Launch the folder picker for an \"include\" directory. */\n    public static void pickIncludeDirectory(Activity activity) {\n        Log.i(TAG, \"pickIncludeDirectory called, API=\" + Build.VERSION.SDK_INT\n                + \" activity=\" + activity.getClass().getName());\n        launchPicker(activity, true);\n    }\n\n    /** Launch the folder picker for an \"exclude\" directory. */\n    public static void pickExcludeDirectory(Activity activity) {\n        Log.i(TAG, \"pickExcludeDirectory called, API=\" + Build.VERSION.SDK_INT\n                + \" activity=\" + activity.getClass().getName());\n        launchPicker(activity, false);\n    }\n\n    // ── internal ──────────────────────────────────────────────────────────\n\n    private static void launchPicker(final Activity activity, final boolean isInclude) {\n        // Try the SAF picker via startActivityForResult only when running in\n        // a CediniaActivity subclass that can intercept onActivityResult.\n        String activityClass = activity.getClass().getName();\n        if (activityClass.contains(\"CediniaActivity\")) {\n            Log.i(TAG, \"launchPicker: using SAF startActivityForResult\");\n            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);\n            intent.addFlags(\n                    Intent.FLAG_GRANT_READ_URI_PERMISSION |\n                    Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);\n            activity.startActivityForResult(intent,\n                    isInclude ? REQ_INCLUDE : REQ_EXCLUDE);\n        } else {\n            // NativeActivity does not intercept onActivityResult via Rust,\n            // so we fall back to a simple path-entry dialog.\n            Log.w(TAG, \"launchPicker: NativeActivity detected – using path-entry dialog\");\n            showPathDialog(activity, isInclude);\n        }\n    }\n\n    /** Text-entry fallback for NativeActivity where SAF result cannot be received. */\n    static void showPathDialog(final Activity activity, final boolean isInclude) {\n        Log.i(TAG, \"showPathDialog: isInclude=\" + isInclude);\n        activity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                final EditText et = new EditText(activity);\n                et.setHint(isInclude ? \"/sdcard/DCIM\" : \"/sdcard/Android\");\n                et.setSingleLine(true);\n\n                LinearLayout layout = new LinearLayout(activity);\n                layout.setOrientation(LinearLayout.VERTICAL);\n                int pad = (int)(16 * activity.getResources().getDisplayMetrics().density);\n                layout.setPadding(pad, pad, pad, pad);\n                layout.addView(et);\n\n                new AlertDialog.Builder(activity)\n                    .setTitle(isInclude ? \"Add scan directory\" : \"Add exclude directory\")\n                    .setMessage(\"Enter a full path (e.g. /sdcard/DCIM):\")\n                    .setView(layout)\n                    .setPositiveButton(\"Add\", new DialogInterface.OnClickListener() {\n                        @Override\n                        public void onClick(DialogInterface dialog, int which) {\n                            String path = et.getText().toString().trim();\n                            Log.i(TAG, \"showPathDialog confirmed: path='\" + path\n                                    + \"' isInclude=\" + isInclude);\n                            if (!path.isEmpty()) {\n                                onDirectoryPicked(path, isInclude);\n                            } else {\n                                Log.w(TAG, \"showPathDialog: empty path, ignoring\");\n                            }\n                        }\n                    })\n                    .setNegativeButton(\"Cancel\", new DialogInterface.OnClickListener() {\n                        @Override\n                        public void onClick(DialogInterface dialog, int which) {\n                            Log.i(TAG, \"showPathDialog: user cancelled\");\n                        }\n                    })\n                    .show();\n            }\n        });\n    }\n\n    /**\n     * Called by CediniaActivity.onActivityResult (SAF path).\n     */\n    public static void handleActivityResult(\n            Activity activity,\n            int requestCode,\n            int resultCode,\n            Intent data) {\n\n        Log.i(TAG, \"handleActivityResult: requestCode=0x\" + Integer.toHexString(requestCode)\n                + \" resultCode=\" + resultCode + \" data=\" + data);\n\n        if (requestCode != REQ_INCLUDE && requestCode != REQ_EXCLUDE) {\n            Log.d(TAG, \"handleActivityResult: not our request, ignoring\");\n            return;\n        }\n        if (resultCode != Activity.RESULT_OK || data == null) {\n            Log.i(TAG, \"handleActivityResult: cancelled or null data – resultCode=\" + resultCode);\n            return;\n        }\n\n        Uri treeUri = data.getData();\n        if (treeUri == null) {\n            Log.w(TAG, \"handleActivityResult: null tree URI\");\n            return;\n        }\n\n        persistPermission(activity, treeUri);\n        String path = resolveTreeUriToPath(treeUri);\n        boolean isInclude = (requestCode == REQ_INCLUDE);\n        Log.i(TAG, \"handleActivityResult: resolved path='\" + path + \"' isInclude=\" + isInclude);\n        onDirectoryPicked(path, isInclude);\n    }\n\n    private static void persistPermission(Activity activity, Uri treeUri) {\n        try {\n            int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;\n            activity.getContentResolver().takePersistableUriPermission(treeUri, flags);\n            Log.d(TAG, \"persistPermission: ok for \" + treeUri);\n        } catch (SecurityException e) {\n            Log.d(TAG, \"persistPermission: provider does not support persistable perms – \" + e.getMessage());\n        }\n    }\n\n    /**\n     * Best-effort conversion of a SAF tree URI to a filesystem path.\n     */\n    private static String resolveTreeUriToPath(Uri treeUri) {\n        Log.d(TAG, \"resolveTreeUriToPath: input=\" + treeUri);\n        try {\n            Uri docUri = DocumentsContract.buildDocumentUriUsingTree(\n                    treeUri,\n                    DocumentsContract.getTreeDocumentId(treeUri));\n            String docId = DocumentsContract.getDocumentId(docUri);\n            Log.d(TAG, \"resolveTreeUriToPath: docId=\" + docId);\n\n            if (docId == null) {\n                return treeUri.toString();\n            }\n\n            String[] parts = docId.split(\":\", 2);\n            if (parts.length < 2) {\n                return treeUri.toString();\n            }\n\n            String volume = parts[0];\n            String subPath = parts[1];\n            String root;\n            if (\"primary\".equalsIgnoreCase(volume) || \"home\".equalsIgnoreCase(volume)) {\n                root = \"/sdcard\";\n            } else {\n                root = \"/storage/\" + volume;\n            }\n\n            String result = subPath.isEmpty() ? root : root + \"/\" + subPath;\n            Log.i(TAG, \"resolveTreeUriToPath: result=\" + result);\n            return result;\n        } catch (Exception e) {\n            Log.w(TAG, \"resolveTreeUriToPath: exception, returning raw URI string: \" + e);\n            return treeUri.toString();\n        }\n    }\n\n    // ── native callback registered by Rust via register_native_methods ────\n\n    /**\n     * Called after the user picks a directory (either SAF or dialog).\n     * Rust registers the native implementation at startup.\n     */\n    public static native void onDirectoryPicked(String path, boolean isInclude);\n}\n"
  },
  {
    "path": "cedinia/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Dark navy background for the adaptive icon. -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <solid android:color=\"#ffffff\"/>\n</shape>\n"
  },
  {
    "path": "cedinia/res/drawable/ic_launcher_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"@drawable/ic_launcher_fg_src\"\n    android:inset=\"22dp\" />\n"
  },
  {
    "path": "cedinia/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Adaptive icon for Android 8.0+ (API 26+).\n     Uses a solid background + vector foreground so the launcher can apply\n     the device's icon shape mask (squircle, circle, etc.) properly. -->\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>\n"
  },
  {
    "path": "cedinia/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">Cedinia</string>\n</resources>\n"
  },
  {
    "path": "cedinia/src/app.rs",
    "content": "use std::path::PathBuf;\nuse std::rc::Rc;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicU32, Ordering};\n\nuse czkawka_core::common::config_cache_path::{print_infos_and_warnings, set_config_cache_path};\nuse czkawka_core::common::image::register_image_decoding_hooks;\nuse czkawka_core::common::logger::{filtering_messages, print_version_mode, setup_logger};\nuse slint::{ComponentHandle, Model, ModelRc, SharedString, Timer, TimerMode, VecModel, Weak};\n\nuse crate::callbacks::{\n    DeleteEvent, build_dir_model, get_model_for_tool, wire_cache_info, wire_collect_test, wire_directories, wire_language_change, wire_open_path, wire_open_url, wire_permission,\n    wire_save_settings_now, wire_scan, wire_selection,\n};\nuse crate::model::make_file_model;\nuse crate::scan_runner::{FileItem, ScanResult, ScanResultHandler, start_worker};\nuse crate::set_initial_gui_infos::set_initial_gui_infos;\nuse crate::settings::{apply_settings_to_gui, collect_settings_from_gui, load_dirs, load_settings, save_dirs, save_settings};\nuse crate::thumbnail_loader::{ThumbnailData, collect_thumb_tasks, make_placeholder_image, rgba_to_slint_image, spawn_thumbnail_loader};\nuse crate::translations::translate_items;\nuse crate::volumes::home_dir;\nuse crate::{AppState, FileEntry, MainWindow, ProgressData, ScanState, SimilarGroupCard, SimilarImageItem};\n\n#[cfg(target_os = \"android\")]\nthread_local! {\n    static DIR_STATE: std::cell::RefCell<Option<(\n        slint::Weak<MainWindow>,\n        Rc<std::cell::RefCell<Vec<PathBuf>>>,\n        Rc<std::cell::RefCell<Vec<PathBuf>>>,\n    )>> = const { std::cell::RefCell::new(None) };\n}\n\n#[cfg(target_os = \"android\")]\npub fn on_directory_picked(path: String, is_include: bool) {\n    log::info!(\"on_directory_picked: path='{}' is_include={}\", path, is_include);\n    DIR_STATE.with(|cell| {\n        let guard = cell.borrow();\n        if let Some((weak, inc, exc)) = guard.as_ref() {\n            if let Some(win) = weak.upgrade() {\n                if is_include {\n                    inc.borrow_mut().push(PathBuf::from(&path));\n                } else {\n                    exc.borrow_mut().push(PathBuf::from(&path));\n                }\n                win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n\n                let settings = crate::settings::collect_settings_from_gui(&win);\n                crate::settings::save_settings(&settings);\n                crate::settings::save_dirs(&inc.borrow(), &exc.borrow());\n            }\n        }\n    });\n}\n\npub fn run_app() {\n    setup_logger_cache();\n\n    #[cfg(target_os = \"android\")]\n    unreachable!(\"use android_main\");\n    #[cfg(not(target_os = \"android\"))]\n    run_app_with_insets(0.0, 1.0, ());\n}\n\n#[cfg(target_os = \"android\")]\npub fn run_app_with_insets(inset_bottom_px: f32, scale: f32, android_app: slint::android::AndroidApp) {\n    run_app_inner(inset_bottom_px, scale, Some(android_app));\n}\n\n#[cfg(not(target_os = \"android\"))]\npub fn run_app_with_insets(inset_bottom_px: f32, scale: f32, _unused: ()) {\n    run_app_inner(inset_bottom_px, scale, None::<()>);\n}\n\nfn build_gallery_groups(items: &[FileItem], placeholder: &slint::Image) -> Vec<SimilarGroupCard> {\n    use slint::{ModelRc, SharedString, VecModel};\n\n    use crate::common::{STR_IDX_NAME, STR_IDX_PATH, STR_IDX_SIZE};\n    let mut groups: Vec<SimilarGroupCard> = Vec::new();\n    let mut cur_label = String::new();\n    let mut cur_items: Vec<SimilarImageItem> = Vec::new();\n\n    for (flat_idx, item) in items.iter().enumerate() {\n        if item.is_header {\n            if !cur_items.is_empty() {\n                groups.push(SimilarGroupCard {\n                    label: SharedString::from(&cur_label),\n                    items: ModelRc::new(VecModel::from(std::mem::take(&mut cur_items))),\n                });\n            }\n            cur_label = item.val_str[STR_IDX_NAME].clone();\n        } else {\n            let name = &item.val_str[STR_IDX_NAME];\n            let path = &item.val_str[STR_IDX_PATH];\n            let size = &item.val_str[STR_IDX_SIZE];\n            let full_path = if path.is_empty() { name.clone() } else { format!(\"{path}/{name}\") };\n            cur_items.push(SimilarImageItem {\n                full_path: SharedString::from(full_path),\n                name: SharedString::from(name),\n                size: SharedString::from(size),\n                val_str: ModelRc::new(VecModel::from(item.val_str.iter().map(|s| SharedString::from(s.as_str())).collect::<Vec<_>>())),\n                flat_idx: flat_idx as i32,\n                thumbnail: placeholder.clone(),\n                checked: false,\n            });\n        }\n    }\n    if !cur_items.is_empty() {\n        groups.push(SimilarGroupCard {\n            label: SharedString::from(&cur_label),\n            items: ModelRc::new(VecModel::from(cur_items)),\n        });\n    }\n    groups\n}\n\nfn show_delete_errors(win: &MainWindow, errors: &[String]) {\n    let mut msg = errors.iter().take(10).cloned().collect::<Vec<_>>().join(\"\\n\\n\");\n    if errors.len() > 10 {\n        msg.push_str(&format!(\"\\n\\n{} {} {}\", crate::flc!(\"and_more_prefix\"), errors.len() - 10, crate::flc!(\"and_more_suffix\")));\n    }\n    win.global::<AppState>().set_delete_errors_text(SharedString::from(msg));\n    win.global::<AppState>().set_delete_errors_visible(true);\n}\n\nfn rebuild_similar_images_after_delete(win: &MainWindow, deleted: &std::collections::HashSet<String>) {\n    let groups = win.get_similar_images_groups();\n    let mut new_groups: Vec<SimilarGroupCard> = Vec::new();\n    let mut new_flat: Vec<FileEntry> = Vec::new();\n\n    for gi in 0..groups.row_count() {\n        if let Some(group) = groups.row_data(gi) {\n            let surviving: Vec<_> = (0..group.items.row_count())\n                .filter_map(|ii| group.items.row_data(ii))\n                .filter(|item| !deleted.contains(item.full_path.as_str()))\n                .map(|mut item| {\n                    item.checked = false;\n                    item\n                })\n                .collect();\n\n            if surviving.is_empty() {\n                continue;\n            }\n\n            new_flat.push(FileEntry {\n                checked: false,\n                is_header: true,\n                val_str: ModelRc::new(VecModel::from(vec![\n                    group.label.clone(),\n                    SharedString::default(),\n                    SharedString::default(),\n                    SharedString::default(),\n                ])),\n                val_int: ModelRc::new(VecModel::from(vec![])),\n            });\n\n            let mut final_items: Vec<SimilarImageItem> = Vec::new();\n            for mut item in surviving {\n                item.flat_idx = new_flat.len() as i32;\n                new_flat.push(FileEntry {\n                    checked: false,\n                    is_header: false,\n                    val_str: item.val_str.clone(),\n                    val_int: ModelRc::new(VecModel::from(vec![])),\n                });\n                final_items.push(item);\n            }\n\n            new_groups.push(SimilarGroupCard {\n                label: group.label.clone(),\n                items: ModelRc::new(VecModel::from(final_items)),\n            });\n        }\n    }\n\n    win.set_similar_images_model(ModelRc::new(VecModel::from(new_flat)));\n    win.set_similar_images_groups(ModelRc::new(VecModel::from(new_groups)));\n    win.global::<AppState>().set_selected_count(0);\n}\n\nstruct GuiHandler {\n    weak: Weak<MainWindow>,\n    scan_gen: Arc<AtomicU32>,\n    thumb_tx: std::sync::mpsc::Sender<crate::thumbnail_loader::ThumbnailResult>,\n    thumb_cancel: Arc<std::sync::Mutex<Arc<AtomicBool>>>,\n}\n\nimpl ScanResultHandler for GuiHandler {\n    fn on_result(&self, result: ScanResult) {\n        let weak = self.weak.clone();\n        let current_gen = self.scan_gen.load(Ordering::SeqCst);\n\n        match result {\n            ScanResult::Progress(p) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    if p.scan_id != current_gen {\n                        return;\n                    }\n                    let pd = ProgressData {\n                        step_name: SharedString::from(p.step_name),\n                        current_progress: p.current,\n                        all_progress: p.all,\n                        is_indeterminate: p.is_indeterminate,\n                    };\n                    win.global::<AppState>().set_progress(pd);\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n\n            ScanResult::DuplicateFiles(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_duplicate_files_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::EmptyFolders(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_empty_folder_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::SimilarImages(items) => {\n                let thumb_tx = self.thumb_tx.clone();\n                let thumb_cancel = Arc::clone(&self.thumb_cancel);\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    let tasks = collect_thumb_tasks(&items);\n                    let ph = make_placeholder_image();\n                    let groups = build_gallery_groups(&items, &ph);\n                    win.set_similar_images_model(make_file_model(items));\n                    win.set_similar_images_groups(ModelRc::new(VecModel::from(groups)));\n\n                    let mut cancel_guard = thumb_cancel.lock().unwrap();\n                    cancel_guard.store(true, Ordering::Relaxed);\n                    let new_cancel = Arc::new(AtomicBool::new(false));\n                    *cancel_guard = new_cancel.clone();\n                    drop(cancel_guard);\n                    spawn_thumbnail_loader(tasks, thumb_tx, new_cancel, current_gen);\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::EmptyFiles(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_empty_files_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::TemporaryFiles(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_temporary_files_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::BigFiles(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_big_files_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::BrokenFiles(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_broken_files_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::BadExtensions(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_bad_extensions_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::SameMusic(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_same_music_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::BadNames(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_bad_names_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::ExifRemover(items) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    win.set_exif_remover_model(make_file_model(items));\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n            ScanResult::Finished(id) => {\n                slint::invoke_from_event_loop(move || {\n                    let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n                    if id != current_gen {\n                        return;\n                    }\n                    let was_stopping = win.global::<AppState>().get_scan_state() == ScanState::Stopping;\n                    if was_stopping {\n                        win.global::<AppState>().set_scan_state(ScanState::Stopped);\n                        win.global::<AppState>().set_status_message(SharedString::from(crate::flc!(\"status_stopped\")));\n                    } else {\n                        win.global::<AppState>().set_scan_state(ScanState::Done);\n                        let tool = win.global::<AppState>().get_active_tool();\n                        let model = get_model_for_tool(&win, tool);\n                        let file_count = (0..model.row_count()).filter(|&i| model.row_data(i).is_some_and(|e| !e.is_header)).count();\n                        let status = if file_count > 0 {\n                            format!(\"{} {file_count} {}\", crate::flc!(\"found_items_prefix\"), crate::flc!(\"found_items_suffix\"))\n                        } else {\n                            crate::flc!(\"status_no_results\")\n                        };\n                        win.global::<AppState>().set_status_message(SharedString::from(status));\n                    }\n                })\n                .expect(\"Failed to invoke progress update in event loop\");\n            }\n        }\n    }\n}\n\nfn run_app_inner(\n    inset_bottom_px: f32,\n    scale: f32,\n    #[cfg(target_os = \"android\")] android_app: Option<slint::android::AndroidApp>,\n    #[cfg(not(target_os = \"android\"))] _android_app: Option<()>,\n) {\n    std::thread::spawn(crate::thumbnail_loader::cleanup_old_thumbnails);\n\n    let window = MainWindow::new().expect(\"Failed to create MainWindow\");\n\n    let loaded_settings = load_settings();\n    crate::localizer_cedinia::apply_language_preference(&loaded_settings.language);\n    apply_settings_to_gui(&window, &loaded_settings);\n    set_initial_gui_infos(&window);\n    translate_items(&window);\n    window.global::<AppState>().set_status_message(SharedString::from(crate::flc!(\"status_ready\")));\n\n    let bot_lp = inset_bottom_px / scale;\n    window.global::<AppState>().set_inset_bottom(bot_lp);\n\n    #[cfg(target_os = \"android\")]\n    {\n        if let Some(app) = android_app {\n            let weak = window.as_weak();\n            let inset_timer = Rc::new(Timer::default());\n            let inset_timer_clone = inset_timer.clone();\n            inset_timer.start(TimerMode::Repeated, std::time::Duration::from_millis(50), move || {\n                let rect = app.content_rect();\n                if rect.bottom > 0 {\n                    if let Some(win) = weak.upgrade() {\n                        let window_height = win.window().size().height as f32;\n                        let nav_bar_px = window_height - rect.bottom as f32;\n                        if nav_bar_px > 0.0 {\n                            win.global::<AppState>().set_inset_bottom(nav_bar_px / scale);\n                        }\n                    }\n                    inset_timer_clone.stop();\n                }\n            });\n        }\n    }\n\n    let (saved_included, saved_excluded) = load_dirs();\n    let included_dirs = Rc::new(std::cell::RefCell::new(if saved_included.is_empty() { vec![home_dir()] } else { saved_included }));\n    let excluded_dirs: Rc<std::cell::RefCell<Vec<PathBuf>>> = Rc::new(std::cell::RefCell::new(saved_excluded));\n    let scan_gen: Arc<AtomicU32> = Arc::new(AtomicU32::new(0));\n\n    let (thumb_tx, thumb_rx) = std::sync::mpsc::channel::<crate::thumbnail_loader::ThumbnailResult>();\n    let thumb_cancel: Arc<std::sync::Mutex<Arc<AtomicBool>>> = Arc::new(std::sync::Mutex::new(Arc::new(AtomicBool::new(false))));\n    let placeholder: Rc<std::cell::OnceCell<slint::Image>> = Rc::new(std::cell::OnceCell::new());\n\n    let handler = GuiHandler {\n        weak: window.as_weak(),\n        scan_gen: Arc::clone(&scan_gen),\n        thumb_tx,\n        thumb_cancel: Arc::clone(&thumb_cancel),\n    };\n    let (scan_tx_inner, stop_flag) = start_worker(handler);\n    let scan_tx = Rc::new(scan_tx_inner);\n\n    #[cfg(target_os = \"android\")]\n    DIR_STATE.with(|cell| {\n        *cell.borrow_mut() = Some((window.as_weak(), included_dirs.clone(), excluded_dirs.clone()));\n    });\n\n    window.set_directories_model(build_dir_model(&included_dirs.borrow(), &excluded_dirs.borrow()));\n\n    let (delete_tx, delete_rx) = std::sync::mpsc::channel::<DeleteEvent>();\n    let delete_rx = Rc::new(std::cell::RefCell::new(delete_rx));\n    let delete_stop: Rc<std::cell::RefCell<Arc<AtomicBool>>> = Rc::new(std::cell::RefCell::new(Arc::new(AtomicBool::new(false))));\n\n    wire_scan(&window, stop_flag, scan_tx, included_dirs.clone(), scan_gen.clone());\n    wire_permission(&window);\n    wire_selection(&window, delete_tx, Rc::clone(&delete_stop));\n    wire_directories(&window, included_dirs.clone(), excluded_dirs.clone());\n    wire_collect_test(&window);\n    wire_open_path(&window);\n    wire_language_change(&window);\n    wire_open_url(&window);\n    wire_cache_info(&window);\n    wire_save_settings_now(&window, included_dirs.clone(), excluded_dirs.clone());\n\n    let weak = window.as_weak();\n    let thumb_rx = Rc::new(std::cell::RefCell::new(thumb_rx));\n    let scan_gen_poll = scan_gen;\n    let delete_rx_poll = delete_rx;\n    let timer = Timer::default();\n    #[cfg(target_os = \"android\")]\n    let mut perm_poll_counter: u32 = 0;\n    timer.start(TimerMode::Repeated, std::time::Duration::from_millis(50), move || {\n        let win = weak.upgrade().expect(\"Failed to upgrade MainWindow weak reference in timer\");\n        let current_gen = scan_gen_poll.load(Ordering::SeqCst);\n\n        {\n            let rx = thumb_rx.borrow();\n            while let Ok(tr) = rx.try_recv() {\n                if tr.scan_id != current_gen {\n                    continue;\n                }\n                let img = match tr.data {\n                    ThumbnailData::Loaded(rgba, w, h) => rgba_to_slint_image(&rgba, w, h),\n                    ThumbnailData::Placeholder => placeholder.get_or_init(make_placeholder_image).clone(),\n                };\n                let groups = win.get_similar_images_groups();\n                if let Some(group) = groups.row_data(tr.group_idx)\n                    && let Some(mut item) = group.items.row_data(tr.item_idx)\n                {\n                    item.thumbnail = img;\n                    group.items.set_row_data(tr.item_idx, item);\n                }\n            }\n        }\n        {\n            let rx = delete_rx_poll.borrow();\n            while let Ok(event) = rx.try_recv() {\n                match event {\n                    DeleteEvent::Progress(done, total) => {\n                        win.global::<AppState>().set_delete_progress_text(SharedString::from(format!(\"{done} / {total}\")));\n                    }\n                    DeleteEvent::Finished(deleted, errors) => {\n                        win.global::<AppState>().set_delete_running(false);\n\n                        if !deleted.is_empty() {\n                            let del_set: std::collections::HashSet<String> = deleted.into_iter().collect();\n                            rebuild_similar_images_after_delete(&win, &del_set);\n                        }\n\n                        let status = if errors.is_empty() {\n                            crate::flc!(\"status_deleted_selected\").to_string()\n                        } else {\n                            crate::flc!(\"status_deleted_with_errors\").to_string()\n                        };\n                        win.global::<AppState>().set_status_message(SharedString::from(status));\n\n                        if !errors.is_empty() {\n                            show_delete_errors(&win, &errors);\n                        }\n                    }\n                    DeleteEvent::ListDeleteFinished(deleted, errors) => {\n                        win.global::<AppState>().set_delete_running(false);\n\n                        let del_set: std::collections::HashSet<String> = deleted.iter().cloned().collect();\n                        if !del_set.is_empty() {\n                            let tool = win.global::<AppState>().get_active_tool();\n                            let model = get_model_for_tool(&win, tool);\n                            if let Some(vm) = model.as_any().downcast_ref::<slint::VecModel<FileEntry>>() {\n                                let mut items: Vec<FileEntry> = vm.iter().collect();\n                                items.retain(|e| {\n                                    if e.is_header {\n                                        return true;\n                                    }\n                                    let name = e.val_str.row_data(0).map(|s| s.to_string()).unwrap_or_default();\n                                    let path = e.val_str.row_data(1).map(|s| s.to_string()).unwrap_or_default();\n                                    let full = if path.is_empty() { name } else { format!(\"{path}/{name}\") };\n                                    !del_set.contains(&full)\n                                });\n\n                                loop {\n                                    let mut removed = false;\n                                    let mut i = 0;\n                                    while i < items.len() {\n                                        if items[i].is_header {\n                                            let group_len = items[i + 1..].iter().take_while(|e| !e.is_header).count();\n                                            if group_len <= 1 {\n                                                let end = i + 1 + group_len;\n                                                items.drain(i..end);\n                                                removed = true;\n                                                continue;\n                                            }\n                                        }\n                                        i += 1;\n                                    }\n                                    if !removed {\n                                        break;\n                                    }\n                                }\n                                vm.set_vec(items);\n                                win.global::<AppState>().set_selected_count(0);\n                            }\n\n                            rebuild_similar_images_after_delete(&win, &del_set);\n                        }\n\n                        let status = if errors.is_empty() {\n                            format!(\"{} {} {}\", crate::flc!(\"deleted_items_prefix\"), deleted.len(), crate::flc!(\"deleted_items_suffix\"))\n                        } else {\n                            format!(\n                                \"{} {} {}, {} {}\",\n                                crate::flc!(\"deleted_items_prefix\"),\n                                deleted.len(),\n                                crate::flc!(\"deleted_items_suffix\"),\n                                errors.len(),\n                                crate::flc!(\"deleted_errors_suffix\")\n                            )\n                        };\n                        win.global::<AppState>().set_status_message(SharedString::from(status));\n\n                        if !errors.is_empty() {\n                            show_delete_errors(&win, &errors);\n                        }\n                    }\n                    DeleteEvent::ListRenameFinished(renamed, errors) => {\n                        win.global::<AppState>().set_delete_running(false);\n\n                        let model = win.get_bad_extensions_model();\n                        if let Some(vm) = model.as_any().downcast_ref::<slint::VecModel<FileEntry>>() {\n                            let items: Vec<FileEntry> = vm.iter().filter(|e| !e.checked).collect();\n                            vm.set_vec(items);\n                            win.global::<AppState>().set_selected_count(0);\n                        }\n\n                        let status = if errors.is_empty() {\n                            format!(\"{} {renamed} {}\", crate::flc!(\"renamed_prefix\"), crate::flc!(\"renamed_files_suffix\"))\n                        } else {\n                            format!(\n                                \"{} {} {}, {} {}\",\n                                crate::flc!(\"renamed_prefix\"),\n                                renamed,\n                                crate::flc!(\"renamed_files_suffix\"),\n                                errors.len(),\n                                crate::flc!(\"renamed_errors_suffix\")\n                            )\n                        };\n                        win.global::<AppState>().set_status_message(SharedString::from(status));\n\n                        if !errors.is_empty() {\n                            show_delete_errors(&win, &errors);\n                        }\n                    }\n                    DeleteEvent::ExifCleanFinished(cleaned, errors) => {\n                        win.global::<AppState>().set_delete_running(false);\n\n                        let cleaned_set: std::collections::HashSet<String> = cleaned.iter().cloned().collect();\n                        if !cleaned_set.is_empty() {\n                            let model = win.get_exif_remover_model();\n                            if let Some(vm) = model.as_any().downcast_ref::<slint::VecModel<FileEntry>>() {\n                                let items: Vec<FileEntry> = vm\n                                    .iter()\n                                    .filter(|e| {\n                                        if e.is_header {\n                                            return true;\n                                        }\n                                        let name = e.val_str.row_data(0).map(|s| s.to_string()).unwrap_or_default();\n                                        let path = e.val_str.row_data(1).map(|s| s.to_string()).unwrap_or_default();\n                                        let full = if path.is_empty() { name } else { format!(\"{path}/{name}\") };\n                                        !cleaned_set.contains(&full)\n                                    })\n                                    .collect();\n                                vm.set_vec(items);\n                                win.global::<AppState>().set_selected_count(0);\n                            }\n                        }\n\n                        let status = if errors.is_empty() {\n                            format!(\"{} {} {}\", crate::flc!(\"cleaned_exif_prefix\"), cleaned.len(), crate::flc!(\"cleaned_exif_suffix\"))\n                        } else {\n                            format!(\n                                \"{} {} {}, {} {}\",\n                                crate::flc!(\"cleaned_exif_prefix\"),\n                                cleaned.len(),\n                                crate::flc!(\"cleaned_exif_suffix\"),\n                                errors.len(),\n                                crate::flc!(\"cleaned_exif_errors_suffix\")\n                            )\n                        };\n                        win.global::<AppState>().set_status_message(SharedString::from(status));\n\n                        if !errors.is_empty() {\n                            show_delete_errors(&win, &errors);\n                        }\n                    }\n                }\n            }\n        }\n        #[cfg(target_os = \"android\")]\n        {\n            perm_poll_counter += 1;\n            if perm_poll_counter >= 40 {\n                perm_poll_counter = 0;\n                let granted = crate::file_picker_android::check_storage_permission();\n                win.global::<AppState>().set_storage_permission_granted(granted);\n            }\n        }\n    });\n\n    window.run().expect(\"Failed to run MainWindow\");\n\n    let current_settings = collect_settings_from_gui(&window);\n    save_settings(&current_settings);\n    save_dirs(&included_dirs.borrow(), &excluded_dirs.borrow());\n}\n\npub(crate) fn setup_logger_cache() {\n    static INIT_DONE: std::sync::OnceLock<()> = std::sync::OnceLock::new();\n    if INIT_DONE.set(()).is_err() {\n        log::info!(\"setup_logger_cache: already initialized, skipping\");\n        return;\n    }\n\n    register_image_decoding_hooks();\n    let config_cache_path_set_result = set_config_cache_path(\"cedinia\", \"cedinia\");\n\n    setup_logger(false, \"cedinia\", filtering_messages);\n    print_version_mode(\"Cedinia\");\n    print_infos_and_warnings(config_cache_path_set_result.infos, config_cache_path_set_result.warnings);\n}\n"
  },
  {
    "path": "cedinia/src/bin/cedinia.rs",
    "content": "fn main() {\n    cedinia::run_app();\n}\n"
  },
  {
    "path": "cedinia/src/callbacks/directories.rs",
    "content": "use std::path::PathBuf;\nuse std::rc::Rc;\n\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel};\n\nuse crate::volumes::{detect_storage_volumes, refresh_volumes_flags};\nuse crate::{AppState, DirectoryEntry, MainWindow, VolumeEntry};\n\npub(crate) fn wire_directories(window: &MainWindow, included_dirs: Rc<std::cell::RefCell<Vec<PathBuf>>>, excluded_dirs: Rc<std::cell::RefCell<Vec<PathBuf>>>) {\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs.clone();\n        let exc = excluded_dirs.clone();\n        window.global::<AppState>().on_pick_include_dir(move || {\n            #[cfg(not(target_os = \"android\"))]\n            {\n                let win = weak.unwrap();\n                if let Some(path) = rfd::FileDialog::new().pick_folder() {\n                    inc.borrow_mut().push(path);\n                    win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n                }\n            }\n            #[cfg(target_os = \"android\")]\n            {\n                let _ = (&weak, &inc, &exc);\n                crate::file_picker_android::launch_pick_directory(true);\n            }\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs.clone();\n        let exc = excluded_dirs.clone();\n        window.global::<AppState>().on_pick_exclude_dir(move || {\n            #[cfg(not(target_os = \"android\"))]\n            {\n                let win = weak.unwrap();\n                if let Some(path) = rfd::FileDialog::new().pick_folder() {\n                    exc.borrow_mut().push(path);\n                    win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n                }\n            }\n            #[cfg(target_os = \"android\")]\n            {\n                let _ = (&weak, &inc, &exc);\n                crate::file_picker_android::launch_pick_directory(false);\n            }\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs.clone();\n        let exc = excluded_dirs.clone();\n        window.global::<AppState>().on_add_include_dir(move |path| {\n            let win = weak.unwrap();\n            inc.borrow_mut().push(PathBuf::from(path.as_str()));\n            win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n            refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow());\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs.clone();\n        let exc = excluded_dirs.clone();\n        window.global::<AppState>().on_remove_include_dir(move |path| {\n            let win = weak.unwrap();\n            inc.borrow_mut().retain(|p| p.to_string_lossy() != path.as_str());\n            win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n            refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow());\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs.clone();\n        let exc = excluded_dirs.clone();\n        window.global::<AppState>().on_add_exclude_dir(move |path| {\n            let win = weak.unwrap();\n            exc.borrow_mut().push(PathBuf::from(path.as_str()));\n            win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n            refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow());\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs.clone();\n        let exc = excluded_dirs.clone();\n        window.global::<AppState>().on_remove_exclude_dir(move |path| {\n            let win = weak.unwrap();\n            exc.borrow_mut().retain(|p| p.to_string_lossy() != path.as_str());\n            win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow()));\n            refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow());\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs;\n        let exc = excluded_dirs;\n        window.global::<AppState>().on_list_storage_volumes(move || {\n            let raw = detect_storage_volumes();\n            let inc_set: Vec<String> = inc.borrow().iter().map(|p| p.to_string_lossy().to_string()).collect();\n            let exc_set: Vec<String> = exc.borrow().iter().map(|p| p.to_string_lossy().to_string()).collect();\n            let volumes: Vec<VolumeEntry> = raw\n                .into_iter()\n                .map(|mut v| {\n                    let path = v.path.to_string();\n                    v.is_included = inc_set.contains(&path);\n                    v.is_excluded = exc_set.contains(&path);\n                    v\n                })\n                .collect();\n            if let Some(win) = weak.upgrade() {\n                win.global::<AppState>().set_storage_volumes(ModelRc::new(VecModel::from(volumes)));\n            }\n        });\n    }\n}\n\npub(crate) fn build_dir_model(included: &[PathBuf], excluded: &[PathBuf]) -> ModelRc<DirectoryEntry> {\n    let mut entries: Vec<DirectoryEntry> = included\n        .iter()\n        .map(|p| DirectoryEntry {\n            path: SharedString::from(p.to_string_lossy().to_string()),\n            is_included: true,\n        })\n        .collect();\n    for p in excluded {\n        entries.push(DirectoryEntry {\n            path: SharedString::from(p.to_string_lossy().to_string()),\n            is_included: false,\n        });\n    }\n    ModelRc::new(VecModel::from(entries))\n}\n"
  },
  {
    "path": "cedinia/src/callbacks/misc.rs",
    "content": "use std::path::{Path, PathBuf};\nuse std::rc::Rc;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\n\nuse slint::ComponentHandle;\n\nuse crate::settings::{collect_settings_from_gui, save_settings};\nuse crate::thumbnail_loader::thumbnail_cache_dir;\nuse crate::volumes::{count_files_and_dirs_stoppable, detect_storage_volumes};\nuse crate::{AppState, CollectTestResult, GeneralSettings, MainWindow};\n\npub(crate) fn wire_open_path(window: &MainWindow) {\n    #[cfg(not(target_os = \"android\"))]\n    {\n        window.global::<AppState>().on_open_path(|path| {\n            let _ = std::process::Command::new(\"xdg-open\").arg(path.as_str()).spawn();\n        });\n        window.global::<AppState>().on_open_parent_folder(|path| {\n            if !path.is_empty() {\n                let _ = std::process::Command::new(\"xdg-open\").arg(path.as_str()).spawn();\n            }\n        });\n    }\n    #[cfg(target_os = \"android\")]\n    {\n        window.global::<AppState>().on_open_path(|_| {});\n        window.global::<AppState>().on_open_parent_folder(|_| {});\n    }\n}\n\npub(crate) fn wire_permission(window: &MainWindow) {\n    #[cfg(target_os = \"android\")]\n    {\n        let perm = crate::file_picker_android::check_storage_permission();\n        window.global::<AppState>().set_storage_permission_granted(perm);\n        if !perm {\n            window.global::<AppState>().set_show_permission_popup(true);\n        }\n        window.global::<AppState>().on_request_storage_permission(move || {\n            crate::file_picker_android::request_storage_permission();\n        });\n    }\n    #[cfg(not(target_os = \"android\"))]\n    {\n        window.global::<AppState>().on_request_storage_permission(|| {});\n    }\n}\n\npub(crate) fn wire_collect_test(window: &MainWindow) {\n    let collect_stop_flag: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));\n\n    {\n        let weak = window.as_weak();\n        let stop = collect_stop_flag.clone();\n        window.global::<AppState>().on_run_collect_test(move || {\n            let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n            stop.store(false, Ordering::Relaxed);\n            win.global::<AppState>().set_collect_test_running(true);\n            win.global::<AppState>().set_collect_test_done(false);\n\n            let weak2 = win.as_weak();\n            let stop2 = stop.clone();\n            std::thread::spawn(move || {\n                let start = std::time::Instant::now();\n                let volumes = detect_storage_volumes();\n                let volume_count = volumes.len() as i32;\n                let mut total_files: i32 = 0;\n                let mut total_folders: i32 = 0;\n                let mut stopped = false;\n                'outer: for vol in &volumes {\n                    let root = std::path::Path::new(vol.path.as_str());\n                    let (f, d) = count_files_and_dirs_stoppable(root, &stop2, &mut stopped);\n                    total_files = total_files.saturating_add(f);\n                    total_folders = total_folders.saturating_add(d);\n                    if stopped {\n                        break 'outer;\n                    }\n                }\n                let elapsed_ms = start.elapsed().as_millis() as i32;\n                let result = CollectTestResult {\n                    volumes: volume_count,\n                    files: total_files,\n                    folders: total_folders,\n                    elapsed_ms,\n                };\n                let _ = slint::invoke_from_event_loop(move || {\n                    if let Some(win) = weak2.upgrade() {\n                        win.global::<AppState>().set_collect_test_result(result);\n                        win.global::<AppState>().set_collect_test_running(false);\n                        if !stopped {\n                            win.global::<AppState>().set_collect_test_done(true);\n                        }\n                    }\n                });\n            });\n        });\n    }\n\n    {\n        let weak = window.as_weak();\n        let stop = collect_stop_flag;\n        window.global::<AppState>().on_stop_collect_test(move || {\n            stop.store(true, Ordering::Relaxed);\n            if let Some(win) = weak.upgrade() {\n                win.global::<AppState>().set_collect_test_running(false);\n            }\n        });\n    }\n}\n\nfn dir_size_recursive(path: &Path) -> u64 {\n    std::fs::read_dir(path).ok().map_or(0, |entries| {\n        entries\n            .flatten()\n            .map(|e| {\n                let p = e.path();\n                if p.is_dir() {\n                    dir_size_recursive(&p)\n                } else {\n                    e.metadata().map(|m| m.len()).unwrap_or(0)\n                }\n            })\n            .sum()\n    })\n}\n\npub(crate) fn wire_cache_info(window: &MainWindow) {\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_refresh_diag_cache_info(move || {\n            let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n\n            if win.global::<AppState>().get_diag_refresh_running() {\n                return;\n            }\n            win.global::<AppState>().set_diag_refresh_running(true);\n\n            let weak2 = win.as_weak();\n            std::thread::spawn(move || {\n                let thumb_dir = thumbnail_cache_dir();\n                let thumb_size = dir_size_recursive(&thumb_dir);\n\n                let app_cache_size = czkawka_core::common::config_cache_path::get_config_cache_path().map_or(0, |p| dir_size_recursive(&p.cache_folder));\n\n                let _ = slint::invoke_from_event_loop(move || {\n                    if let Some(win) = weak2.upgrade() {\n                        win.global::<AppState>()\n                            .set_diag_thumbnails_size(humansize::format_size(thumb_size, humansize::BINARY).into());\n                        win.global::<AppState>()\n                            .set_diag_app_cache_size(humansize::format_size(app_cache_size, humansize::BINARY).into());\n                        win.global::<AppState>().set_diag_refresh_running(false);\n                    }\n                });\n            });\n        });\n    }\n\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_clear_thumbnails_cache(move || {\n            let thumb_dir = thumbnail_cache_dir();\n            if let Ok(entries) = std::fs::read_dir(&thumb_dir) {\n                for entry in entries.flatten() {\n                    let _ = std::fs::remove_file(entry.path());\n                }\n            }\n            if let Some(win) = weak.upgrade() {\n                win.global::<AppState>().set_diag_thumbnails_size(\"0 B\".into());\n            }\n        });\n    }\n\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_clear_app_cache(move || {\n            if let Some(cache_path) = czkawka_core::common::config_cache_path::get_config_cache_path() {\n                let _ = std::fs::remove_dir_all(&cache_path.cache_folder);\n            }\n            if let Some(win) = weak.upgrade() {\n                win.global::<AppState>().set_diag_app_cache_size(\"0 B\".into());\n            }\n        });\n    }\n}\n\npub(crate) fn wire_language_change(window: &MainWindow) {\n    let weak = window.as_weak();\n    window.global::<AppState>().on_apply_language_change(move || {\n        let win = weak.upgrade().expect(\"MainWindow dropped in on_apply_language_change\");\n        let idx = win.global::<GeneralSettings>().get_language_idx();\n        let lang = if idx == 1 { \"pl\" } else { \"en\" };\n        crate::localizer_cedinia::apply_language_preference(lang);\n        crate::translations::translate_items(&win);\n    });\n}\n\npub(crate) fn wire_open_url(window: &MainWindow) {\n    #[cfg(not(target_os = \"android\"))]\n    {\n        window.global::<AppState>().on_open_url(|url| {\n            let _ = std::process::Command::new(\"xdg-open\").arg(url.as_str()).spawn();\n        });\n    }\n    #[cfg(target_os = \"android\")]\n    {\n        window.global::<AppState>().on_open_url(|_| {});\n    }\n}\n\npub(crate) fn wire_save_settings_now(window: &MainWindow, included_dirs: Rc<std::cell::RefCell<Vec<PathBuf>>>, excluded_dirs: Rc<std::cell::RefCell<Vec<PathBuf>>>) {\n    let weak = window.as_weak();\n    window.global::<AppState>().on_save_settings_now(move || {\n        let win = weak.upgrade().expect(\"Failed to upgrade app :(\");\n        let settings = collect_settings_from_gui(&win);\n        save_settings(&settings);\n        crate::settings::save_dirs(&included_dirs.borrow(), &excluded_dirs.borrow());\n    });\n}\n"
  },
  {
    "path": "cedinia/src/callbacks/scan.rs",
    "content": "use std::path::PathBuf;\nuse std::rc::Rc;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicU32, Ordering};\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::re_exported::{FilterType, HashAlg};\nuse czkawka_core::tools::big_file::SearchMode;\nuse czkawka_core::tools::similar_images::SimilarityPreset;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel};\n\nuse crate::scan_runner::{CommonFilters, ScanRequest};\nuse crate::settings::gui_settings_values::StringComboBoxItems;\nuse crate::{\n    ActiveTool, AppState, BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, FileEntry, GeneralSettings, MainWindow, SameMusicSettings, ScanState,\n    SimilarGroupCard, SimilarImagesSettings,\n};\n\npub(crate) fn wire_scan(\n    window: &MainWindow,\n    stop_flag: Arc<AtomicBool>,\n    scan_tx: Rc<Sender<ScanRequest>>,\n    included_dirs: Rc<std::cell::RefCell<Vec<PathBuf>>>,\n    scan_gen: Arc<AtomicU32>,\n) {\n    {\n        let weak = window.as_weak();\n        let inc = included_dirs;\n        let stop = stop_flag.clone();\n        let tx = scan_tx.clone();\n        let scan_gen2 = scan_gen;\n        window.global::<AppState>().on_scan_requested(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_scan_requested\");\n            scan_gen2.fetch_add(1, Ordering::SeqCst);\n            win.global::<AppState>().set_scan_state(ScanState::Scanning);\n            win.global::<AppState>().set_status_message(SharedString::from(\"Skanowanie…\"));\n            stop.store(false, Ordering::Relaxed);\n            let dirs = inc.borrow().clone();\n            let tool = win.global::<AppState>().get_active_tool();\n            clear_tool_results(&win, tool);\n            let req = build_scan_request(&win, tool, dirs);\n            let _ = tx.send(req);\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let stop = stop_flag;\n        let tx = scan_tx;\n        window.global::<AppState>().on_stop_requested(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_stop_requested\");\n            stop.store(true, Ordering::Relaxed);\n            let _ = tx.send(ScanRequest::Stop);\n            win.global::<AppState>().set_scan_state(ScanState::Stopping);\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_tool_changed(move |_| {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_tool_changed\");\n            win.global::<AppState>().set_selected_count(0);\n            win.global::<AppState>().set_status_message(SharedString::default());\n        });\n    }\n}\n\nfn empty_entries() -> ModelRc<FileEntry> {\n    ModelRc::new(VecModel::from(vec![]))\n}\n\nfn clear_tool_results(win: &MainWindow, tool: ActiveTool) {\n    match tool {\n        ActiveTool::DuplicateFiles => win.set_duplicate_files_model(empty_entries()),\n        ActiveTool::EmptyFolders => win.set_empty_folder_model(empty_entries()),\n        ActiveTool::SimilarImages => {\n            win.set_similar_images_model(empty_entries());\n            win.set_similar_images_groups(ModelRc::new(VecModel::<SimilarGroupCard>::from(vec![])));\n        }\n        ActiveTool::EmptyFiles => win.set_empty_files_model(empty_entries()),\n        ActiveTool::TemporaryFiles => win.set_temporary_files_model(empty_entries()),\n        ActiveTool::BigFiles => win.set_big_files_model(empty_entries()),\n        ActiveTool::BrokenFiles => win.set_broken_files_model(empty_entries()),\n        ActiveTool::BadExtensions => win.set_bad_extensions_model(empty_entries()),\n        ActiveTool::SameMusic => win.set_same_music_model(empty_entries()),\n        ActiveTool::BadNames => win.set_bad_names_model(empty_entries()),\n        ActiveTool::ExifRemover => win.set_exif_remover_model(empty_entries()),\n        ActiveTool::Home | ActiveTool::Directories | ActiveTool::Settings => {}\n    }\n    win.global::<AppState>().set_selected_count(0);\n}\n\nfn build_common_filters(win: &MainWindow) -> CommonFilters {\n    let g = win.global::<GeneralSettings>();\n    let items = StringComboBoxItems::new();\n    let min_file_size_bytes = items.min_file_size.get(g.get_min_file_size_idx() as usize).map_or(0, |e| e.value.to_bytes());\n    let max_file_size_bytes = items.max_file_size.get(g.get_max_file_size_idx() as usize).and_then(|e| e.value.to_bytes());\n    let split_csv = |s: slint::SharedString| -> Vec<String> { s.as_str().split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect() };\n    let mut excluded_items = split_csv(g.get_excluded_items());\n    let cache_dir = crate::thumbnail_loader::thumbnail_cache_dir();\n    if let Some(s) = cache_dir.to_str() {\n        excluded_items.push(format!(\"{s}/*\"));\n    }\n    if g.get_ignore_hidden() {\n        excluded_items.push(\"*/.*\".to_string());\n        excluded_items.push(\"*/.*/*\".to_string());\n    }\n    CommonFilters {\n        excluded_items,\n        allowed_extensions: split_csv(g.get_allowed_extensions()),\n        excluded_extensions: split_csv(g.get_excluded_extensions()),\n        min_file_size_bytes,\n        max_file_size_bytes,\n    }\n}\n\nfn build_scan_request(win: &MainWindow, tool: ActiveTool, dirs: Vec<PathBuf>) -> ScanRequest {\n    let filters = build_common_filters(win);\n    let items = StringComboBoxItems::new();\n\n    let duplicate_request = || {\n        let d = win.global::<DuplicateSettings>();\n        ScanRequest::DuplicateFiles {\n            dirs: dirs.clone(),\n            check_method: StringComboBoxItems::value_from_idx(&items.duplicates_check_method, d.get_check_method(), CheckingMethod::Hash),\n            hash_type: StringComboBoxItems::value_from_idx(&items.duplicates_hash_type, d.get_hash_type(), HashType::Blake3),\n            use_cache: win.global::<GeneralSettings>().get_use_cache(),\n            filters: filters.clone(),\n        }\n    };\n\n    match tool {\n        ActiveTool::DuplicateFiles => duplicate_request(),\n        ActiveTool::EmptyFolders => ScanRequest::EmptyFolders { dirs, filters },\n        ActiveTool::SimilarImages => {\n            let s = win.global::<SimilarImagesSettings>();\n            ScanRequest::SimilarImages {\n                dirs,\n                similarity_preset: StringComboBoxItems::value_from_idx(&items.similarity_preset, s.get_similarity_preset(), SimilarityPreset::Medium),\n                hash_size: StringComboBoxItems::value_from_idx(&items.hash_size, s.get_hash_size_idx(), 16),\n                hash_alg: StringComboBoxItems::value_from_idx(&items.hash_alg, s.get_hash_alg_idx(), HashAlg::Mean),\n                image_filter: StringComboBoxItems::value_from_idx(&items.image_filter, s.get_image_filter_idx(), FilterType::Triangle),\n                ignore_same_size: s.get_ignore_same_size(),\n                filters,\n            }\n        }\n        ActiveTool::EmptyFiles => ScanRequest::EmptyFiles { dirs, filters },\n        ActiveTool::TemporaryFiles => ScanRequest::TemporaryFiles { dirs, filters },\n        ActiveTool::BigFiles => {\n            let b = win.global::<BigFilesSettings>();\n            ScanRequest::BigFiles {\n                dirs,\n                search_mode: StringComboBoxItems::value_from_idx(&items.biggest_files_method, b.get_search_mode_idx(), SearchMode::BiggestFiles),\n                count: StringComboBoxItems::value_from_idx(&items.big_files_count, b.get_count_idx(), 50),\n                filters,\n            }\n        }\n        ActiveTool::BrokenFiles => {\n            let b = win.global::<BrokenFilesSettings>();\n            use czkawka_core::tools::broken_files::CheckedTypes;\n            let mut types = CheckedTypes::empty();\n            if b.get_check_audio() {\n                types |= CheckedTypes::AUDIO;\n            }\n            if b.get_check_pdf() {\n                types |= CheckedTypes::PDF;\n            }\n            if b.get_check_archive() {\n                types |= CheckedTypes::ARCHIVE;\n            }\n            if b.get_check_image() {\n                types |= CheckedTypes::IMAGE;\n            }\n            ScanRequest::BrokenFiles {\n                dirs,\n                filters,\n                checked_types: types.bits(),\n            }\n        }\n        ActiveTool::BadExtensions => ScanRequest::BadExtensions { dirs, filters },\n        ActiveTool::SameMusic => {\n            let m = win.global::<SameMusicSettings>();\n            use czkawka_core::tools::same_music::MusicSimilarity;\n            let mut sim = MusicSimilarity::NONE;\n            if m.get_title() {\n                sim |= MusicSimilarity::TRACK_TITLE;\n            }\n            if m.get_artist() {\n                sim |= MusicSimilarity::TRACK_ARTIST;\n            }\n            if m.get_year() {\n                sim |= MusicSimilarity::YEAR;\n            }\n            if m.get_length() {\n                sim |= MusicSimilarity::LENGTH;\n            }\n            if m.get_genre() {\n                sim |= MusicSimilarity::GENRE;\n            }\n            if m.get_bitrate() {\n                sim |= MusicSimilarity::BITRATE;\n            }\n            if sim.is_empty() {\n                sim = MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST;\n            }\n            ScanRequest::SameMusic {\n                dirs,\n                filters,\n                music_similarity: sim.bits(),\n                approximate: m.get_approximate(),\n                check_method: StringComboBoxItems::value_from_idx(&items.same_music_check_method, m.get_check_method_idx(), CheckingMethod::AudioTags),\n            }\n        }\n        ActiveTool::BadNames => {\n            let bn = win.global::<BadNamesSettings>();\n            ScanRequest::BadNames {\n                dirs,\n                filters,\n                uppercase_extension: bn.get_uppercase_extension(),\n                emoji_used: bn.get_emoji_used(),\n                space_at_start_or_end: bn.get_space_at_start_or_end(),\n                non_ascii_graphical: bn.get_non_ascii_graphical(),\n                remove_duplicated_non_alpha: bn.get_remove_duplicated_non_alpha(),\n            }\n        }\n        ActiveTool::ExifRemover => ScanRequest::ExifRemover { dirs, filters },\n        ActiveTool::Home | ActiveTool::Directories | ActiveTool::Settings => {\n            unreachable!(\"scan cannot be triggered from Home/Directories/Settings tab\")\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/src/callbacks/selection.rs",
    "content": "use std::rc::Rc;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\n\nuse slint::{ComponentHandle, Model, ModelRc, VecModel};\n\nuse crate::common::{INT_IDX_SIZE_HI, INT_IDX_SIZE_LO, IntDataSimilarImages, STR_IDX_NAME, STR_IDX_PATH, StrDataBadExtensions, StrDataBadNames};\nuse crate::model::{count_checked, toggle_row};\nuse crate::{ActiveTool, AppState, FileEntry, MainWindow, SimilarGroupCard, SimilarImageItem};\n\n#[cfg(not(target_os = \"android\"))]\nfn delete_path(path: &str) -> Result<(), String> {\n    trash::delete(path).map_err(|e| e.to_string())\n}\n\n#[cfg(target_os = \"android\")]\nfn delete_path(path: &str) -> Result<(), String> {\n    std::fs::remove_file(path).or_else(|_| std::fs::remove_dir_all(path)).map_err(|e| e.to_string())\n}\n\npub(crate) enum DeleteEvent {\n    Progress(usize, usize),\n\n    Finished(Vec<String>, Vec<String>),\n\n    ListDeleteFinished(Vec<String>, Vec<String>),\n\n    ListRenameFinished(usize, Vec<String>),\n\n    ExifCleanFinished(Vec<String>, Vec<String>),\n}\n\nfn vm_of(model: &ModelRc<FileEntry>) -> Option<&VecModel<FileEntry>> {\n    model.as_any().downcast_ref::<VecModel<FileEntry>>()\n}\n\nfn size_from_entry(e: &FileEntry) -> u64 {\n    let hi = get_val_int(e, INT_IDX_SIZE_HI) as u64;\n    let lo = get_val_int(e, INT_IDX_SIZE_LO) as u64;\n    (hi << 32) | (lo & 0xFFFF_FFFF)\n}\n\nfn get_val_str(e: &FileEntry, idx: usize) -> String {\n    e.val_str.row_data(idx).map(|s| s.to_string()).unwrap_or_default()\n}\n\nfn get_val_int(e: &FileEntry, idx: usize) -> i32 {\n    e.val_int.row_data(idx).unwrap_or(0)\n}\n\nfn full_path_of(e: &FileEntry) -> String {\n    let name = get_val_str(e, STR_IDX_NAME);\n    let path = get_val_str(e, STR_IDX_PATH);\n    if path.is_empty() { name } else { format!(\"{path}/{name}\") }\n}\n\nfn execute_delete_selected(win: &MainWindow, tx: std::sync::mpsc::Sender<DeleteEvent>) {\n    let tool = win.global::<AppState>().get_active_tool();\n    let model = get_model_for_tool(win, tool);\n    let Some(vm) = vm_of(&model) else { return };\n\n    let items: Vec<FileEntry> = vm.iter().collect();\n    let to_delete: Vec<(usize, String)> = items\n        .iter()\n        .enumerate()\n        .filter(|(_, e)| e.checked && !e.is_header)\n        .map(|(i, e)| (i, full_path_of(e)))\n        .collect();\n\n    if to_delete.is_empty() {\n        return;\n    }\n\n    let total = to_delete.len();\n    win.global::<AppState>().set_delete_running(true);\n    win.global::<AppState>().set_delete_progress_text(slint::SharedString::from(format!(\"0 / {total}\")));\n\n    std::thread::spawn(move || {\n        let mut deleted_paths: Vec<String> = Vec::new();\n        let mut errors: Vec<String> = Vec::new();\n\n        for (i, (_idx, path)) in to_delete.iter().enumerate() {\n            match delete_path(path) {\n                Ok(()) => {\n                    deleted_paths.push(path.clone());\n                }\n                Err(err) => errors.push(format!(\"{path}\\n  {err}\")),\n            }\n            if i % 5 == 4 || i + 1 == total {\n                let _ = tx.send(DeleteEvent::Progress(i + 1, total));\n            }\n        }\n        let _ = tx.send(DeleteEvent::ListDeleteFinished(deleted_paths, errors));\n    });\n}\n\nfn execute_rename_selected(win: &MainWindow, tx: std::sync::mpsc::Sender<DeleteEvent>) {\n    let model = win.get_bad_extensions_model();\n    let Some(vm) = vm_of(&model) else { return };\n\n    let items: Vec<FileEntry> = vm.iter().collect();\n    let to_rename: Vec<(usize, String, String)> = items\n        .iter()\n        .enumerate()\n        .filter(|(_, e)| e.checked && !e.is_header)\n        .filter_map(|(i, e)| {\n            let full = full_path_of(e);\n            let proper_ext = get_val_str(e, StrDataBadExtensions::ProperExtension as usize);\n            if proper_ext.is_empty() {\n                return None;\n            }\n            Some((i, full, proper_ext))\n        })\n        .collect();\n\n    if to_rename.is_empty() {\n        return;\n    }\n\n    let total = to_rename.len();\n    win.global::<AppState>().set_delete_running(true);\n    win.global::<AppState>().set_delete_progress_text(slint::SharedString::from(format!(\"0 / {total}\")));\n\n    std::thread::spawn(move || {\n        let mut renamed_indices: Vec<usize> = Vec::new();\n        let mut errors: Vec<String> = Vec::new();\n\n        for (i, (idx, full, proper_ext)) in to_rename.iter().enumerate() {\n            let src = std::path::Path::new(full.as_str());\n            let new_path = match src.file_stem() {\n                Some(stem) => {\n                    let parent = src.parent().unwrap_or(std::path::Path::new(\"\"));\n                    parent.join(format!(\"{}.{}\", stem.to_string_lossy(), proper_ext))\n                }\n                None => {\n                    errors.push(format!(\"{full}\\n  Nie można odczytać nazwy pliku\"));\n                    continue;\n                }\n            };\n            match std::fs::rename(full, &new_path) {\n                Ok(()) => renamed_indices.push(*idx),\n                Err(err) => errors.push(format!(\"{full}\\n  {err}\")),\n            }\n            if i % 5 == 4 || i + 1 == total {\n                let _ = tx.send(DeleteEvent::Progress(i + 1, total));\n            }\n        }\n        let renamed = renamed_indices.len();\n        let _ = tx.send(DeleteEvent::ListRenameFinished(renamed, errors));\n    });\n}\n\nfn execute_rename_bad_names(win: &MainWindow, tx: std::sync::mpsc::Sender<DeleteEvent>) {\n    let model = win.get_bad_names_model();\n    let Some(vm) = vm_of(&model) else { return };\n\n    let items: Vec<FileEntry> = vm.iter().collect();\n    let to_rename: Vec<(usize, String, String)> = items\n        .iter()\n        .enumerate()\n        .filter(|(_, e)| e.checked && !e.is_header)\n        .filter_map(|(i, e)| {\n            let new_name = get_val_str(e, StrDataBadNames::NewName as usize);\n            if new_name.is_empty() {\n                return None;\n            }\n            let full = full_path_of(e);\n            Some((i, full, new_name))\n        })\n        .collect();\n\n    if to_rename.is_empty() {\n        return;\n    }\n\n    let total = to_rename.len();\n    win.global::<AppState>().set_delete_running(true);\n    win.global::<AppState>().set_delete_progress_text(slint::SharedString::from(format!(\"0 / {total}\")));\n\n    std::thread::spawn(move || {\n        let mut renamed_count = 0usize;\n        let mut errors: Vec<String> = Vec::new();\n\n        for (i, (_idx, full, new_name)) in to_rename.iter().enumerate() {\n            let src = std::path::Path::new(full.as_str());\n            let new_path = match src.parent() {\n                Some(parent) => parent.join(new_name),\n                None => {\n                    errors.push(format!(\"{full}\\n  Nie można odczytać katalogu\"));\n                    continue;\n                }\n            };\n            match std::fs::rename(full, &new_path) {\n                Ok(()) => renamed_count += 1,\n                Err(err) => errors.push(format!(\"{full}\\n  {err}\")),\n            }\n            if i % 5 == 4 || i + 1 == total {\n                let _ = tx.send(DeleteEvent::Progress(i + 1, total));\n            }\n        }\n        let _ = tx.send(DeleteEvent::ListRenameFinished(renamed_count, errors));\n    });\n}\nfn execute_clean_exif_selected(win: &MainWindow, tx: std::sync::mpsc::Sender<DeleteEvent>) {\n    let model = win.get_exif_remover_model();\n    let Some(vm) = vm_of(&model) else { return };\n\n    let items: Vec<FileEntry> = vm.iter().collect();\n    let to_clean: Vec<(usize, String)> = items\n        .iter()\n        .enumerate()\n        .filter(|(_, e)| e.checked && !e.is_header)\n        .map(|(i, e)| (i, full_path_of(e)))\n        .collect();\n\n    if to_clean.is_empty() {\n        return;\n    }\n\n    let total = to_clean.len();\n    win.global::<AppState>().set_delete_running(true);\n    win.global::<AppState>().set_delete_progress_text(slint::SharedString::from(format!(\"0 / {total}\")));\n\n    std::thread::spawn(move || {\n        let mut cleaned_paths: Vec<String> = Vec::new();\n        let mut errors: Vec<String> = Vec::new();\n\n        for (i, (_idx, path)) in to_clean.iter().enumerate() {\n            match clean_exif_all_tags(path) {\n                Ok(()) => cleaned_paths.push(path.clone()),\n                Err(e) => errors.push(format!(\"{path}\\n  {e}\")),\n            }\n            if i % 5 == 4 || i + 1 == total {\n                let _ = tx.send(DeleteEvent::Progress(i + 1, total));\n            }\n        }\n        let _ = tx.send(DeleteEvent::ExifCleanFinished(cleaned_paths, errors));\n    });\n}\n\nfn clean_exif_all_tags(path: &str) -> Result<(), String> {\n    use czkawka_core::tools::exif_remover::core::{clean_exif_tags, extract_exif_tags_public};\n    let tags = extract_exif_tags_public(std::path::Path::new(path))?;\n    clean_exif_tags(path, &tags, true).map(|_| ())\n}\n\npub(crate) fn wire_selection(window: &MainWindow, delete_tx: std::sync::mpsc::Sender<DeleteEvent>, delete_stop: Rc<std::cell::RefCell<Arc<AtomicBool>>>) {\n    {\n        let weak = window.as_weak();\n        let tx = delete_tx.clone();\n        window.global::<AppState>().on_clean_exif_selected(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_clean_exif_selected\");\n            let model = win.get_exif_remover_model();\n            let n = count_checked(&model);\n            if n == 0 {\n                return;\n            }\n            let state = win.global::<AppState>();\n            state.set_confirm_popup_message(slint::SharedString::from(format!(\"Czy na pewno chcesz wyczyścić tagi EXIF z {n} zaznaczonych plików?\")));\n            state.set_confirm_popup_action(slint::SharedString::from(\"clean_exif\"));\n            state.set_confirm_popup_visible(true);\n            let _ = tx.clone();\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let tx = delete_tx.clone();\n        window.global::<AppState>().on_delete_selected(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_delete_selected\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            let n = count_checked(&model);\n            if n == 0 {\n                return;\n            }\n            let state = win.global::<AppState>();\n            state.set_confirm_popup_message(slint::SharedString::from(format!(\"Czy na pewno chcesz usunąć {n} zaznaczonych elementów?\")));\n            state.set_confirm_popup_action(slint::SharedString::from(\"delete\"));\n            state.set_confirm_popup_visible(true);\n            let _ = tx.clone();\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_all(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_all\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            set_all_checked(&model, true);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_deselect_all(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_deselect_all\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            set_all_checked(&model, false);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(0);\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_all_except_one(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_all_except_one\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_except_one_per_group(&model, true);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_deselect_all_except_one(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_deselect_all_except_one\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_except_one_per_group(&model, false);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_invert_selection(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_invert_selection\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            if let Some(vm) = vm_of(&model) {\n                let mut items: Vec<FileEntry> = vm.iter().collect::<Vec<_>>();\n                for e in &mut items {\n                    if !e.is_header {\n                        e.checked = !e.checked;\n                    }\n                }\n                vm.set_vec(items);\n            }\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_largest_per_group(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_largest_per_group\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_largest_per_group(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_all_except_largest(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_all_except_largest\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_all_except_largest(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_smallest_per_group(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_smallest_per_group\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_smallest_per_group(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_all_except_smallest(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_all_except_smallest\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_all_except_smallest(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_highest_resolution_per_group(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_highest_resolution_per_group\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_highest_resolution_per_group(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_all_except_highest_resolution(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_all_except_highest_resolution\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_all_except_highest_resolution(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_lowest_resolution_per_group(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_lowest_resolution_per_group\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_lowest_resolution_per_group(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_select_all_except_lowest_resolution(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_select_all_except_lowest_resolution\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            select_all_except_lowest_resolution(&model);\n            sync_gallery_if_similar(&win, tool);\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_toggle_file_checked(move |idx| {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_toggle_file_checked\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            toggle_row(&model, idx as usize);\n            if tool == ActiveTool::SimilarImages {\n                sync_gallery_checked_from_flat(&win);\n            }\n            win.global::<AppState>().set_selected_count(count_checked(&model));\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_request_gallery_delete(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_request_gallery_delete\");\n            let groups: Vec<SimilarGroupCard> = win.get_similar_images_groups().iter().collect::<Vec<_>>();\n\n            let mut total_images = 0i32;\n            let mut total_groups = 0i32;\n            let mut unsafe_groups = 0i32;\n\n            for group in &groups {\n                let items: Vec<SimilarImageItem> = group.items.iter().collect::<Vec<_>>();\n                let checked = items.iter().filter(|it| it.checked).count();\n                if checked > 0 {\n                    total_groups += 1;\n                    total_images += checked as i32;\n                    if checked == items.len() {\n                        unsafe_groups += 1;\n                    }\n                }\n            }\n\n            let msg = slint::SharedString::from(format!(\"Zamierzasz usunąć {total_images} obrazów w {total_groups} grupach?\"));\n            let warn = if unsafe_groups > 0 {\n                slint::SharedString::from(format!(\"⚠ W {unsafe_groups} grupach zaznaczono wszystkie elementy!\"))\n            } else {\n                slint::SharedString::default()\n            };\n\n            let state = win.global::<AppState>();\n            state.set_gallery_delete_message(msg);\n            state.set_gallery_delete_warning(warn);\n            state.set_gallery_delete_popup_visible(true);\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let tx = delete_tx.clone();\n        let stop_cell = Rc::clone(&delete_stop);\n        window.global::<AppState>().on_confirm_gallery_delete(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_confirm_gallery_delete\");\n\n            let files: Vec<String> = win\n                .get_similar_images_groups()\n                .iter()\n                .flat_map(|g: SimilarGroupCard| {\n                    g.items\n                        .iter()\n                        .filter(|it: &SimilarImageItem| it.checked)\n                        .map(|it: SimilarImageItem| it.full_path.to_string())\n                        .collect::<Vec<_>>()\n                })\n                .collect::<Vec<_>>();\n\n            if files.is_empty() {\n                win.global::<AppState>().set_gallery_delete_popup_visible(false);\n                return;\n            }\n\n            let new_stop = Arc::new(AtomicBool::new(false));\n            *stop_cell.borrow_mut() = new_stop.clone();\n\n            let state = win.global::<AppState>();\n            state.set_gallery_delete_popup_visible(false);\n            state.set_delete_running(true);\n            state.set_delete_progress_text(slint::SharedString::from(format!(\"0 / {}\", files.len())));\n\n            let tx = tx.clone();\n            let total = files.len();\n            std::thread::spawn(move || {\n                let mut deleted: Vec<String> = Vec::new();\n                let mut errors: Vec<String> = Vec::new();\n\n                for (i, path) in files.iter().enumerate() {\n                    if new_stop.load(Ordering::Relaxed) {\n                        break;\n                    }\n                    match delete_path(path) {\n                        Ok(()) => deleted.push(path.clone()),\n                        Err(e) => errors.push(format!(\"{path}\\n  {e}\")),\n                    }\n                    if i % 5 == 4 || i + 1 == total {\n                        let _ = tx.send(DeleteEvent::Progress(i + 1, total));\n                    }\n                }\n                let _ = tx.send(DeleteEvent::Finished(deleted, errors));\n            });\n        });\n    }\n    {\n        window.global::<AppState>().on_delete_stop_requested(move || {\n            delete_stop.borrow().store(true, Ordering::Relaxed);\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let tx = delete_tx.clone();\n        window.global::<AppState>().on_rename_selected(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_rename_selected\");\n            let tool = win.global::<AppState>().get_active_tool();\n            let model = get_model_for_tool(&win, tool);\n            let n = count_checked(&model);\n            if n == 0 {\n                return;\n            }\n            let state = win.global::<AppState>();\n            state.set_confirm_popup_message(slint::SharedString::from(format!(\"Czy na pewno chcesz zmienić nazwy {n} zaznaczonych plików?\")));\n            let action = if tool == ActiveTool::BadNames { \"rename_bad_names\" } else { \"rename\" };\n            state.set_confirm_popup_action(slint::SharedString::from(action));\n            state.set_confirm_popup_visible(true);\n            let _ = tx.clone();\n        });\n    }\n    {\n        let weak = window.as_weak();\n        let tx_confirm = delete_tx;\n        window.global::<AppState>().on_confirm_popup_ok(move || {\n            let win = weak.upgrade().expect(\"MainWindow dropped in on_confirm_popup_ok\");\n            let action = win.global::<AppState>().get_confirm_popup_action().to_string();\n            win.global::<AppState>().set_confirm_popup_visible(false);\n            match action.as_str() {\n                \"delete\" => execute_delete_selected(&win, tx_confirm.clone()),\n                \"rename\" => execute_rename_selected(&win, tx_confirm.clone()),\n                \"rename_bad_names\" => execute_rename_bad_names(&win, tx_confirm.clone()),\n                \"clean_exif\" => execute_clean_exif_selected(&win, tx_confirm.clone()),\n                _ => {}\n            }\n        });\n    }\n    {\n        let weak = window.as_weak();\n        window.global::<AppState>().on_confirm_popup_cancel(move || {\n            weak.upgrade()\n                .expect(\"MainWindow dropped in on_confirm_popup_cancel\")\n                .global::<AppState>()\n                .set_confirm_popup_visible(false);\n        });\n    }\n}\n\npub(crate) fn get_model_for_tool(win: &MainWindow, tool: ActiveTool) -> ModelRc<FileEntry> {\n    match tool {\n        ActiveTool::DuplicateFiles => win.get_duplicate_files_model(),\n        ActiveTool::EmptyFolders => win.get_empty_folder_model(),\n        ActiveTool::SimilarImages => win.get_similar_images_model(),\n        ActiveTool::EmptyFiles => win.get_empty_files_model(),\n        ActiveTool::TemporaryFiles => win.get_temporary_files_model(),\n        ActiveTool::BigFiles => win.get_big_files_model(),\n        ActiveTool::BrokenFiles => win.get_broken_files_model(),\n        ActiveTool::BadExtensions => win.get_bad_extensions_model(),\n        ActiveTool::SameMusic => win.get_same_music_model(),\n        ActiveTool::BadNames => win.get_bad_names_model(),\n        ActiveTool::ExifRemover => win.get_exif_remover_model(),\n        ActiveTool::Home | ActiveTool::Directories | ActiveTool::Settings => ModelRc::new(VecModel::from(vec![])),\n    }\n}\n\npub(crate) fn set_all_checked(model: &ModelRc<FileEntry>, state: bool) {\n    if let Some(vm) = vm_of(model) {\n        let mut items: Vec<FileEntry> = vm.iter().collect::<Vec<_>>();\n        for e in &mut items {\n            if !e.is_header {\n                e.checked = state;\n            }\n        }\n        vm.set_vec(items);\n    }\n}\n\npub(crate) fn select_except_one_per_group(model: &ModelRc<FileEntry>, select: bool) {\n    let Some(vm) = vm_of(model) else { return };\n    let mut items: Vec<FileEntry> = vm.iter().collect::<Vec<_>>();\n    let has_headers = items.iter().any(|e| e.is_header);\n\n    if !has_headers {\n        for e in &mut items {\n            if !e.is_header {\n                e.checked = select;\n            }\n        }\n        vm.set_vec(items);\n        return;\n    }\n\n    if select {\n        let mut first_in_group = false;\n        for e in &mut items {\n            if e.is_header {\n                first_in_group = true;\n                continue;\n            }\n            e.checked = if first_in_group {\n                first_in_group = false;\n                false\n            } else {\n                true\n            };\n        }\n    } else {\n        let mut i = 0;\n        while i < items.len() {\n            if items[i].is_header {\n                let group_end = items[i + 1..].iter().position(|e| e.is_header).map_or(items.len(), |p| i + 1 + p);\n                let checked_count = items[i + 1..group_end].iter().filter(|e| e.checked).count();\n                if checked_count >= 2 {\n                    let mut kept = false;\n                    for j in i + 1..group_end {\n                        if items[j].checked {\n                            if kept {\n                                items[j].checked = false;\n                            } else {\n                                kept = true;\n                            }\n                        }\n                    }\n                }\n                i = group_end;\n                continue;\n            }\n            i += 1;\n        }\n    }\n\n    vm.set_vec(items);\n}\n\nfn sync_gallery_if_similar(win: &MainWindow, tool: ActiveTool) {\n    if tool == ActiveTool::SimilarImages {\n        sync_gallery_checked_from_flat(win);\n    }\n}\n\npub(crate) fn select_largest_per_group(model: &ModelRc<FileEntry>) {\n    select_by_size_per_group(model, true, true);\n}\n\npub(crate) fn select_all_except_largest(model: &ModelRc<FileEntry>) {\n    select_by_size_per_group(model, true, false);\n}\n\npub(crate) fn select_smallest_per_group(model: &ModelRc<FileEntry>) {\n    select_by_size_per_group(model, false, true);\n}\n\npub(crate) fn select_all_except_smallest(model: &ModelRc<FileEntry>) {\n    select_by_size_per_group(model, false, false);\n}\n\nfn select_by_size_per_group(model: &ModelRc<FileEntry>, largest: bool, select_target: bool) {\n    let Some(vm) = vm_of(model) else { return };\n    let mut items: Vec<FileEntry> = vm.iter().collect();\n\n    let mut i = 0;\n    while i < items.len() {\n        if items[i].is_header {\n            let group_end = items[i + 1..].iter().position(|e| e.is_header).map_or(items.len(), |p| i + 1 + p);\n\n            let target_idx = if largest {\n                items[i + 1..group_end].iter().enumerate().max_by_key(|(_, e)| size_from_entry(e)).map(|(j, _)| i + 1 + j)\n            } else {\n                items[i + 1..group_end].iter().enumerate().min_by_key(|(_, e)| size_from_entry(e)).map(|(j, _)| i + 1 + j)\n            };\n\n            for j in i + 1..group_end {\n                let is_target = target_idx == Some(j);\n                items[j].checked = if select_target { is_target } else { !is_target };\n            }\n\n            i = group_end;\n            continue;\n        }\n        i += 1;\n    }\n\n    vm.set_vec(items);\n}\n\nfn resolution_from_entry(e: &FileEntry) -> u64 {\n    let w = get_val_int(e, IntDataSimilarImages::Width as usize) as u64;\n    let h = get_val_int(e, IntDataSimilarImages::Height as usize) as u64;\n    w * h\n}\n\nfn select_by_resolution_per_group(model: &ModelRc<FileEntry>, highest: bool, select_target: bool) {\n    let Some(vm) = vm_of(model) else { return };\n    let mut items: Vec<FileEntry> = vm.iter().collect();\n\n    let mut i = 0;\n    while i < items.len() {\n        if items[i].is_header {\n            let group_end = items[i + 1..].iter().position(|e| e.is_header).map_or(items.len(), |p| i + 1 + p);\n\n            let target_idx = if highest {\n                items[i + 1..group_end]\n                    .iter()\n                    .enumerate()\n                    .max_by_key(|(_, e)| resolution_from_entry(e))\n                    .map(|(j, _)| i + 1 + j)\n            } else {\n                items[i + 1..group_end]\n                    .iter()\n                    .enumerate()\n                    .min_by_key(|(_, e)| resolution_from_entry(e))\n                    .map(|(j, _)| i + 1 + j)\n            };\n\n            for j in i + 1..group_end {\n                let is_target = target_idx == Some(j);\n                items[j].checked = if select_target { is_target } else { !is_target };\n            }\n\n            i = group_end;\n            continue;\n        }\n        i += 1;\n    }\n    vm.set_vec(items);\n}\n\npub(crate) fn select_highest_resolution_per_group(model: &ModelRc<FileEntry>) {\n    select_by_resolution_per_group(model, true, true);\n}\npub(crate) fn select_all_except_highest_resolution(model: &ModelRc<FileEntry>) {\n    select_by_resolution_per_group(model, true, false);\n}\npub(crate) fn select_lowest_resolution_per_group(model: &ModelRc<FileEntry>) {\n    select_by_resolution_per_group(model, false, true);\n}\npub(crate) fn select_all_except_lowest_resolution(model: &ModelRc<FileEntry>) {\n    select_by_resolution_per_group(model, false, false);\n}\npub(crate) fn sync_gallery_checked_from_flat(win: &MainWindow) {\n    let flat: Vec<FileEntry> = win.get_similar_images_model().iter().collect::<Vec<_>>();\n    let groups: Vec<SimilarGroupCard> = win.get_similar_images_groups().iter().collect::<Vec<_>>();\n\n    for group in &groups {\n        let mut items: Vec<SimilarImageItem> = group.items.iter().collect::<Vec<_>>();\n        let mut changed = false;\n        for item in &mut items {\n            if let Some(entry) = flat.get(item.flat_idx as usize)\n                && item.checked != entry.checked\n            {\n                item.checked = entry.checked;\n                changed = true;\n            }\n        }\n        if changed && let Some(vm) = group.items.as_any().downcast_ref::<VecModel<SimilarImageItem>>() {\n            vm.set_vec(items);\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/src/callbacks.rs",
    "content": "mod directories;\nmod misc;\nmod scan;\nmod selection;\n\npub(crate) use directories::{build_dir_model, wire_directories};\npub(crate) use misc::{wire_cache_info, wire_collect_test, wire_language_change, wire_open_path, wire_open_url, wire_permission, wire_save_settings_now};\npub(crate) use scan::wire_scan;\npub(crate) use selection::{DeleteEvent, get_model_for_tool, wire_selection};\n"
  },
  {
    "path": "cedinia/src/common.rs",
    "content": "pub const STR_IDX_NAME: usize = 0;\npub const STR_IDX_PATH: usize = 1;\npub const STR_IDX_SIZE: usize = 2;\npub const STR_IDX_MODIFIED: usize = 3;\npub const STR_BASE_COUNT: usize = 4;\n\npub const INT_IDX_MOD_HI: usize = 0;\npub const INT_IDX_MOD_LO: usize = 1;\npub const INT_IDX_SIZE_HI: usize = 2;\npub const INT_IDX_SIZE_LO: usize = 3;\npub const INT_BASE_COUNT: usize = 4;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum StrDataSimilarImages {\n    DimsDisplay = STR_BASE_COUNT,\n}\npub const MAX_STR_DATA_SIMILAR_IMAGES: usize = StrDataSimilarImages::DimsDisplay as usize + 1;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum IntDataSimilarImages {\n    Width = INT_BASE_COUNT,\n    Height,\n    Diff,\n}\npub const MAX_INT_DATA_SIMILAR_IMAGES: usize = IntDataSimilarImages::Diff as usize + 1;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum StrDataBrokenFiles {\n    ErrorString = STR_BASE_COUNT,\n}\npub const MAX_STR_DATA_BROKEN_FILES: usize = StrDataBrokenFiles::ErrorString as usize + 1;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum StrDataBadExtensions {\n    Display = STR_BASE_COUNT,\n    ProperExtension,\n}\npub const MAX_STR_DATA_BAD_EXTENSIONS: usize = StrDataBadExtensions::ProperExtension as usize + 1;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum StrDataSameMusic {\n    Display = STR_BASE_COUNT,\n    Title,\n}\npub const MAX_STR_DATA_SAME_MUSIC: usize = StrDataSameMusic::Title as usize + 1;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum StrDataBadNames {\n    NewName = STR_BASE_COUNT,\n}\npub const MAX_STR_DATA_BAD_NAMES: usize = StrDataBadNames::NewName as usize + 1;\n\n#[repr(usize)]\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum IntDataExifRemover {\n    ExifTagCount = INT_BASE_COUNT,\n}\npub const MAX_INT_DATA_EXIF_REMOVER: usize = IntDataExifRemover::ExifTagCount as usize + 1;\n"
  },
  {
    "path": "cedinia/src/file_picker_android.rs",
    "content": "use std::ffi::c_void;\nuse std::sync::{Arc, Mutex};\n\nuse android_activity::AndroidApp;\nuse jni::objects::{Global, JClass, JObject, JString, JValue};\nuse jni::sys::jboolean;\nuse jni::{EnvUnowned, jni_sig, jni_str};\nuse slint::invoke_from_event_loop;\n\nstatic PENDING_PICK: std::sync::OnceLock<Arc<Mutex<Option<(String, bool)>>>> = std::sync::OnceLock::new();\n\nfn pending_pick() -> &'static Arc<Mutex<Option<(String, bool)>>> {\n    PENDING_PICK.get_or_init(|| Arc::new(Mutex::new(None)))\n}\n\n#[unsafe(no_mangle)]\n#[allow(non_snake_case)]\nunsafe extern \"system\" fn Java_CediniaFilePicker_onDirectoryPicked(mut unowned: EnvUnowned<'_>, _class: JClass<'_>, path: JString<'_>, is_include: jboolean) {\n    log::info!(\"Java_CediniaFilePicker_onDirectoryPicked: entered, is_include={}\", is_include);\n\n    let outcome = unowned.with_env_no_catch(|env| path.try_to_string(env));\n    let path_str: String = match outcome.into_outcome() {\n        jni::Outcome::Ok(s) => s,\n        jni::Outcome::Err(e) => {\n            log::error!(\"onDirectoryPicked: path read error: {:?}\", e);\n            return;\n        }\n        jni::Outcome::Panic(_) => {\n            log::error!(\"onDirectoryPicked: panic reading path\");\n            return;\n        }\n    };\n\n    if path_str.is_empty() {\n        log::error!(\"onDirectoryPicked: empty path, aborting\");\n        return;\n    }\n    log::info!(\"onDirectoryPicked: path='{}' include={}\", path_str, is_include);\n\n    if let Ok(mut guard) = pending_pick().lock() {\n        *guard = Some((path_str.clone(), is_include));\n    }\n    let pending = pending_pick().clone();\n    let dispatch_result = invoke_from_event_loop(move || match pending.lock().ok().and_then(|mut g| g.take()) {\n        Some((p, inc)) => {\n            log::info!(\"invoke_from_event_loop: on_directory_picked path='{}' inc={}\", p, inc);\n            crate::app::on_directory_picked(p, inc);\n        }\n        None => log::warn!(\"invoke_from_event_loop: PENDING_PICK was empty\"),\n    });\n    if let Err(e) = dispatch_result {\n        log::error!(\"onDirectoryPicked: invoke_from_event_loop failed: {:?}\", e);\n    }\n}\n\npub fn init(app: &AndroidApp) {\n    log::info!(\"file_picker_android::init: starting\");\n    APP_HANDLE.get_or_init(|| app.clone());\n\n    let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) };\n    let dex_data: &'static [u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/classes.dex\"));\n    log::info!(\"file_picker_android::init: DEX size={}\", dex_data.len());\n\n    vm.attach_current_thread(|env| -> jni::errors::Result<()> {\n        log::info!(\"file_picker_android::init: JVM attached\");\n\n        let dex_buffer = unsafe { env.new_direct_byte_buffer(dex_data.as_ptr() as *mut _, dex_data.len()) }?;\n        let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) };\n\n        let parent_class_loader = env\n            .call_method(&native_activity, jni_str!(\"getClassLoader\"), jni_sig!(() -> java.lang.ClassLoader), &[])?\n            .l()?;\n\n        let dex_loader = match env.new_object(\n            jni_str!(\"dalvik/system/InMemoryDexClassLoader\"),\n            jni_sig!((buf: java.nio.ByteBuffer, loader: java.lang.ClassLoader) -> void),\n            &[JValue::Object(&dex_buffer), JValue::Object(&parent_class_loader)],\n        ) {\n            Ok(obj) => {\n                log::info!(\"file_picker_android::init: using InMemoryDexClassLoader\");\n                env.exception_clear();\n                obj\n            }\n            Err(e) => {\n                log::info!(\"file_picker_android::init: InMemoryDexClassLoader failed ({:?}), trying DexClassLoader\", e);\n                env.exception_clear();\n                let code_cache_dir = env.call_method(&native_activity, jni_str!(\"getCodeCacheDir\"), jni_sig!(() -> java.io.File), &[])?.l()?;\n                let path_obj = env.call_method(&code_cache_dir, jni_str!(\"getAbsolutePath\"), jni_sig!(() -> java.lang.String), &[])?.l()?;\n                let j_path = unsafe { JString::from_raw(env, path_obj.as_raw()) };\n                let path_str = j_path.try_to_string(env)?;\n                let dex_path = format!(\"{}/cedinia_picker.dex\", path_str);\n                std::fs::write(&dex_path, dex_data).expect(\"write dex\");\n                let oats_path = format!(\"{}/oats\", path_str);\n                let _ = std::fs::create_dir(&oats_path);\n                let j_dex = env.new_string(&dex_path)?;\n                let j_oats = env.new_string(&oats_path)?;\n                env.new_object(\n                    jni_str!(\"dalvik/system/DexClassLoader\"),\n                    jni_sig!((dexPath: java.lang.String, optimizedDirectory: java.lang.String,\n                               librarySearchPath: java.lang.String, parent: java.lang.ClassLoader) -> void),\n                    &[\n                        JValue::Object(&j_dex),\n                        JValue::Object(&j_oats),\n                        JValue::Object(&JObject::null()),\n                        JValue::Object(&parent_class_loader),\n                    ],\n                )?\n            }\n        };\n\n        let class_name = env.new_string(\"CediniaFilePicker\")?;\n        let picker_class_obj = env\n            .call_method(\n                &dex_loader,\n                jni_str!(\"findClass\"),\n                jni_sig!((name: java.lang.String) -> java.lang.Class),\n                &[JValue::Object(&class_name)],\n            )?\n            .l()?;\n        let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) };\n\n        let native_methods = [unsafe {\n            jni::NativeMethod::from_raw_parts(\n                jni_str!(\"onDirectoryPicked\"),\n                jni_str!(\"(Ljava/lang/String;Z)V\"),\n                Java_CediniaFilePicker_onDirectoryPicked as *mut c_void,\n            )\n        }];\n        unsafe { env.register_native_methods(&picker_class, &native_methods) }?;\n        log::info!(\"file_picker_android::init: native method registered\");\n\n        let loader_global: Global<JObject<'static>> = env.new_global_ref(dex_loader)?;\n        let _ = DEX_LOADER_REF.set(loader_global);\n        log::info!(\"file_picker_android::init: complete\");\n        Ok(())\n    })\n    .expect(\"init JNI attachment failed\");\n}\n\nstatic APP_HANDLE: std::sync::OnceLock<AndroidApp> = std::sync::OnceLock::new();\nstatic DEX_LOADER_REF: std::sync::OnceLock<Global<JObject<'static>>> = std::sync::OnceLock::new();\n\npub fn launch_pick_directory(is_include: bool) {\n    log::info!(\"launch_pick_directory: is_include={}\", is_include);\n    let Some(app) = APP_HANDLE.get() else {\n        log::error!(\"launch_pick_directory: AndroidApp not initialised\");\n        return;\n    };\n    let Some(loader_ref) = DEX_LOADER_REF.get() else {\n        log::error!(\"launch_pick_directory: DEX loader not initialised\");\n        return;\n    };\n\n    let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) };\n    vm.attach_current_thread(|env| -> jni::errors::Result<()> {\n        let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) };\n\n        let class_name = env.new_string(\"CediniaFilePicker\")?;\n        let picker_class_obj = env\n            .call_method(\n                loader_ref.as_obj(),\n                jni_str!(\"findClass\"),\n                jni_sig!((name: java.lang.String) -> java.lang.Class),\n                &[JValue::Object(&class_name)],\n            )?\n            .l()?;\n        let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) };\n\n        let method = if is_include {\n            jni_str!(\"pickIncludeDirectory\")\n        } else {\n            jni_str!(\"pickExcludeDirectory\")\n        };\n        log::info!(\"launch_pick_directory: calling Java CediniaFilePicker method\");\n        env.call_static_method(\n            &picker_class,\n            method,\n            jni_sig!((activity: android.app.Activity) -> void),\n            &[JValue::Object(&native_activity)],\n        )?;\n        log::info!(\"launch_pick_directory: Java call succeeded\");\n        Ok(())\n    })\n    .unwrap_or_else(|e| log::error!(\"launch_pick_directory: JNI failed: {:?}\", e));\n}\n\npub fn setup_nav_bar() {\n    let Some(app) = APP_HANDLE.get() else {\n        log::error!(\"setup_nav_bar: AndroidApp not initialised\");\n        return;\n    };\n    let Some(loader_ref) = DEX_LOADER_REF.get() else {\n        log::error!(\"setup_nav_bar: DEX loader not initialised\");\n        return;\n    };\n\n    let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) };\n    vm.attach_current_thread(|env| -> jni::errors::Result<()> {\n        let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) };\n        let class_name = env.new_string(\"CediniaFilePicker\")?;\n        let picker_class_obj = env\n            .call_method(\n                loader_ref.as_obj(),\n                jni_str!(\"findClass\"),\n                jni_sig!((name: java.lang.String) -> java.lang.Class),\n                &[JValue::Object(&class_name)],\n            )?\n            .l()?;\n        let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) };\n        env.call_static_method(\n            &picker_class,\n            jni_str!(\"setupNavBar\"),\n            jni_sig!((activity: android.app.Activity) -> void),\n            &[JValue::Object(&native_activity)],\n        )?;\n        log::info!(\"setup_nav_bar: Java call succeeded\");\n        Ok(())\n    })\n    .unwrap_or_else(|e| log::error!(\"setup_nav_bar: JNI failed: {:?}\", e));\n}\n\npub fn check_storage_permission() -> bool {\n    let Some(app) = APP_HANDLE.get() else { return false };\n    let Some(loader_ref) = DEX_LOADER_REF.get() else { return false };\n\n    let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) };\n    let mut result = false;\n    vm.attach_current_thread(|env| -> jni::errors::Result<()> {\n        let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) };\n        let class_name = env.new_string(\"CediniaFilePicker\")?;\n        let picker_class_obj = env\n            .call_method(\n                loader_ref.as_obj(),\n                jni_str!(\"findClass\"),\n                jni_sig!((name: java.lang.String) -> java.lang.Class),\n                &[JValue::Object(&class_name)],\n            )?\n            .l()?;\n        let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) };\n        let val = env\n            .call_static_method(\n                &picker_class,\n                jni_str!(\"hasStoragePermission\"),\n                jni_sig!((activity: android.app.Activity) -> boolean),\n                &[JValue::Object(&native_activity)],\n            )?\n            .z()?;\n        result = val;\n        Ok(())\n    })\n    .unwrap_or_else(|e| log::error!(\"check_storage_permission: JNI failed: {:?}\", e));\n    result\n}\n\npub fn request_storage_permission() {\n    let Some(app) = APP_HANDLE.get() else { return };\n    let Some(loader_ref) = DEX_LOADER_REF.get() else { return };\n\n    let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) };\n    vm.attach_current_thread(|env| -> jni::errors::Result<()> {\n        let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) };\n        let class_name = env.new_string(\"CediniaFilePicker\")?;\n        let picker_class_obj = env\n            .call_method(\n                loader_ref.as_obj(),\n                jni_str!(\"findClass\"),\n                jni_sig!((name: java.lang.String) -> java.lang.Class),\n                &[JValue::Object(&class_name)],\n            )?\n            .l()?;\n        let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) };\n        env.call_static_method(\n            &picker_class,\n            jni_str!(\"requestStoragePermission\"),\n            jni_sig!((activity: android.app.Activity) -> void),\n            &[JValue::Object(&native_activity)],\n        )?;\n        Ok(())\n    })\n    .unwrap_or_else(|e| log::error!(\"request_storage_permission: JNI failed: {:?}\", e));\n}\n"
  },
  {
    "path": "cedinia/src/lib.rs",
    "content": "#![allow(clippy::unwrap_used)]\n#![allow(clippy::indexing_slicing)]\n#![allow(clippy::todo)]\nmod app;\nmod callbacks;\npub mod common;\n#[cfg(target_os = \"android\")]\nmod file_picker_android;\npub mod localizer_cedinia;\nmod model;\nmod scan_runner;\nmod scanners;\nmod set_initial_gui_infos;\npub mod settings;\nmod thumbnail_loader;\npub mod translations;\nmod volumes;\nslint::include_modules!();\npub use app::run_app;\n\nstatic ANDROID_FILES_PATH: std::sync::OnceLock<String> = std::sync::OnceLock::new();\nstatic ANDROID_CACHE_PATH: std::sync::OnceLock<String> = std::sync::OnceLock::new();\n\npub fn android_files_path() -> Option<&'static str> {\n    ANDROID_FILES_PATH.get().map(String::as_str)\n}\npub fn android_cache_path() -> Option<&'static str> {\n    ANDROID_CACHE_PATH.get().map(String::as_str)\n}\n\n#[cfg(target_os = \"android\")]\nfn setup_android_paths(android_app: &slint::android::AndroidApp) {\n    use jni::objects::{JObject, JString};\n    use jni::{jni_sig, jni_str};\n\n    let vm = unsafe { jni::JavaVM::from_raw(android_app.vm_as_ptr() as *mut _) };\n    let _ = vm.attach_current_thread(|env| -> jni::errors::Result<()> {\n        let activity_raw = unsafe { JObject::from_raw(env, android_app.activity_as_ptr() as *mut _) };\n\n        let files_dir: JObject = env.call_method(&activity_raw, jni_str!(\"getFilesDir\"), jni_sig!(() -> java.io.File), &[])?.l()?;\n        let files_path_obj = env.call_method(&files_dir, jni_str!(\"getAbsolutePath\"), jni_sig!(() -> java.lang.String), &[])?.l()?;\n        let files_path: JString = env.cast_local::<JString>(files_path_obj)?;\n        let files_str: String = files_path.try_to_string(env)?;\n\n        let cache_dir: JObject = env.call_method(&activity_raw, jni_str!(\"getCacheDir\"), jni_sig!(() -> java.io.File), &[])?.l()?;\n        let cache_path_obj = env.call_method(&cache_dir, jni_str!(\"getAbsolutePath\"), jni_sig!(() -> java.lang.String), &[])?.l()?;\n        let cache_path: JString = env.cast_local::<JString>(cache_path_obj)?;\n        let cache_str: String = cache_path.try_to_string(env)?;\n\n        let _ = ANDROID_FILES_PATH.set(files_str.clone());\n        let _ = ANDROID_CACHE_PATH.set(cache_str.clone());\n\n        unsafe { std::env::set_var(\"DATA_DIR\", &files_str) };\n\n        eprintln!(\"setup_android_paths: config='{}' cache='{}'\", files_str, cache_str);\n        Ok(())\n    });\n}\n\n#[cfg(target_os = \"android\")]\n#[unsafe(no_mangle)]\nfn android_main(android_app: slint::android::AndroidApp) {\n    // Init logcat logging FIRST so every log::* call and every panic message\n    // appears under `adb logcat -s cedinia`.  Without this all output goes to\n    // stdout/stderr which Android silently discards for native code.\n    android_logger::init_once(android_logger::Config::default().with_max_level(log::LevelFilter::Debug).with_tag(\"cedinia\"));\n    setup_android_paths(&android_app);\n    crate::app::setup_logger_cache();\n    log::info!(\"android_main: started\");\n    let scale = android_app.config().density().unwrap_or(160) as f32 / 160.0;\n    log::info!(\"android_main: display scale={:.2}\", scale);\n    log::info!(\"android_main: initialising file picker (JNI + DEX)\");\n    file_picker_android::init(&android_app);\n    log::info!(\"android_main: file picker initialised\");\n    slint::android::init(android_app.clone()).expect(\"Failed to initialise Slint Android backend\");\n    log::info!(\"android_main: Slint backend initialised\");\n    file_picker_android::setup_nav_bar();\n    log::info!(\"android_main: nav bar pinned\");\n    let rect = android_app.content_rect();\n    let inset_bottom_px = if rect.bottom > 0 { 0.0f32 } else { 48.0 * scale };\n    log::info!(\"android_main: content_rect={:?} inset_bottom={}\", rect, inset_bottom_px);\n    log::info!(\"android_main: launching app UI\");\n    app::run_app_with_insets(inset_bottom_px, scale, android_app);\n    log::info!(\"android_main: app UI returned (exiting)\");\n}\n"
  },
  {
    "path": "cedinia/src/localizer_cedinia.rs",
    "content": "use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};\nuse i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};\nuse rust_embed::RustEmbed;\n\n#[derive(RustEmbed)]\n#[folder = \"i18n/\"]\nstruct Localizations;\n\npub static LANGUAGE_LOADER_CEDINIA: std::sync::LazyLock<FluentLanguageLoader> = std::sync::LazyLock::new(|| {\n    let loader: FluentLanguageLoader = fluent_language_loader!();\n    loader.load_fallback_language(&Localizations).expect(\"Error while loading fallback language for cedinia\");\n    loader\n});\n\n#[macro_export]\nmacro_rules! flc {\n    ( $($tt:tt)* ) => {{\n        i18n_embed_fl::fl!($crate::localizer_cedinia::LANGUAGE_LOADER_CEDINIA, $($tt)*)\n    }};\n}\n\npub(crate) fn localizer_cedinia() -> Box<dyn Localizer> {\n    Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_CEDINIA, &Localizations))\n}\n\n/// Returns 1 for Polish, 0 for English – determined by the OS locale.\n/// Used to pick the default UI index when no explicit language has been saved.\npub(crate) fn detect_os_language_idx() -> i32 {\n    #[cfg(not(target_os = \"android\"))]\n    {\n        let requested = i18n_embed::DesktopLanguageRequester::requested_languages();\n        if requested.iter().any(|l| l.language.as_str() == \"pl\") {\n            return 1;\n        }\n    }\n    0\n}\n\n/// Load the given language preference. \"auto\" uses the OS locale; \"pl\"/\"en\" forces a specific\n/// language. Call this before `translate_items`.\npub(crate) fn apply_language_preference(lang: &str) {\n    let localizer = localizer_cedinia();\n    match lang {\n        \"pl\" | \"en\" => {\n            if let Ok(lang_id) = lang.parse::<i18n_embed::unic_langid::LanguageIdentifier>() {\n                let _ = localizer.select(&[lang_id]);\n            }\n        }\n        _ => {\n            // \"auto\" – use the OS-requested languages on desktop\n            #[cfg(not(target_os = \"android\"))]\n            {\n                let requested = i18n_embed::DesktopLanguageRequester::requested_languages();\n                let _ = localizer.select(&requested);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/src/model.rs",
    "content": "use slint::{Model, ModelRc, SharedString, VecModel};\n\nuse crate::FileEntry;\nuse crate::scan_runner::FileItem;\n\npub fn make_file_model(items: Vec<FileItem>) -> ModelRc<FileEntry> {\n    let entries: Vec<FileEntry> = items\n        .into_iter()\n        .map(|item| {\n            let val_str: Vec<SharedString> = item.val_str.into_iter().map(SharedString::from).collect();\n            let val_int: Vec<i32> = item.val_int;\n            FileEntry {\n                checked: false,\n                is_header: item.is_header,\n                val_str: ModelRc::new(VecModel::from(val_str)),\n                val_int: ModelRc::new(VecModel::from(val_int)),\n            }\n        })\n        .collect();\n\n    ModelRc::new(VecModel::from(entries))\n}\n\npub fn toggle_row(model: &ModelRc<FileEntry>, index: usize) {\n    if let Some(vm) = model.as_any().downcast_ref::<VecModel<FileEntry>>() {\n        let mut items: Vec<FileEntry> = vm.iter().collect::<Vec<_>>();\n        if let Some(entry) = items.get_mut(index)\n            && !entry.is_header\n        {\n            entry.checked = !entry.checked;\n        }\n        vm.set_vec(items);\n    }\n}\n\npub fn count_checked(model: &ModelRc<FileEntry>) -> i32 {\n    model\n        .as_any()\n        .downcast_ref::<VecModel<FileEntry>>()\n        .map_or(0, |vm| vm.iter().filter(|e: &FileEntry| e.checked).count() as i32)\n}\n"
  },
  {
    "path": "cedinia/src/scan_runner.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::thread;\n\nuse crossbeam_channel::{Receiver, Sender, unbounded};\nuse czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::common::progress_data::{CurrentStage, ProgressData as CoreProgress};\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::re_exported::{FilterType, HashAlg};\nuse czkawka_core::tools::big_file::SearchMode;\nuse czkawka_core::tools::similar_images::SimilarityPreset;\n\nuse crate::scanners::{\n    scan_bad_extensions, scan_bad_names, scan_big_files, scan_broken_files, scan_duplicate_files, scan_empty_files, scan_empty_folders, scan_exif_remover, scan_same_music,\n    scan_similar_images, scan_temporary_files,\n};\n\n#[derive(Debug, Clone)]\npub struct FileItem {\n    pub is_header: bool,\n    pub val_str: Vec<String>,\n    pub val_int: Vec<i32>,\n}\n\n#[derive(Debug, Clone, Default)]\npub struct CommonFilters {\n    pub excluded_items: Vec<String>,\n    pub allowed_extensions: Vec<String>,\n    pub excluded_extensions: Vec<String>,\n    pub min_file_size_bytes: u64,\n    pub max_file_size_bytes: Option<u64>,\n}\n\n#[derive(Debug)]\npub enum ScanRequest {\n    DuplicateFiles {\n        dirs: Vec<PathBuf>,\n        check_method: CheckingMethod,\n        hash_type: HashType,\n        use_cache: bool,\n        filters: CommonFilters,\n    },\n    EmptyFolders {\n        dirs: Vec<PathBuf>,\n        filters: CommonFilters,\n    },\n    SimilarImages {\n        dirs: Vec<PathBuf>,\n        similarity_preset: SimilarityPreset,\n        hash_size: u8,\n        hash_alg: HashAlg,\n        image_filter: FilterType,\n        ignore_same_size: bool,\n        filters: CommonFilters,\n    },\n    EmptyFiles {\n        dirs: Vec<PathBuf>,\n        filters: CommonFilters,\n    },\n    TemporaryFiles {\n        dirs: Vec<PathBuf>,\n        filters: CommonFilters,\n    },\n    BigFiles {\n        dirs: Vec<PathBuf>,\n        search_mode: SearchMode,\n        count: usize,\n        filters: CommonFilters,\n    },\n    BrokenFiles {\n        dirs: Vec<PathBuf>,\n        checked_types: u32,\n        filters: CommonFilters,\n    },\n    BadExtensions {\n        dirs: Vec<PathBuf>,\n        filters: CommonFilters,\n    },\n    SameMusic {\n        dirs: Vec<PathBuf>,\n        music_similarity: u32,\n        approximate: bool,\n        check_method: CheckingMethod,\n        filters: CommonFilters,\n    },\n    BadNames {\n        dirs: Vec<PathBuf>,\n        filters: CommonFilters,\n        uppercase_extension: bool,\n        emoji_used: bool,\n        space_at_start_or_end: bool,\n        non_ascii_graphical: bool,\n        remove_duplicated_non_alpha: bool,\n    },\n    ExifRemover {\n        dirs: Vec<PathBuf>,\n        filters: CommonFilters,\n    },\n    Stop,\n}\n\n#[derive(Debug, Clone)]\npub struct ProgressUpdate {\n    pub step_name: String,\n    pub current: i32,\n    pub all: i32,\n    pub is_indeterminate: bool,\n    pub scan_id: u32,\n}\n\n#[derive(Debug)]\npub enum ScanResult {\n    Progress(ProgressUpdate),\n    DuplicateFiles(Vec<FileItem>),\n    EmptyFolders(Vec<FileItem>),\n    SimilarImages(Vec<FileItem>),\n    EmptyFiles(Vec<FileItem>),\n    TemporaryFiles(Vec<FileItem>),\n    BigFiles(Vec<FileItem>),\n    BrokenFiles(Vec<FileItem>),\n    BadExtensions(Vec<FileItem>),\n    SameMusic(Vec<FileItem>),\n    BadNames(Vec<FileItem>),\n    ExifRemover(Vec<FileItem>),\n    Finished(u32),\n}\n\npub trait ScanResultHandler: Send + Sync + 'static {\n    fn on_result(&self, result: ScanResult);\n}\n\npub fn start_worker<H: ScanResultHandler>(handler: H) -> (Sender<ScanRequest>, Arc<AtomicBool>) {\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    let (req_tx, req_rx) = unbounded::<ScanRequest>();\n    thread::Builder::new()\n        .name(\"cedinia-scanner\".into())\n        .spawn({\n            let stop_flag = Arc::clone(&stop_flag);\n            move || worker_loop(&req_rx, handler, &stop_flag)\n        })\n        .expect(\"Failed to spawn scanner thread\");\n    (req_tx, stop_flag)\n}\n\nfn worker_loop<H: ScanResultHandler + Sync>(req_rx: &Receiver<ScanRequest>, handler: H, stop_flag: &Arc<AtomicBool>) {\n    use std::sync::atomic::Ordering;\n    let mut scan_id: u32 = 0;\n\n    let handler = Arc::new(handler);\n\n    while let Ok(req) = req_rx.recv() {\n        match req {\n            ScanRequest::Stop => {\n                stop_flag.store(true, Ordering::Relaxed);\n            }\n            ScanRequest::DuplicateFiles {\n                dirs,\n                check_method,\n                hash_type,\n                use_cache,\n                filters,\n            } => {\n                scan_id += 1;\n                let items = scan_duplicate_files(dirs, check_method, hash_type, use_cache, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::DuplicateFiles(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::EmptyFolders { dirs, filters } => {\n                scan_id += 1;\n                let items = scan_empty_folders(dirs, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::EmptyFolders(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::SimilarImages {\n                dirs,\n                similarity_preset,\n                hash_size,\n                hash_alg,\n                image_filter,\n                ignore_same_size,\n                filters,\n            } => {\n                scan_id += 1;\n                let items = scan_similar_images(\n                    dirs,\n                    similarity_preset,\n                    hash_size,\n                    hash_alg,\n                    image_filter,\n                    ignore_same_size,\n                    &filters,\n                    stop_flag,\n                    &handler,\n                    scan_id,\n                );\n                handler.on_result(ScanResult::SimilarImages(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::EmptyFiles { dirs, filters } => {\n                scan_id += 1;\n                let items = scan_empty_files(dirs, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::EmptyFiles(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::TemporaryFiles { dirs, filters } => {\n                scan_id += 1;\n                let items = scan_temporary_files(dirs, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::TemporaryFiles(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::BigFiles {\n                dirs,\n                search_mode,\n                count,\n                filters,\n            } => {\n                scan_id += 1;\n                let items = scan_big_files(dirs, search_mode, count, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::BigFiles(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::BrokenFiles { dirs, checked_types, filters } => {\n                scan_id += 1;\n                let items = scan_broken_files(dirs, checked_types, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::BrokenFiles(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::BadExtensions { dirs, filters } => {\n                scan_id += 1;\n                let items = scan_bad_extensions(dirs, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::BadExtensions(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::SameMusic {\n                dirs,\n                music_similarity,\n                approximate,\n                check_method,\n                filters,\n            } => {\n                scan_id += 1;\n                let items = scan_same_music(dirs, music_similarity, approximate, check_method, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::SameMusic(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::BadNames {\n                dirs,\n                filters,\n                uppercase_extension,\n                emoji_used,\n                space_at_start_or_end,\n                non_ascii_graphical,\n                remove_duplicated_non_alpha,\n            } => {\n                scan_id += 1;\n                let items = scan_bad_names(\n                    dirs,\n                    &filters,\n                    stop_flag,\n                    &handler,\n                    scan_id,\n                    uppercase_extension,\n                    emoji_used,\n                    space_at_start_or_end,\n                    non_ascii_graphical,\n                    remove_duplicated_non_alpha,\n                );\n                handler.on_result(ScanResult::BadNames(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n            ScanRequest::ExifRemover { dirs, filters } => {\n                scan_id += 1;\n                let items = scan_exif_remover(dirs, &filters, stop_flag, &handler, scan_id);\n                handler.on_result(ScanResult::ExifRemover(items));\n                handler.on_result(ScanResult::Finished(scan_id));\n            }\n        }\n    }\n}\n\nfn stage_uses_bytes(stage: CurrentStage) -> bool {\n    matches!(\n        stage,\n        CurrentStage::DuplicatePreHashing | CurrentStage::DuplicateFullHashing | CurrentStage::SimilarImagesCalculatingHashes | CurrentStage::SameMusicCalculatingFingerprints\n    )\n}\n\nfn stage_label(stage: CurrentStage) -> &'static str {\n    match stage {\n        CurrentStage::CollectingFiles => \"Zbieranie plików\",\n        CurrentStage::DuplicateScanningName => \"Skanowanie po nazwie\",\n        CurrentStage::DuplicateScanningSizeName => \"Skanowanie po nazwie i rozmiarze\",\n        CurrentStage::DuplicateScanningSize => \"Skanowanie po rozmiarze\",\n        CurrentStage::DuplicatePreHashing => \"Pre-hash\",\n        CurrentStage::DuplicateFullHashing => \"Haszowanie\",\n        CurrentStage::DuplicateCacheLoading\n        | CurrentStage::DuplicatePreHashCacheLoading\n        | CurrentStage::SameMusicCacheLoadingTags\n        | CurrentStage::SameMusicCacheLoadingFingerprints\n        | CurrentStage::ExifRemoverCacheLoading => \"Ładowanie cache\",\n        CurrentStage::DuplicateCacheSaving\n        | CurrentStage::DuplicatePreHashCacheSaving\n        | CurrentStage::SameMusicCacheSavingTags\n        | CurrentStage::SameMusicCacheSavingFingerprints\n        | CurrentStage::ExifRemoverCacheSaving => \"Zapisywanie cache\",\n        CurrentStage::SimilarImagesCalculatingHashes => \"Obliczanie hashy obrazów\",\n        CurrentStage::SimilarImagesComparingHashes => \"Porównywanie obrazów\",\n        CurrentStage::SimilarVideosCalculatingHashes => \"Obliczanie hashy wideo\",\n        CurrentStage::BrokenFilesChecking => \"Sprawdzanie plików\",\n        CurrentStage::BadExtensionsChecking => \"Sprawdzanie rozszerzeń\",\n        CurrentStage::BadNamesChecking => \"Sprawdzanie nazw\",\n        CurrentStage::SameMusicReadingTags => \"Odczyt tagów muzycznych\",\n        CurrentStage::SameMusicComparingTags => \"Porównywanie tagów\",\n        CurrentStage::SameMusicCalculatingFingerprints => \"Obliczanie odcisków muzycznych\",\n        CurrentStage::SameMusicComparingFingerprints => \"Porównywanie odcisków muzycznych\",\n        CurrentStage::ExifRemoverExtractingTags => \"Odczyt tagów EXIF\",\n        CurrentStage::VideoOptimizerCreatingThumbnails | CurrentStage::SimilarVideosCreatingThumbnails => \"Tworzenie miniatur wideo\",\n        CurrentStage::VideoOptimizerProcessingVideos => \"Przetwarzanie wideo\",\n        CurrentStage::DeletingFiles => \"Usuwanie plików\",\n        CurrentStage::RenamingFiles => \"Zmiana nazw plików\",\n        CurrentStage::MovingFiles => \"Przenoszenie plików\",\n        CurrentStage::HardlinkingFiles => \"Tworzenie hardlinków\",\n        CurrentStage::SymlinkingFiles => \"Tworzenie dowiązań\",\n        CurrentStage::OptimizingVideos => \"Optymalizacja wideo\",\n        CurrentStage::CleaningExif => \"Czyszczenie EXIF\",\n    }\n}\n\nfn stage_label_full(pd: &CoreProgress) -> String {\n    let base = stage_label(pd.sstage);\n    let label = if stage_uses_bytes(pd.sstage) && pd.bytes_to_check > 0 {\n        format!(\"{base}  ({} / {})\", fmt_size(pd.bytes_checked), fmt_size(pd.bytes_to_check))\n    } else {\n        base.to_string()\n    };\n    if pd.max_stage_idx > 0 {\n        format!(\"{}/{}\\u{2002}{label}\", pd.current_stage_idx + 1, pd.max_stage_idx + 1)\n    } else {\n        label\n    }\n}\n\npub(crate) fn apply_filters<T: CommonData>(tool: &mut T, filters: &CommonFilters) {\n    if !filters.excluded_items.is_empty() {\n        tool.set_excluded_items(filters.excluded_items.clone());\n    }\n    if !filters.allowed_extensions.is_empty() {\n        tool.set_allowed_extensions(filters.allowed_extensions.clone());\n    }\n    if !filters.excluded_extensions.is_empty() {\n        tool.set_excluded_extensions(filters.excluded_extensions.clone());\n    }\n    if filters.min_file_size_bytes > 0 {\n        tool.set_minimal_file_size(filters.min_file_size_bytes);\n    }\n    if let Some(max) = filters.max_file_size_bytes {\n        tool.set_maximal_file_size(max);\n    }\n}\n\npub(crate) fn spawn_progress_forwarder<H: ScanResultHandler + Sync>(handler: Arc<H>, scan_id: u32) -> (Sender<CoreProgress>, thread::JoinHandle<()>) {\n    let (ptx, prx) = unbounded::<CoreProgress>();\n    let handle = thread::spawn(move || {\n        while let Ok(pd) = prx.recv() {\n            let is_indeterminate = pd.sstage.check_if_loading_saving_cache();\n            let update = ProgressUpdate {\n                step_name: stage_label_full(&pd),\n                current: pd.entries_checked as i32,\n                all: pd.entries_to_check as i32,\n                is_indeterminate,\n                scan_id,\n            };\n            handler.on_result(ScanResult::Progress(update));\n        }\n    });\n    (ptx, handle)\n}\n\npub(crate) fn fmt_size(bytes: u64) -> String {\n    humansize::format_size(bytes, humansize::BINARY)\n}\n\npub(crate) fn fmt_date(unix_secs: u64) -> String {\n    let secs = unix_secs;\n    let mins = secs / 60;\n    let hours = mins / 60;\n    let days = hours / 24;\n\n    let min = mins % 60;\n    let hour = hours % 24;\n\n    let mut remaining_days = days;\n    let mut year = 1970u64;\n    loop {\n        let days_in_year = if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) {\n            366\n        } else {\n            365\n        };\n        if remaining_days < days_in_year {\n            break;\n        }\n        remaining_days -= days_in_year;\n        year += 1;\n    }\n    let leap = year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400));\n    let months_days: [u64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n    let mut month = 1u64;\n    for &md in &months_days {\n        if remaining_days < md {\n            break;\n        }\n        remaining_days -= md;\n        month += 1;\n    }\n    let day = remaining_days + 1;\n\n    format!(\"{year}-{month:02}-{day:02} {hour:02}:{min:02}\")\n}\n\npub(crate) fn size_to_hi_lo(size: u64) -> (i32, i32) {\n    let hi = (size >> 32) as i32;\n    let lo = (size & 0xFFFF_FFFF) as i32;\n    (hi, lo)\n}\n\npub(crate) fn file_name(p: &std::path::Path) -> String {\n    p.file_name().unwrap_or_default().to_string_lossy().to_string()\n}\n\npub(crate) fn parent_str(p: &std::path::Path) -> String {\n    p.parent().map(|x| x.to_string_lossy().to_string()).unwrap_or_default()\n}\n"
  },
  {
    "path": "cedinia/src/scanners.rs",
    "content": "use std::cmp::Reverse;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::Search;\n\nuse crate::common::{\n    INT_BASE_COUNT, INT_IDX_SIZE_HI, INT_IDX_SIZE_LO, MAX_INT_DATA_EXIF_REMOVER, MAX_INT_DATA_SIMILAR_IMAGES, MAX_STR_DATA_BAD_EXTENSIONS, MAX_STR_DATA_BAD_NAMES,\n    MAX_STR_DATA_BROKEN_FILES, MAX_STR_DATA_SAME_MUSIC, MAX_STR_DATA_SIMILAR_IMAGES, STR_BASE_COUNT, STR_IDX_NAME, STR_IDX_PATH,\n};\nuse crate::scan_runner::{CommonFilters, FileItem, ScanResultHandler, apply_filters, file_name, fmt_date, fmt_size, parent_str, size_to_hi_lo, spawn_progress_forwarder};\n\nfn base_item(is_header: bool, name: String, path: String, size_str: String, modified_str: String, mod_secs: u64, size_bytes: u64) -> FileItem {\n    let (mod_hi, mod_lo) = size_to_hi_lo(mod_secs);\n    let (size_hi, size_lo) = size_to_hi_lo(size_bytes);\n    let val_str: [String; STR_BASE_COUNT] = [name, path, size_str, modified_str];\n    let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, size_hi, size_lo];\n    FileItem {\n        is_header,\n        val_str: val_str.into(),\n        val_int: val_int.into(),\n    }\n}\n\nfn header_item(label: String) -> FileItem {\n    let val_str: [String; STR_BASE_COUNT] = [label, String::new(), String::new(), String::new()];\n    let val_int: [i32; INT_BASE_COUNT] = [0, 0, 0, 0];\n    FileItem {\n        is_header: true,\n        val_str: val_str.into(),\n        val_int: val_int.into(),\n    }\n}\n\nfn item_name(item: &FileItem) -> &str {\n    &item.val_str[STR_IDX_NAME]\n}\n\nfn item_path(item: &FileItem) -> &str {\n    &item.val_str[STR_IDX_PATH]\n}\n\nfn item_size_u64(item: &FileItem) -> u64 {\n    let hi = item.val_int[INT_IDX_SIZE_HI] as u64;\n    let lo = item.val_int[INT_IDX_SIZE_LO] as u64;\n    (hi << 32) | (lo & 0xFFFF_FFFF)\n}\n\npub(crate) fn scan_duplicate_files<H: ScanResultHandler>(\n    dirs: Vec<PathBuf>,\n    check_method: czkawka_core::common::model::CheckingMethod,\n    hash_type: czkawka_core::common::model::HashType,\n    use_cache: bool,\n    filters: &CommonFilters,\n    stop: &Arc<AtomicBool>,\n    handler: &Arc<H>,\n    scan_id: u32,\n) -> Vec<FileItem> {\n    use czkawka_core::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = DuplicateFinderParameters::new(check_method, hash_type, use_cache, 8 * 1024, 0, false);\n    let mut tool = DuplicateFinder::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = Vec::new();\n    for groups in tool.get_files_sorted_by_hash().values().rev() {\n        let mut sorted_groups: Vec<&Vec<_>> = groups.iter().collect();\n        sorted_groups.sort_by_key(|g| Reverse(g.len()));\n        for group in sorted_groups {\n            if group.len() < 2 {\n                continue;\n            }\n            let file_size = group[0].size;\n            let total = fmt_size(file_size * group.len() as u64);\n            let per = fmt_size(file_size);\n            items.push(header_item(format!(\"{} pliki  \\u{00d7}  {} / plik  =  {} \\u{0142}\\u{0105}cznie\", group.len(), per, total)));\n            for fe in group {\n                items.push(base_item(\n                    false,\n                    file_name(&fe.path),\n                    parent_str(&fe.path),\n                    fmt_size(fe.size),\n                    fmt_date(fe.modified_date),\n                    fe.modified_date,\n                    fe.size,\n                ));\n            }\n        }\n    }\n    items\n}\n\npub(crate) fn scan_empty_folders<H: ScanResultHandler>(dirs: Vec<PathBuf>, filters: &CommonFilters, stop: &Arc<AtomicBool>, handler: &Arc<H>, scan_id: u32) -> Vec<FileItem> {\n    use czkawka_core::tools::empty_folder::EmptyFolder;\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let mut tool = EmptyFolder::new();\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = tool\n        .get_empty_folder_list()\n        .values()\n        .map(|fe| {\n            base_item(\n                false,\n                file_name(&fe.path),\n                parent_str(&fe.path),\n                String::new(),\n                fmt_date(fe.modified_date),\n                fe.modified_date,\n                0,\n            )\n        })\n        .collect();\n    items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b))));\n    items\n}\n\npub(crate) fn scan_similar_images<H: ScanResultHandler>(\n    dirs: Vec<PathBuf>,\n    similarity_preset: czkawka_core::tools::similar_images::SimilarityPreset,\n    hash_size: u8,\n    hash_alg: czkawka_core::re_exported::HashAlg,\n    image_filter: czkawka_core::re_exported::FilterType,\n    ignore_same_size: bool,\n    filters: &CommonFilters,\n    stop: &Arc<AtomicBool>,\n    handler: &Arc<H>,\n    scan_id: u32,\n) -> Vec<FileItem> {\n    use czkawka_core::tools::similar_images::{SimilarImages, SimilarImagesParameters, return_similarity_from_similarity_preset};\n    let max_diff = return_similarity_from_similarity_preset(similarity_preset, hash_size);\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = SimilarImagesParameters::new(max_diff, hash_size, hash_alg, image_filter, ignore_same_size);\n    let mut tool = SimilarImages::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let raw_groups: &Vec<Vec<_>> = tool.get_similar_images();\n    let mut groups_with_size: Vec<(&Vec<_>, u64)> = raw_groups\n        .iter()\n        .filter(|g| g.len() >= 2)\n        .map(|g| {\n            let total: u64 = g.iter().map(|img| img.size).sum();\n            (g, total)\n        })\n        .collect();\n    groups_with_size.sort_by_key(|&(_, total)| Reverse(total));\n    let mut items: Vec<FileItem> = Vec::new();\n    for (group, _) in groups_with_size {\n        items.push(header_item(format!(\"{} podobnych obraz\\u{00f3}w\", group.len())));\n        for img in group {\n            let dims = format!(\"{}\\u{00d7}{}  \\u{0394}{}\", img.width, img.height, img.difference);\n            let (mod_hi, mod_lo) = size_to_hi_lo(img.modified_date);\n            let (size_hi, size_lo) = size_to_hi_lo(img.size);\n            let val_str: [String; MAX_STR_DATA_SIMILAR_IMAGES] = [file_name(&img.path), parent_str(&img.path), fmt_size(img.size), fmt_date(img.modified_date), dims];\n            let val_int: [i32; MAX_INT_DATA_SIMILAR_IMAGES] = [mod_hi, mod_lo, size_hi, size_lo, img.width as i32, img.height as i32, img.difference as i32];\n            items.push(FileItem {\n                is_header: false,\n                val_str: val_str.into(),\n                val_int: val_int.into(),\n            });\n        }\n    }\n    items\n}\n\npub(crate) fn scan_empty_files<H: ScanResultHandler>(dirs: Vec<PathBuf>, filters: &CommonFilters, stop: &Arc<AtomicBool>, handler: &Arc<H>, scan_id: u32) -> Vec<FileItem> {\n    use czkawka_core::tools::empty_files::EmptyFiles;\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let mut tool = EmptyFiles::new();\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = tool\n        .get_empty_files()\n        .iter()\n        .map(|fe| {\n            base_item(\n                false,\n                file_name(&fe.path),\n                parent_str(&fe.path),\n                fmt_size(fe.size),\n                fmt_date(fe.modified_date),\n                fe.modified_date,\n                fe.size,\n            )\n        })\n        .collect();\n    items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b))));\n    items\n}\n\npub(crate) fn scan_temporary_files<H: ScanResultHandler>(dirs: Vec<PathBuf>, filters: &CommonFilters, stop: &Arc<AtomicBool>, handler: &Arc<H>, scan_id: u32) -> Vec<FileItem> {\n    use czkawka_core::tools::temporary::Temporary;\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let mut tool = Temporary::new();\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = tool\n        .get_temporary_files()\n        .iter()\n        .map(|fe| {\n            base_item(\n                false,\n                file_name(&fe.path),\n                parent_str(&fe.path),\n                fmt_size(fe.size),\n                fmt_date(fe.modified_date),\n                fe.modified_date,\n                fe.size,\n            )\n        })\n        .collect();\n    items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b))));\n    items\n}\n\npub(crate) fn scan_big_files<H: ScanResultHandler>(\n    dirs: Vec<PathBuf>,\n    search_mode: czkawka_core::tools::big_file::SearchMode,\n    count: usize,\n    filters: &CommonFilters,\n    stop: &Arc<AtomicBool>,\n    handler: &Arc<H>,\n    scan_id: u32,\n) -> Vec<FileItem> {\n    use czkawka_core::tools::big_file::{BigFile, BigFileParameters};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = BigFileParameters::new(count, search_mode);\n    let mut tool = BigFile::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    tool.get_big_files()\n        .iter()\n        .map(|fe| {\n            base_item(\n                false,\n                file_name(&fe.path),\n                parent_str(&fe.path),\n                fmt_size(fe.size),\n                fmt_date(fe.modified_date),\n                fe.modified_date,\n                fe.size,\n            )\n        })\n        .collect()\n}\n\npub(crate) fn scan_broken_files<H: ScanResultHandler>(\n    dirs: Vec<PathBuf>,\n    checked_types: u32,\n    filters: &CommonFilters,\n    stop: &Arc<AtomicBool>,\n    handler: &Arc<H>,\n    scan_id: u32,\n) -> Vec<FileItem> {\n    use czkawka_core::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = BrokenFilesParameters::new(CheckedTypes::from_bits_truncate(checked_types));\n    let mut tool = BrokenFiles::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = tool\n        .get_broken_files()\n        .iter()\n        .map(|be| {\n            let (mod_hi, mod_lo) = size_to_hi_lo(be.modified_date);\n            let val_str: [String; MAX_STR_DATA_BROKEN_FILES] = [\n                file_name(&be.path),\n                parent_str(&be.path),\n                fmt_size(be.size),\n                fmt_date(be.modified_date),\n                be.error_string.clone(),\n            ];\n            let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, 0, 0];\n            FileItem {\n                is_header: false,\n                val_str: val_str.into(),\n                val_int: val_int.into(),\n            }\n        })\n        .collect();\n    items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b))));\n    items\n}\n\npub(crate) fn scan_bad_extensions<H: ScanResultHandler>(dirs: Vec<PathBuf>, filters: &CommonFilters, stop: &Arc<AtomicBool>, handler: &Arc<H>, scan_id: u32) -> Vec<FileItem> {\n    use czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsParameters};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = BadExtensionsParameters::new();\n    let mut tool = BadExtensions::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = tool\n        .get_bad_extensions_files()\n        .iter()\n        .map(|be| {\n            let (mod_hi, mod_lo) = size_to_hi_lo(be.modified_date);\n            let (size_hi, size_lo) = size_to_hi_lo(be.size);\n            let val_str: [String; MAX_STR_DATA_BAD_EXTENSIONS] = [\n                file_name(&be.path),\n                parent_str(&be.path),\n                fmt_size(be.size),\n                fmt_date(be.modified_date),\n                format!(\".{} \\u{2192} .{}\", be.current_extension, be.proper_extension),\n                be.proper_extension.clone(),\n            ];\n            let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, size_hi, size_lo];\n            FileItem {\n                is_header: false,\n                val_str: val_str.into(),\n                val_int: val_int.into(),\n            }\n        })\n        .collect();\n    items.sort_by(|a, b| {\n        item_size_u64(b)\n            .cmp(&item_size_u64(a))\n            .then(item_path(a).cmp(item_path(b)))\n            .then(item_name(a).cmp(item_name(b)))\n    });\n    items\n}\n\npub(crate) fn scan_bad_names<H: ScanResultHandler>(\n    dirs: Vec<PathBuf>,\n    filters: &CommonFilters,\n    stop: &Arc<AtomicBool>,\n    handler: &Arc<H>,\n    scan_id: u32,\n    uppercase_extension: bool,\n    emoji_used: bool,\n    space_at_start_or_end: bool,\n    non_ascii_graphical: bool,\n    remove_duplicated_non_alpha: bool,\n) -> Vec<FileItem> {\n    use czkawka_core::tools::bad_names::{BadNames, BadNamesParameters, NameIssues};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = BadNamesParameters::new(NameIssues {\n        uppercase_extension,\n        emoji_used,\n        space_at_start_or_end,\n        non_ascii_graphical,\n        restricted_charset_allowed: if non_ascii_graphical { Some(vec!['_', '-', ' ', '.']) } else { None },\n        remove_duplicated_non_alphanumeric: remove_duplicated_non_alpha,\n    });\n    let mut tool = BadNames::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<FileItem> = tool\n        .get_bad_names_files()\n        .iter()\n        .map(|bn| {\n            let (mod_hi, mod_lo) = size_to_hi_lo(bn.modified_date);\n            let val_str: [String; MAX_STR_DATA_BAD_NAMES] = [\n                file_name(&bn.path),\n                parent_str(&bn.path),\n                fmt_size(bn.size),\n                fmt_date(bn.modified_date),\n                bn.new_name.clone(),\n            ];\n            let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, 0, 0];\n            FileItem {\n                is_header: false,\n                val_str: val_str.into(),\n                val_int: val_int.into(),\n            }\n        })\n        .collect();\n    items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b))));\n    items\n}\n\npub(crate) fn scan_exif_remover<H: ScanResultHandler>(dirs: Vec<PathBuf>, filters: &CommonFilters, stop: &Arc<AtomicBool>, handler: &Arc<H>, scan_id: u32) -> Vec<FileItem> {\n    use czkawka_core::tools::exif_remover::{ExifRemover, ExifRemoverParameters};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = ExifRemoverParameters::new(vec![]);\n    let mut tool = ExifRemover::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let mut items: Vec<(u64, FileItem)> = tool\n        .get_exif_files()\n        .iter()\n        .map(|ee| {\n            let (mod_hi, mod_lo) = size_to_hi_lo(ee.modified_date);\n            let (size_hi, size_lo) = size_to_hi_lo(ee.size);\n            let val_str: [String; STR_BASE_COUNT] = [file_name(&ee.path), parent_str(&ee.path), fmt_size(ee.size), fmt_date(ee.modified_date)];\n            let val_int: [i32; MAX_INT_DATA_EXIF_REMOVER] = [mod_hi, mod_lo, size_hi, size_lo, ee.exif_tags.len() as i32];\n            (\n                ee.size,\n                FileItem {\n                    is_header: false,\n                    val_str: val_str.into(),\n                    val_int: val_int.into(),\n                },\n            )\n        })\n        .collect();\n    items.sort_by_key(|(size, _)| std::cmp::Reverse(*size));\n    items.into_iter().map(|(_, item)| item).collect()\n}\n\npub(crate) fn scan_same_music<H: ScanResultHandler>(\n    dirs: Vec<PathBuf>,\n    music_similarity: u32,\n    approximate: bool,\n    check_method: czkawka_core::common::model::CheckingMethod,\n    filters: &CommonFilters,\n    stop: &Arc<AtomicBool>,\n    handler: &Arc<H>,\n    scan_id: u32,\n) -> Vec<FileItem> {\n    use czkawka_core::tools::same_music::{MusicSimilarity, SameMusic, SameMusicParameters};\n    let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id);\n    let params = SameMusicParameters::new(MusicSimilarity::from_bits_truncate(music_similarity), approximate, check_method, 0.0, 0.0, false);\n    let mut tool = SameMusic::new(params);\n    tool.set_included_paths(dirs);\n    apply_filters(&mut tool, filters);\n    tool.set_recursive_search(true);\n    tool.search(stop, Some(&ptx));\n    drop(ptx);\n    fwd.join().expect(\"Failed to join progress forwarder thread\");\n    let raw_groups = tool.get_duplicated_music_entries();\n    let mut groups_with_size: Vec<(&Vec<_>, u64)> = raw_groups\n        .iter()\n        .filter(|g| g.len() >= 2)\n        .map(|g| {\n            let total: u64 = g.iter().map(|me| me.size).sum();\n            (g, total)\n        })\n        .collect();\n    groups_with_size.sort_by_key(|&(_, total)| Reverse(total));\n    let mut items: Vec<FileItem> = Vec::new();\n    for (group, _) in groups_with_size {\n        items.push(header_item(format!(\"{} podobnych utw\\u{00f3}r\\u{00f3}w\", group.len())));\n        for me in group {\n            let artist = if me.track_artist.is_empty() { \"?\" } else { &me.track_artist };\n            let title = if me.track_title.is_empty() { \"?\" } else { &me.track_title };\n            let (mod_hi, mod_lo) = size_to_hi_lo(me.modified_date);\n            let (size_hi, size_lo) = size_to_hi_lo(me.size);\n            let val_str: [String; MAX_STR_DATA_SAME_MUSIC] = [\n                file_name(&me.path),\n                parent_str(&me.path),\n                fmt_size(me.size),\n                fmt_date(me.modified_date),\n                format!(\"{artist} \\u{2013} {title}\"),\n                title.to_string(),\n            ];\n            let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, size_hi, size_lo];\n            items.push(FileItem {\n                is_header: false,\n                val_str: val_str.into(),\n                val_int: val_int.into(),\n            });\n        }\n    }\n    items\n}\n"
  },
  {
    "path": "cedinia/src/set_initial_gui_infos.rs",
    "content": "use slint::{ComponentHandle, Model, SharedString};\n\nuse crate::settings::gui_settings_values::StringComboBoxItems;\nuse crate::{BigFilesSettings, DuplicateSettings, GeneralSettings, MainWindow, SameMusicSettings, SimilarImagesSettings};\n\npub(crate) fn set_initial_gui_infos(app: &MainWindow) {\n    let items = StringComboBoxItems::new();\n\n    fn display_names<T: std::fmt::Debug + Clone>(items: &[crate::settings::gui_settings_values::StringComboBoxItem<T>]) -> Vec<SharedString> {\n        items.iter().map(|e| SharedString::from(e.display_name.as_str())).collect()\n    }\n\n    let general = app.global::<GeneralSettings>();\n    let dup = app.global::<DuplicateSettings>();\n    let si = app.global::<SimilarImagesSettings>();\n    let bf = app.global::<BigFilesSettings>();\n    let sm = app.global::<SameMusicSettings>();\n\n    let slint_vec = |model: slint::ModelRc<SharedString>| model.iter().collect::<Vec<SharedString>>();\n\n    assert_eq!(\n        slint_vec(general.get_min_file_size_options()),\n        display_names(&items.min_file_size),\n        \"GeneralSettings.min_file_size_options out of sync with Rust\"\n    );\n\n    assert_eq!(\n        slint_vec(dup.get_check_method_options()),\n        display_names(&items.duplicates_check_method),\n        \"DuplicateSettings.check_method_options out of sync with Rust\"\n    );\n    assert_eq!(\n        slint_vec(dup.get_hash_type_options()),\n        display_names(&items.duplicates_hash_type),\n        \"DuplicateSettings.hash_type_options out of sync with Rust\"\n    );\n\n    assert_eq!(\n        slint_vec(si.get_similarity_preset_options()),\n        display_names(&items.similarity_preset),\n        \"SimilarImagesSettings.similarity_preset_options out of sync with Rust\"\n    );\n    assert_eq!(\n        slint_vec(si.get_hash_size_options()),\n        display_names(&items.hash_size),\n        \"SimilarImagesSettings.hash_size_options out of sync with Rust\"\n    );\n    assert_eq!(\n        slint_vec(si.get_hash_alg_options()),\n        display_names(&items.hash_alg),\n        \"SimilarImagesSettings.hash_alg_options out of sync with Rust\"\n    );\n    assert_eq!(\n        slint_vec(si.get_image_filter_options()),\n        display_names(&items.image_filter),\n        \"SimilarImagesSettings.image_filter_options out of sync with Rust\"\n    );\n\n    assert_eq!(\n        slint_vec(bf.get_search_mode_options()),\n        display_names(&items.biggest_files_method),\n        \"BigFilesSettings.search_mode_options out of sync with Rust\"\n    );\n    assert_eq!(\n        slint_vec(bf.get_count_options()),\n        display_names(&items.big_files_count),\n        \"BigFilesSettings.count_options out of sync with Rust\"\n    );\n\n    assert_eq!(\n        slint_vec(sm.get_check_method_options()),\n        display_names(&items.same_music_check_method),\n        \"SameMusicSettings.check_method_options out of sync with Rust\"\n    );\n}\n"
  },
  {
    "path": "cedinia/src/settings/gui_settings_values.rs",
    "content": "use std::fmt::Debug;\n\nuse czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::re_exported::{FilterType, HashAlg};\nuse czkawka_core::tools::big_file::SearchMode;\nuse czkawka_core::tools::similar_images::SimilarityPreset;\nuse log::warn;\n\n#[derive(Debug, Clone)]\npub struct StringComboBoxItem<T>\nwhere\n    T: Clone + Debug,\n{\n    pub config_name: String,\n    pub display_name: String,\n    pub value: T,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum MinFileSize {\n    None,\n    OneKb,\n    EightKb,\n    SixtyFourKb,\n    OneMb,\n}\n\nimpl MinFileSize {\n    pub fn to_bytes(self) -> u64 {\n        match self {\n            Self::None => 0,\n            Self::OneKb => 1_024,\n            Self::EightKb => 8 * 1_024,\n            Self::SixtyFourKb => 64 * 1_024,\n            Self::OneMb => 1_024 * 1_024,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum MaxFileSize {\n    SixteenKb,\n    OneMb,\n    TenMb,\n    HundredMb,\n    Unlimited,\n}\n\nimpl MaxFileSize {\n    /// Returns `None` for Unlimited (no limit imposed).\n    pub fn to_bytes(self) -> Option<u64> {\n        match self {\n            Self::SixteenKb => Some(16 * 1_024),\n            Self::OneMb => Some(1_024 * 1_024),\n            Self::TenMb => Some(10 * 1_024 * 1_024),\n            Self::HundredMb => Some(100 * 1_024 * 1_024),\n            Self::Unlimited => None,\n        }\n    }\n}\n\npub struct StringComboBoxItems {\n    pub min_file_size: Vec<StringComboBoxItem<MinFileSize>>,\n    pub max_file_size: Vec<StringComboBoxItem<MaxFileSize>>,\n    pub duplicates_check_method: Vec<StringComboBoxItem<CheckingMethod>>,\n    pub duplicates_hash_type: Vec<StringComboBoxItem<HashType>>,\n    pub hash_size: Vec<StringComboBoxItem<u8>>,\n    pub biggest_files_method: Vec<StringComboBoxItem<SearchMode>>,\n    pub big_files_count: Vec<StringComboBoxItem<usize>>,\n    pub similarity_preset: Vec<StringComboBoxItem<SimilarityPreset>>,\n    pub hash_alg: Vec<StringComboBoxItem<HashAlg>>,\n    pub image_filter: Vec<StringComboBoxItem<FilterType>>,\n    pub same_music_check_method: Vec<StringComboBoxItem<CheckingMethod>>,\n}\n\nimpl Default for StringComboBoxItems {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl StringComboBoxItems {\n    pub fn new() -> Self {\n        let min_file_size = Self::convert(&[\n            (\"none\", \"Brak\", MinFileSize::None),\n            (\"1kb\", \"1 KB\", MinFileSize::OneKb),\n            (\"8kb\", \"8 KB\", MinFileSize::EightKb),\n            (\"64kb\", \"64 KB\", MinFileSize::SixtyFourKb),\n            (\"1mb\", \"1 MB\", MinFileSize::OneMb),\n        ]);\n\n        let max_file_size = Self::convert(&[\n            (\"16kb\", \"16 KB\", MaxFileSize::SixteenKb),\n            (\"1mb\", \"1 MB\", MaxFileSize::OneMb),\n            (\"10mb\", \"10 MB\", MaxFileSize::TenMb),\n            (\"100mb\", \"100 MB\", MaxFileSize::HundredMb),\n            (\"unlimited\", \"Bez limitu\", MaxFileSize::Unlimited),\n        ]);\n\n        let duplicates_check_method = Self::convert(&[\n            (\"hash\", \"Hash\", CheckingMethod::Hash),\n            (\"name\", \"Nazwa\", CheckingMethod::Name),\n            (\"size_and_name\", \"Rozm+Naz\", CheckingMethod::SizeName),\n            (\"size\", \"Rozmiar\", CheckingMethod::Size),\n        ]);\n\n        let duplicates_hash_type = Self::convert(&[\n            (\"blake3\", \"Blake3\", HashType::Blake3),\n            (\"crc32\", \"CRC32\", HashType::Crc32),\n            (\"xxh3\", \"XXH3\", HashType::Xxh3),\n        ]);\n\n        let hash_size = Self::convert(&[(\"8\", \"8\", 8u8), (\"16\", \"16\", 16), (\"32\", \"32\", 32), (\"64\", \"64\", 64)]);\n\n        let biggest_files_method = Self::convert(&[(\"biggest\", \"Największe\", SearchMode::BiggestFiles), (\"smallest\", \"Najmniejsze\", SearchMode::SmallestFiles)]);\n\n        let big_files_count = Self::convert(&[(\"5\", \"5\", 5usize), (\"50\", \"50\", 50), (\"500\", \"500\", 500), (\"5000\", \"5000\", 5000)]);\n\n        let similarity_preset = Self::convert(&[\n            (\"very_high\", \"B.Wys.\", SimilarityPreset::VeryHigh),\n            (\"high\", \"Wysoki\", SimilarityPreset::High),\n            (\"medium\", \"Średni\", SimilarityPreset::Medium),\n            (\"small\", \"Niski\", SimilarityPreset::Small),\n            (\"very_small\", \"B.Niski\", SimilarityPreset::VerySmall),\n            (\"minimal\", \"Min.\", SimilarityPreset::Minimal),\n        ]);\n\n        let hash_alg = Self::convert(&[\n            (\"mean\", \"Mean\", HashAlg::Mean),\n            (\"gradient\", \"Gradient\", HashAlg::Gradient),\n            (\"double_gradient\", \"D.Grad.\", HashAlg::DoubleGradient),\n            (\"vert_gradient\", \"V.Grad.\", HashAlg::VertGradient),\n            (\"median\", \"Median\", HashAlg::Median),\n            (\"blockhash\", \"Blockhash\", HashAlg::Blockhash),\n        ]);\n\n        let image_filter = Self::convert(&[\n            (\"nearest\", \"Nearest\", FilterType::Nearest),\n            (\"triangle\", \"Triangle\", FilterType::Triangle),\n            (\"catmull_rom\", \"CatmullRom\", FilterType::CatmullRom),\n            (\"gaussian\", \"Gaussian\", FilterType::Gaussian),\n            (\"lanczos3\", \"Lanczos3\", FilterType::Lanczos3),\n        ]);\n\n        let same_music_check_method = Self::convert(&[(\"tags\", \"Tagi\", CheckingMethod::AudioTags), (\"audio\", \"Audio\", CheckingMethod::AudioContent)]);\n\n        Self {\n            min_file_size,\n            max_file_size,\n            duplicates_check_method,\n            duplicates_hash_type,\n            hash_size,\n            biggest_files_method,\n            big_files_count,\n            similarity_preset,\n            hash_alg,\n            image_filter,\n            same_music_check_method,\n        }\n    }\n\n    fn convert<T>(input: &[(&str, &str, T)]) -> Vec<StringComboBoxItem<T>>\n    where\n        T: Clone + Debug,\n    {\n        input\n            .iter()\n            .map(|(config_name, display_name, value)| StringComboBoxItem {\n                config_name: config_name.to_string(),\n                display_name: display_name.to_string(),\n                value: value.clone(),\n            })\n            .collect()\n    }\n\n    pub fn idx_from_config_name<T: Clone + Debug>(config_name: &str, items: &[StringComboBoxItem<T>]) -> usize {\n        items.iter().position(|e| e.config_name == config_name).unwrap_or_else(|| {\n            warn!(\"Unknown config_name \\\"{config_name}\\\" in {items:?}, falling back to index 0\");\n            0\n        })\n    }\n\n    /// Look up enum value by UI index. Use instead of `value_from_config_name` when only the\n    /// SegmentRow idx is available (the `_value` string property may be stale).\n    pub fn value_from_idx<T: Clone + Debug>(items: &[StringComboBoxItem<T>], idx: i32, default: T) -> T {\n        items.get(idx as usize).map_or_else(\n            || {\n                warn!(\"idx {idx} out of range in {items:?}, using default\");\n                default\n            },\n            |e| e.value.clone(),\n        )\n    }\n\n    /// Look up the config_name string by UI index. Use in `collect_settings_from_gui`.\n    pub fn config_name_from_idx<T: Clone + Debug>(items: &[StringComboBoxItem<T>], idx: i32, default: &str) -> String {\n        items.get(idx as usize).map_or_else(\n            || {\n                warn!(\"idx {idx} out of range in {items:?}, defaulting to \\\"{default}\\\"\");\n                default.to_string()\n            },\n            |e| e.config_name.clone(),\n        )\n    }\n\n    pub fn value_from_config_name<T: Clone + Debug>(config_name: &str, items: &[StringComboBoxItem<T>], default: T) -> T {\n        items.iter().find(|e| e.config_name == config_name).map_or_else(\n            || {\n                warn!(\"Unknown config_name \\\"{config_name}\\\" in {items:?}, using default\");\n                default\n            },\n            |e| e.value.clone(),\n        )\n    }\n}\n"
  },
  {
    "path": "cedinia/src/settings/mod.rs",
    "content": "pub mod gui_settings_values;\n\nuse std::path::PathBuf;\n\nuse czkawka_core::common::config_cache_path::get_config_cache_path;\nuse log::{error, info};\nuse serde::{Deserialize, Serialize};\n\nuse crate::settings::gui_settings_values::StringComboBoxItems;\n\nfn default_check_method() -> String {\n    \"hash\".to_string()\n}\nfn default_hash_type() -> String {\n    \"blake3\".to_string()\n}\nfn default_hash_size() -> String {\n    \"16\".to_string()\n}\nfn ttrue() -> bool {\n    true\n}\nfn default_similarity_preset() -> String {\n    \"medium\".to_string()\n}\nfn default_search_mode() -> String {\n    \"biggest\".to_string()\n}\nfn default_big_files_count() -> String {\n    \"50\".to_string()\n}\nfn default_min_file_size_idx() -> i32 {\n    0\n}\nfn default_max_file_size_idx() -> i32 {\n    4 // Unlimited\n}\nfn default_language() -> String {\n    \"auto\".to_string()\n}\nfn default_hash_alg() -> String {\n    \"mean\".to_string()\n}\nfn default_image_filter() -> String {\n    \"triangle\".to_string()\n}\nfn default_same_music_check_method() -> String {\n    \"tags\".to_string()\n}\nfn default_excluded_items() -> String {\n    #[cfg(not(target_os = \"android\"))]\n    {\n        \"*/.*\".to_string()\n    }\n    #[cfg(target_os = \"android\")]\n    {\n        String::new()\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CediniaSettings {\n    #[serde(default = \"ttrue\")]\n    pub use_cache: bool,\n    #[serde(default = \"ttrue\")]\n    pub ignore_hidden: bool,\n    #[serde(default = \"default_min_file_size_idx\")]\n    pub min_file_size_idx: i32,\n    #[serde(default = \"default_max_file_size_idx\")]\n    pub max_file_size_idx: i32,\n    #[serde(default = \"default_language\")]\n    pub language: String,\n    #[serde(default = \"default_excluded_items\")]\n    pub excluded_items: String,\n    #[serde(default)]\n    pub allowed_extensions: String,\n    #[serde(default)]\n    pub excluded_extensions: String,\n\n    #[serde(default = \"default_check_method\")]\n    pub duplicates_check_method: String,\n    #[serde(default = \"default_hash_type\")]\n    pub duplicates_hash_type: String,\n\n    #[serde(default = \"default_similarity_preset\")]\n    pub similar_images_similarity_preset: String,\n    #[serde(default = \"default_hash_size\")]\n    pub similar_images_hash_size: String,\n    #[serde(default = \"default_hash_alg\")]\n    pub similar_images_hash_alg: String,\n    #[serde(default = \"default_image_filter\")]\n    pub similar_images_image_filter: String,\n    #[serde(default)]\n    pub similar_images_ignore_same_size: bool,\n\n    #[serde(default = \"default_search_mode\")]\n    pub big_files_search_mode: String,\n    #[serde(default = \"default_big_files_count\")]\n    pub big_files_count: String,\n\n    #[serde(default = \"ttrue\")]\n    pub same_music_title: bool,\n    #[serde(default = \"ttrue\")]\n    pub same_music_artist: bool,\n    #[serde(default)]\n    pub same_music_year: bool,\n    #[serde(default)]\n    pub same_music_length: bool,\n    #[serde(default)]\n    pub same_music_genre: bool,\n    #[serde(default)]\n    pub same_music_bitrate: bool,\n    #[serde(default)]\n    pub same_music_approximate: bool,\n    #[serde(default = \"default_same_music_check_method\")]\n    pub same_music_check_method: String,\n\n    #[serde(default = \"ttrue\")]\n    pub broken_files_audio: bool,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_pdf: bool,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_archive: bool,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_image: bool,\n\n    #[serde(default = \"ttrue\")]\n    pub bad_names_uppercase_extension: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_emoji_used: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_space_at_start_or_end: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_non_ascii_graphical: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_remove_duplicated_non_alpha: bool,\n}\n\nimpl Default for CediniaSettings {\n    fn default() -> Self {\n        Self {\n            use_cache: true,\n            ignore_hidden: true,\n            min_file_size_idx: default_min_file_size_idx(),\n            max_file_size_idx: default_max_file_size_idx(),\n            language: default_language(),\n            excluded_items: default_excluded_items(),\n            allowed_extensions: String::new(),\n            excluded_extensions: String::new(),\n            duplicates_check_method: default_check_method(),\n            duplicates_hash_type: default_hash_type(),\n            similar_images_similarity_preset: default_similarity_preset(),\n            similar_images_hash_size: default_hash_size(),\n            similar_images_hash_alg: default_hash_alg(),\n            similar_images_image_filter: default_image_filter(),\n            similar_images_ignore_same_size: false,\n            big_files_search_mode: default_search_mode(),\n            big_files_count: default_big_files_count(),\n            same_music_title: true,\n            same_music_artist: true,\n            same_music_year: false,\n            same_music_length: false,\n            same_music_genre: false,\n            same_music_bitrate: false,\n            same_music_approximate: false,\n            same_music_check_method: default_same_music_check_method(),\n            broken_files_audio: true,\n            broken_files_pdf: true,\n            broken_files_archive: true,\n            broken_files_image: true,\n            bad_names_uppercase_extension: true,\n            bad_names_emoji_used: true,\n            bad_names_space_at_start_or_end: true,\n            bad_names_non_ascii_graphical: true,\n            bad_names_remove_duplicated_non_alpha: true,\n        }\n    }\n}\n\nfn get_dirs_file() -> Option<PathBuf> {\n    let config_folder = get_config_cache_path()?.config_folder;\n    Some(config_folder.join(\"cedinia_dirs.json\"))\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\nstruct DirConfig {\n    included: Vec<String>,\n    excluded: Vec<String>,\n}\n\npub fn save_dirs(included: &[PathBuf], excluded: &[PathBuf]) {\n    let Some(path) = get_dirs_file() else {\n        error!(\"Cannot determine dirs config path – dirs not saved\");\n        return;\n    };\n    if let Some(parent) = path.parent()\n        && let Err(e) = std::fs::create_dir_all(parent)\n    {\n        error!(\"Cannot create config dir {}: {e}\", parent.display());\n        return;\n    }\n    let config = DirConfig {\n        included: included.iter().map(|p| p.to_string_lossy().to_string()).collect(),\n        excluded: excluded.iter().map(|p| p.to_string_lossy().to_string()).collect(),\n    };\n    match serde_json::to_string_pretty(&config) {\n        Ok(json) => {\n            if let Err(e) = std::fs::write(&path, json) {\n                error!(\"Cannot write dirs to {}: {e}\", path.display());\n            } else {\n                info!(\"Dirs saved to {}\", path.display());\n            }\n        }\n        Err(e) => error!(\"Cannot serialize dirs: {e}\"),\n    }\n}\n\npub fn load_dirs() -> (Vec<PathBuf>, Vec<PathBuf>) {\n    let Some(path) = get_dirs_file() else {\n        return (vec![], vec![]);\n    };\n    if !path.is_file() {\n        return (vec![], vec![]);\n    }\n    match std::fs::read_to_string(&path) {\n        Ok(json) => match serde_json::from_str::<DirConfig>(&json) {\n            Ok(c) => {\n                let inc = c.included.iter().map(PathBuf::from).collect();\n                let exc = c.excluded.iter().map(PathBuf::from).collect();\n                (inc, exc)\n            }\n            Err(e) => {\n                error!(\"Cannot parse dirs config: {e}\");\n                (vec![], vec![])\n            }\n        },\n        Err(e) => {\n            error!(\"Cannot read dirs config {}: {e}\", path.display());\n            (vec![], vec![])\n        }\n    }\n}\n\nfn get_config_file() -> Option<PathBuf> {\n    let config_folder = get_config_cache_path()?.config_folder;\n    Some(config_folder.join(\"cedinia_settings.json\"))\n}\n\npub fn load_settings() -> CediniaSettings {\n    let Some(path) = get_config_file() else {\n        info!(\"Cannot determine config path – using defaults\");\n        return CediniaSettings::default();\n    };\n\n    if !path.is_file() {\n        info!(\"Settings file does not exist yet – using defaults\");\n        return CediniaSettings::default();\n    }\n\n    match std::fs::read_to_string(&path) {\n        Ok(json) => match serde_json::from_str::<CediniaSettings>(&json) {\n            Ok(s) => {\n                info!(\"Settings loaded from {}\", path.display());\n                s\n            }\n            Err(e) => {\n                error!(\"Cannot parse settings from {}: {e} – using defaults\", path.display());\n                CediniaSettings::default()\n            }\n        },\n        Err(e) => {\n            error!(\"Cannot read settings file {}: {e} – using defaults\", path.display());\n            CediniaSettings::default()\n        }\n    }\n}\n\npub fn save_settings(settings: &CediniaSettings) {\n    let Some(path) = get_config_file() else {\n        error!(\"Cannot determine config path – settings not saved\");\n        return;\n    };\n\n    if let Some(parent) = path.parent()\n        && let Err(e) = std::fs::create_dir_all(parent)\n    {\n        error!(\"Cannot create config dir {}: {e}\", parent.display());\n        return;\n    }\n\n    match serde_json::to_string_pretty(settings) {\n        Ok(json) => {\n            if let Err(e) = std::fs::write(&path, json) {\n                error!(\"Cannot write settings to {}: {e}\", path.display());\n            } else {\n                info!(\"Settings saved to {}\", path.display());\n            }\n        }\n        Err(e) => error!(\"Cannot serialize settings: {e}\"),\n    }\n}\n\nuse slint::ComponentHandle;\n\nuse crate::{BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, GeneralSettings, MainWindow, SameMusicSettings, SimilarImagesSettings};\n\npub fn apply_settings_to_gui(win: &MainWindow, s: &CediniaSettings) {\n    let items = StringComboBoxItems::new();\n\n    win.global::<GeneralSettings>().set_use_cache(s.use_cache);\n    win.global::<GeneralSettings>().set_ignore_hidden(s.ignore_hidden);\n    win.global::<GeneralSettings>().set_min_file_size_idx(s.min_file_size_idx);\n    win.global::<GeneralSettings>().set_max_file_size_idx(s.max_file_size_idx);\n    let lang_idx = match s.language.as_str() {\n        \"pl\" => 1,\n        \"en\" => 0,\n        _ => crate::localizer_cedinia::detect_os_language_idx(),\n    };\n    win.global::<GeneralSettings>().set_language_idx(lang_idx);\n    win.global::<GeneralSettings>().set_excluded_items(s.excluded_items.clone().into());\n    win.global::<GeneralSettings>().set_allowed_extensions(s.allowed_extensions.clone().into());\n    win.global::<GeneralSettings>().set_excluded_extensions(s.excluded_extensions.clone().into());\n\n    let cm_idx = StringComboBoxItems::idx_from_config_name(&s.duplicates_check_method, &items.duplicates_check_method);\n    win.global::<DuplicateSettings>().set_check_method(cm_idx as i32);\n    win.global::<DuplicateSettings>().set_check_method_value(s.duplicates_check_method.clone().into());\n\n    let ht_idx = StringComboBoxItems::idx_from_config_name(&s.duplicates_hash_type, &items.duplicates_hash_type);\n    win.global::<DuplicateSettings>().set_hash_type(ht_idx as i32);\n    win.global::<DuplicateSettings>().set_hash_type_value(s.duplicates_hash_type.clone().into());\n\n    let sp_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_similarity_preset, &items.similarity_preset);\n    win.global::<SimilarImagesSettings>().set_similarity_preset(sp_idx as i32);\n    win.global::<SimilarImagesSettings>()\n        .set_similarity_preset_value(s.similar_images_similarity_preset.clone().into());\n\n    let hs_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_hash_size, &items.hash_size);\n    win.global::<SimilarImagesSettings>().set_hash_size_idx(hs_idx as i32);\n    win.global::<SimilarImagesSettings>().set_hash_size_value(s.similar_images_hash_size.clone().into());\n\n    let ha_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_hash_alg, &items.hash_alg);\n    win.global::<SimilarImagesSettings>().set_hash_alg_idx(ha_idx as i32);\n    win.global::<SimilarImagesSettings>().set_hash_alg_value(s.similar_images_hash_alg.clone().into());\n\n    let if_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_image_filter, &items.image_filter);\n    win.global::<SimilarImagesSettings>().set_image_filter_idx(if_idx as i32);\n    win.global::<SimilarImagesSettings>().set_image_filter_value(s.similar_images_image_filter.clone().into());\n\n    win.global::<SimilarImagesSettings>().set_ignore_same_size(s.similar_images_ignore_same_size);\n\n    let sm_idx = StringComboBoxItems::idx_from_config_name(&s.big_files_search_mode, &items.biggest_files_method);\n    win.global::<BigFilesSettings>().set_search_mode_idx(sm_idx as i32);\n    win.global::<BigFilesSettings>().set_search_mode_value(s.big_files_search_mode.clone().into());\n\n    let cnt_idx = StringComboBoxItems::idx_from_config_name(&s.big_files_count, &items.big_files_count);\n    win.global::<BigFilesSettings>().set_count_idx(cnt_idx as i32);\n    win.global::<BigFilesSettings>().set_count_value(s.big_files_count.clone().into());\n\n    let sm = win.global::<SameMusicSettings>();\n    sm.set_title(s.same_music_title);\n    sm.set_artist(s.same_music_artist);\n    sm.set_year(s.same_music_year);\n    sm.set_length(s.same_music_length);\n    sm.set_genre(s.same_music_genre);\n    sm.set_bitrate(s.same_music_bitrate);\n    sm.set_approximate(s.same_music_approximate);\n    let smc_idx = StringComboBoxItems::idx_from_config_name(&s.same_music_check_method, &items.same_music_check_method);\n    sm.set_check_method_idx(smc_idx as i32);\n    sm.set_check_method_value(s.same_music_check_method.clone().into());\n\n    let bf = win.global::<BrokenFilesSettings>();\n    bf.set_check_audio(s.broken_files_audio);\n    bf.set_check_pdf(s.broken_files_pdf);\n    bf.set_check_archive(s.broken_files_archive);\n    bf.set_check_image(s.broken_files_image);\n\n    let bn = win.global::<BadNamesSettings>();\n    bn.set_uppercase_extension(s.bad_names_uppercase_extension);\n    bn.set_emoji_used(s.bad_names_emoji_used);\n    bn.set_space_at_start_or_end(s.bad_names_space_at_start_or_end);\n    bn.set_non_ascii_graphical(s.bad_names_non_ascii_graphical);\n    bn.set_remove_duplicated_non_alpha(s.bad_names_remove_duplicated_non_alpha);\n}\n\npub fn collect_settings_from_gui(win: &MainWindow) -> CediniaSettings {\n    let items = StringComboBoxItems::new();\n    let g = win.global::<GeneralSettings>();\n    let d = win.global::<DuplicateSettings>();\n    let si = win.global::<SimilarImagesSettings>();\n    let bfiles = win.global::<BigFilesSettings>();\n    let sm = win.global::<SameMusicSettings>();\n    let bf = win.global::<BrokenFilesSettings>();\n    let bn = win.global::<BadNamesSettings>();\n\n    CediniaSettings {\n        use_cache: g.get_use_cache(),\n        ignore_hidden: g.get_ignore_hidden(),\n        min_file_size_idx: g.get_min_file_size_idx(),\n        max_file_size_idx: g.get_max_file_size_idx(),\n        language: match g.get_language_idx() {\n            1 => \"pl\".to_string(),\n            _ => \"en\".to_string(),\n        },\n        excluded_items: g.get_excluded_items().to_string(),\n        allowed_extensions: g.get_allowed_extensions().to_string(),\n        excluded_extensions: g.get_excluded_extensions().to_string(),\n        duplicates_check_method: items\n            .duplicates_check_method\n            .get(d.get_check_method() as usize)\n            .map_or_else(|| panic!(\"Invalid check_method idx {} in GUI\", d.get_check_method()), |e| e.config_name.clone()),\n        duplicates_hash_type: items\n            .duplicates_hash_type\n            .get(d.get_hash_type() as usize)\n            .map_or_else(|| panic!(\"Invalid hash_type idx {} in GUI\", d.get_hash_type()), |e| e.config_name.clone()),\n        similar_images_similarity_preset: items\n            .similarity_preset\n            .get(si.get_similarity_preset() as usize)\n            .map_or_else(|| panic!(\"Invalid similarity_preset idx {} in GUI\", si.get_similarity_preset()), |e| e.config_name.clone()),\n        similar_images_hash_size: items\n            .hash_size\n            .get(si.get_hash_size_idx() as usize)\n            .map_or_else(|| panic!(\"Invalid hash_size_idx {} in GUI\", si.get_hash_size_idx()), |e| e.config_name.clone()),\n        similar_images_hash_alg: items\n            .hash_alg\n            .get(si.get_hash_alg_idx() as usize)\n            .map_or_else(|| panic!(\"Invalid hash_alg_idx {} in GUI\", si.get_hash_alg_idx()), |e| e.config_name.clone()),\n        similar_images_image_filter: items\n            .image_filter\n            .get(si.get_image_filter_idx() as usize)\n            .map_or_else(|| panic!(\"Invalid image_filter_idx {} in GUI\", si.get_image_filter_idx()), |e| e.config_name.clone()),\n        similar_images_ignore_same_size: si.get_ignore_same_size(),\n        big_files_search_mode: items\n            .biggest_files_method\n            .get(bfiles.get_search_mode_idx() as usize)\n            .map_or_else(|| panic!(\"Invalid search_mode_idx {} in GUI\", bfiles.get_search_mode_idx()), |e| e.config_name.clone()),\n        big_files_count: items\n            .big_files_count\n            .get(bfiles.get_count_idx() as usize)\n            .map_or_else(|| panic!(\"Invalid count_idx {} in GUI\", bfiles.get_count_idx()), |e| e.config_name.clone()),\n        same_music_title: sm.get_title(),\n        same_music_artist: sm.get_artist(),\n        same_music_year: sm.get_year(),\n        same_music_length: sm.get_length(),\n        same_music_genre: sm.get_genre(),\n        same_music_bitrate: sm.get_bitrate(),\n        same_music_approximate: sm.get_approximate(),\n        same_music_check_method: items.same_music_check_method.get(sm.get_check_method_idx() as usize).map_or_else(\n            || panic!(\"Invalid same_music_check_method_idx {} in GUI\", sm.get_check_method_idx()),\n            |e| e.config_name.clone(),\n        ),\n        broken_files_audio: bf.get_check_audio(),\n        broken_files_pdf: bf.get_check_pdf(),\n        broken_files_archive: bf.get_check_archive(),\n        broken_files_image: bf.get_check_image(),\n        bad_names_uppercase_extension: bn.get_uppercase_extension(),\n        bad_names_emoji_used: bn.get_emoji_used(),\n        bad_names_space_at_start_or_end: bn.get_space_at_start_or_end(),\n        bad_names_non_ascii_graphical: bn.get_non_ascii_graphical(),\n        bad_names_remove_duplicated_non_alpha: bn.get_remove_duplicated_non_alpha(),\n    }\n}\n"
  },
  {
    "path": "cedinia/src/thumbnail_loader.rs",
    "content": "use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};\nuse std::time::{Duration, SystemTime};\n\nuse czkawka_core::common::image::{ImgResizeOptions, LoadedImage};\nuse log::trace;\n\nuse crate::scan_runner::FileItem;\n\npub enum ThumbnailData {\n    Loaded(Vec<u8>, u32, u32),\n    Placeholder,\n}\n\npub struct ThumbnailResult {\n    pub scan_id: u32,\n    pub group_idx: usize,\n    pub item_idx: usize,\n    pub data: ThumbnailData,\n}\n\nfn get_total_ram_mb() -> u64 {\n    if let Ok(content) = std::fs::read_to_string(\"/proc/meminfo\") {\n        for line in content.lines() {\n            if line.starts_with(\"MemTotal:\")\n                && let Some(kb_str) = line.split_whitespace().nth(1)\n                && let Ok(kb) = kb_str.parse::<u64>()\n            {\n                return kb / 1024;\n            }\n        }\n    }\n    4096\n}\n\npub fn cache_limit_bytes() -> u64 {\n    let ram_mb = get_total_ram_mb();\n    let limit_mb: u64 = if ram_mb <= 2048 {\n        256\n    } else if ram_mb <= 4096 {\n        1024\n    } else if ram_mb <= 8192 {\n        2048\n    } else {\n        4096\n    };\n    limit_mb * 1024 * 1024\n}\n\npub fn thumbnail_cache_dir() -> PathBuf {\n    #[cfg(target_os = \"android\")]\n    {\n        let base = crate::android_cache_path().unwrap_or(\"/data/data/io.github.qarmin.cedinia/cache\");\n        PathBuf::from(base).join(\"img_thumbnails\")\n    }\n    #[cfg(not(target_os = \"android\"))]\n    {\n        let base = std::env::var(\"XDG_CACHE_HOME\").map_or_else(|_| PathBuf::from(std::env::var(\"HOME\").unwrap_or_default()).join(\".cache\"), PathBuf::from);\n        base.join(\"cedinia\").join(\"img_thumbnails\")\n    }\n}\n\nfn cache_key(path: &str, mtime_secs: u64, file_size: u64) -> String {\n    let mut h = DefaultHasher::new();\n    path.hash(&mut h);\n    mtime_secs.hash(&mut h);\n    file_size.hash(&mut h);\n    format!(\"{:016x}.png\", h.finish())\n}\n\nfn try_read_png_cache(cache_path: &Path) -> Option<(Vec<u8>, u32, u32)> {\n    let data = std::fs::read(cache_path).ok()?;\n    let img = image::load_from_memory_with_format(&data, image::ImageFormat::Png).ok()?;\n    let rgba = img.into_rgba8();\n    let w = rgba.width();\n    let h = rgba.height();\n    Some((rgba.into_raw(), w, h))\n}\n\nfn try_write_png_cache(cache_path: &Path, rgba: &[u8], w: u32, h: u32) {\n    let tmp = cache_path.with_extension(\"tmp\");\n    let write = || -> image::ImageResult<()> {\n        use image::ImageEncoder;\n        let f = std::fs::File::create(&tmp).map_err(image::ImageError::IoError)?;\n        image::codecs::png::PngEncoder::new(f).write_image(rgba, w, h, image::ExtendedColorType::Rgba8)\n    };\n    if write().is_ok() {\n        let _ = std::fs::rename(&tmp, cache_path);\n    } else {\n        let _ = std::fs::remove_file(&tmp);\n    }\n}\n\npub fn make_placeholder_image() -> slint::Image {\n    const W: u32 = 32;\n    const H: u32 = 32;\n    const CELL: u32 = 16;\n    let mut rgba = vec![0u8; (W * H * 4) as usize];\n    for y in 0..H {\n        for x in 0..W {\n            let off = ((y * W + x) * 4) as usize;\n            let v = if ((x / CELL) + (y / CELL)).is_multiple_of(2) { 160u8 } else { 80u8 };\n            rgba[off] = v;\n            rgba[off + 1] = v;\n            rgba[off + 2] = v;\n            rgba[off + 3] = 255;\n        }\n    }\n    rgba_to_slint_image(&rgba, W, H)\n}\n\npub fn rgba_to_slint_image(rgba: &[u8], width: u32, height: u32) -> slint::Image {\n    let buffer = slint::SharedPixelBuffer::<slint::Rgba8Pixel>::clone_from_slice(rgba, width, height);\n    slint::Image::from_rgba8(buffer)\n}\n\npub fn load_and_resize_thumbnail(path: &str, cache_dir: &Path) -> Option<(Vec<u8>, u32, u32)> {\n    use czkawka_core::common::image::get_dynamic_image_from_path;\n    use fast_image_resize::FilterType;\n\n    let meta = std::fs::metadata(path).ok();\n    let (mtime_secs, file_size) = meta.as_ref().map_or((0, 0), |m| {\n        let mtime = m.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map_or(0, |d| d.as_secs());\n        (mtime, m.len())\n    });\n\n    let cache_file = cache_dir.join(cache_key(path, mtime_secs, file_size));\n\n    if let Some(cached) = try_read_png_cache(&cache_file) {\n        let now = filetime::FileTime::now();\n        let _ = filetime::set_file_mtime(&cache_file, now);\n        trace!(\"Loaded thumbnail from cache for {path} ({file_size} bytes)\");\n        return Some(cached);\n    }\n    trace!(\"Generating thumbnail for {path} ({file_size} bytes)\");\n    let loaded_data = get_dynamic_image_from_path(\n        path,\n        Some(ImgResizeOptions {\n            max_width: 256,\n            max_height: 256,\n            filter: FilterType::Lanczos3,\n        }),\n    )\n    .ok()?;\n\n    let LoadedImage {\n        image,\n        original_width,\n        original_height,\n    } = loaded_data;\n\n    let should_cache = original_width >= 256 || original_height >= 256;\n\n    let rgba = image.into_rgba8();\n    let w = rgba.width();\n    let h = rgba.height();\n    let raw = rgba.into_raw();\n\n    if should_cache {\n        trace!(\"Caching thumbnail for {path} at {w}x{h}\");\n        try_write_png_cache(&cache_file, &raw, w, h);\n    } else {\n        trace!(\"Not caching thumbnail for {path} since it's smaller than 256x256 ({original_width}x{original_height})\");\n    }\n\n    Some((raw, w, h))\n}\n\npub fn collect_thumb_tasks(items: &[FileItem]) -> Vec<(usize, usize, String)> {\n    use crate::common::{STR_IDX_NAME, STR_IDX_PATH};\n    let mut tasks = Vec::new();\n    let mut group_idx: i32 = -1;\n    let mut item_idx = 0usize;\n    for item in items {\n        if item.is_header {\n            group_idx += 1;\n            item_idx = 0;\n        } else if group_idx >= 0 {\n            let name = &item.val_str[STR_IDX_NAME];\n            let path = &item.val_str[STR_IDX_PATH];\n            let full = if path.is_empty() { name.clone() } else { format!(\"{path}/{name}\") };\n            tasks.push((group_idx as usize, item_idx, full));\n            item_idx += 1;\n        }\n    }\n    tasks\n}\n\npub fn cleanup_old_thumbnails() {\n    let cache_dir = thumbnail_cache_dir();\n    let cutoff = SystemTime::now().checked_sub(Duration::from_secs(30 * 24 * 3600)).unwrap_or(SystemTime::UNIX_EPOCH);\n    if let Ok(entries) = std::fs::read_dir(&cache_dir) {\n        for entry in entries.flatten() {\n            if let Ok(meta) = entry.metadata()\n                && meta.modified().map(|t| t < cutoff).unwrap_or(false)\n            {\n                let _ = std::fs::remove_file(entry.path());\n            }\n        }\n    }\n}\n\npub fn spawn_thumbnail_loader(tasks: Vec<(usize, usize, String)>, tx: std::sync::mpsc::Sender<ThumbnailResult>, cancel: Arc<AtomicBool>, scan_id: u32) {\n    let cache_dir = thumbnail_cache_dir();\n    let _ = std::fs::create_dir_all(&cache_dir);\n    let cache_dir = Arc::new(cache_dir);\n\n    std::thread::spawn(move || {\n        if tasks.is_empty() {\n            return;\n        }\n\n        let num_workers = std::thread::available_parallelism().map(|n| n.get().min(4)).unwrap_or(2);\n\n        let limit = cache_limit_bytes();\n        let used_bytes = Arc::new(AtomicU64::new(0));\n        let next_idx = Arc::new(AtomicUsize::new(0));\n        let tasks = Arc::new(tasks);\n        let mut handles = Vec::with_capacity(num_workers);\n\n        for _ in 0..num_workers {\n            let tasks = tasks.clone();\n            let tx = tx.clone();\n            let cancel = cancel.clone();\n            let used_bytes = used_bytes.clone();\n            let next_idx = next_idx.clone();\n            let cache_dir = cache_dir.clone();\n\n            handles.push(std::thread::spawn(move || {\n                loop {\n                    let idx = next_idx.fetch_add(1, Ordering::Relaxed);\n                    if idx >= tasks.len() || cancel.load(Ordering::Relaxed) {\n                        break;\n                    }\n\n                    let (group_idx, item_idx, ref path) = tasks[idx];\n                    let cur = used_bytes.load(Ordering::Relaxed);\n                    let data = if cur >= limit {\n                        ThumbnailData::Placeholder\n                    } else {\n                        match load_and_resize_thumbnail(path, &cache_dir) {\n                            Some((rgba, w, h)) => {\n                                let size = rgba.len() as u64;\n                                let prev = used_bytes.fetch_add(size, Ordering::SeqCst);\n                                if prev + size <= limit {\n                                    ThumbnailData::Loaded(rgba, w, h)\n                                } else {\n                                    used_bytes.fetch_sub(size, Ordering::SeqCst);\n                                    ThumbnailData::Placeholder\n                                }\n                            }\n                            None => ThumbnailData::Placeholder,\n                        }\n                    };\n\n                    if tx\n                        .send(ThumbnailResult {\n                            scan_id,\n                            group_idx,\n                            item_idx,\n                            data,\n                        })\n                        .is_err()\n                    {\n                        break;\n                    }\n                }\n            }));\n        }\n\n        for h in handles {\n            h.join().expect(\"Thumbnail loader panicked\");\n        }\n    });\n}\n"
  },
  {
    "path": "cedinia/src/translations.rs",
    "content": "use slint::ComponentHandle;\n\nuse crate::{MainWindow, Translations, flc};\n\npub(crate) fn translate_items(app: &MainWindow) {\n    let t = app.global::<Translations>();\n\n    t.set_app_name_text(flc!(\"app_name\").into());\n    t.set_tool_duplicate_files_text(flc!(\"tool_duplicate_files\").into());\n    t.set_tool_empty_folders_text(flc!(\"tool_empty_folders\").into());\n    t.set_tool_similar_images_text(flc!(\"tool_similar_images\").into());\n    t.set_tool_empty_files_text(flc!(\"tool_empty_files\").into());\n    t.set_tool_temporary_files_text(flc!(\"tool_temporary_files\").into());\n    t.set_tool_big_files_text(flc!(\"tool_big_files\").into());\n    t.set_tool_broken_files_text(flc!(\"tool_broken_files\").into());\n    t.set_tool_bad_extensions_text(flc!(\"tool_bad_extensions\").into());\n    t.set_tool_same_music_text(flc!(\"tool_same_music\").into());\n    t.set_tool_bad_names_text(flc!(\"tool_bad_names\").into());\n    t.set_tool_exif_remover_text(flc!(\"tool_exif_remover\").into());\n    t.set_tool_directories_text(flc!(\"tool_directories\").into());\n    t.set_tool_settings_text(flc!(\"tool_settings\").into());\n\n    t.set_home_dup_description_text(flc!(\"home_dup_description\").into());\n    t.set_home_empty_folders_description_text(flc!(\"home_empty_folders_description\").into());\n    t.set_home_similar_images_description_text(flc!(\"home_similar_images_description\").into());\n    t.set_home_empty_files_description_text(flc!(\"home_empty_files_description\").into());\n    t.set_home_temp_files_description_text(flc!(\"home_temp_files_description\").into());\n    t.set_home_big_files_description_text(flc!(\"home_big_files_description\").into());\n    t.set_home_broken_files_description_text(flc!(\"home_broken_files_description\").into());\n    t.set_home_bad_extensions_description_text(flc!(\"home_bad_extensions_description\").into());\n    t.set_home_same_music_description_text(flc!(\"home_same_music_description\").into());\n    t.set_home_bad_names_description_text(flc!(\"home_bad_names_description\").into());\n    t.set_home_exif_description_text(flc!(\"home_exif_description\").into());\n\n    t.set_scanning_text(flc!(\"scanning\").into());\n    t.set_stopping_text(flc!(\"stopping\").into());\n    t.set_no_results_text(flc!(\"no_results\").into());\n    t.set_press_start_text(flc!(\"press_start\").into());\n    t.set_select_label_text(flc!(\"select_label\").into());\n    t.set_deselect_label_text(flc!(\"deselect_label\").into());\n    t.set_list_label_text(flc!(\"list_label\").into());\n    t.set_gallery_label_text(flc!(\"gallery_label\").into());\n\n    t.set_selection_popup_title_text(flc!(\"selection_popup_title\").into());\n    t.set_select_all_text(flc!(\"select_all\").into());\n    t.set_select_except_one_text(flc!(\"select_except_one\").into());\n    t.set_select_except_largest_text(flc!(\"select_except_largest\").into());\n    t.set_select_except_smallest_text(flc!(\"select_except_smallest\").into());\n    t.set_select_largest_text(flc!(\"select_largest\").into());\n    t.set_select_smallest_text(flc!(\"select_smallest\").into());\n    t.set_select_except_highest_res_text(flc!(\"select_except_highest_res\").into());\n    t.set_select_except_lowest_res_text(flc!(\"select_except_lowest_res\").into());\n    t.set_select_highest_res_text(flc!(\"select_highest_res\").into());\n    t.set_select_lowest_res_text(flc!(\"select_lowest_res\").into());\n    t.set_invert_selection_text(flc!(\"invert_selection\").into());\n    t.set_close_text(flc!(\"close\").into());\n\n    t.set_deselection_popup_title_text(flc!(\"deselection_popup_title\").into());\n    t.set_deselect_all_text(flc!(\"deselect_all\").into());\n    t.set_deselect_except_one_text(flc!(\"deselect_except_one\").into());\n\n    t.set_cancel_text(flc!(\"cancel\").into());\n    t.set_delete_text(flc!(\"delete\").into());\n    t.set_rename_text(flc!(\"rename\").into());\n\n    t.set_delete_errors_title_text(flc!(\"delete_errors_title\").into());\n    t.set_ok_text(flc!(\"ok\").into());\n\n    t.set_stopping_overlay_title_text(flc!(\"stopping_overlay_title\").into());\n    t.set_stopping_overlay_body_text(flc!(\"stopping_overlay_body\").into());\n\n    t.set_permission_title_text(flc!(\"permission_title\").into());\n    t.set_permission_body_text(flc!(\"permission_body\").into());\n    t.set_grant_text(flc!(\"grant\").into());\n    t.set_no_permission_scan_warning_text(flc!(\"no_permission_scan_warning\").into());\n\n    t.set_settings_tab_general_text(flc!(\"settings_tab_general\").into());\n    t.set_settings_tab_tools_text(flc!(\"settings_tab_tools\").into());\n    t.set_settings_tab_diagnostics_text(flc!(\"settings_tab_diagnostics\").into());\n\n    t.set_settings_use_cache_text(flc!(\"settings_use_cache\").into());\n    t.set_settings_use_cache_desc_text(flc!(\"settings_use_cache_desc\").into());\n    t.set_settings_ignore_hidden_text(flc!(\"settings_ignore_hidden\").into());\n    t.set_settings_ignore_hidden_desc_text(flc!(\"settings_ignore_hidden_desc\").into());\n    t.set_settings_scan_label_text(flc!(\"settings_scan_label\").into());\n    t.set_settings_filters_label_text(flc!(\"settings_filters_label\").into());\n    t.set_settings_min_file_size_text(flc!(\"settings_min_file_size\").into());\n    t.set_settings_max_file_size_text(flc!(\"settings_max_file_size\").into());\n    t.set_settings_language_text(flc!(\"settings_language\").into());\n    t.set_settings_language_restart_text(flc!(\"settings_language_restart\").into());\n    t.set_settings_common_label_text(flc!(\"settings_common_label\").into());\n    t.set_settings_hash_type_desc_text(flc!(\"settings_hash_type_desc\").into());\n    t.set_settings_similarity_desc_text(flc!(\"settings_similarity_desc\").into());\n    t.set_settings_hash_size_desc_text(flc!(\"settings_hash_size_desc\").into());\n    t.set_settings_excluded_items_text(flc!(\"settings_excluded_items\").into());\n    t.set_settings_excluded_items_placeholder_text(flc!(\"settings_excluded_items_placeholder\").into());\n    t.set_settings_allowed_extensions_text(flc!(\"settings_allowed_extensions\").into());\n    t.set_settings_allowed_extensions_placeholder_text(flc!(\"settings_allowed_extensions_placeholder\").into());\n    t.set_settings_excluded_extensions_text(flc!(\"settings_excluded_extensions\").into());\n    t.set_settings_excluded_extensions_placeholder_text(flc!(\"settings_excluded_extensions_placeholder\").into());\n\n    t.set_settings_duplicates_header_text(flc!(\"settings_duplicates_header\").into());\n    t.set_settings_check_method_label_text(flc!(\"settings_check_method_label\").into());\n    t.set_settings_check_method_text(flc!(\"settings_check_method\").into());\n    t.set_settings_hash_type_label_text(flc!(\"settings_hash_type_label\").into());\n    t.set_settings_hash_type_text(flc!(\"settings_hash_type\").into());\n    t.set_settings_similar_images_header_text(flc!(\"settings_similar_images_header\").into());\n    t.set_settings_similarity_preset_text(flc!(\"settings_similarity_preset\").into());\n    t.set_settings_hash_size_text(flc!(\"settings_hash_size\").into());\n    t.set_settings_hash_alg_text(flc!(\"settings_hash_alg\").into());\n    t.set_settings_image_filter_text(flc!(\"settings_image_filter\").into());\n    t.set_settings_ignore_same_size_text(flc!(\"settings_ignore_same_size\").into());\n    t.set_settings_big_files_header_text(flc!(\"settings_big_files_header\").into());\n    t.set_settings_search_mode_text(flc!(\"settings_search_mode\").into());\n    t.set_settings_file_count_text(flc!(\"settings_file_count\").into());\n    t.set_settings_same_music_header_text(flc!(\"settings_same_music_header\").into());\n    t.set_settings_music_check_method_text(flc!(\"settings_music_check_method\").into());\n    t.set_settings_music_compare_tags_label_text(flc!(\"settings_music_compare_tags_label\").into());\n    t.set_settings_music_title_text(flc!(\"settings_music_title\").into());\n    t.set_settings_music_artist_text(flc!(\"settings_music_artist\").into());\n    t.set_settings_music_year_text(flc!(\"settings_music_year\").into());\n    t.set_settings_music_length_text(flc!(\"settings_music_length\").into());\n    t.set_settings_music_genre_text(flc!(\"settings_music_genre\").into());\n    t.set_settings_music_bitrate_text(flc!(\"settings_music_bitrate\").into());\n    t.set_settings_music_approx_text(flc!(\"settings_music_approx\").into());\n    t.set_settings_broken_files_header_text(flc!(\"settings_broken_files_header\").into());\n    t.set_settings_broken_files_types_label_text(flc!(\"settings_broken_files_types_label\").into());\n    t.set_settings_broken_audio_text(flc!(\"settings_broken_audio\").into());\n    t.set_settings_broken_pdf_text(flc!(\"settings_broken_pdf\").into());\n    t.set_settings_broken_archive_text(flc!(\"settings_broken_archive\").into());\n    t.set_settings_broken_image_text(flc!(\"settings_broken_image\").into());\n    t.set_settings_bad_names_header_text(flc!(\"settings_bad_names_header\").into());\n    t.set_settings_bad_names_checks_label_text(flc!(\"settings_bad_names_checks_label\").into());\n    t.set_settings_bad_names_uppercase_ext_text(flc!(\"settings_bad_names_uppercase_ext\").into());\n    t.set_settings_bad_names_emoji_text(flc!(\"settings_bad_names_emoji\").into());\n    t.set_settings_bad_names_space_text(flc!(\"settings_bad_names_space\").into());\n    t.set_settings_bad_names_non_ascii_text(flc!(\"settings_bad_names_non_ascii\").into());\n    t.set_settings_bad_names_duplicated_text(flc!(\"settings_bad_names_duplicated\").into());\n\n    t.set_diagnostics_header_text(flc!(\"diagnostics_header\").into());\n    t.set_diagnostics_thumbnails_text(flc!(\"diagnostics_thumbnails\").into());\n    t.set_diagnostics_app_cache_text(flc!(\"diagnostics_app_cache\").into());\n    t.set_diagnostics_refresh_text(flc!(\"diagnostics_refresh\").into());\n    t.set_diagnostics_clear_thumbnails_text(flc!(\"diagnostics_clear_thumbnails\").into());\n    t.set_diagnostics_clear_cache_text(flc!(\"diagnostics_clear_cache\").into());\n    t.set_diagnostics_collect_test_text(flc!(\"diagnostics_collect_test\").into());\n    t.set_diagnostics_collect_test_desc_text(flc!(\"diagnostics_collect_test_desc\").into());\n    t.set_diagnostics_collect_test_run_text(flc!(\"diagnostics_collect_test_run\").into());\n    t.set_diagnostics_collect_test_stop_text(flc!(\"diagnostics_collect_test_stop\").into());\n\n    t.set_collect_test_title_text(flc!(\"collect_test_title\").into());\n    t.set_collect_test_volumes_text(flc!(\"collect_test_volumes\").into());\n    t.set_collect_test_folders_text(flc!(\"collect_test_folders\").into());\n    t.set_collect_test_files_text(flc!(\"collect_test_files\").into());\n    t.set_collect_test_time_text(flc!(\"collect_test_time\").into());\n    t.set_collect_test_ms_text(flc!(\"collect_test_ms\").into());\n\n    t.set_directories_include_header_text(flc!(\"directories_include_header\").into());\n    t.set_directories_exclude_header_text(flc!(\"directories_exclude_header\").into());\n    t.set_directories_add_text(flc!(\"directories_add\").into());\n    t.set_directories_volume_header_text(flc!(\"directories_volume_header\").into());\n    t.set_directories_volume_refresh_text(flc!(\"directories_volume_refresh\").into());\n    t.set_directories_volume_add_text(flc!(\"directories_volume_add\").into());\n    t.set_no_paths_text(flc!(\"no_paths\").into());\n    t.set_gallery_delete_button_text(flc!(\"gallery_delete_button\").into());\n    t.set_gallery_back_text(flc!(\"gallery_back\").into());\n    t.set_gallery_confirm_delete_text(flc!(\"gallery_confirm_delete\").into());\n    t.set_deleting_files_text(flc!(\"deleting_files\").into());\n    t.set_stop_text(flc!(\"stop\").into());\n    t.set_files_suffix_text(flc!(\"files_suffix\").into());\n    t.set_scanning_fallback_text(flc!(\"scanning_fallback\").into());\n    t.set_app_subtitle_text(flc!(\"app_subtitle\").into());\n    t.set_app_license_text(flc!(\"app_license\").into());\n    t.set_about_app_label_text(flc!(\"about_app_label\").into());\n    t.set_cache_label_text(flc!(\"cache_label\").into());\n\n    t.set_nav_home_text(flc!(\"nav_home\").into());\n    t.set_nav_dirs_text(flc!(\"nav_dirs\").into());\n    t.set_nav_settings_text(flc!(\"nav_settings\").into());\n\n    t.set_status_ready_text(flc!(\"status_ready\").into());\n    t.set_status_stopped_text(flc!(\"status_stopped\").into());\n    t.set_status_no_results_text(flc!(\"status_no_results\").into());\n    t.set_status_deleted_selected_text(flc!(\"status_deleted_selected\").into());\n    t.set_status_deleted_with_errors_text(flc!(\"status_deleted_with_errors\").into());\n    t.set_scan_not_started_text(flc!(\"scan_not_started\").into());\n    t.set_found_items_prefix_text(flc!(\"found_items_prefix\").into());\n    t.set_found_items_suffix_text(flc!(\"found_items_suffix\").into());\n    t.set_deleted_items_prefix_text(flc!(\"deleted_items_prefix\").into());\n    t.set_deleted_items_suffix_text(flc!(\"deleted_items_suffix\").into());\n    t.set_deleted_errors_suffix_text(flc!(\"deleted_errors_suffix\").into());\n    t.set_renamed_prefix_text(flc!(\"renamed_prefix\").into());\n    t.set_renamed_files_suffix_text(flc!(\"renamed_files_suffix\").into());\n    t.set_renamed_errors_suffix_text(flc!(\"renamed_errors_suffix\").into());\n    t.set_cleaned_exif_prefix_text(flc!(\"cleaned_exif_prefix\").into());\n    t.set_cleaned_exif_suffix_text(flc!(\"cleaned_exif_suffix\").into());\n    t.set_cleaned_exif_errors_suffix_text(flc!(\"cleaned_exif_errors_suffix\").into());\n    t.set_and_more_prefix_text(flc!(\"and_more_prefix\").into());\n    t.set_and_more_suffix_text(flc!(\"and_more_suffix\").into());\n\n    t.set_about_repo_text(flc!(\"about_repo\").into());\n    t.set_about_translate_text(flc!(\"about_translate\").into());\n    t.set_about_donate_text(flc!(\"about_donate\").into());\n}\n"
  },
  {
    "path": "cedinia/src/volumes.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\n\nuse slint::{ComponentHandle, Model, SharedString};\n\nuse crate::{AppState, MainWindow, VolumeEntry};\n\npub(crate) fn home_dir() -> PathBuf {\n    #[cfg(target_os = \"android\")]\n    {\n        PathBuf::from(\"/sdcard\")\n    }\n    #[cfg(not(target_os = \"android\"))]\n    {\n        std::env::var(\"HOME\").map_or_else(|_| PathBuf::from(\"/\"), PathBuf::from)\n    }\n}\n\npub(crate) fn detect_storage_volumes() -> Vec<VolumeEntry> {\n    let mut result: Vec<VolumeEntry> = Vec::new();\n\n    #[cfg(target_os = \"android\")]\n    let candidates = vec![\n        \"/sdcard\",\n        \"/storage/emulated/0\",\n        \"/storage/emulated/1\",\n        \"/storage/self/primary\",\n        \"/mnt/sdcard\",\n        \"/mnt/extSdCard\",\n        \"/mnt/external_sd\",\n        \"/mnt/media_rw\",\n    ];\n    #[cfg(not(target_os = \"android\"))]\n    let candidates: Vec<&str> = vec![];\n\n    let mut mounts: Vec<(String, String)> = Vec::new();\n    if let Ok(content) = std::fs::read_to_string(\"/proc/mounts\") {\n        for line in content.lines() {\n            let parts: Vec<&str> = line.split_whitespace().collect();\n            if parts.len() < 3 {\n                continue;\n            }\n            let device = parts[0];\n            let mountpoint = parts[1];\n            let fstype = parts[2];\n            if fstype == \"vfat\"\n                || fstype == \"exfat\"\n                || fstype == \"ntfs\"\n                || fstype == \"sdcardfs\"\n                || fstype == \"fuse\"\n                || mountpoint.starts_with(\"/storage/\")\n                || mountpoint.starts_with(\"/sdcard\")\n                || mountpoint.starts_with(\"/mnt/\")\n            {\n                if device == \"none\" && !mountpoint.starts_with(\"/storage/\") && !mountpoint.starts_with(\"/sdcard\") {\n                    continue;\n                }\n                mounts.push((mountpoint.to_string(), fstype.to_string()));\n            }\n        }\n    }\n\n    mounts.sort_by(|a, b| a.0.cmp(&b.0));\n    mounts.dedup_by(|a, b| a.0 == b.0);\n\n    for (mountpoint, _fstype) in &mounts {\n        if std::fs::read_dir(mountpoint).is_ok() {\n            let label = classify_mountpoint(mountpoint);\n            result.push(VolumeEntry {\n                path: SharedString::from(mountpoint.as_str()),\n                label: SharedString::from(label),\n                is_included: false,\n                is_excluded: false,\n            });\n        }\n    }\n\n    #[cfg(target_os = \"android\")]\n    for path in &candidates {\n        let already_listed = result.iter().any(|v| v.path.as_str() == *path);\n        if !already_listed && std::fs::read_dir(path).is_ok() {\n            let label = classify_mountpoint(path);\n            result.push(VolumeEntry {\n                path: SharedString::from(*path),\n                label: SharedString::from(label),\n                is_included: false,\n                is_excluded: false,\n            });\n        }\n    }\n    #[cfg(not(target_os = \"android\"))]\n    let _ = candidates;\n\n    // Deduplicate by canonical path: /sdcard, /storage/emulated/0, /storage/self/primary\n    // are often symlinks pointing to the same directory.\n    let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();\n    result.retain(|v| {\n        let canonical = std::fs::canonicalize(v.path.as_str()).unwrap_or_else(|_| PathBuf::from(v.path.as_str()));\n        seen.insert(canonical)\n    });\n\n    result\n}\n\npub(crate) fn classify_mountpoint(path: &str) -> &'static str {\n    if path.contains(\"emulated/0\") || path == \"/sdcard\" || path == \"/storage/self/primary\" || path == \"/mnt/sdcard\" {\n        \"💾 Pamięć wbudowana (internal)\"\n    } else if path.contains(\"emulated/1\")\n        || path.contains(\"extSdCard\")\n        || path.contains(\"external_sd\")\n        || path.contains(\"sdcard1\")\n        || path.contains(\"sdcard2\")\n        || path.starts_with(\"/storage/\")\n        || path.starts_with(\"/mnt/media_rw/\")\n    {\n        \"💳 Karta pamięci (SD card)\"\n    } else {\n        \"📦 Wolumin pamięci\"\n    }\n}\n\npub(crate) fn refresh_volumes_flags(win: &MainWindow, included: &[PathBuf], excluded: &[PathBuf]) {\n    let inc_set: Vec<String> = included.iter().map(|p| p.to_string_lossy().to_string()).collect();\n    let exc_set: Vec<String> = excluded.iter().map(|p| p.to_string_lossy().to_string()).collect();\n    let model = win.global::<AppState>().get_storage_volumes();\n    for i in 0..model.row_count() {\n        if let Some(mut vol) = model.row_data(i) {\n            let path = vol.path.to_string();\n            vol.is_included = inc_set.contains(&path);\n            vol.is_excluded = exc_set.contains(&path);\n            model.set_row_data(i, vol);\n        }\n    }\n}\n\npub(crate) fn count_files_and_dirs_stoppable(root: &std::path::Path, stop: &Arc<AtomicBool>, stopped: &mut bool) -> (i32, i32) {\n    if stop.load(Ordering::Relaxed) {\n        *stopped = true;\n        return (0, 0);\n    }\n    let mut files: i32 = 0;\n    let mut dirs: i32 = 0;\n    let Ok(rd) = std::fs::read_dir(root) else {\n        return (0, 0);\n    };\n    for entry in rd.flatten() {\n        if stop.load(Ordering::Relaxed) {\n            *stopped = true;\n            return (files, dirs);\n        }\n        let Ok(ft) = entry.file_type() else {\n            continue;\n        };\n        if ft.is_dir() {\n            dirs = dirs.saturating_add(1);\n            let (f, d) = count_files_and_dirs_stoppable(&entry.path(), stop, stopped);\n            files = files.saturating_add(f);\n            dirs = dirs.saturating_add(d);\n            if *stopped {\n                return (files, dirs);\n            }\n        } else {\n            files = files.saturating_add(1);\n        }\n    }\n    (files, dirs)\n}\n"
  },
  {
    "path": "cedinia/ui/app_state.slint",
    "content": "import { ActiveTool, CollectTestResult, ProgressData, ScanState, VolumeEntry } from \"common.slint\";\n\nexport global GeneralSettings {\n    in-out property <bool>   use_cache:           true;\n    in-out property <bool>   ignore_hidden:        true;\n\n    in-out property <int>    min_file_size_idx:    0;\n    in-out property <[string]> min_file_size_options: [\"Brak\", \"1 KB\", \"8 KB\", \"64 KB\", \"1 MB\"];\n\n    in-out property <int>    max_file_size_idx:    4;\n    in-out property <[string]> max_file_size_options: [\"16 KB\", \"1 MB\", \"10 MB\", \"100 MB\", \"Bez limitu\"];\n\n    in-out property <int>    language_idx:         0;\n    in-out property <[string]> language_options:   [\"English\", \"Polski (Polish)\"];\n\n    in-out property <string> excluded_items:       \"\";\n\n    in-out property <string> allowed_extensions:   \"\";\n\n    in-out property <string> excluded_extensions:  \"\";\n}\n\n\nexport global DuplicateSettings {\n    in-out property <int>    check_method:         0;\n    in-out property <string> check_method_value:   \"hash\";\n    in-out property <[string]> check_method_options: [\"Hash\", \"Nazwa\", \"Rozm+Naz\", \"Rozmiar\"];\n\n    in-out property <int>    hash_type:            0;\n    in-out property <string> hash_type_value:      \"blake3\";\n    in-out property <[string]> hash_type_options:  [\"Blake3\", \"CRC32\", \"XXH3\"];\n}\n\n\nexport global SimilarImagesSettings {\n    in-out property <int>    similarity_preset:       2;\n    in-out property <string> similarity_preset_value: \"medium\";\n    in-out property <[string]> similarity_preset_options: [\"B.Wys.\", \"Wysoki\", \"Średni\", \"Niski\", \"B.Niski\", \"Min.\"];\n\n    in-out property <int>    hash_size_idx:           1;\n    in-out property <string> hash_size_value:         \"16\";\n    in-out property <[string]> hash_size_options:     [\"8\", \"16\", \"32\", \"64\"];\n\n    in-out property <int>    hash_alg_idx:            0;\n    in-out property <string> hash_alg_value:          \"mean\";\n    in-out property <[string]> hash_alg_options:      [\"Mean\", \"Gradient\", \"D.Grad.\", \"V.Grad.\", \"Median\", \"Blockhash\"];\n\n    in-out property <int>    image_filter_idx:        1;\n    in-out property <string> image_filter_value:      \"triangle\";\n    in-out property <[string]> image_filter_options:  [\"Nearest\", \"Triangle\", \"CatmullRom\", \"Gaussian\", \"Lanczos3\"];\n\n    in-out property <bool>   ignore_same_size:        false;\n}\n\n\nexport global SameMusicSettings {\n    in-out property <bool>   title:              true;\n    in-out property <bool>   artist:             true;\n    in-out property <bool>   year:               false;\n    in-out property <bool>   length:             false;\n    in-out property <bool>   genre:              false;\n    in-out property <bool>   bitrate:            false;\n    in-out property <bool>   approximate:        false;\n    in-out property <int>    check_method_idx:   0;\n    in-out property <string> check_method_value: \"tags\";\n    in-out property <[string]> check_method_options: [\"Tagi\", \"Audio\"];\n}\n\n\nexport global BrokenFilesSettings {\n    in-out property <bool> check_audio:   true;\n    in-out property <bool> check_pdf:     true;\n    in-out property <bool> check_archive: true;\n    in-out property <bool> check_image:   true;\n}\n\n\nexport global BadNamesSettings {\n    in-out property <bool> uppercase_extension:         true;\n    in-out property <bool> emoji_used:                  true;\n    in-out property <bool> space_at_start_or_end:       true;\n    in-out property <bool> non_ascii_graphical:         true;\n    in-out property <bool> remove_duplicated_non_alpha: true;\n}\n\n\nexport global BigFilesSettings {\n    in-out property <int>    search_mode_idx:   0;\n    in-out property <string> search_mode_value: \"biggest\";\n    in-out property <[string]> search_mode_options: [\"Największe\", \"Najmniejsze\"];\n\n    in-out property <int>    count_idx:         1;\n    in-out property <string> count_value:       \"50\";\n    in-out property <[string]> count_options:   [\"5\", \"50\", \"500\", \"5000\"];\n}\n\n\nexport global AppState {\n    in-out property <ActiveTool> active_tool:    ActiveTool.Home;\n\n    in-out property <ActiveTool> last_scan_tool: ActiveTool.DuplicateFiles;\n    in-out property <ScanState>  scan_state:  ScanState.Idle;\n    in-out property <ProgressData> progress: { step_name: \"\", current_progress: 0, all_progress: 0, is_indeterminate: false };\n    in-out property <string>     status_message: \"Gotowy / Ready\";\n    in-out property <bool>       show_included_dirs: false;\n    in-out property <bool>       show_excluded_dirs: false;\n    in-out property <bool>       bottom_sheet_open: false;\n    in-out property <string>     bottom_sheet_title: \"\";\n    in-out property <int>        selected_count: 0;\n\n\n    in-out property <length> inset_top:    0px;\n    in-out property <length> inset_bottom: 0px;\n\n\n    callback tool_changed(ActiveTool);\n    callback scan_requested();\n    callback stop_requested();\n    callback delete_selected();\n    callback rename_selected();\n    callback clean_exif_selected();\n    callback select_all();\n    callback deselect_all();\n    callback select_all_except_one();\n    callback deselect_all_except_one();\n    callback invert_selection();\n    callback select_largest_per_group();\n    callback select_all_except_largest();\n    callback select_smallest_per_group();\n    callback select_all_except_smallest();\n    callback select_highest_resolution_per_group();\n    callback select_all_except_highest_resolution();\n    callback select_lowest_resolution_per_group();\n    callback select_all_except_lowest_resolution();\n    callback toggle_file_checked(int);\n    callback pick_include_dir();\n    callback pick_exclude_dir();\n    callback add_include_dir(string);\n    callback remove_include_dir(string);\n    callback add_exclude_dir(string);\n    callback remove_exclude_dir(string);\n\n    in-out property <[VolumeEntry]> storage_volumes: [];\n    callback list_storage_volumes();\n    callback save_settings_now();\n\n    callback open_path(string);\n    callback open_parent_folder(string);\n\n    callback request_storage_permission();\n    in-out property <bool> storage_permission_granted: true;\n    in-out property <bool> show_permission_popup: false;\n\n    callback run_collect_test();\n    callback stop_collect_test();\n    in-out property <CollectTestResult> collect_test_result: { volumes: 0, files: 0, folders: 0, elapsed_ms: 0 };\n    in-out property <bool> collect_test_running: false;\n    in-out property <bool> collect_test_done: false;\n\n    in-out property <bool> similar_images_gallery_mode: true;\n\n\n    in-out property <string> diag_thumbnails_size: \"–\";\n    in-out property <string> diag_app_cache_size:  \"–\";\n    in-out property <bool>   diag_refresh_running:  false;\n    callback refresh_diag_cache_info();\n    callback clear_thumbnails_cache();\n    callback apply_language_change();\n    callback clear_app_cache();\n\n    callback open_url(string);\n\n\n    in-out property <bool>   gallery_delete_popup_visible: false;\n    in-out property <string> gallery_delete_message:       \"\";\n    in-out property <string> gallery_delete_warning:       \"\";\n    callback request_gallery_delete();\n    callback confirm_gallery_delete();\n\n\n    in-out property <bool>   delete_running:        false;\n    in-out property <string> delete_progress_text:  \"\";\n    callback delete_stop_requested();\n\n\n    in-out property <bool>   delete_errors_visible: false;\n    in-out property <string> delete_errors_text:    \"\";\n\n\n    in-out property <bool>   confirm_popup_visible: false;\n    in-out property <string> confirm_popup_message: \"\";\n    in-out property <string> confirm_popup_action:  \"\";\n    callback confirm_popup_ok();\n    callback confirm_popup_cancel();\n}\n"
  },
  {
    "path": "cedinia/ui/bottom_nav.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { ActiveTool } from \"common.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { Translations } from \"translations.slint\";\n\ncomponent NavItem {\n    in property <string>     label;\n    in property <ActiveTool> tool;\n    in property <image>      icon;\n\n    height: 64px;\n\n    property <bool> is_active: AppState.active_tool == root.tool;\n\n    Rectangle {\n        width: root.width;\n        height: root.height;\n        background: ta.has-hover ? CediniaColors.ripple : transparent;\n        animate background { duration: 100ms; }\n        border-radius: 8px;\n\n        ta := TouchArea {\n            clicked => {\n                if AppState.active_tool != root.tool {\n                    AppState.active_tool = root.tool;\n                    AppState.tool_changed(root.tool);\n                }\n            }\n        }\n\n        VerticalLayout {\n            alignment: LayoutAlignment.center;\n            spacing: 3px;\n            padding-top: 6px;\n            padding-bottom: 6px;\n\n            Rectangle {\n                height: 3px;\n                horizontal-stretch: 0.0;\n                background: root.is_active ? CediniaColors.accent : transparent;\n                border-radius: 2px;\n                animate background { duration: 150ms; }\n            }\n\n            Image {\n                source: root.icon;\n                colorize: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive;\n                horizontal-alignment: ImageHorizontalAlignment.center;\n                vertical-alignment: ImageVerticalAlignment.center;\n                animate colorize { duration: 150ms; }\n            }\n\n            Text {\n                text: root.label;\n                color: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive;\n                font-size: 10px;\n                font-weight: root.is_active ? 700 : 400;\n                horizontal-alignment: TextHorizontalAlignment.center;\n                overflow: elide;\n                animate color { duration: 150ms; }\n            }\n        }\n    }\n}\n\n\n\ncomponent DynamicToolItem {\n    height: 64px;\n\n\n    property <ActiveTool> t: AppState.last_scan_tool;\n\n    property <string> t_label:\n        t == ActiveTool.DuplicateFiles   ? Translations.tool_duplicate_files_text\n      : t == ActiveTool.EmptyFolders     ? Translations.tool_empty_folders_text\n      : t == ActiveTool.SimilarImages    ? Translations.tool_similar_images_text\n      : t == ActiveTool.EmptyFiles       ? Translations.tool_empty_files_text\n      : t == ActiveTool.TemporaryFiles   ? Translations.tool_temporary_files_text\n      : t == ActiveTool.BigFiles         ? Translations.tool_big_files_text\n      : t == ActiveTool.BrokenFiles      ? Translations.tool_broken_files_text\n      : t == ActiveTool.BadExtensions    ? Translations.tool_bad_extensions_text\n      : t == ActiveTool.SameMusic        ? Translations.tool_same_music_text\n      : t == ActiveTool.BadNames         ? Translations.tool_bad_names_text\n      : t == ActiveTool.ExifRemover      ? Translations.tool_exif_remover_text\n      : Translations.app_name_text;\n\n    property <image> t_icon:\n        t == ActiveTool.SimilarImages    ? @image-url(\"icons/image.svg\")\n      : t == ActiveTool.EmptyFolders     ? @image-url(\"icons/folder_empty.svg\")\n      : @image-url(\"icons/duplicate.svg\");\n\n    property <bool> is_active: AppState.active_tool == root.t;\n\n    Rectangle {\n        width: root.width;\n        height: root.height;\n        background: ta.has-hover ? CediniaColors.ripple : transparent;\n        animate background { duration: 100ms; }\n        border-radius: 8px;\n\n        ta := TouchArea {\n            clicked => {\n                if AppState.active_tool != root.t {\n                    AppState.active_tool = root.t;\n                    AppState.tool_changed(root.t);\n                }\n            }\n        }\n\n        VerticalLayout {\n            alignment: LayoutAlignment.center;\n            spacing: 3px;\n            padding-top: 6px;\n            padding-bottom: 6px;\n\n            Rectangle {\n                height: 3px;\n                horizontal-stretch: 0.0;\n                background: root.is_active ? CediniaColors.accent : transparent;\n                border-radius: 2px;\n                animate background { duration: 150ms; }\n            }\n\n            Image {\n                source: root.t_icon;\n                colorize: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive;\n                horizontal-alignment: ImageHorizontalAlignment.center;\n                vertical-alignment: ImageVerticalAlignment.center;\n                animate colorize { duration: 150ms; }\n            }\n\n            Text {\n                text: root.t_label;\n                color: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive;\n                font-size: 10px;\n                font-weight: root.is_active ? 700 : 400;\n                horizontal-alignment: TextHorizontalAlignment.center;\n                overflow: elide;\n                animate color { duration: 150ms; }\n            }\n        }\n    }\n}\n\n\nexport component BottomNavBar {\n    height: 64px;\n    horizontal-stretch: 1.0;\n\n    Rectangle {\n        width: parent.width;\n        height: parent.height;\n        background: CediniaColors.nav_bg;\n        border-width: 1px;\n        border-color: CediniaColors.divider;\n\n        HorizontalLayout {\n            alignment: LayoutAlignment.stretch;\n            padding-left: 0px;\n            padding-right: 0px;\n\n            NavItem {\n                horizontal-stretch: 1.0;\n                label: Translations.nav_home_text;\n                tool: ActiveTool.Home;\n                icon: @image-url(\"icons/home.svg\");\n            }\n\n            DynamicToolItem {\n                horizontal-stretch: 1.0;\n            }\n\n            NavItem {\n                horizontal-stretch: 1.0;\n                label: Translations.nav_dirs_text;\n                tool: ActiveTool.Directories;\n                icon: @image-url(\"icons/folder.svg\");\n            }\n\n            NavItem {\n                horizontal-stretch: 1.0;\n                label: Translations.nav_settings_text;\n                tool: ActiveTool.Settings;\n                icon: @image-url(\"icons/more.svg\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/colors.slint",
    "content": "export global CediniaColors {\n\n    out property <color> bg_primary:    #0d0d0d;       out property <color> bg_surface:    #1a1a1a;\n    out property <color> bg_elevated:   #252525;\n    out property <color> bg_card:       #1e1e1e;\n\n\n    out property <color> accent:        #c8960c;\n    out property <color> accent_light:  #f0b429;\n    out property <color> accent_muted:  #7a5900;\n\n\n    out property <color> text_primary:  #f0ece0;\n    out property <color> text_secondary:#a89878;\n    out property <color> text_disabled: #505050;\n\n\n    out property <color> success:       #4caf50;\n    out property <color> warning:       #ff9800;\n    out property <color> danger:        #f44336;\n\n\n    out property <color> divider:       #333333;\n    out property <color> border:        #3a3a3a;\n\n\n    out property <color> ripple:        #c8960c40;\n\n\n    out property <color> row_normal:    #1a1a1a;\n    out property <color> row_alt:       #202020;\n    out property <color> row_selected:  #3a2e00;\n    out property <color> row_header:    #111111;\n\n\n    out property <color> nav_bg:        #111111;\n    out property <color> nav_active:    #c8960c;\n    out property <color> nav_inactive:  #606060;\n}\n"
  },
  {
    "path": "cedinia/ui/common.slint",
    "content": "export enum SettingsTab {\n    General,\n    Tools,\n    Diagnostics,\n}\n\nexport enum ActiveTool {\n    Home,\n    DuplicateFiles,\n    EmptyFolders,\n    SimilarImages,\n    EmptyFiles,\n    TemporaryFiles,\n    BigFiles,\n    BrokenFiles,\n    BadExtensions,\n    SameMusic,\n    BadNames,\n    ExifRemover,\n    Directories,\n    Settings,\n}\nexport enum ScanState {\n    Idle,\n    Scanning,\n\n    Stopping,\n    Stopped,\n    Done,\n}\n\n\nexport struct SimilarImageItem {\n    full_path:  string,\n    name:       string,\n    size:       string,\n\n    val_str:    [string],\n    flat_idx:   int,\n    thumbnail:  image,\n\n\n    checked:    bool,\n}\n\n\nexport struct SimilarGroupCard {\n    label: string,\n    items: [SimilarImageItem],\n}\nexport struct ProgressData {\n    step_name: string,\n    current_progress: int,\n    all_progress: int,\n    is_indeterminate: bool,\n}\nexport struct FileEntry {\n    checked: bool,\n    is_header: bool,\n\n    val_str: [string],\n    val_int: [int],\n}\nexport global FileEntryIdx {\n    out property <int> name_idx: 0;\n    out property <int> path_idx: 1;\n    out property <int> size_idx: 2;\n    out property <int> extra_idx: 4;\n}\nexport struct DirectoryEntry {\n    path: string,\n    is_included: bool,\n}\nexport struct VolumeEntry {\n    path: string,\n    label: string,\n    is_included: bool,\n    is_excluded: bool,\n}\nexport struct CollectTestResult {\n    volumes:      int,\n    files:        int,\n    folders:      int,\n    elapsed_ms:   int,\n}\n"
  },
  {
    "path": "cedinia/ui/components.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { FileEntryIdx } from \"common.slint\";\n\nexport component TouchButton {\n    in property <string>  label;\n    in property <color>   bg:       CediniaColors.accent;\n    in property <color>   fg:       #000000;\n    in property <bool>    enabled:  true;\n    in property <length>  min_h:    52px;\n    callback clicked;\n\n    height: max(min_h, txt.preferred-height + 24px);\n    min-width: txt.preferred-width + 32px;\n\n    Rectangle {\n        border-radius: 8px;\n        background: ta.has-hover && root.enabled ? root.bg.brighter(0.15) : root.enabled ? root.bg : CediniaColors.bg_elevated;\n        animate background { duration: 120ms; }\n\n        ta := TouchArea {\n            enabled: root.enabled;\n            clicked => { root.clicked(); }\n        }\n\n        txt := Text {\n            text: root.label;\n            color: root.enabled ? root.fg : CediniaColors.text_disabled;\n            font-size: 16px;\n            font-weight: 600;\n            horizontal-alignment: center;\n            vertical-alignment: center;\n        }\n    }\n}\n\n\n\n\nexport component IconButton {\n    in property <image>  icon;\n    in property <string> tooltip;\n    in property <bool>   enabled: true;\n    in property <color>  tint: CediniaColors.text_primary;\n    callback clicked;\n\n    width: 48px;\n    height: 48px;\n\n    Rectangle {\n        border-radius: 24px;\n        background: ta.has-hover && root.enabled ? CediniaColors.ripple : transparent;\n        animate background { duration: 120ms; }\n\n        ta := TouchArea {\n            enabled: root.enabled;\n            clicked => { root.clicked(); }\n        }\n\n        Image {\n            source: root.icon;\n            colorize: root.enabled ? root.tint : CediniaColors.text_disabled;\n            width: 24px;\n            height: 24px;\n            horizontal-alignment: ImageHorizontalAlignment.center;\n            vertical-alignment: ImageVerticalAlignment.center;\n        }\n    }\n}\n\n\n\n\nexport component SectionHeader {\n    in property <string> title;\n    in property <string> subtitle: \"\";\n\n    height: subtitle == \"\" ? 44px : 60px;\n\n    Rectangle {\n        width: parent.width;\n        height: parent.height;\n        background: CediniaColors.bg_elevated;\n        border-width: 1px;\n        border-color: CediniaColors.divider;\n\n        VerticalLayout {\n            padding-left: 16px;\n            padding-right: 16px;\n            alignment: LayoutAlignment.center;\n            spacing: 2px;\n\n            Text {\n                text: root.title;\n                color: CediniaColors.accent_light;\n                font-size: 13px;\n                font-weight: 700;\n                letter-spacing: 0.8px;\n            }\n\n            if root.subtitle != \"\" : Text {\n                text: root.subtitle;\n                color: CediniaColors.text_secondary;\n                font-size: 12px;\n            }\n        }\n    }\n}\n\nexport component FileRow {\n    in property <string>  name;\n    in property <string>  path;\n    in property <string>  size;\n    in property <[string]> val_str: [];\n    in property <bool>    checked: false;\n    in property <bool>    is_header: false;\n    in property <bool>    is_even: false;\n    in property <bool>    extra_as_line2: false;\n    callback toggle_checked();\n    callback long_pressed();\n    callback open_item();\n    callback open_parent();\n\n    height: root.is_header ? 36px : 80px;\n\n    Rectangle {\n        background: root.is_header\n            ? CediniaColors.row_header\n            : root.checked\n                ? CediniaColors.row_selected\n                : root.is_even ? CediniaColors.row_alt : CediniaColors.row_normal;\n        animate background { duration: 80ms; }\n\n\n        if !root.is_header : Rectangle {\n            x: 0;\n            y: 0;\n            width: 4px;\n            height: parent.height;\n            background: root.checked ? CediniaColors.accent : transparent;\n        }\n\n        ta := TouchArea {\n            clicked => { if (!root.is_header) { root.toggle_checked(); } }\n            pointer-event(e) => {\n                if (!root.is_header) {\n                    if (e.button == PointerEventButton.middle && e.kind == PointerEventKind.up) {\n                        root.open_item();\n                    }\n                    if (e.button == PointerEventButton.right && e.kind == PointerEventKind.up) {\n                        root.open_parent();\n                    }\n                }\n            }\n        }\n\n        if root.is_header : HorizontalLayout {\n            padding-left: 12px;\n            padding-right: 12px;\n            alignment: LayoutAlignment.start;\n            spacing: 4px;\n            Text {\n                text: root.name;\n                color: CediniaColors.text_secondary;\n                font-size: 12px;\n                font-weight: 700;\n                vertical-alignment: TextVerticalAlignment.center;\n                overflow: elide;\n            }\n        }\n\n        if !root.is_header : HorizontalLayout {\n            padding-left: 12px;\n            padding-right: 8px;\n            spacing: 8px;\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n\n                Rectangle {\n                    width: 24px;\n                    height: 24px;\n                    border-radius: 12px;\n                    border-width: 2px;\n                    border-color: root.checked ? CediniaColors.accent : CediniaColors.border;\n                    background: root.checked ? CediniaColors.accent : transparent;\n                    vertical-stretch: 0.0;\n                    animate background { duration: 80ms; }\n                }\n        }\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                spacing: 2px;\n                horizontal-stretch: 1.0;\n\n                Text {\n                    text: root.name;\n                    color: CediniaColors.text_primary;\n                    font-size: 14px;\n                    font-weight: 600;\n                    overflow: elide;\n                    wrap: no-wrap;\n                }\n\n                if !root.extra_as_line2 : Text {\n                    text: root.path;\n                    color: CediniaColors.text_secondary;\n                    font-size: 11px;\n                    wrap: word-wrap;\n                }\n\n                if root.extra_as_line2 && root.path != \"\" : Text {\n                    text: root.path;\n                    color: CediniaColors.text_secondary;\n                    font-size: 11px;\n                    overflow: elide;\n                    wrap: no-wrap;\n                }\n\n                if !root.extra_as_line2 && root.val_str.length > FileEntryIdx.extra_idx && root.val_str[FileEntryIdx.extra_idx] != \"\" : Text {\n                    text: root.val_str[FileEntryIdx.extra_idx];\n                    color: CediniaColors.text_disabled;\n                    font-size: 11px;\n                    overflow: elide;\n                    wrap: no-wrap;\n                }\n\n                if root.extra_as_line2 && root.val_str[FileEntryIdx.extra_idx] != \"\" : Text {\n                    text: root.val_str[FileEntryIdx.extra_idx];\n                    color: CediniaColors.text_disabled;\n                    font-size: 11px;\n                    wrap: word-wrap;\n                }\n            }\n\n            Text {\n                text: root.size;\n                color: CediniaColors.accent_light;\n                font-size: 12px;\n                font-weight: 700;\n                vertical-alignment: TextVerticalAlignment.center;\n                horizontal-alignment: TextHorizontalAlignment.right;\n                min-width: 60px;\n            }\n        }\n    }\n}\n\n\n\n\nexport component Divider {\n    height: 1px;\n    Rectangle {\n        background: CediniaColors.divider;\n    }\n}\n\n\n\n\nexport component StatusChip {\n    in property <string> label;\n    in property <color>  chip_color: CediniaColors.accent_muted;\n    in property <color>  text_color: CediniaColors.accent_light;\n\n    height: 24px;\n    min-width: txt.preferred-width + 16px;\n\n    Rectangle {\n        border-radius: 12px;\n        background: root.chip_color;\n        txt := Text {\n            text: root.label;\n            color: root.text_color;\n            font-size: 11px;\n            font-weight: 700;\n            horizontal-alignment: center;\n            vertical-alignment: center;\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/directories_screen.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { DirectoryEntry } from \"common.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { Translations } from \"translations.slint\";\nimport { Divider, TouchButton } from \"components.slint\";\n\ncomponent DirRow {\n    in property <string>  path;\n    in property <bool>    is_included;\n    callback remove();\n\n    height: 56px;\n\n    Rectangle {\n        background: ta.has-hover ? CediniaColors.bg_elevated : CediniaColors.bg_surface;\n        animate background { duration: 100ms; }\n\n        HorizontalLayout {\n            padding-left: 12px;\n            padding-right: 8px;\n            spacing: 8px;\n            alignment: LayoutAlignment.start;\n\n\n            Rectangle {\n                width: 4px;\n                height: 36px;\n                border-radius: 2px;\n                background: root.is_included ? CediniaColors.success : CediniaColors.danger;\n                vertical-stretch: 0.0;\n            }\n\n            ta := TouchArea {\n                horizontal-stretch: 1.0;\n            }\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                horizontal-stretch: 1.0;\n                Text {\n                    text: root.path;\n                    color: CediniaColors.text_primary;\n                    font-size: 13px;\n                    overflow: elide;\n                }\n                Text {\n                    text: root.is_included ? Translations.directories_include_header_text : Translations.directories_exclude_header_text;\n                    color: root.is_included ? CediniaColors.success : CediniaColors.danger;\n                    font-size: 11px;\n                }\n            }\n\n\n            Rectangle {\n                width: 40px;\n                height: 40px;\n                border-radius: 20px;\n                background: rm_ta.has-hover ? CediniaColors.danger.with-alpha(0.2) : transparent;\n                vertical-stretch: 0.0;\n\n                rm_ta := TouchArea {\n                    clicked => { root.remove(); }\n                }\n                Text {\n                    text: \"X\";\n                    color: CediniaColors.danger;\n                    font-size: 18px;\n                    horizontal-alignment: center;\n                    vertical-alignment: center;\n                }\n            }\n        }\n        Divider {}\n    }\n}\n\nexport component DirectoriesScreen {\n    in property <[DirectoryEntry]> directories: [];\n\n    VerticalLayout {\n        height: parent.height;\n        spacing: 0px;\n\n\n        if directories.length == 0 : Rectangle {}\n\n        Flickable {\n            vertical-stretch: 1.0;\n            viewport-height: max(dir_layout.preferred-height, self.height);\n\n            dir_layout := VerticalLayout {\n                spacing: 0px;\n\n                if directories.length == 0 : Rectangle {\n                    height: 120px;\n                    VerticalLayout {\n                        alignment: LayoutAlignment.center;\n                        spacing: 8px;\n                        Text {\n                            text: \"📂\";\n                            font-size: 36px;\n                            horizontal-alignment: TextHorizontalAlignment.center;\n                            color: CediniaColors.text_disabled;\n                        }\n                        Text {\n                            text: Translations.no_paths_text;\n                            color: CediniaColors.text_disabled;\n                            font-size: 13px;\n                            horizontal-alignment: TextHorizontalAlignment.center;\n                        }\n                    }\n                }\n\n                for entry in directories : DirRow {\n                    path: entry.path;\n                    is_included: entry.is_included;\n                    remove => {\n                        if (entry.is_included) {\n                            AppState.remove_include_dir(entry.path);\n                        } else {\n                            AppState.remove_exclude_dir(entry.path);\n                        }\n                        AppState.save_settings_now();\n                    }\n                }\n            }\n        }\n        Rectangle {}\n\n\n\n        Rectangle {\n            vertical-stretch: 0.0;\n            height: 64px;\n            background: CediniaColors.bg_surface;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n\n            HorizontalLayout {\n                padding: 10px;\n                spacing: 10px;\n                alignment: LayoutAlignment.stretch;\n\n                TouchButton {\n                    label: Translations.directories_add_text;\n                    horizontal-stretch: 1.0;\n                    bg: CediniaColors.success.with-alpha(0.8);\n                    fg: #000000;\n                    clicked => {\n                        AppState.pick_include_dir();\n                        AppState.save_settings_now();\n                    }\n                }\n\n                TouchButton {\n                    label: \"- \" + Translations.directories_exclude_header_text;\n                    horizontal-stretch: 1.0;\n                    bg: CediniaColors.danger.with-alpha(0.8);\n                    fg: #ffffff;\n                    clicked => {\n                        AppState.pick_exclude_dir();\n                        AppState.save_settings_now();\n                    }\n                }\n\n                TouchButton {\n                    label: Translations.directories_volume_header_text;\n                    horizontal-stretch: 1.0;\n                    bg: AppState.storage_volumes.length > 0 ? CediniaColors.accent_muted : CediniaColors.bg_elevated;\n                    fg: AppState.storage_volumes.length > 0 ? CediniaColors.accent_light : CediniaColors.text_primary;\n                    clicked => {\n                        if (AppState.storage_volumes.length > 0) {\n                            AppState.storage_volumes = [];\n                        } else {\n                            AppState.list_storage_volumes();\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n\n    if AppState.storage_volumes.length > 0 : Rectangle {\n        x: 0;\n        y: root.height - self.height - 64px;\n        width: root.width;\n        height: min(vol_popup_layout.preferred-height, root.height - 64px - 8px);\n        background: CediniaColors.bg_elevated;\n        border-width: 1px;\n        border-color: CediniaColors.divider;\n        drop-shadow-blur: 16px;\n        drop-shadow-color: #00000066;\n\n        vol_popup_layout := VerticalLayout {\n            spacing: 0px;\n\n\n            Rectangle {\n                height: 44px;\n                background: CediniaColors.bg_surface;\n\n                HorizontalLayout {\n                    padding-left: 16px;\n                    padding-right: 8px;\n                    spacing: 8px;\n\n                    Text {\n                        text: \"💾 \" + Translations.directories_volume_header_text;\n                        color: CediniaColors.accent_light;\n                        font-size: 15px;\n                        font-weight: 700;\n                        vertical-alignment: TextVerticalAlignment.center;\n                        horizontal-stretch: 1.0;\n                    }\n\n                    Rectangle {\n                        width: 36px;\n                        height: 36px;\n                        border-radius: 18px;\n                        vertical-stretch: 0.0;\n                        background: close_ta.has-hover ? CediniaColors.ripple : transparent;\n\n                        close_ta := TouchArea {\n                            clicked => { AppState.storage_volumes = []; }\n                        }\n                        Text {\n                            text: \"X\";\n                            color: CediniaColors.text_secondary;\n                            font-size: 16px;\n                            horizontal-alignment: center;\n                            vertical-alignment: center;\n                        }\n                    }\n                }\n            }\n\n            Rectangle { height: 1px; background: CediniaColors.divider; }\n\n            Flickable {\n                vertical-stretch: 1.0;\n                viewport-height: max(vol_list.preferred-height, self.height);\n\n                vol_list := VerticalLayout {\n                    spacing: 0px;\n\n                    for vol in AppState.storage_volumes : VerticalLayout {\n                        spacing: 0px;\n\n                        Rectangle {\n                            height: 64px;\n                            background: CediniaColors.bg_surface;\n\n                            HorizontalLayout {\n                                padding-left: 12px;\n                                padding-right: 8px;\n                                padding-top: 8px;\n                                padding-bottom: 8px;\n                                spacing: 8px;\n                                alignment: LayoutAlignment.start;\n\n                                Rectangle {\n                                    width: 4px;\n                                    height: 40px;\n                                    border-radius: 2px;\n                                    vertical-stretch: 0.0;\n                                    background: vol.is_included ? CediniaColors.success\n                                              : vol.is_excluded ? CediniaColors.danger\n                                              : CediniaColors.text_disabled;\n                                }\n\n                                VerticalLayout {\n                                    alignment: LayoutAlignment.center;\n                                    horizontal-stretch: 1.0;\n                                    spacing: 2px;\n                                    Text {\n                                        text: vol.label != \"\" ? vol.label : vol.path;\n                                        color: CediniaColors.text_primary;\n                                        font-size: 13px;\n                                        font-weight: 600;\n                                        overflow: elide;\n                                    }\n                                    Text {\n                                        text: vol.path;\n                                        color: CediniaColors.text_secondary;\n                                        font-size: 11px;\n                                        overflow: elide;\n                                    }\n                                }\n\n                                TouchButton {\n                                    label: vol.is_included ? \"X \" + Translations.directories_include_header_text : \"+ \" + Translations.directories_volume_add_text;\n                                    min_h: 32px;\n                                    bg: vol.is_included\n                                        ? CediniaColors.success.with-alpha(0.2)\n                                        : CediniaColors.success.with-alpha(0.8);\n                                    fg: vol.is_included ? CediniaColors.success : #000000;\n                                    clicked => {\n                                        if (vol.is_included) {\n                                            AppState.remove_include_dir(vol.path);\n                                        } else {\n                                            AppState.add_include_dir(vol.path);\n                                        }\n                                        AppState.save_settings_now();\n                                    }\n                                }\n\n                                TouchButton {\n                                    label: vol.is_excluded ? \"X \" + Translations.directories_exclude_header_text : \"- \" + Translations.directories_exclude_header_text;\n                                    min_h: 32px;\n                                    bg: vol.is_excluded\n                                        ? CediniaColors.danger.with-alpha(0.2)\n                                        : CediniaColors.danger.with-alpha(0.8);\n                                    fg: vol.is_excluded ? CediniaColors.danger : #ffffff;\n                                    clicked => {\n                                        if (vol.is_excluded) {\n                                            AppState.remove_exclude_dir(vol.path);\n                                        } else {\n                                            AppState.add_exclude_dir(vol.path);\n                                        }\n                                        AppState.save_settings_now();\n                                    }\n                                }\n                            }\n                        }\n\n                        Divider {}\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/home_screen.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { ActiveTool } from \"common.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { Translations } from \"translations.slint\";\n\ncomponent ToolCard {\n    in property <string>  emoji;\n    in property <string>  title;\n    in property <string>  description;\n    in property <ActiveTool> tool;\n    in property <color>   accent: CediniaColors.accent;\n    height: 88px;\n    horizontal-stretch: 1.0;\n    Rectangle {\n        width: parent.width;\n        height: parent.height;\n        border-radius: 10px;\n        background: CediniaColors.bg_card;\n        border-width: 1px;\n        border-color: ta.has-hover ? root.accent : CediniaColors.border;\n        animate border-color { duration: 120ms; }\n        ta := TouchArea {\n            clicked => {\n                AppState.active_tool    = root.tool;\n                AppState.last_scan_tool = root.tool;\n                AppState.tool_changed(root.tool);\n            }\n        }\n        HorizontalLayout {\n            padding: 12px;\n            spacing: 12px;\n            alignment: LayoutAlignment.start;\n\n            Rectangle {\n                width: 48px;\n                height: 48px;\n                border-radius: 24px;\n                background: root.accent.with-alpha(0.15);\n                vertical-stretch: 0.0;\n                Text {\n                    text: root.emoji;\n                    font-size: 24px;\n                    horizontal-alignment: center;\n                    vertical-alignment: center;\n                }\n            }\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                spacing: 4px;\n                horizontal-stretch: 1.0;\n                Text {\n                    text: root.title;\n                    color: CediniaColors.text_primary;\n                    font-size: 15px;\n                    font-weight: 700;\n                }\n                Text {\n                    text: root.description;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    overflow: elide;\n                }\n            }\n\n            Text {\n                text: \"›\";\n                color: root.accent;\n                font-size: 22px;\n                vertical-alignment: TextVerticalAlignment.center;\n            }\n        }\n    }\n}\nexport component HomeScreen {\n    VerticalLayout {\n        horizontal-stretch: 1.0;\n        Flickable {\n            horizontal-stretch: 1.0;\n            vertical-stretch: 1.0;\n            cards := VerticalLayout {\n                width: parent.width;\n                padding: 12px;\n                spacing: 10px;\n                ToolCard {\n                    emoji: \"📂\";\n                    title: Translations.tool_duplicate_files_text;\n                    description: Translations.home_dup_description_text;\n                    tool: ActiveTool.DuplicateFiles;\n                }\n                ToolCard {\n                    emoji: \"📁\";\n                    title: Translations.tool_empty_folders_text;\n                    description: Translations.home_empty_folders_description_text;\n                    tool: ActiveTool.EmptyFolders;\n                    accent: #ff9800;\n                }\n                ToolCard {\n                    emoji: \"🖼\";\n                    title: Translations.tool_similar_images_text;\n                    description: Translations.home_similar_images_description_text;\n                    tool: ActiveTool.SimilarImages;\n                    accent: #9c27b0;\n                }\n                ToolCard {\n                    emoji: \"📄\";\n                    title: Translations.tool_empty_files_text;\n                    description: Translations.home_empty_files_description_text;\n                    tool: ActiveTool.EmptyFiles;\n                    accent: #607d8b;\n                }\n                ToolCard {\n                    emoji: \"🗑\";\n                    title: Translations.tool_temporary_files_text;\n                    description: Translations.home_temp_files_description_text;\n                    tool: ActiveTool.TemporaryFiles;\n                    accent: #795548;\n                }\n                ToolCard {\n                    emoji: \"📦\";\n                    title: Translations.tool_big_files_text;\n                    description: Translations.home_big_files_description_text;\n                    tool: ActiveTool.BigFiles;\n                    accent: #f44336;\n                }\n                ToolCard {\n                    emoji: \"⚠\";\n                    title: Translations.tool_broken_files_text;\n                    description: Translations.home_broken_files_description_text;\n                    tool: ActiveTool.BrokenFiles;\n                    accent: #e91e63;\n                }\n                ToolCard {\n                    emoji: \"🏷\";\n                    title: Translations.tool_bad_extensions_text;\n                    description: Translations.home_bad_extensions_description_text;\n                    tool: ActiveTool.BadExtensions;\n                    accent: #009688;\n                }\n                ToolCard {\n                    emoji: \"🎵\";\n                    title: Translations.tool_same_music_text;\n                    description: Translations.home_same_music_description_text;\n                    tool: ActiveTool.SameMusic;\n                    accent: #3f51b5;\n                }\n                ToolCard {\n                    emoji: \"✏\";\n                    title: Translations.tool_bad_names_text;\n                    description: Translations.home_bad_names_description_text;\n                    tool: ActiveTool.BadNames;\n                    accent: #ff5722;\n                }\n                ToolCard {\n                    emoji: \"📷\";\n                    title: Translations.tool_exif_remover_text;\n                    description: Translations.home_exif_description_text;\n                    tool: ActiveTool.ExifRemover;\n                    accent: #00bcd4;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/main_window.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { ActiveTool, DirectoryEntry, FileEntry, ScanState, SimilarGroupCard } from \"common.slint\";\nimport { AppState, BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, GeneralSettings, SameMusicSettings, SimilarImagesSettings } from \"app_state.slint\";\nimport { TopAppBar } from \"top_bar.slint\";\nimport { BottomNavBar } from \"bottom_nav.slint\";\nimport { ScanProgressBar } from \"scan_progress.slint\";\nimport { ResultsList } from \"results_list.slint\";\nimport { SimilarImagesGallery } from \"similar_images_gallery.slint\";\nimport { SettingsScreen } from \"settings_screen.slint\";\nimport { HomeScreen } from \"home_screen.slint\";\nimport { DirectoriesScreen } from \"directories_screen.slint\";\nimport { TouchButton } from \"components.slint\";\nimport { Translations } from \"translations.slint\";\n\nexport { AppState, GeneralSettings, DuplicateSettings, SimilarImagesSettings, BigFilesSettings, SameMusicSettings, BrokenFilesSettings, BadNamesSettings, Translations }\nexport component MainWindow inherits Window {\n    title: \"Cedinia\";\n    background: CediniaColors.bg_primary;\n\n    min-width: 320px;\n    min-height: 480px;\n    preferred-width: 390px;\n    preferred-height: 844px;\n\n    in-out property <[FileEntry]>        duplicate_files_model:    [];\n    in-out property <[FileEntry]>        empty_folder_model:       [];\n    in-out property <[FileEntry]>        similar_images_model:     [];\n    in-out property <[SimilarGroupCard]> similar_images_groups:    [];\n    in-out property <[FileEntry]>        empty_files_model:        [];\n    in-out property <[FileEntry]>        temporary_files_model:    [];\n    in-out property <[FileEntry]>        big_files_model:          [];\n    in-out property <[FileEntry]>        broken_files_model:       [];\n    in-out property <[FileEntry]>        bad_extensions_model:     [];\n    in-out property <[FileEntry]>        same_music_model:         [];\n    in-out property <[FileEntry]>        bad_names_model:          [];\n    in-out property <[FileEntry]>        exif_remover_model:       [];\n    in-out property <[DirectoryEntry]>   directories_model:        [];\n\n    VerticalLayout {\n        spacing: 0px;\n        width: parent.width;\n        padding-top:    root.safe-area-insets.top;\n        padding-bottom: max(AppState.inset_bottom, root.safe-area-insets.bottom);\n\n        TopAppBar {\n            title: AppState.active_tool == ActiveTool.Home             ? Translations.app_name_text\n                 : AppState.active_tool == ActiveTool.DuplicateFiles   ? Translations.tool_duplicate_files_text\n                 : AppState.active_tool == ActiveTool.EmptyFolders     ? Translations.tool_empty_folders_text\n                 : AppState.active_tool == ActiveTool.SimilarImages    ? Translations.tool_similar_images_text\n                 : AppState.active_tool == ActiveTool.EmptyFiles       ? Translations.tool_empty_files_text\n                 : AppState.active_tool == ActiveTool.TemporaryFiles   ? Translations.tool_temporary_files_text\n                 : AppState.active_tool == ActiveTool.BigFiles         ? Translations.tool_big_files_text\n                 : AppState.active_tool == ActiveTool.BrokenFiles      ? Translations.tool_broken_files_text\n                 : AppState.active_tool == ActiveTool.BadExtensions    ? Translations.tool_bad_extensions_text\n                 : AppState.active_tool == ActiveTool.SameMusic        ? Translations.tool_same_music_text\n                 : AppState.active_tool == ActiveTool.BadNames         ? Translations.tool_bad_names_text\n                 : AppState.active_tool == ActiveTool.ExifRemover      ? Translations.tool_exif_remover_text\n                 : AppState.active_tool == ActiveTool.Directories      ? Translations.tool_directories_text\n                 : AppState.active_tool == ActiveTool.Settings         ? Translations.tool_settings_text\n                 : Translations.app_name_text;\n        }\n\n        ScanProgressBar {}\n\n        Rectangle {\n            vertical-stretch: 1.0;\n            background: CediniaColors.bg_primary;\n\n            if AppState.active_tool == ActiveTool.Home : HomeScreen {\n                height: parent.height;\n                width: parent.width;\n            }\n\n            if AppState.active_tool == ActiveTool.Settings : SettingsScreen {\n                height: parent.height;\n                width: parent.width;\n            }\n\n            if AppState.active_tool == ActiveTool.Directories : DirectoriesScreen {\n                height: parent.height;\n                width: parent.width;\n                directories: directories_model;\n            }\n\n            if AppState.active_tool == ActiveTool.DuplicateFiles : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: duplicate_files_model;\n                tool_title: Translations.tool_duplicate_files_text;\n                tool_emoji: \"📂\";\n                is_grouped: true;\n            }\n\n            if AppState.active_tool == ActiveTool.EmptyFolders : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: empty_folder_model;\n                tool_title: Translations.tool_empty_folders_text;\n                tool_emoji: \"📁\";\n            }\n\n            if AppState.active_tool == ActiveTool.SimilarImages && !AppState.similar_images_gallery_mode : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: similar_images_model;\n                tool_title: Translations.tool_similar_images_text;\n                tool_emoji: \"🖼\";\n                is_grouped: true;\n                has_gallery_mode: true;\n                has_size_select: true;\n                has_resolution_select: true;\n            }\n\n            if AppState.active_tool == ActiveTool.SimilarImages && AppState.similar_images_gallery_mode : SimilarImagesGallery {\n                height: parent.height;\n                width: parent.width;\n                groups: similar_images_groups;\n            }\n\n            if AppState.active_tool == ActiveTool.EmptyFiles : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: empty_files_model;\n                tool_title: Translations.tool_empty_files_text;\n                tool_emoji: \"📄\";\n            }\n\n            if AppState.active_tool == ActiveTool.TemporaryFiles : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: temporary_files_model;\n                tool_title: Translations.tool_temporary_files_text;\n                tool_emoji: \"🗑\";\n            }\n\n            if AppState.active_tool == ActiveTool.BigFiles : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: big_files_model;\n                tool_title: Translations.tool_big_files_text;\n                tool_emoji: \"📦\";\n            }\n\n            if AppState.active_tool == ActiveTool.BrokenFiles : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: broken_files_model;\n                tool_title: Translations.tool_broken_files_text;\n                tool_emoji: \"⚠\";\n                extra_as_line2: true;\n            }\n\n            if AppState.active_tool == ActiveTool.BadExtensions : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: bad_extensions_model;\n                tool_title: Translations.tool_bad_extensions_text;\n                tool_emoji: \"🏷\";\n                can_delete: false;\n                can_rename: true;\n                extra_as_line2: true;\n            }\n\n            if AppState.active_tool == ActiveTool.SameMusic : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: same_music_model;\n                tool_title: Translations.tool_same_music_text;\n                tool_emoji: \"🎵\";\n                is_grouped: true;\n                has_size_select: true;\n            }\n\n            if AppState.active_tool == ActiveTool.BadNames : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: bad_names_model;\n                tool_title: Translations.tool_bad_names_text;\n                tool_emoji: \"✏\";\n                can_rename: true;\n                extra_as_line2: true;\n            }\n\n            if AppState.active_tool == ActiveTool.ExifRemover : ResultsList {\n                height: parent.height;\n                width: parent.width;\n                entries: exif_remover_model;\n                tool_title: Translations.tool_exif_remover_text;\n                tool_emoji: \"📷\";\n                can_delete: false;\n                can_clean_exif: true;\n            }\n        }\n\n        BottomNavBar {}\n    }\n\n\n    if AppState.scan_state == ScanState.Stopping : Rectangle {\n        z: 150;\n        background: #000000bb;\n\n        Rectangle {\n            width: min(parent.width - 48px, 300px);\n            height: stopping_content.preferred-height + 48px;\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - self.height) / 2;\n            border-radius: 16px;\n            background: CediniaColors.bg_elevated;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #00000088;\n\n            stopping_content := VerticalLayout {\n                padding: 24px;\n                spacing: 12px;\n                alignment: LayoutAlignment.center;\n\n                Text {\n                    text: Translations.stopping_overlay_title_text;\n                    font-size: 17px;\n                    font-weight: 700;\n                    color: CediniaColors.warning;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n                Text {\n                    text: Translations.stopping_overlay_body_text;\n                    font-size: 13px;\n                    color: CediniaColors.text_secondary;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                    wrap: word-wrap;\n                }\n            }\n        }\n    }\n\n\n    if AppState.show_permission_popup : Rectangle {\n        z: 200;\n        background: #00000099;\n        TouchArea { clicked => { AppState.show_permission_popup = false; } }\n\n        Rectangle {\n            width: min(parent.width - 48px, 340px);\n            height: perm_popup.preferred-height + 32px;\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - self.height) / 2;\n            border-radius: 16px;\n            background: CediniaColors.bg_elevated;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #00000088;\n\n            perm_popup := VerticalLayout {\n                padding: 24px;\n                spacing: 16px;\n\n                Text {\n                    text: Translations.permission_title_text;\n                    color: CediniaColors.warning;\n                    font-size: 18px;\n                    font-weight: 700;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                Text {\n                    text: Translations.permission_body_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 13px;\n                    wrap: word-wrap;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                HorizontalLayout {\n                    spacing: 12px;\n                    TouchButton {\n                        label: Translations.cancel_text;\n                        bg: CediniaColors.bg_surface;\n                        fg: CediniaColors.text_secondary;\n                        min_h: 48px;\n                        horizontal-stretch: 1.0;\n                        clicked => { AppState.show_permission_popup = false; }\n                    }\n                    TouchButton {\n                        label: Translations.grant_text;\n                        bg: CediniaColors.accent;\n                        fg: #000000;\n                        min_h: 48px;\n                        horizontal-stretch: 1.0;\n                        clicked => {\n                            AppState.request_storage_permission();\n                            AppState.show_permission_popup = false;\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/results_list.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { FileEntry, FileEntryIdx, ScanState } from \"common.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { Translations } from \"translations.slint\";\nimport { FileRow, StatusChip, TouchButton } from \"components.slint\";\nimport { ListView } from \"std-widgets.slint\";\n\nexport component ResultsList {\n    in property <[FileEntry]> entries: [];\n    in property <string>      tool_title: \"Wyniki\";\n    in property <string>      tool_emoji: \"?\";\n    in property <bool>        can_delete: true;\n    in property <bool>        can_rename: false;\n    in property <bool>        can_clean_exif: false;\n\n    in property <bool>        is_grouped: false;\n\n    in property <bool>        has_gallery_mode: false;\n    in property <bool>        has_size_select: false;\n    in property <bool>        has_resolution_select: false;\n    in property <bool>        extra_as_line2: false;\n\n    property <int> menu_state: 0;\n\n\n    VerticalLayout {\n        spacing: 0px;\n\n\n        if entries.length > 0 : Rectangle {\n            height: 52px;\n            background: CediniaColors.bg_elevated;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n\n            HorizontalLayout {\n                padding-left: 12px;\n                padding-right: 12px;\n                padding-top: 8px;\n                padding-bottom: 8px;\n                spacing: 8px;\n\n                if root.can_delete && AppState.selected_count > 0 : TouchButton {\n                    label: \"X\";\n                    min_h: 36px;\n                    bg: CediniaColors.danger;\n                    fg: #ffffff;\n                    clicked => { AppState.delete_selected(); }\n                }\n\n                if root.can_clean_exif && AppState.selected_count > 0 : TouchButton {\n                    label: \"Clean\";\n                    min_h: 36px;\n                    bg: CediniaColors.accent_muted;\n                    fg: CediniaColors.accent_light;\n                    clicked => { AppState.clean_exif_selected(); }\n                }\n\n                if root.can_rename && AppState.selected_count > 0 : TouchButton {\n                    label: \"🏷\";\n                    min_h: 36px;\n                    bg: CediniaColors.accent_muted;\n                    fg: CediniaColors.accent_light;\n                    clicked => { AppState.rename_selected(); }\n                }\n\n                if AppState.selected_count > 0 : StatusChip {\n                    label: AppState.selected_count;\n                    chip_color: CediniaColors.accent_muted;\n                    text_color: CediniaColors.accent_light;\n                    vertical-stretch: 0.0;\n                }\n\n                Rectangle { horizontal-stretch: 1.0; }\n\n                TouchButton {\n                    label: Translations.select_label_text;\n                    min_h: 36px;\n                    bg: CediniaColors.bg_elevated;\n                    fg: CediniaColors.text_secondary;\n                    clicked => { root.menu_state = 1; }\n                }\n\n                TouchButton {\n                    label: Translations.deselect_label_text;\n                    min_h: 36px;\n                    bg: CediniaColors.bg_elevated;\n                    fg: CediniaColors.text_secondary;\n                    clicked => { root.menu_state = 2; }\n                }\n\n                if root.has_gallery_mode : TouchButton {\n                    label: AppState.similar_images_gallery_mode ? Translations.list_label_text : Translations.gallery_label_text;\n                    min_h: 36px;\n                    bg: CediniaColors.bg_elevated;\n                    fg: CediniaColors.text_secondary;\n                    clicked => { AppState.similar_images_gallery_mode = !AppState.similar_images_gallery_mode; }\n                }\n            }\n        }\n\n\n        if entries.length > 0 : ListView {\n            vertical-stretch: 1.0;\n\n            for entry[idx] in entries : FileRow {\n                name:           entry.val_str[FileEntryIdx.name_idx];\n                path:           entry.val_str[FileEntryIdx.path_idx];\n                size:           entry.val_str[FileEntryIdx.size_idx];\n                val_str:        entry.val_str;\n                checked:        entry.checked;\n                is_header:      entry.is_header;\n                is_even:        mod(idx, 2) == 0;\n                extra_as_line2: root.extra_as_line2 && !entry.is_header;\n                toggle_checked => { AppState.toggle_file_checked(idx); }\n                open_item   => { AppState.open_path(entry.val_str[FileEntryIdx.path_idx] == \"\" ? entry.val_str[FileEntryIdx.name_idx] : entry.val_str[FileEntryIdx.path_idx] + \"/\" + entry.val_str[FileEntryIdx.name_idx]); }\n                open_parent => { AppState.open_parent_folder(entry.val_str[FileEntryIdx.path_idx]); }\n            }\n        }\n\n\n        if entries.length == 0 : Rectangle {\n            vertical-stretch: 1.0;\n            background: CediniaColors.bg_primary;\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                spacing: 12px;\n\n                Text {\n                    text: root.tool_emoji;\n                    font-size: 56px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                    color: CediniaColors.text_disabled;\n                }\n                Text {\n                    text: AppState.scan_state == ScanState.Scanning\n                        ? Translations.scanning_text\n                        : AppState.scan_state == ScanState.Stopping\n                            ? Translations.stopping_text\n                            : AppState.scan_state == ScanState.Done\n                                ? Translations.no_results_text\n                                : Translations.press_start_text;\n                    color: CediniaColors.text_disabled;\n                    font-size: 15px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                if !AppState.storage_permission_granted : Rectangle {\n                    height: perm_vl.preferred-height + 24px;\n                    width: min(parent.width - 48px, 320px);\n                    x: (parent.width - self.width) / 2;\n                    border-radius: 10px;\n                    background: CediniaColors.bg_elevated;\n                    border-width: 1px;\n                    border-color: CediniaColors.warning.with-alpha(0.5);\n\n                    perm_vl := VerticalLayout {\n                        padding: 12px;\n                        spacing: 10px;\n\n                        Text {\n                            text: Translations.no_permission_scan_warning_text;\n                            color: CediniaColors.warning;\n                            font-size: 13px;\n                            wrap: TextWrap.word-wrap;\n                            horizontal-alignment: TextHorizontalAlignment.center;\n                        }\n\n                        TouchArea {\n                            height: 40px;\n                            clicked => { AppState.request_storage_permission(); }\n\n                            Rectangle {\n                                border-radius: 8px;\n                                background: CediniaColors.accent_muted;\n\n                                Text {\n                                    text: Translations.grant_text;\n                                    color: CediniaColors.accent_light;\n                                    font-size: 14px;\n                                    font-weight: 600;\n                                    horizontal-alignment: TextHorizontalAlignment.center;\n                                    vertical-alignment: TextVerticalAlignment.center;\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n\n\n    if root.menu_state == 1 : Rectangle {\n        x: 0; y: 0;\n        width: root.width;\n        height: root.height;\n        z: 50;\n        background: #00000099;\n        TouchArea {}\n\n        Rectangle {\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - popup_sel_vl.preferred-height - 24px) / 2;\n            width: min(parent.width - 48px, 300px);\n            height: popup_sel_vl.preferred-height + 24px;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000aa;\n            clip: true;\n\n            popup_sel_vl := VerticalLayout {\n                padding: 12px;\n                spacing: 6px;\n\n                Text {\n                    text: Translations.selection_popup_title_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    font-weight: 700;\n                    letter-spacing: 0.5px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                TouchButton {\n                    label: Translations.select_all_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.text_primary;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all(); root.menu_state = 0; }\n                }\n\n                if root.is_grouped : TouchButton {\n                    label: Translations.select_except_one_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_one(); root.menu_state = 0; }\n                }\n\n                if root.has_size_select : TouchButton {\n                    label: Translations.select_except_largest_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_largest(); root.menu_state = 0; }\n                }\n\n                if root.has_size_select : TouchButton {\n                    label: Translations.select_except_smallest_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_smallest(); root.menu_state = 0; }\n                }\n\n                if root.has_size_select : TouchButton {\n                    label: Translations.select_largest_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_largest_per_group(); root.menu_state = 0; }\n                }\n\n                if root.has_size_select : TouchButton {\n                    label: Translations.select_smallest_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_smallest_per_group(); root.menu_state = 0; }\n                }\n\n                if root.has_resolution_select : TouchButton {\n                    label: Translations.select_except_highest_res_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_highest_resolution(); root.menu_state = 0; }\n                }\n\n                if root.has_resolution_select : TouchButton {\n                    label: Translations.select_except_lowest_res_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_lowest_resolution(); root.menu_state = 0; }\n                }\n\n                if root.has_resolution_select : TouchButton {\n                    label: Translations.select_highest_res_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_highest_resolution_per_group(); root.menu_state = 0; }\n                }\n\n                if root.has_resolution_select : TouchButton {\n                    label: Translations.select_lowest_res_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_lowest_resolution_per_group(); root.menu_state = 0; }\n                }\n\n                TouchButton {\n                    label: Translations.invert_selection_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.text_secondary;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.invert_selection(); root.menu_state = 0; }\n                }\n\n                TouchButton {\n                    label: Translations.close_text;\n                    min_h: 40px;\n                    bg: CediniaColors.bg_elevated;\n                    fg: CediniaColors.text_disabled;\n                    horizontal-stretch: 1.0;\n                    clicked => { root.menu_state = 0; }\n                }\n            }\n        }\n    }\n\n\n\n    if root.menu_state == 2 : Rectangle {\n        x: 0; y: 0;\n        width: root.width;\n        height: root.height;\n        z: 50;\n        background: #00000099;\n        TouchArea {}\n\n        Rectangle {\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - popup_desel_vl.preferred-height - 24px) / 2;\n            width: min(parent.width - 48px, 300px);\n            height: popup_desel_vl.preferred-height + 24px;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000aa;\n            clip: true;\n\n            popup_desel_vl := VerticalLayout {\n                padding: 12px;\n                spacing: 6px;\n\n                Text {\n                    text: Translations.deselection_popup_title_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    font-weight: 700;\n                    letter-spacing: 0.5px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                TouchButton {\n                    label: Translations.deselect_all_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.text_primary;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.deselect_all(); root.menu_state = 0; }\n                }\n\n                if root.is_grouped : TouchButton {\n                    label: Translations.deselect_except_one_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.deselect_all_except_one(); root.menu_state = 0; }\n                }\n\n                TouchButton {\n                    label: Translations.close_text;\n                    min_h: 40px;\n                    bg: CediniaColors.bg_elevated;\n                    fg: CediniaColors.text_disabled;\n                    horizontal-stretch: 1.0;\n                    clicked => { root.menu_state = 0; }\n                }\n            }\n        }\n    }\n\n\n\n    if AppState.confirm_popup_visible : Rectangle {\n        x: 0;\n        y: 0;\n        width: root.width;\n        height: root.height;\n        z: 40;\n        background: #000000bb;\n\n        TouchArea {}\n\n        Rectangle {\n            x: parent.width / 2 - self.width / 2;\n            y: parent.height / 2 - self.height / 2;\n            width: min(parent.width - 32px, 400px);\n            height: confirm-card.preferred-height;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000cc;\n\n            confirm-card := VerticalLayout {\n                padding: 20px;\n                spacing: 12px;\n\n                Text {\n                    text: AppState.confirm_popup_message;\n                    color: CediniaColors.text_primary;\n                    font-size: 15px;\n                    font-weight: 600;\n                    wrap: TextWrap.word-wrap;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                HorizontalLayout {\n                    spacing: 8px;\n\n                    TouchButton {\n                        label: Translations.cancel_text;\n                        horizontal-stretch: 1.0;\n                        min_h: 44px;\n                        bg: CediniaColors.bg_surface;\n                        fg: CediniaColors.text_secondary;\n                        clicked => { AppState.confirm_popup_cancel(); }\n                    }\n\n                    TouchButton {\n                        label: (AppState.confirm_popup_action == \"rename\" || AppState.confirm_popup_action == \"rename_bad_names\") ? Translations.rename_text : Translations.delete_text;\n                        horizontal-stretch: 1.0;\n                        min_h: 44px;\n                        bg: (AppState.confirm_popup_action == \"rename\" || AppState.confirm_popup_action == \"rename_bad_names\") ? CediniaColors.accent_muted : CediniaColors.danger;\n                        fg: (AppState.confirm_popup_action == \"rename\" || AppState.confirm_popup_action == \"rename_bad_names\") ? CediniaColors.accent_light : #ffffff;\n                        clicked => { AppState.confirm_popup_ok(); }\n                    }\n                }\n            }\n        }\n    }\n\n\n\n    if AppState.delete_errors_visible : Rectangle {\n        x: 0;\n        y: 0;\n        width: root.width;\n        height: root.height;\n        z: 30;\n        background: #000000bb;\n\n        TouchArea {}\n\n        Rectangle {\n            x: parent.width / 2 - self.width / 2;\n            y: parent.height / 2 - self.height / 2;\n            width: min(parent.width - 32px, 400px);\n            height: err-card.preferred-height;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000cc;\n\n            err-card := VerticalLayout {\n                padding: 20px;\n                spacing: 12px;\n\n                Text {\n                    text: Translations.delete_errors_title_text;\n                    color: CediniaColors.warning;\n                    font-size: 15px;\n                    font-weight: 600;\n                    wrap: TextWrap.word-wrap;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                Text {\n                    text: AppState.delete_errors_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 11px;\n                    wrap: TextWrap.word-wrap;\n                }\n\n                TouchButton {\n                    label: Translations.ok_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.text_secondary;\n                    clicked => { AppState.delete_errors_visible = false; }\n                }\n            }\n        }\n    }\n\n\n\n    Rectangle {\n        x: root.width - self.width - 16px;\n        y: root.height - self.height - 16px;\n        z: 20;\n        width: 56px;\n        height: 56px;\n        border-radius: 28px;\n        background: !AppState.storage_permission_granted\n            ? CediniaColors.text_disabled\n            : AppState.scan_state == ScanState.Scanning\n                ? CediniaColors.warning\n                : AppState.scan_state == ScanState.Stopping\n                    ? CediniaColors.warning.with-alpha(0.5)\n                    : CediniaColors.accent;\n        drop-shadow-blur: 12px;\n        drop-shadow-color: #00000088;\n        animate background { duration: 200ms; }\n\n        TouchArea {\n            enabled: AppState.storage_permission_granted && AppState.scan_state != ScanState.Stopping;\n            clicked => {\n                if (AppState.scan_state == ScanState.Scanning) {\n                    AppState.stop_requested();\n                } else {\n                    AppState.scan_requested();\n                }\n            }\n        }\n\n        Text {\n            text: AppState.scan_state == ScanState.Scanning || AppState.scan_state == ScanState.Stopping\n                ? \"⏹\" : \"▶\";\n            font-size: 22px;\n            color: (!AppState.storage_permission_granted || AppState.scan_state == ScanState.Stopping) ? #00000066 : #000000;\n            horizontal-alignment: TextHorizontalAlignment.center;\n            vertical-alignment: TextVerticalAlignment.center;\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/scan_progress.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { ScanState } from \"common.slint\";\nimport { Translations } from \"translations.slint\";\n\nexport component ScanProgressBar {\n    visible: AppState.scan_state == ScanState.Scanning;\n    height: self.visible ? 52px : 0px;\n    animate height { duration: 200ms; easing: ease; }\n\n    Rectangle {\n        background: CediniaColors.bg_surface;\n        border-width: 1px;\n        border-color: CediniaColors.divider;\n\n        VerticalLayout {\n            padding-left: 16px;\n            padding-right: 16px;\n            padding-top: 6px;\n            padding-bottom: 6px;\n            spacing: 4px;\n\n            HorizontalLayout {\n                Text {\n                    text: AppState.progress.step_name != \"\" ? AppState.progress.step_name : Translations.scanning_fallback_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    horizontal-stretch: 1.0;\n                    overflow: elide;\n                }\n\n                if !AppState.progress.is_indeterminate : Text {\n                    text: AppState.progress.all_progress > 0\n                        ? (AppState.progress.current_progress + \" / \" + AppState.progress.all_progress)\n                        : (AppState.progress.current_progress + \" \" + Translations.files_suffix_text);\n                    color: CediniaColors.accent_light;\n                    font-size: 12px;\n                    font-weight: 700;\n                }\n                if AppState.progress.is_indeterminate : Text {\n                    text: \"…\";\n                    color: CediniaColors.text_disabled;\n                    font-size: 12px;\n                    font-weight: 700;\n                }\n            }\n\n\n            if !AppState.progress.is_indeterminate && AppState.progress.all_progress > 0 : Rectangle {\n                horizontal-stretch: 1.0;\n                height: 4px;\n                border-radius: 2px;\n                background: CediniaColors.bg_elevated;\n\n                Rectangle {\n                    x: 0;\n                    height: parent.height;\n                    width: parent.width * AppState.progress.current_progress / AppState.progress.all_progress;\n                    border-radius: 2px;\n                    background: CediniaColors.accent;\n                    animate width { duration: 300ms; easing: ease-out; }\n                }\n            }\n\n\n            if AppState.progress.is_indeterminate : Rectangle {\n                horizontal-stretch: 1.0;\n                height: 4px;\n                border-radius: 2px;\n                background: CediniaColors.accent.with-alpha(0.35);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/settings_components.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { AppState } from \"app_state.slint\";\n\nexport component ToggleRow {\n    in property <string> label;\n    in property <string> description: \"\";\n    in-out property <bool> value;\n\n    height: root.description == \"\" ? 56px : 72px;\n\n    Rectangle {\n        background: ta.has-hover ? CediniaColors.bg_elevated : transparent;\n        animate background { duration: 100ms; }\n\n        ta := TouchArea {\n            clicked => {\n                root.value = !root.value;\n                AppState.save_settings_now();\n            }\n        }\n\n        HorizontalLayout {\n            padding-left: 16px;\n            padding-right: 16px;\n            spacing: 12px;\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                horizontal-stretch: 1.0;\n                spacing: 2px;\n                Text {\n                    text: root.label;\n                    color: CediniaColors.text_primary;\n                    font-size: 15px;\n                }\n                if root.description != \"\" : Text {\n                    text: root.description;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    wrap: word-wrap;\n                }\n            }\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                Rectangle {\n                    width: 50px;\n                    height: 28px;\n                    vertical-stretch: 0.0;\n                    border-radius: 14px;\n                    background: root.value ? CediniaColors.accent : CediniaColors.bg_elevated;\n                    animate background { duration: 150ms; }\n\n                    Rectangle {\n                        x: root.value ? parent.width - self.height - 2px : 2px;\n                        y: 2px;\n                        width: 24px;\n                        height: 24px;\n                        border-radius: 12px;\n                        background: #ffffff;\n                        animate x { duration: 150ms; easing: ease-out; }\n                    }\n                }\n            }\n        }\n    }\n}\n\n\nexport component SegmentRow {\n    in property <string>   label;\n    in property <string>   description: \"\";\n    in property <[string]> options;\n    in-out property <int>  selected;\n\n    height: root.description == \"\" ? 72px : 88px;\n\n    VerticalLayout {\n        padding-left: 16px;\n        padding-right: 16px;\n        padding-top: 8px;\n        padding-bottom: 8px;\n        spacing: 6px;\n\n        HorizontalLayout {\n            VerticalLayout {\n                horizontal-stretch: 1.0;\n                alignment: LayoutAlignment.center;\n                Text {\n                    text: root.label;\n                    color: CediniaColors.text_primary;\n                    font-size: 14px;\n                }\n                if root.description != \"\" : Text {\n                    text: root.description;\n                    color: CediniaColors.text_secondary;\n                    font-size: 11px;\n                }\n            }\n        }\n\n        HorizontalLayout {\n            spacing: 4px;\n            for opt[i] in root.options : Rectangle {\n                horizontal-stretch: 1.0;\n                height: 30px;\n                border-radius: 6px;\n                background: root.selected == i ? CediniaColors.accent_muted : CediniaColors.bg_elevated;\n                border-width: 1px;\n                border-color: root.selected == i ? CediniaColors.accent_light : CediniaColors.divider;\n                animate background { duration: 100ms; }\n\n                TouchArea {\n                    clicked => {\n                        root.selected = i;\n                        AppState.save_settings_now();\n                    }\n                }\n\n                Text {\n                    text: opt;\n                    color: root.selected == i ? CediniaColors.accent_light : CediniaColors.text_secondary;\n                    font-size: 11px;\n                    font-weight: root.selected == i ? 700 : 400;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    overflow: elide;\n                }\n            }\n        }\n    }\n}\n\n\nexport component ToolGroupHeader inherits Rectangle {\n    in property <string> label;\n    in property <string> emoji: \"\";\n\n    height: 40px;\n    background: CediniaColors.accent_muted;\n\n    HorizontalLayout {\n        padding-left: 16px;\n        padding-right: 16px;\n\n        Text {\n            text: (root.emoji != \"\" ? root.emoji + \"  \" : \"\") + root.label;\n            color: CediniaColors.accent_light;\n            font-size: 12px;\n            font-weight: 700;\n            vertical-alignment: TextVerticalAlignment.center;\n            letter-spacing: 0.8px;\n        }\n    }\n}\n\n\nexport component CategoryLabel inherits Rectangle {\n    in property <string> label;\n\n    height: 28px;\n    background: CediniaColors.bg_primary;\n\n    HorizontalLayout {\n        padding-left: 16px;\n        padding-right: 16px;\n\n        Text {\n            text: label;\n            color: CediniaColors.text_disabled;\n            font-size: 10px;\n            font-weight: 700;\n            vertical-alignment: TextVerticalAlignment.center;\n            letter-spacing: 1px;\n        }\n    }\n}\n\n\nexport component DropdownRow {\n//    in property <string>   label;\n    in property <string>   description: \"\";\n    in property <[string]> options;\n    in-out property <int>  selected;\n    callback changed_selection(int);\n\n    height: description == \"\" ? 56px : 72px;\n\n    Rectangle {\n        background: row_ta.has-hover ? CediniaColors.bg_elevated : CediniaColors.bg_surface;\n        animate background { duration: 100ms; }\n\n        row_ta := TouchArea {\n            clicked => { dropdown.show(); }\n        }\n\n        HorizontalLayout {\n            padding-left: 16px;\n            padding-right: 16px;\n            spacing: 12px;\n            alignment: LayoutAlignment.center;\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                horizontal-stretch: 1.0;\n                spacing: 2px;\n//                Text {\n//                    text: root.label;\n//                    color: CediniaColors.text_primary;\n//                    font-size: 15px;\n//                }\n//                if root.description != \"\" :\n                Text {\n                    text: root.description;\n                    color: CediniaColors.text_disabled;\n                    font-size: 11px;\n                    wrap: word-wrap;\n                }\n            }\n\n            Text {\n                text: root.options[root.selected];\n                color: CediniaColors.accent_light;\n                font-size: 13px;\n                font-weight: 700;\n                vertical-alignment: TextVerticalAlignment.center;\n                overflow: TextOverflow.elide;\n                max-width: 180px;\n            }\n        }\n    }\n\n    dropdown := PopupWindow {\n        x: 16px;\n        y: root.height;\n        width: root.width - 32px;\n\n        Rectangle {\n            background: CediniaColors.bg_elevated;\n            border-radius: 8px;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n            drop-shadow-blur: 16px;\n            drop-shadow-color: #000000aa;\n            clip: true;\n\n            VerticalLayout {\n                for opt[i] in root.options : Rectangle {\n                    height: 48px;\n                    background: item_ta.has-hover ? CediniaColors.accent_muted : (i == root.selected ? CediniaColors.accent_muted.with-alpha(0.5) : transparent);\n                    animate background { duration: 80ms; }\n\n                    item_ta := TouchArea {\n                        clicked => {\n                            root.selected = i;\n                            root.changed_selection(i);\n                            AppState.save_settings_now();\n                            dropdown.close();\n                        }\n                    }\n\n                    HorizontalLayout {\n                        padding-left: 20px;\n                        padding-right: 16px;\n                        spacing: 8px;\n\n                        Text {\n                            text: opt;\n                            color: i == root.selected ? CediniaColors.accent_light : CediniaColors.text_primary;\n                            font-size: 15px;\n                            font-weight: i == root.selected ? 700 : 400;\n                            vertical-alignment: TextVerticalAlignment.center;\n                            horizontal-stretch: 1;\n                        }\n\n                        if i == root.selected : Text {\n                            text: \"✓\";\n                            color: CediniaColors.accent_light;\n                            font-size: 14px;\n                            vertical-alignment: TextVerticalAlignment.center;\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n\nexport component TextInputRow {\n    in property <string> label;\n    in property <string> placeholder: \"\";\n    in-out property <string> value;\n\n    height: 68px;\n\n    Rectangle {\n        background: CediniaColors.bg_surface;\n\n        VerticalLayout {\n            padding-left: 16px;\n            padding-right: 16px;\n            padding-top: 8px;\n            padding-bottom: 8px;\n            spacing: 4px;\n\n            Text {\n                text: root.label;\n                color: CediniaColors.text_secondary;\n                font-size: 11px;\n                font-weight: 600;\n            }\n\n            Rectangle {\n                height: 36px;\n                border-radius: 6px;\n                background: CediniaColors.bg_elevated;\n                border-width: 1px;\n                border-color: inp.has-focus ? CediniaColors.accent : CediniaColors.divider;\n                animate border-color { duration: 120ms; }\n\n                HorizontalLayout {\n                    padding-left: 10px;\n                    padding-right: 10px;\n\n                    inp := TextInput {\n                        text <=> root.value;\n                        color: CediniaColors.text_primary;\n                        font-size: 13px;\n                        vertical-alignment: TextVerticalAlignment.center;\n                        single-line: true;\n                        accepted => { AppState.save_settings_now(); }\n                    }\n                }\n\n\n                if inp.text == \"\" && !inp.has-focus : Text {\n                    x: 10px;\n                    y: 0px;\n                    height: parent.height;\n                    width: parent.width - 20px;\n                    text: root.placeholder;\n                    color: CediniaColors.text_disabled;\n                    font-size: 13px;\n                    vertical-alignment: TextVerticalAlignment.center;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/settings_screen.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { Divider, TouchButton } from \"components.slint\";\nimport { AppState, BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, GeneralSettings, SameMusicSettings, SimilarImagesSettings } from \"app_state.slint\";\nimport { CategoryLabel, DropdownRow, SegmentRow, TextInputRow, ToggleRow, ToolGroupHeader } from \"settings_components.slint\";\nimport { SettingsTab } from \"common.slint\";\nimport { Translations } from \"translations.slint\";\n\nexport component SettingsScreen {\n\n    if AppState.collect_test_done : Rectangle {\n        z: 100;\n        background: #00000099;\n        TouchArea { clicked => { AppState.collect_test_done = false; } }\n\n        Rectangle {\n            width: min(parent.width - 48px, 360px);\n            height: popup_content.preferred-height + 32px;\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - self.height) / 2;\n            border-radius: 16px;\n            background: CediniaColors.bg_elevated;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #00000088;\n\n            popup_content := VerticalLayout {\n                padding: 24px;\n                spacing: 16px;\n\n                Text {\n                    text: Translations.collect_test_title_text;\n                    color: CediniaColors.accent_light;\n                    font-size: 18px;\n                    font-weight: 700;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                Rectangle { height: 1px; background: CediniaColors.divider; }\n\n                GridLayout {\n                    spacing: 8px;\n                    Row {\n                        Text { text: Translations.collect_test_volumes_text; color: CediniaColors.text_secondary; font-size: 14px; }\n                        Text { text: AppState.collect_test_result.volumes; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; }\n                    }\n                    Row {\n                        Text { text: Translations.collect_test_folders_text; color: CediniaColors.text_secondary; font-size: 14px; }\n                        Text { text: AppState.collect_test_result.folders; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; }\n                    }\n                    Row {\n                        Text { text: Translations.collect_test_files_text; color: CediniaColors.text_secondary; font-size: 14px; }\n                        Text { text: AppState.collect_test_result.files; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; }\n                    }\n                    Row {\n                        Text { text: Translations.collect_test_time_text; color: CediniaColors.text_secondary; font-size: 14px; }\n                        Text { text: AppState.collect_test_result.elapsed_ms + Translations.collect_test_ms_text; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; }\n                    }\n                }\n\n                TouchButton {\n                    label: Translations.ok_text;\n                    bg: CediniaColors.accent;\n                    fg: #000000;\n                    clicked => { AppState.collect_test_done = false; }\n                }\n            }\n        }\n    }\n\n\n    property <SettingsTab> active_tab: SettingsTab.General;\n\n\n    changed active_tab => {\n        if (active_tab == SettingsTab.Diagnostics && !AppState.diag_refresh_running) {\n            AppState.refresh_diag_cache_info();\n        }\n    }\n\n    VerticalLayout {\n        spacing: 0px;\n\n\n        Rectangle {\n            height: 44px;\n            background: CediniaColors.bg_surface;\n            border-width: 1px;\n            border-color: CediniaColors.divider;\n\n            HorizontalLayout {\n                spacing: 0px;\n                alignment: LayoutAlignment.stretch;\n\n                for tab_info in [\n                    { label: Translations.settings_tab_general_text,      tab: SettingsTab.General      },\n                    { label: Translations.settings_tab_tools_text,         tab: SettingsTab.Tools        },\n                    { label: Translations.settings_tab_diagnostics_text,   tab: SettingsTab.Diagnostics  },\n                ] : Rectangle {\n                    horizontal-stretch: 1.0;\n                    background: transparent;\n\n                    TouchArea {\n                        clicked => { active_tab = tab_info.tab; }\n                    }\n\n                    VerticalLayout {\n                        Rectangle { vertical-stretch: 1.0; }\n                        Rectangle {\n                            height: 3px;\n                            background: active_tab == tab_info.tab ? CediniaColors.accent : transparent;\n                            animate background { duration: 150ms; }\n                        }\n                    }\n\n                    Text {\n                        text: tab_info.label;\n                        color: active_tab == tab_info.tab ? CediniaColors.accent_light : CediniaColors.text_secondary;\n                        font-size: 13px;\n                        font-weight: active_tab == tab_info.tab ? 700 : 400;\n                        horizontal-alignment: TextHorizontalAlignment.center;\n                        vertical-alignment: TextVerticalAlignment.center;\n                        animate color { duration: 150ms; }\n                    }\n                }\n            }\n        }\n\n\n        Flickable {\n            vertical-stretch: 1.0;\n            viewport-height: max(tab_content.preferred-height, self.height);\n\n            tab_content := VerticalLayout {\n                spacing: 0px;\n\n\n                if active_tab == SettingsTab.General : VerticalLayout {\n                    spacing: 0px;\n\n                    CategoryLabel { label: Translations.settings_scan_label_text; }\n\n                    ToggleRow {\n                        label: Translations.settings_use_cache_text;\n                        description: Translations.settings_use_cache_desc_text;\n                        value <=> GeneralSettings.use_cache;\n                    }\n                    Divider {}\n                    ToggleRow {\n                        label: Translations.settings_ignore_hidden_text;\n                        description: Translations.settings_ignore_hidden_desc_text;\n                        value <=> GeneralSettings.ignore_hidden;\n                    }\n                    Divider {}\n\n                    CategoryLabel { label: Translations.settings_filters_label_text; }\n\n                    SegmentRow {\n                        label: Translations.settings_min_file_size_text;\n                        options: GeneralSettings.min_file_size_options;\n                        selected <=> GeneralSettings.min_file_size_idx;\n                    }\n                    Divider {}\n\n                    SegmentRow {\n                        label: Translations.settings_max_file_size_text;\n                        options: GeneralSettings.max_file_size_options;\n                        selected <=> GeneralSettings.max_file_size_idx;\n                    }\n                    Divider {}\n\n                    CategoryLabel { label: \"LANGUAGE / JĘZYK\"; } // This should not be translated\n\n                    DropdownRow {\n                        options: GeneralSettings.language_options;\n                        selected <=> GeneralSettings.language_idx;\n                        changed_selection(_) => { AppState.apply_language_change(); }\n                    }\n                    Divider {}\n\n                    CategoryLabel { label: Translations.settings_common_label_text; }\n\n                    TextInputRow {\n                        label: Translations.settings_excluded_items_text;\n                        placeholder: Translations.settings_excluded_items_placeholder_text;\n                        value <=> GeneralSettings.excluded_items;\n                    }\n                    Divider {}\n\n                    TextInputRow {\n                        label: Translations.settings_allowed_extensions_text;\n                        placeholder: Translations.settings_allowed_extensions_placeholder_text;\n                        value <=> GeneralSettings.allowed_extensions;\n                    }\n                    Divider {}\n\n                    TextInputRow {\n                        label: Translations.settings_excluded_extensions_text;\n                        placeholder: Translations.settings_excluded_extensions_placeholder_text;\n                        value <=> GeneralSettings.excluded_extensions;\n                    }\n                    Divider {}\n                }\n\n\n                if active_tab == SettingsTab.Tools : VerticalLayout {\n                    spacing: 0px;\n\n\n                    ToolGroupHeader { label: Translations.settings_duplicates_header_text; emoji: \"📂\"; }\n\n                    SegmentRow {\n                        label: Translations.settings_check_method_text;\n                        options: DuplicateSettings.check_method_options;\n                        selected <=> DuplicateSettings.check_method;\n                    }\n                    Divider {}\n\n                    SegmentRow {\n                        label: Translations.settings_hash_type_text;\n                        description: Translations.settings_hash_type_desc_text;\n                        options: DuplicateSettings.hash_type_options;\n                        selected <=> DuplicateSettings.hash_type;\n                    }\n                    Divider {}\n\n\n                    ToolGroupHeader { label: Translations.settings_similar_images_header_text; emoji: \"🖼\"; }\n\n                    SegmentRow {\n                        label: Translations.settings_similarity_preset_text;\n                        description: Translations.settings_similarity_desc_text;\n                        options: SimilarImagesSettings.similarity_preset_options;\n                        selected <=> SimilarImagesSettings.similarity_preset;\n                    }\n                    Divider {}\n\n                    SegmentRow {\n                        label: Translations.settings_hash_size_text;\n                        description: Translations.settings_hash_size_desc_text;\n                        options: SimilarImagesSettings.hash_size_options;\n                        selected <=> SimilarImagesSettings.hash_size_idx;\n                    }\n                    Divider {}\n\n                    SegmentRow {\n                        label: Translations.settings_hash_alg_text;\n                        options: SimilarImagesSettings.hash_alg_options;\n                        selected <=> SimilarImagesSettings.hash_alg_idx;\n                    }\n                    Divider {}\n\n                    SegmentRow {\n                        label: Translations.settings_image_filter_text;\n                        options: SimilarImagesSettings.image_filter_options;\n                        selected <=> SimilarImagesSettings.image_filter_idx;\n                    }\n                    Divider {}\n\n                    ToggleRow { label: Translations.settings_ignore_same_size_text; value <=> SimilarImagesSettings.ignore_same_size; }\n                    Divider {}\n\n\n                    ToolGroupHeader { label: Translations.settings_big_files_header_text; emoji: \"📦\"; }\n\n                    SegmentRow {\n                        label: Translations.settings_search_mode_text;\n                        options: BigFilesSettings.search_mode_options;\n                        selected <=> BigFilesSettings.search_mode_idx;\n                    }\n                    Divider {}\n\n                    SegmentRow {\n                        label: Translations.settings_file_count_text;\n                        options: BigFilesSettings.count_options;\n                        selected <=> BigFilesSettings.count_idx;\n                    }\n                    Divider {}\n\n\n                    ToolGroupHeader { label: Translations.settings_same_music_header_text; emoji: \"🎵\"; }\n\n                    CategoryLabel { label: Translations.settings_music_compare_tags_label_text; }\n                    ToggleRow { label: Translations.settings_music_title_text;   value <=> SameMusicSettings.title; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_music_artist_text; value <=> SameMusicSettings.artist; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_music_year_text;     value <=> SameMusicSettings.year; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_music_length_text;    value <=> SameMusicSettings.length; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_music_genre_text; value <=> SameMusicSettings.genre; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_music_bitrate_text; value <=> SameMusicSettings.bitrate; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_music_approx_text; value <=> SameMusicSettings.approximate; }\n                    Divider {}\n                    CategoryLabel { label: Translations.settings_music_check_method_text; }\n                    SegmentRow {\n                        label: Translations.settings_music_check_method_text;\n                        options: SameMusicSettings.check_method_options;\n                        selected <=> SameMusicSettings.check_method_idx;\n                    }\n                    Divider {}\n\n\n                    ToolGroupHeader { label: Translations.settings_broken_files_header_text; emoji: \"⚠\"; }\n\n                    CategoryLabel { label: Translations.settings_broken_files_types_label_text; }\n                    ToggleRow { label: Translations.settings_broken_image_text;  value <=> BrokenFilesSettings.check_image; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_broken_audio_text;   value <=> BrokenFilesSettings.check_audio; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_broken_pdf_text;     value <=> BrokenFilesSettings.check_pdf; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_broken_archive_text; value <=> BrokenFilesSettings.check_archive; }\n                    Divider {}\n\n\n                    ToolGroupHeader { label: Translations.settings_bad_names_header_text; emoji: \"✏\"; }\n\n                    CategoryLabel { label: Translations.settings_bad_names_checks_label_text; }\n                    ToggleRow { label: Translations.settings_bad_names_uppercase_ext_text; value <=> BadNamesSettings.uppercase_extension; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_bad_names_emoji_text; value <=> BadNamesSettings.emoji_used; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_bad_names_space_text; value <=> BadNamesSettings.space_at_start_or_end; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_bad_names_non_ascii_text; value <=> BadNamesSettings.non_ascii_graphical; }\n                    Divider {}\n                    ToggleRow { label: Translations.settings_bad_names_duplicated_text; value <=> BadNamesSettings.remove_duplicated_non_alpha; }\n                    Divider {}\n                }\n\n\n                if active_tab == SettingsTab.Diagnostics : VerticalLayout {\n                    spacing: 0px;\n\n                    CategoryLabel { label: Translations.diagnostics_header_text; }\n\n                    // Storage permission row\n                    Rectangle {\n                        height: 72px;\n                        background: CediniaColors.bg_surface;\n                        HorizontalLayout {\n                            padding-left: 16px;\n                            padding-right: 16px;\n                            padding-top: 10px;\n                            padding-bottom: 10px;\n                            spacing: 12px;\n\n                            VerticalLayout {\n                                alignment: LayoutAlignment.center;\n                                horizontal-stretch: 1.0;\n                                spacing: 2px;\n                                Text {\n                                    text: Translations.permission_title_text;\n                                    color: CediniaColors.text_primary;\n                                    font-size: 15px;\n                                }\n                                Text {\n                                    text: AppState.storage_permission_granted\n                                        ? \"✅ \" + Translations.grant_text\n                                        : \"❌ \" + Translations.no_permission_scan_warning_text;\n                                    color: AppState.storage_permission_granted\n                                        ? CediniaColors.success\n                                        : CediniaColors.danger;\n                                    font-size: 12px;\n                                }\n                            }\n\n                            if !AppState.storage_permission_granted : TouchButton {\n                                label: Translations.grant_text;\n                                bg: CediniaColors.warning;\n                                fg: #000000;\n                                min_h: 44px;\n                                clicked => { AppState.request_storage_permission(); }\n                            }\n                        }\n                    }\n                    Divider {}\n\n                    // Collect test row\n                    Rectangle {\n                        height: 72px;\n                        background: CediniaColors.bg_surface;\n                        HorizontalLayout {\n                            padding-left: 16px;\n                            padding-right: 16px;\n                            padding-top: 10px;\n                            padding-bottom: 10px;\n                            spacing: 12px;\n\n                            VerticalLayout {\n                                alignment: LayoutAlignment.center;\n                                horizontal-stretch: 1.0;\n                                spacing: 2px;\n                                Text {\n                                    text: Translations.diagnostics_collect_test_text;\n                                    color: CediniaColors.text_primary;\n                                    font-size: 15px;\n                                }\n                                Text {\n                                    text: Translations.diagnostics_collect_test_desc_text;\n                                    color: CediniaColors.text_secondary;\n                                    font-size: 12px;\n                                }\n                            }\n\n                            if !AppState.collect_test_running : TouchButton {\n                                label: Translations.diagnostics_collect_test_run_text;\n                                bg: CediniaColors.accent;\n                                fg: #000000;\n                                min_h: 44px;\n                                clicked => { AppState.run_collect_test(); }\n                            }\n\n                            if AppState.collect_test_running : TouchButton {\n                                label: Translations.diagnostics_collect_test_stop_text;\n                                bg: CediniaColors.warning;\n                                fg: #000000;\n                                min_h: 44px;\n                                clicked => { AppState.stop_collect_test(); }\n                            }\n                        }\n                    }\n                    Divider {}\n\n                    CategoryLabel { label: Translations.cache_label_text; }\n\n                    // Cache sizes + refresh row\n                    Rectangle {\n                        height: 60px;\n                        background: CediniaColors.bg_surface;\n                        HorizontalLayout {\n                            padding-left: 16px;\n                            padding-right: 16px;\n                            padding-top: 10px;\n                            padding-bottom: 10px;\n                            spacing: 8px;\n\n                            VerticalLayout {\n                                alignment: LayoutAlignment.center;\n                                horizontal-stretch: 1.0;\n                                spacing: 2px;\n                                Text {\n                                    text: Translations.diagnostics_thumbnails_text + \": \" + AppState.diag_thumbnails_size;\n                                    color: CediniaColors.text_primary;\n                                    font-size: 14px;\n                                }\n                                Text {\n                                    text: Translations.diagnostics_app_cache_text + \": \" + AppState.diag_app_cache_size;\n                                    color: CediniaColors.text_secondary;\n                                    font-size: 12px;\n                                }\n                            }\n\n                            TouchButton {\n                                label: AppState.diag_refresh_running ? \"...\" : Translations.diagnostics_refresh_text;\n                                min_h: 36px;\n                                bg: CediniaColors.bg_elevated;\n                                fg: CediniaColors.text_secondary;\n                                enabled: !AppState.diag_refresh_running;\n                                clicked => { AppState.refresh_diag_cache_info(); }\n                            }\n                        }\n                    }\n                    Divider {}\n\n                    // Clear cache buttons row\n                    Rectangle {\n                        height: 56px;\n                        background: CediniaColors.bg_surface;\n                        HorizontalLayout {\n                            padding-left: 16px;\n                            padding-right: 16px;\n                            padding-top: 8px;\n                            padding-bottom: 8px;\n                            spacing: 8px;\n\n                            TouchButton {\n                                label: Translations.diagnostics_clear_thumbnails_text;\n                                horizontal-stretch: 1.0;\n                                min_h: 40px;\n                                bg: CediniaColors.danger.with-alpha(0.7);\n                                fg: #ffffff;\n                                clicked => { AppState.clear_thumbnails_cache(); }\n                            }\n\n                            TouchButton {\n                                label: Translations.diagnostics_clear_cache_text;\n                                horizontal-stretch: 1.0;\n                                min_h: 40px;\n                                bg: CediniaColors.danger.with-alpha(0.5);\n                                fg: #ffffff;\n                                clicked => { AppState.clear_app_cache(); }\n                            }\n                        }\n                    }\n                    Divider {}\n\n                    CategoryLabel { label: Translations.about_app_label_text; }\n\n                    // Logo – full width, nothing beside it\n                    Rectangle {\n                        height: 140px;\n                        background: CediniaColors.bg_surface;\n                        Image {\n                            source: @image-url(\"../icons/logo.svg\");\n                            width: min(parent.width - 32px, 260px);\n                            height: 120px;\n                            x: (parent.width - self.width) / 2;\n                            y: (parent.height - self.height) / 2;\n                            image-fit: ImageFit.contain;\n                        }\n                    }\n                    Divider {}\n\n                    // App name and description\n                    Rectangle {\n                        background: CediniaColors.bg_surface;\n                        height: info_vl.preferred-height + 24px;\n                        info_vl := VerticalLayout {\n                            padding-left: 16px;\n                            padding-right: 16px;\n                            padding-top: 12px;\n                            padding-bottom: 12px;\n                            spacing: 4px;\n                            alignment: LayoutAlignment.center;\n                            Text {\n                                text: \"Cedinia 11.0.1\";\n                                color: CediniaColors.text_primary;\n                                font-size: 18px;\n                                font-weight: 700;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                            }\n                            Text {\n                                text: Translations.app_subtitle_text;\n                                color: CediniaColors.text_secondary;\n                                font-size: 13px;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                            }\n                            Text {\n                                text: Translations.app_license_text;\n                                color: CediniaColors.text_disabled;\n                                font-size: 11px;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                            }\n                        }\n                    }\n                    Divider {}\n\n                    // Links row\n                    Rectangle {\n                        height: 56px;\n                        background: CediniaColors.bg_surface;\n                        HorizontalLayout {\n                            padding-left: 12px;\n                            padding-right: 12px;\n                            padding-top: 8px;\n                            padding-bottom: 8px;\n                            spacing: 8px;\n\n                            TouchButton {\n                                label: Translations.about_repo_text;\n                                horizontal-stretch: 1.0;\n                                min_h: 40px;\n                                bg: CediniaColors.accent_muted;\n                                fg: CediniaColors.accent_light;\n                                clicked => { AppState.open_url(\"https://github.com/qarmin/czkawka\"); }\n                            }\n\n                            TouchButton {\n                                label: Translations.about_translate_text;\n                                horizontal-stretch: 1.0;\n                                min_h: 40px;\n                                bg: CediniaColors.accent_muted;\n                                fg: CediniaColors.accent_light;\n                                clicked => { AppState.open_url(\"https://crowdin.com/project/czkawka\"); }\n                            }\n\n                            TouchButton {\n                                label: Translations.about_donate_text;\n                                horizontal-stretch: 1.0;\n                                min_h: 40px;\n                                bg: CediniaColors.accent_muted;\n                                fg: CediniaColors.accent_light;\n                                clicked => { AppState.open_url(\"https://github.com/sponsors/qarmin\"); }\n                            }\n                        }\n                    }\n                    Divider {}\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/similar_images_gallery.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { ScanState, SimilarGroupCard } from \"common.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { TouchButton } from \"components.slint\";\nimport { ListView } from \"std-widgets.slint\";\nimport { Translations } from \"translations.slint\";\n\ncomponent GalleryImageCell {\n    in property <image>  thumbnail;\n    in property <string> img_name;\n    in property <string> img_size;\n    in property <[string]> img_val_str: [];\n    in property <int>    flat_idx;\n    in property <bool>   checked: false;\n\n    width: 170px;\n\n    ta := TouchArea {\n\n        clicked => { AppState.toggle_file_checked(root.flat_idx); }\n    }\n\n    VerticalLayout {\n        padding: 4px;\n        spacing: 4px;\n\n\n        Rectangle {\n            height: 140px;\n            border-radius: 8px;\n            clip: true;\n\n            border-width: root.checked ? 3px : 0px;\n            border-color: CediniaColors.accent;\n            background: CediniaColors.bg_elevated;\n            animate border-width { duration: 100ms; }\n\n            Image {\n                width: parent.width;\n                height: parent.height;\n                source: root.thumbnail;\n                image-fit: ImageFit.cover;\n            }\n\n\n            Rectangle {\n                border-radius: 8px;\n                background: ta.pressed ? #ffffff22 : transparent;\n            }\n\n\n            if root.checked : Rectangle {\n                x: parent.width - 30px;\n                y: 6px;\n                width: 24px;\n                height: 24px;\n                border-radius: 12px;\n                background: CediniaColors.accent;\n                z: 5;\n                Text {\n                    text: Translations.ok_text;\n                    color: #000000;\n                    font-size: 11px;\n                    font-weight: 700;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                    vertical-alignment: TextVerticalAlignment.center;\n                }\n            }\n        }\n\n        Text {\n            text: root.img_name;\n            font-size: 11px;\n            color: root.checked ? CediniaColors.accent_light : CediniaColors.text_primary;\n            overflow: TextOverflow.elide;\n            horizontal-alignment: TextHorizontalAlignment.center;\n        }\n        Text {\n            text: root.img_size;\n            font-size: 10px;\n            color: CediniaColors.text_secondary;\n            horizontal-alignment: TextHorizontalAlignment.center;\n        }\n        if root.img_val_str.length > 0 && root.img_val_str[0] != \"\" : Text {\n            text: root.img_val_str[0];\n            font-size: 10px;\n            color: CediniaColors.text_secondary;\n            horizontal-alignment: TextHorizontalAlignment.center;\n            overflow: TextOverflow.elide;\n        }\n    }\n}\n\n\ncomponent GalleryToggleBar {\n    height: 52px;\n\n    in-out property <int> menu_state: 0;\n\n    Rectangle {\n        background: CediniaColors.bg_elevated;\n        border-width: 1px;\n        border-color: CediniaColors.divider;\n\n        HorizontalLayout {\n            padding-left: 12px;\n            padding-right: 12px;\n            padding-top: 8px;\n            padding-bottom: 8px;\n            spacing: 8px;\n\n\n            if AppState.selected_count > 0 : TouchButton {\n                label: Translations.gallery_delete_button_text;\n                min_h: 36px;\n                bg: CediniaColors.danger;\n                fg: #ffffff;\n                clicked => { AppState.request_gallery_delete(); }\n            }\n\n            if AppState.selected_count > 0 : Rectangle {\n                width: selected_chip.preferred-width + 16px;\n                height: 28px;\n                border-radius: 14px;\n                background: CediniaColors.accent_muted;\n                vertical-stretch: 0.0;\n                y: (parent.height - self.height) / 2;\n                selected_chip := Text {\n                    text: \"[\" + AppState.selected_count + \"]\";\n                    color: CediniaColors.accent_light;\n                    font-size: 11px;\n                    font-weight: 700;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                    vertical-alignment: TextVerticalAlignment.center;\n                }\n            }\n\n            Rectangle { horizontal-stretch: 1.0; }\n\n\n            TouchButton {\n                label: Translations.select_label_text;\n                min_h: 36px;\n                bg: CediniaColors.bg_surface;\n                fg: CediniaColors.text_secondary;\n                clicked => { root.menu_state = 1; }\n            }\n\n            TouchButton {\n                label: Translations.deselect_label_text;\n                min_h: 36px;\n                bg: CediniaColors.bg_surface;\n                fg: CediniaColors.text_secondary;\n                clicked => { root.menu_state = 2; }\n            }\n\n            TouchButton {\n                label: Translations.list_label_text;\n                min_h: 36px;\n                bg: CediniaColors.bg_surface;\n                fg: CediniaColors.text_secondary;\n                clicked => { AppState.similar_images_gallery_mode = false; }\n            }\n        }\n    }\n}\n\n\nexport component SimilarImagesGallery {\n    in property <[SimilarGroupCard]> groups: [];\n\n    VerticalLayout {\n        spacing: 0px;\n\n        toggle_bar := GalleryToggleBar {}\n\n        if groups.length == 0 : Rectangle {\n            vertical-stretch: 1.0;\n            background: CediniaColors.bg_primary;\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                spacing: 12px;\n\n                Text {\n                    text: AppState.scan_state == ScanState.Scanning\n                        ? Translations.scanning_fallback_text\n                        : Translations.no_results_text;\n                    color: CediniaColors.text_disabled;\n                    font-size: 15px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n            }\n        }\n\n\n        if groups.length > 0 : ListView {\n            vertical-stretch: 1.0;\n\n            for group in groups : VerticalLayout {\n                padding-left: 8px;\n                padding-right: 8px;\n                padding-top: 8px;\n                padding-bottom: 4px;\n                spacing: 6px;\n\n\n                Rectangle {\n                    height: 34px;\n                    border-radius: 8px;\n                    background: CediniaColors.accent.with-alpha(0.85);\n\n                    Text {\n                        x: 12px;\n                        y: 0px;\n                        height: parent.height;\n                        width: parent.width - 24px;\n                        text: group.label;\n                        color: #000000;\n                        font-size: 13px;\n                        font-weight: 700;\n                        vertical-alignment: TextVerticalAlignment.center;\n                        overflow: TextOverflow.elide;\n                    }\n                }\n\n\n                Flickable {\n                    height: 210px;\n                    interactive: true;\n                    viewport-width: max(root.width - 16px, img-row.min-width);\n\n                    img-row := HorizontalLayout {\n                        alignment: LayoutAlignment.start;\n                        spacing: 6px;\n\n                        for img in group.items : GalleryImageCell {\n                            thumbnail:  img.thumbnail;\n                            img_name:   img.name;\n                            img_size:   img.size;\n                            img_val_str: img.val_str;\n                            flat_idx:   img.flat_idx;\n                            checked:    img.checked;\n                        }\n                    }\n                }\n\n\n                Rectangle { height: 1px; background: CediniaColors.divider; }\n            }\n        }\n    }\n\n\n\n    if toggle_bar.menu_state == 1 : Rectangle {\n        x: 0; y: 0;\n        width: root.width;\n        height: root.height;\n        z: 50;\n        background: #00000099;\n        TouchArea {}\n\n        Rectangle {\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - popup_sel_vl.preferred-height - 24px) / 2;\n            width: min(parent.width - 48px, 300px);\n            height: popup_sel_vl.preferred-height + 24px;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000aa;\n            clip: true;\n\n            popup_sel_vl := VerticalLayout {\n                padding: 12px;\n                spacing: 6px;\n\n                Text {\n                    text: Translations.selection_popup_title_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    font-weight: 700;\n                    letter-spacing: 0.5px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                TouchButton {\n                    label: Translations.select_all_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_primary;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_except_one_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_one(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_except_largest_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_largest(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_except_smallest_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_smallest(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_largest_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_largest_per_group(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_smallest_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_smallest_per_group(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_except_highest_res_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_highest_resolution(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_except_lowest_res_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_all_except_lowest_resolution(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_highest_res_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_highest_resolution_per_group(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.select_lowest_res_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.select_lowest_resolution_per_group(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.invert_selection_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.invert_selection(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.close_text;\n                    min_h: 40px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_disabled;\n                    horizontal-stretch: 1.0;\n                    clicked => { toggle_bar.menu_state = 0; }\n                }\n            }\n        }\n    }\n\n\n    if toggle_bar.menu_state == 2 : Rectangle {\n        x: 0; y: 0;\n        width: root.width;\n        height: root.height;\n        z: 50;\n        background: #00000099;\n        TouchArea {}\n\n        Rectangle {\n            x: (parent.width - self.width) / 2;\n            y: (parent.height - popup_desel_vl.preferred-height - 24px) / 2;\n            width: min(parent.width - 48px, 300px);\n            height: popup_desel_vl.preferred-height + 24px;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000aa;\n            clip: true;\n\n            popup_desel_vl := VerticalLayout {\n                padding: 12px;\n                spacing: 6px;\n\n                Text {\n                    text: Translations.deselection_popup_title_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 12px;\n                    font-weight: 700;\n                    letter-spacing: 0.5px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n                TouchButton {\n                    label: Translations.deselect_all_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_primary;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.deselect_all(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.deselect_except_one_text;\n                    min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light;\n                    horizontal-stretch: 1.0;\n                    clicked => { AppState.deselect_all_except_one(); toggle_bar.menu_state = 0; }\n                }\n                TouchButton {\n                    label: Translations.close_text;\n                    min_h: 40px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_disabled;\n                    horizontal-stretch: 1.0;\n                    clicked => { toggle_bar.menu_state = 0; }\n                }\n            }\n        }\n    }\n\n\n    if AppState.gallery_delete_popup_visible : Rectangle {\n        x: 0;\n        y: 0;\n        width: root.width;\n        height: root.height;\n        z: 25;\n        background: #000000bb;\n\n\n        TouchArea {}\n\n\n        Rectangle {\n            x: parent.width / 2 - self.width / 2;\n            y: parent.height / 2 - self.height / 2;\n            width: min(parent.width - 32px, 380px);\n            height: card.preferred-height;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 28px;\n            drop-shadow-color: #000000cc;\n\n            card := VerticalLayout {\n                padding: 24px;\n                spacing: 14px;\n\n                Text {\n                    text: AppState.gallery_delete_message;\n                    color: CediniaColors.text_primary;\n                    font-size: 16px;\n                    font-weight: 600;\n                    wrap: TextWrap.word-wrap;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                if AppState.gallery_delete_warning != \"\" : Text {\n                    text: AppState.gallery_delete_warning;\n                    color: CediniaColors.warning;\n                    font-size: 13px;\n                    wrap: TextWrap.word-wrap;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                HorizontalLayout {\n                    spacing: 10px;\n\n                    TouchButton {\n                        label: Translations.gallery_back_text;\n                        horizontal-stretch: 1.0;\n                        min_h: 48px;\n                        bg: CediniaColors.bg_surface;\n                        fg: CediniaColors.text_secondary;\n                        clicked => { AppState.gallery_delete_popup_visible = false; }\n                    }\n\n                    TouchButton {\n                        label: Translations.gallery_confirm_delete_text;\n                        horizontal-stretch: 1.0;\n                        min_h: 48px;\n                        bg: CediniaColors.danger;\n                        fg: #ffffff;\n                        clicked => { AppState.confirm_gallery_delete(); }\n                    }\n                }\n            }\n        }\n    }\n\n\n    if AppState.delete_running : Rectangle {\n        x: 0;\n        y: 0;\n        width: root.width;\n        height: root.height;\n        z: 26;\n        background: #000000bb;\n\n        TouchArea {} \n        Rectangle {\n            x: parent.width / 2 - self.width / 2;\n            y: parent.height / 2 - self.height / 2;\n            width: min(parent.width - 32px, 340px);\n            height: prog-card.preferred-height;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000cc;\n\n            prog-card := VerticalLayout {\n                padding: 24px;\n                spacing: 14px;\n\n                Text {\n                    text: Translations.deleting_files_text;\n                    color: CediniaColors.text_primary;\n                    font-size: 16px;\n                    font-weight: 600;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                Text {\n                    text: AppState.delete_progress_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 13px;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                TouchButton {\n                    label: Translations.stop_text;\n                    min_h: 44px;\n                    bg: CediniaColors.warning;\n                    fg: #000000;\n                    clicked => { AppState.delete_stop_requested(); }\n                }\n            }\n        }\n    }\n\n\n    if AppState.delete_errors_visible : Rectangle {\n        x: 0;\n        y: 0;\n        width: root.width;\n        height: root.height;\n        z: 27;\n        background: #000000bb;\n\n        TouchArea {}\n\n        Rectangle {\n            x: parent.width / 2 - self.width / 2;\n            y: parent.height / 2 - self.height / 2;\n            width: min(parent.width - 32px, 400px);\n            height: err-card.preferred-height;\n            border-radius: 14px;\n            background: CediniaColors.bg_elevated;\n            drop-shadow-blur: 24px;\n            drop-shadow-color: #000000cc;\n\n            err-card := VerticalLayout {\n                padding: 20px;\n                spacing: 12px;\n\n                Text {\n                    text: Translations.delete_errors_title_text;\n                    color: CediniaColors.warning;\n                    font-size: 15px;\n                    font-weight: 600;\n                    wrap: TextWrap.word-wrap;\n                    horizontal-alignment: TextHorizontalAlignment.center;\n                }\n\n                Text {\n                    text: AppState.delete_errors_text;\n                    color: CediniaColors.text_secondary;\n                    font-size: 11px;\n                    wrap: TextWrap.word-wrap;\n                }\n\n                TouchButton {\n                    label: Translations.ok_text;\n                    min_h: 44px;\n                    bg: CediniaColors.bg_surface;\n                    fg: CediniaColors.text_secondary;\n                    clicked => { AppState.delete_errors_visible = false; }\n                }\n            }\n        }\n    }\n\n\n    Rectangle {\n        x: root.width - self.width - 16px;\n        y: root.height - self.height - 16px;\n        z: 20;\n        width: 56px;\n        height: 56px;\n        border-radius: 28px;\n        background: AppState.scan_state == ScanState.Scanning\n            ? CediniaColors.warning\n            : AppState.scan_state == ScanState.Stopping\n                ? CediniaColors.warning.with-alpha(0.5)\n                : CediniaColors.accent;\n        drop-shadow-blur: 12px;\n        drop-shadow-color: #00000088;\n        animate background { duration: 200ms; }\n\n        TouchArea {\n            enabled: AppState.scan_state != ScanState.Stopping;\n            clicked => {\n                if (AppState.scan_state == ScanState.Scanning) {\n                    AppState.stop_requested();\n                } else {\n                    AppState.scan_requested();\n                }\n            }\n        }\n\n        Text {\n            text: AppState.scan_state == ScanState.Scanning\n                || AppState.scan_state == ScanState.Stopping ? \"■\" : \"▶\";\n            font-size: 22px;\n            color: AppState.scan_state == ScanState.Stopping ? #00000066 : #000000;\n            horizontal-alignment: TextHorizontalAlignment.center;\n            vertical-alignment: TextVerticalAlignment.center;\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/top_bar.slint",
    "content": "import { CediniaColors } from \"colors.slint\";\nimport { AppState } from \"app_state.slint\";\nimport { ScanState } from \"common.slint\";\n\nexport component TopAppBar {\n    in property <string> title: \"Cedinia\";\n    height: 56px;\n\n    Rectangle {\n        background: CediniaColors.bg_elevated;\n        drop-shadow-color: #00000066;\n        drop-shadow-blur: 6px;\n        drop-shadow-offset-y: 2px;\n\n        HorizontalLayout {\n            padding-left: 16px;\n            padding-right: 12px;\n            padding-top: 8px;\n            padding-bottom: 8px;\n            spacing: 10px;\n            alignment: LayoutAlignment.start;\n\n\n            Image {\n                source: @image-url(\"../icons/logo.svg\");\n                width: 48px;\n                height: 48px;\n                horizontal-alignment: ImageHorizontalAlignment.center;\n                vertical-alignment: ImageVerticalAlignment.center;\n            }\n\n            VerticalLayout {\n                alignment: LayoutAlignment.center;\n                horizontal-stretch: 1.0;\n                spacing: 2px;\n                Text {\n                    text: root.title;\n                    color: CediniaColors.accent_light;\n                    font-size: 18px;\n                    font-weight: 700;\n                }\n                Text {\n                    text: AppState.status_message;\n                    color: CediniaColors.text_secondary;\n                    font-size: 11px;\n                    overflow: elide;\n                }\n            }\n\n\n            if AppState.scan_state == ScanState.Scanning : Rectangle {\n                width: 10px;\n                height: 10px;\n                border-radius: 5px;\n                background: CediniaColors.accent;\n                vertical-stretch: 0.0;\n                animate opacity {\n                    duration: 800ms;\n                    iteration-count: -1;\n                    easing: ease-in-out;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cedinia/ui/translations.slint",
    "content": "export global Translations {\n    // App / top bar titles\n    in-out property <string> app_name_text:                \"Cedinia\";\n    in-out property <string> tool_duplicate_files_text:    \"Duplicates\";\n    in-out property <string> tool_empty_folders_text:      \"Empty Folders\";\n    in-out property <string> tool_similar_images_text:     \"Similar Images\";\n    in-out property <string> tool_empty_files_text:        \"Empty Files\";\n    in-out property <string> tool_temporary_files_text:    \"Temporary Files\";\n    in-out property <string> tool_big_files_text:          \"Biggest Files\";\n    in-out property <string> tool_broken_files_text:       \"Broken Files\";\n    in-out property <string> tool_bad_extensions_text:     \"Bad Extensions\";\n    in-out property <string> tool_same_music_text:         \"Duplicate Music\";\n    in-out property <string> tool_bad_names_text:          \"Bad Names\";\n    in-out property <string> tool_exif_remover_text:       \"EXIF Data\";\n    in-out property <string> tool_directories_text:        \"Directories\";\n    in-out property <string> tool_settings_text:           \"Settings\";\n\n    // Home screen tool cards\n    in-out property <string> home_dup_description_text:        \"Find files with identical content\";\n    in-out property <string> home_empty_folders_description_text: \"Directories without content\";\n    in-out property <string> home_similar_images_description_text: \"Find visually similar photos\";\n    in-out property <string> home_empty_files_description_text: \"Files with zero size\";\n    in-out property <string> home_temp_files_description_text: \"Temporary and cached files\";\n    in-out property <string> home_big_files_description_text:  \"Biggest/Smallest files on disk\";\n    in-out property <string> home_broken_files_description_text: \"PDF, audio, images, archives\";\n    in-out property <string> home_bad_extensions_description_text: \"Files with incorrect extension\";\n    in-out property <string> home_same_music_description_text: \"Similar audio files by tags\";\n    in-out property <string> home_bad_names_description_text:  \"Files with problematic characters in name\";\n    in-out property <string> home_exif_description_text:       \"Images with EXIF metadata\";\n\n    // Results list\n    in-out property <string> scanning_text:        \"Scanning…\";\n    in-out property <string> stopping_text:        \"Stopping…\";\n    in-out property <string> no_results_text:      \"No results\";\n    in-out property <string> press_start_text:     \"Press START to scan\";\n    in-out property <string> select_label_text:    \"Sel.\";\n    in-out property <string> deselect_label_text:  \"Des.\";\n    in-out property <string> list_label_text:      \"List\";\n    in-out property <string> gallery_label_text:   \"Gal.\";\n\n    // Selection popup\n    in-out property <string> selection_popup_title_text:      \"Select\";\n    in-out property <string> select_all_text:                 \"Select all\";\n    in-out property <string> select_except_one_text:          \"Select except one\";\n    in-out property <string> select_except_largest_text:      \"Select except largest\";\n    in-out property <string> select_except_smallest_text:     \"Select except smallest\";\n    in-out property <string> select_largest_text:             \"Select largest\";\n    in-out property <string> select_smallest_text:            \"Select smallest\";\n    in-out property <string> select_except_highest_res_text:  \"Select except highest resolution\";\n    in-out property <string> select_except_lowest_res_text:   \"Select except lowest resolution\";\n    in-out property <string> select_highest_res_text:         \"Select highest resolution\";\n    in-out property <string> select_lowest_res_text:          \"Select lowest resolution\";\n    in-out property <string> invert_selection_text:           \"Invert selection\";\n    in-out property <string> close_text:                      \"Close\";\n\n    // Deselection popup\n    in-out property <string> deselection_popup_title_text:    \"Deselect\";\n    in-out property <string> deselect_all_text:               \"Deselect all\";\n    in-out property <string> deselect_except_one_text:        \"Deselect except one\";\n\n    // Confirm popup\n    in-out property <string> cancel_text:                     \"Cancel\";\n    in-out property <string> delete_text:                     \"Delete\";\n    in-out property <string> rename_text:                     \"Rename\";\n\n    // Delete errors popup\n    in-out property <string> delete_errors_title_text:        \"Failed to delete some files:\";\n    in-out property <string> ok_text:                         \"OK\";\n\n    // Stopping overlay\n    in-out property <string> stopping_overlay_title_text:     \"■ Stopping\";\n    in-out property <string> stopping_overlay_body_text:      \"Finishing current scan…\\nPlease wait.\";\n\n    // Permission popup\n    in-out property <string> permission_title_text:           \"🔒 File Access\";\n    in-out property <string> permission_body_text:            \"To scan files, the app needs access to device storage. Without this permission, scanning will not be possible.\";\n    in-out property <string> grant_text:                      \"Grant\";\n    in-out property <string> no_permission_scan_warning_text: \"No file permission – grant access to scan\";\n\n    // Settings screen\n    in-out property <string> settings_tab_general_text:       \"General\";\n    in-out property <string> settings_tab_tools_text:         \"Tools\";\n    in-out property <string> settings_tab_diagnostics_text:   \"Info\";\n\n    in-out property <string> settings_use_cache_text:         \"Use cache\";\n    in-out property <string> settings_use_cache_desc_text:    \"Speeds up subsequent scans (hash/images)\";\n    in-out property <string> settings_ignore_hidden_text:     \"Ignore hidden files\";\n    in-out property <string> settings_ignore_hidden_desc_text: \"Files and folders starting with '.'\";\n    in-out property <string> settings_scan_label_text:        \"SCANNING\";\n    in-out property <string> settings_filters_label_text:     \"FILTERS (all tools)\";\n    in-out property <string> settings_min_file_size_text:     \"Min. file size\";\n    in-out property <string> settings_excluded_items_text:    \"EXCLUDED ITEMS (glob patterns, comma-separated)\";\n    in-out property <string> settings_excluded_items_placeholder_text: \"e.g. *.tmp, */.git/*, */node_modules/*\";\n    in-out property <string> settings_allowed_extensions_text: \"ALLOWED EXTENSIONS (empty = all)\";\n    in-out property <string> settings_allowed_extensions_placeholder_text: \"e.g. jpg, png, mp4\";\n    in-out property <string> settings_excluded_extensions_text: \"EXCLUDED EXTENSIONS\";\n    in-out property <string> settings_excluded_extensions_placeholder_text: \"e.g. bak, tmp, log\";\n\n    // Settings — Tools section labels\n    in-out property <string> settings_duplicates_header_text: \"DUPLICATES\";\n    in-out property <string> settings_check_method_label_text: \"COMPARISON METHOD\";\n    in-out property <string> settings_check_method_text:      \"Method\";\n    in-out property <string> settings_hash_type_label_text:   \"HASH TYPE\";\n    in-out property <string> settings_hash_type_text:         \"Hash type\";\n    in-out property <string> settings_similar_images_header_text: \"SIMILAR IMAGES\";\n    in-out property <string> settings_similarity_preset_text: \"Similarity threshold\";\n    in-out property <string> settings_hash_size_text:         \"Hash size\";\n    in-out property <string> settings_hash_alg_text:          \"Hash algorithm\";\n    in-out property <string> settings_image_filter_text:      \"Resize filter\";\n    in-out property <string> settings_ignore_same_size_text:  \"Ignore images with the same dimensions\";\n    in-out property <string> settings_big_files_header_text:  \"BIGGEST FILES\";\n    in-out property <string> settings_search_mode_text:       \"Search mode\";\n    in-out property <string> settings_file_count_text:        \"File count\";\n    in-out property <string> settings_same_music_header_text: \"DUPLICATE MUSIC\";\n    in-out property <string> settings_music_check_method_text: \"Comparison mode\";\n    in-out property <string> settings_music_compare_tags_label_text: \"COMPARED TAGS\";\n    in-out property <string> settings_music_title_text:       \"Title\";\n    in-out property <string> settings_music_artist_text:      \"Artist\";\n    in-out property <string> settings_music_year_text:        \"Year\";\n    in-out property <string> settings_music_length_text:      \"Length\";\n    in-out property <string> settings_music_genre_text:       \"Genre\";\n    in-out property <string> settings_music_bitrate_text:     \"Bitrate\";\n    in-out property <string> settings_music_approx_text:      \"Approximate tag comparison\";\n    in-out property <string> settings_broken_files_header_text: \"BROKEN FILES\";\n    in-out property <string> settings_broken_files_types_label_text: \"CHECKED TYPES\";\n    in-out property <string> settings_broken_audio_text:      \"Audio\";\n    in-out property <string> settings_broken_pdf_text:        \"PDF\";\n    in-out property <string> settings_broken_archive_text:    \"Archive\";\n    in-out property <string> settings_broken_image_text:      \"Image\";\n    in-out property <string> settings_bad_names_header_text:  \"BAD NAMES\";\n    in-out property <string> settings_bad_names_checks_label_text: \"CHECKS\";\n    in-out property <string> settings_bad_names_uppercase_ext_text: \"Uppercase extension\";\n    in-out property <string> settings_bad_names_emoji_text:   \"Emoji in name\";\n    in-out property <string> settings_bad_names_space_text:   \"Spaces at start/end\";\n    in-out property <string> settings_bad_names_non_ascii_text: \"Non-ASCII characters\";\n    in-out property <string> settings_bad_names_duplicated_text: \"Duplicated characters\";\n\n    // Settings — General: new filter strings\n    in-out property <string> settings_max_file_size_text:     \"Max. file size\";\n    in-out property <string> settings_language_text:          \"Language\";\n    in-out property <string> settings_language_restart_text:  \"Requires app restart\";\n    in-out property <string> settings_common_label_text:      \"COMMON SETTINGS\";\n\n    // Settings — Tools descriptions\n    in-out property <string> settings_hash_type_desc_text:    \"Blake3 – fastest; CRC32/xxH3 – alternatives\";\n    in-out property <string> settings_similarity_desc_text:   \"Very High = only near-identical\";\n    in-out property <string> settings_hash_size_desc_text:    \"Larger = more accurate, slower\";\n\n    // Settings — Info/Diagnostics tab\n    in-out property <string> diagnostics_header_text:         \"DIAGNOSTICS\";\n    in-out property <string> diagnostics_thumbnails_text:     \"Thumbnails\";\n    in-out property <string> diagnostics_app_cache_text:      \"App cache\";\n    in-out property <string> diagnostics_refresh_text:        \"Refresh\";\n    in-out property <string> diagnostics_clear_thumbnails_text: \"Clear thumbnails\";\n    in-out property <string> diagnostics_clear_cache_text:    \"Clear cache\";\n    in-out property <string> diagnostics_collect_test_text:   \"Scan test\";\n    in-out property <string> diagnostics_collect_test_desc_text: \"Scans each volume recursively\";\n    in-out property <string> diagnostics_collect_test_run_text: \"Run\";\n    in-out property <string> diagnostics_collect_test_stop_text: \"Stop\";\n\n    // About section links\n    in-out property <string> about_repo_text:                 \"Repository\";\n    in-out property <string> about_donate_text:               \"Donate\";\n    in-out property <string> about_translate_text:            \"Translations\";\n\n    // Diagnostics collect-test popup\n    in-out property <string> collect_test_title_text:         \"📊 Test results\";\n    in-out property <string> collect_test_volumes_text:       \"💾 Volumes:\";\n    in-out property <string> collect_test_folders_text:       \"📁 Folders:\";\n    in-out property <string> collect_test_files_text:         \"📄 Files:\";\n    in-out property <string> collect_test_time_text:          \"⏱ Time:\";\n    in-out property <string> collect_test_ms_text:            \" ms\";\n\n    // Directories screen\n    in-out property <string> directories_include_header_text: \"Directories to scan\";\n    in-out property <string> directories_exclude_header_text: \"Excluded directories\";\n    in-out property <string> directories_add_text:            \"+ Add\";\n    in-out property <string> directories_volume_header_text:  \"Volumes\";\n    in-out property <string> directories_volume_refresh_text: \"Refresh\";\n    in-out property <string> directories_volume_add_text:     \"Add\";\n    in-out property <string> no_paths_text:                   \"No paths – add below\";\n\n    // Gallery / delete popups\n    in-out property <string> gallery_delete_button_text:      \"Delete\";\n    in-out property <string> gallery_back_text:               \"Back\";\n    in-out property <string> gallery_confirm_delete_text:     \"Yes, delete\";\n    in-out property <string> deleting_files_text:             \"Deleting files…\";\n    in-out property <string> stop_text:                       \"Stop\";\n    in-out property <string> files_suffix_text:               \"files\";\n    in-out property <string> scanning_fallback_text:          \"Scanning…\";\n\n    // About section in diagnostics tab\n    in-out property <string> about_app_label_text:            \"ABOUT\";\n    in-out property <string> cache_label_text:                \"CACHE\";\n    in-out property <string> app_subtitle_text:               \"In honour of the Battle of Cedynia (972 CE)\";\n    in-out property <string> app_license_text:                \"Frontend for Czkawka Core  •  GPL-3.0\";\n\n    // Bottom nav\n    in-out property <string> nav_home_text:       \"Home\";\n    in-out property <string> nav_dirs_text:       \"Directories\";\n    in-out property <string> nav_settings_text:   \"Settings\";\n\n    // Status messages set from Rust\n    in-out property <string> status_ready_text:                \"Ready\";\n    in-out property <string> status_stopped_text:              \"Stopped\";\n    in-out property <string> status_no_results_text:           \"No results\";\n    in-out property <string> status_deleted_selected_text:     \"Deleted selected\";\n    in-out property <string> status_deleted_with_errors_text:  \"Deleted with errors\";\n    in-out property <string> scan_not_started_text:            \"Scan not started\";\n    in-out property <string> found_items_prefix_text:          \"Found\";\n    in-out property <string> found_items_suffix_text:          \"items\";\n    in-out property <string> deleted_items_prefix_text:        \"Deleted\";\n    in-out property <string> deleted_items_suffix_text:        \"items\";\n    in-out property <string> deleted_errors_suffix_text:       \"errors\";\n    in-out property <string> renamed_prefix_text:              \"Renamed\";\n    in-out property <string> renamed_files_suffix_text:        \"files\";\n    in-out property <string> renamed_errors_suffix_text:       \"errors\";\n    in-out property <string> cleaned_exif_prefix_text:         \"Cleaned EXIF from\";\n    in-out property <string> cleaned_exif_suffix_text:         \"files\";\n    in-out property <string> cleaned_exif_errors_suffix_text:  \"errors\";\n\n    // Delete errors popup (more items)\n    in-out property <string> and_more_prefix_text:             \"…and\";\n    in-out property <string> and_more_suffix_text:             \"more\";\n}\n"
  },
  {
    "path": "ci_tester/Cargo.toml",
    "content": "[package]\nname = \"ci_tester\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n\n[profile.release]\ndebug-assertions = true\noverflow-checks = true\ndebug = true\n\n[dependencies]\nstate = \"0.6.0\"\nhandsome_logger = \"0.9.1\"\nlog = \"0.4.20\""
  },
  {
    "path": "ci_tester/src/main.rs",
    "content": "use std::collections::BTreeSet;\nuse std::fs;\nuse std::process::{Command, Stdio};\nuse std::env;\nuse log::info;\nuse std::path::Path;\nuse std::process::Output;\n\n#[derive(Default, Clone, Debug)]\nstruct CollectedFiles {\n    files: BTreeSet<String>,\n    folders: BTreeSet<String>,\n    symlinks: BTreeSet<String>,\n}\n\nstatic CZKAWKA_PATH: state::InitCell<String> = state::InitCell::new();\nstatic COLLECTED_FILES: state::InitCell<CollectedFiles> = state::InitCell::new();\n\nconst ATTEMPTS: u32 = 10;\nconst PRINT_MESSAGES_TO_TERMINAL_INSTEAD_OUTPUT: bool = true;\n\npub(crate) fn collect_output(output: &Output) -> String {\n    let stdout = &output.stdout;\n    let stderr = &output.stderr;\n    let stdout_str = String::from_utf8_lossy(stdout);\n    let stderr_str = String::from_utf8_lossy(stderr);\n    format!(\"{stdout_str}\\n{stderr_str}\")\n}\n\nfn test_args() {\n    let modes = [\"dup\", \"big\", \"empty-folders\", \"empty-files\", \"temp\", \"image\", \"symlinks\", \"broken\", \"ext\", \"video\", \"music\"];\n    for mode in modes {\n        println!(\"Testing mode {}\", mode);\n        let _ = fs::remove_dir_all(\"RandomDirWithoutContent\");\n        fs::create_dir_all(\"RandomDirWithoutContent\").expect(\"Should not fail in tests\");\n        run_with_good_status(&[CZKAWKA_PATH.get().as_str(), mode, \"-d\", \"RandomDirWithoutContent\", \"-W\"], true);\n    }\n}\n\n// App runs - ./ci_tester PATH_TO_CZKAWKA\nfn main() {\n    handsome_logger::init().expect(\"Should not fail in tests\");\n    let args: Vec<String> = std::env::args().collect();\n    let path_to_czkawka = args[1].clone();\n    CZKAWKA_PATH.set(path_to_czkawka);\n\n    test_args();\n    remove_test_dir();\n    run_with_good_status(&[\"ls\"], false);\n    unzip_files();\n\n    let all_files = collect_all_files_and_dirs(\"TestFiles\").expect(\"Should not fail in tests\");\n    COLLECTED_FILES.set(all_files);\n    remove_test_dir();\n\n    println!(\"Starting checking\");\n\n    for _ in 0..ATTEMPTS {\n        test_empty_files();\n        test_big_files();\n        test_smallest_files();\n        test_biggest_files();\n        test_empty_folders();\n        test_temporary_files();\n        test_symlinks_files();\n        test_remove_duplicates_one_oldest();\n        test_remove_duplicates_one_newest();\n        test_remove_duplicates_all_expect_newest();\n        test_remove_duplicates_all_expect_oldest();\n        test_remove_duplicates_one_smallest();\n        test_remove_duplicates_one_biggest();\n        test_remove_duplicates_all_expect_biggest();\n        test_remove_duplicates_all_expect_smallest();\n        test_remove_same_music_tags_one_oldest();\n        test_remove_same_music_tags_one_newest();\n        test_remove_same_music_tags_all_expect_oldest();\n        test_remove_same_music_tags_all_expect_newest();\n        test_remove_same_music_tags_one_smallest();\n        test_remove_same_music_tags_one_biggest();\n        test_remove_same_music_tags_all_expect_biggest();\n        test_remove_same_music_tags_all_expect_smallest();\n        test_remove_same_music_content_one_oldest();\n        test_remove_same_music_content_all_expect_oldest();\n        test_remove_same_music_content_one_newest();\n        test_remove_same_music_content_all_expect_newest();\n        test_remove_same_music_content_one_smallest();\n        test_remove_same_music_content_one_biggest();\n        test_remove_same_music_content_all_expect_biggest();\n        test_remove_same_music_content_all_expect_smallest();\n        test_remove_videos_one_oldest();\n        test_remove_videos_one_newest();\n        test_remove_videos_all_expect_oldest();\n        test_remove_videos_all_expect_newest();\n        test_remove_videos_one_smallest();\n        test_remove_videos_one_biggest();\n        test_remove_videos_all_expect_biggest();\n        test_remove_videos_all_expect_smallest();\n    }\n\n    println!(\"Completed checking\");\n}\nfn test_remove_videos_one_oldest() {\n    info!(\"test_remove_videos_one_oldest\");\n    run_test(&[\"video\", \"-d\", \"TestFiles\", \"-D\", \"OO\", \"-W\"], vec![\"Videos/V3.webm\"], Vec::new(), Vec::new());\n}\nfn test_remove_videos_one_newest() {\n    info!(\"test_remove_videos_one_newest\");\n    run_test(&[\"video\", \"-d\", \"TestFiles\", \"-D\", \"ON\", \"-W\"], vec![\"Videos/V5.mp4\"], Vec::new(), Vec::new());\n}\nfn test_remove_videos_all_expect_oldest() {\n    info!(\"test_remove_videos_all_expect_oldest\");\n    run_test(\n        &[\"video\", \"-d\", \"TestFiles\", \"-D\", \"AEO\", \"-W\"],\n        vec![\"Videos/V1.mp4\", \"Videos/V2.mp4\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_videos_all_expect_newest() {\n    info!(\"test_remove_videos_all_expect_newest\");\n    run_test(\n        &[\"video\", \"-d\", \"TestFiles\", \"-D\", \"AEN\", \"-W\"],\n        vec![\"Videos/V1.mp4\", \"Videos/V2.mp4\", \"Videos/V3.webm\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_videos_one_smallest() {\n    info!(\"test_remove_videos_one_smallest\");\n    run_test(&[\"video\", \"-d\", \"TestFiles\", \"-D\", \"OS\", \"-W\"], vec![\"Videos/V2.mp4\"], Vec::new(), Vec::new());\n}\nfn test_remove_videos_one_biggest() {\n    info!(\"test_remove_videos_one_biggest\");\n    run_test(&[\"video\", \"-d\", \"TestFiles\", \"-D\", \"OB\", \"-W\"], vec![\"Videos/V3.webm\"], Vec::new(), Vec::new());\n}\nfn test_remove_videos_all_expect_smallest() {\n    info!(\"test_remove_videos_all_expect_smallest\");\n    run_test(\n        &[\"video\", \"-d\", \"TestFiles\", \"-D\", \"AES\", \"-W\"],\n        vec![\"Videos/V1.mp4\", \"Videos/V3.webm\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_videos_all_expect_biggest() {\n    info!(\"test_remove_videos_all_expect_biggest\");\n    run_test(\n        &[\"video\", \"-d\", \"TestFiles\", \"-D\", \"AEB\", \"-W\"],\n        vec![\"Videos/V1.mp4\", \"Videos/V2.mp4\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_same_music_content_one_newest() {\n    info!(\"test_remove_same_music_content_one_newest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"ON\", \"-W\"],\n        vec![\"Music/M2.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_content_all_expect_newest() {\n    info!(\"test_remove_same_music_content_all_expect_newest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"AEN\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M3.flac\", \"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_same_music_content_all_expect_oldest() {\n    info!(\"test_remove_same_music_content_all_expect_oldest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"AEO\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M2.mp3\", \"Music/M3.flac\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_same_music_content_one_oldest() {\n    info!(\"test_remove_same_music_content_one_oldest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"OO\", \"-W\"],\n        vec![\"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_content_one_biggest() {\n    info!(\"test_remove_same_music_content_one_biggest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"OB\", \"-W\"],\n        vec![\"Music/M3.flac\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_content_all_expect_biggest() {\n    info!(\"test_remove_same_music_content_all_expect_biggest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"AEB\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M2.mp3\", \"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_same_music_content_all_expect_smallest() {\n    info!(\"test_remove_same_music_content_all_expect_smallest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"AES\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M3.flac\", \"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_same_music_content_one_smallest() {\n    info!(\"test_remove_same_music_content_one_smallest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-s\", \"CONTENT\", \"-l\", \"2.0\", \"-D\", \"OS\", \"-W\"],\n        vec![\"Music/M2.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_tags_one_oldest() {\n    info!(\"test_remove_same_music_one_oldest\");\n    run_test(&[\"music\", \"-d\", \"TestFiles\", \"-D\", \"OO\", \"-W\"], vec![\"Music/M5.mp3\"], Vec::new(), Vec::new());\n}\nfn test_remove_same_music_tags_one_newest() {\n    info!(\"test_remove_same_music_one_newest\");\n    run_test(&[\"music\", \"-d\", \"TestFiles\", \"-D\", \"ON\", \"-W\"], vec![\"Music/M2.mp3\"], Vec::new(), Vec::new());\n}\nfn test_remove_same_music_tags_all_expect_oldest() {\n    info!(\"test_remove_same_music_all_expect_oldest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-D\", \"AEO\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M2.mp3\", \"Music/M3.flac\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_tags_all_expect_newest() {\n    info!(\"test_remove_same_music_all_expect_newest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-D\", \"AEN\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M3.flac\", \"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_tags_one_smallest() {\n    info!(\"test_remove_same_music_one_smallest\");\n    run_test(&[\"music\", \"-d\", \"TestFiles\", \"-D\", \"OS\", \"-W\"], vec![\"Music/M1.mp3\"], Vec::new(), Vec::new());\n}\nfn test_remove_same_music_tags_one_biggest() {\n    info!(\"test_remove_same_music_one_biggest\");\n    run_test(&[\"music\", \"-d\", \"TestFiles\", \"-D\", \"OB\", \"-W\"], vec![\"Music/M3.flac\"], Vec::new(), Vec::new());\n}\nfn test_remove_same_music_tags_all_expect_smallest() {\n    info!(\"test_remove_same_music_all_expect_smallest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-D\", \"AES\", \"-W\"],\n        vec![\"Music/M2.mp3\", \"Music/M3.flac\", \"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_same_music_tags_all_expect_biggest() {\n    info!(\"test_remove_same_music_all_expect_biggest\");\n    run_test(\n        &[\"music\", \"-d\", \"TestFiles\", \"-D\", \"AEB\", \"-W\"],\n        vec![\"Music/M1.mp3\", \"Music/M2.mp3\", \"Music/M5.mp3\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_duplicates_all_expect_oldest() {\n    info!(\"test_remove_duplicates_all_expect_oldest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"AEO\", \"-W\"],\n        vec![\"Images/A1.jpg\", \"Images/A5.jpg\", \"Music/M1.mp3\", \"Music/M2.mp3\", \"Videos/V1.mp4\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_duplicates_all_expect_newest() {\n    info!(\"test_remove_duplicates_all_expect_newest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"AEN\", \"-W\"],\n        vec![\"Images/A2.jpg\", \"Images/A5.jpg\", \"Music/M1.mp3\", \"Music/M5.mp3\", \"Videos/V1.mp4\", \"Videos/V2.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_duplicates_one_newest() {\n    info!(\"test_remove_duplicates_one_newest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"ON\", \"-W\"],\n        vec![\"Images/A1.jpg\", \"Music/M2.mp3\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_duplicates_one_oldest() {\n    info!(\"test_remove_duplicates_one_oldest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"OO\", \"-W\"],\n        vec![\"Images/A2.jpg\", \"Music/M5.mp3\", \"Videos/V2.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_duplicates_all_expect_smallest() {\n    info!(\"test_remove_duplicates_all_expect_smallest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"AES\", \"-W\"],\n        vec![\"Images/A2.jpg\", \"Images/A5.jpg\", \"Music/M2.mp3\", \"Music/M5.mp3\", \"Videos/V2.mp4\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_duplicates_all_expect_biggest() {\n    info!(\"test_remove_duplicates_all_expect_biggest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"AEN\", \"-W\"],\n        vec![\"Images/A2.jpg\", \"Images/A5.jpg\", \"Music/M1.mp3\", \"Music/M5.mp3\", \"Videos/V1.mp4\", \"Videos/V2.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_remove_duplicates_one_biggest() {\n    info!(\"test_remove_duplicates_one_biggest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"ON\", \"-W\"],\n        vec![\"Images/A1.jpg\", \"Music/M2.mp3\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\nfn test_remove_duplicates_one_smallest() {\n    info!(\"test_remove_duplicates_one_smallest\");\n    run_test(\n        &[\"dup\", \"-d\", \"TestFiles\", \"-D\", \"OS\", \"-W\"],\n        vec![\"Images/A1.jpg\", \"Music/M1.mp3\", \"Videos/V1.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_symlinks_files() {\n    info!(\"test_symlinks_files\");\n    run_test(&[\"symlinks\", \"-d\", \"TestFiles\", \"-D\", \"-W\"], Vec::new(), Vec::new(), vec![\"Symlinks/EmptyFiles\"]);\n}\nfn test_temporary_files() {\n    info!(\"test_temporary_files\");\n    run_test(&[\"temp\", \"-d\", \"TestFiles\", \"-D\", \"-W\"], vec![\"Temporary/Boczze.cache\"], Vec::new(), Vec::new());\n}\nfn test_empty_folders() {\n    info!(\"test_empty_folders\");\n    run_test(\n        &[\"empty-folders\", \"-d\", \"TestFiles\", \"-D\", \"-W\"],\n        Vec::new(),\n        vec![\"EmptyFolders/One\", \"EmptyFolders/Two\", \"EmptyFolders/Two/TwoInside\"],\n        Vec::new(),\n    );\n}\n\nfn test_biggest_files() {\n    info!(\"test_biggest_files\");\n    run_test(\n        &[\"big\", \"-d\", \"TestFiles\", \"-n\", \"6\", \"-D\", \"-W\"],\n        vec![\"Music/M3.flac\", \"Music/M4.mp3\", \"Videos/V2.mp4\", \"Videos/V3.webm\", \"Videos/V1.mp4\", \"Videos/V5.mp4\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_smallest_files() {\n    info!(\"test_smallest_files\");\n    run_test(\n        &[\"big\", \"-d\", \"TestFiles\", \"-J\", \"-n\", \"5\", \"-D\", \"-W\"],\n        vec![\"Broken/Br.jpg\", \"Broken/Br.mp3\", \"Broken/Br.pdf\", \"Broken/Br.zip\", \"EmptyFolders/ThreeButNot/KEKEKE\"],\n        Vec::new(),\n        Vec::new(),\n    );\n}\n\nfn test_empty_files() {\n    info!(\"test_empty_files\");\n    run_test(&[\"empty-files\", \"-d\", \"TestFiles\", \"-D\", \"-W\"], vec![\"EmptyFile\"], Vec::new(), Vec::new());\n}\n\nfn test_big_files() {\n    info!(\"test_big_files\");\n    run_test(&[\"big\", \"-d\", \"TestFiles\", \"-n\", \"2\", \"-D\", \"-W\"], vec![\"Music/M4.mp3\", \"Videos/V3.webm\"], Vec::new(), Vec::new());\n}\n\n////////////////////////////////////\n////////////////////////////////////\n/////////HELPER FUNCTIONS///////////\n////////////////////////////////////\n////////////////////////////////////\n\nfn run_test(arguments: &[&str], expected_files_differences: Vec<&'static str>, expected_folders_differences: Vec<&'static str>, expected_symlinks_differences: Vec<&'static str>) {\n    println!(\"=====================================================\");\n    unzip_files();\n    assert!(Path::new(\"TestFiles\").exists());\n    // Add path_to_czkawka to arguments\n    let mut all_arguments = Vec::new();\n    all_arguments.push(CZKAWKA_PATH.get().as_str());\n    all_arguments.extend_from_slice(arguments);\n    run_with_good_status(&all_arguments, PRINT_MESSAGES_TO_TERMINAL_INSTEAD_OUTPUT);\n    file_folder_diffs(\n        COLLECTED_FILES.get(),\n        expected_files_differences,\n        expected_folders_differences,\n        expected_symlinks_differences,\n    );\n\n    remove_test_dir();\n}\nfn unzip_files() {\n    run_with_good_status(&[\"unzip\", \"-qq\", \"-X\", \"TestFiles.zip\", \"-d\", \"TestFiles\"], false);\n}\nfn remove_test_dir() {\n    let _ = fs::remove_dir_all(\"TestFiles\");\n}\n\nfn run_with_good_status(str_command: &[&str], print_messages: bool) {\n    let mut command = Command::new(str_command[0]);\n    let mut com = command.args(&str_command[1..]);\n    com.env(\"ENABLE_TERMINAL_LOGS_IN_CLI\", \"1\");\n    com.env(\"RUST_BACKTRACE\", \"1\");\n\n    if !print_messages {\n        com = com.stderr(Stdio::piped()).stdout(Stdio::piped());\n    }\n    let output = com.spawn().unwrap().wait_with_output().unwrap();\n    let all_output = collect_output(&output);\n    let command_copyable = str_command.join(\" \");\n    assert!(output.status.success(), \"Command \\\"{command_copyable}\\\" failed with status: {:?}, from folder {:?}\\n\\n and output: {all_output}\",env::current_dir() ,output.status.code());\n}\n\nfn file_folder_diffs(\n    all_files: &CollectedFiles,\n    mut expected_files_differences: Vec<&'static str>,\n    mut expected_folders_differences: Vec<&'static str>,\n    mut expected_symlinks_differences: Vec<&'static str>,\n) {\n    let current_files = collect_all_files_and_dirs(\"TestFiles\").expect(\"Should not fail in tests\");\n    let mut diff_files = all_files\n        .files\n        .difference(&current_files.files)\n        .map(|e| e.strip_prefix(\"TestFiles/\").expect(\"Should not fail in tests\").to_string())\n        .collect::<Vec<_>>();\n    let mut diff_folders = all_files\n        .folders\n        .difference(&current_files.folders)\n        .map(|e| e.strip_prefix(\"TestFiles/\").expect(\"Should not fail in tests\").to_string())\n        .collect::<Vec<_>>();\n    let mut diff_symlinks = all_files\n        .symlinks\n        .difference(&current_files.symlinks)\n        .map(|e| e.strip_prefix(\"TestFiles/\").expect(\"Should not fail in tests\").to_string())\n        .collect::<Vec<_>>();\n\n    expected_symlinks_differences.sort();\n    expected_folders_differences.sort();\n    expected_files_differences.sort();\n\n    diff_files.sort();\n    diff_folders.sort();\n    diff_symlinks.sort();\n\n    assert_eq!(diff_files, expected_files_differences);\n    assert_eq!(diff_folders, expected_folders_differences);\n    assert_eq!(diff_symlinks, expected_symlinks_differences);\n}\n\nfn collect_all_files_and_dirs(dir: &str) -> std::io::Result<CollectedFiles> {\n    let mut files = BTreeSet::new();\n    let mut folders = BTreeSet::new();\n    let mut symlinks = BTreeSet::new();\n\n    let mut folders_to_check = vec![dir.to_string()];\n    while let Some(folder) = folders_to_check.pop() {\n        let rd = fs::read_dir(folder)?;\n        for entry in rd {\n            let entry = entry?;\n            let file_type = entry.file_type()?;\n            let path_str = entry.path().to_string_lossy().to_string();\n\n            if file_type.is_dir() {\n                folders.insert(path_str.clone());\n                folders_to_check.push(path_str);\n            } else if file_type.is_symlink() {\n                symlinks.insert(path_str);\n            } else if file_type.is_file() {\n                files.insert(path_str);\n            } else {\n                panic!(\"Unknown type of file {path_str}\");\n            }\n        }\n    }\n\n    // for dir in &folders_to_check {\n    //     println!(\"Folder \\\"{}\\\"\", dir)\n    // }\n    // for symlink in &symlinks {\n    //     println!(\"Symlink \\\"{}\\\"\", symlink)\n    // }\n    // for file in &files {\n    //     let metadata = fs::metadata(file)?;\n    //     println!(\"File \\\"{}\\\" with size {} bytes\", file, metadata.len());\n    // }\n\n    folders.remove(dir);\n    // println!(\"Found {} files, {} folders and {} symlinks\", files.len(), folders.len(), symlinks.len());\n    Ok(CollectedFiles { files, folders, symlinks })\n}\n"
  },
  {
    "path": "clippy.toml",
    "content": "allow-indexing-slicing-in-tests = true\nallow-unwrap-in-tests = true\navoid-breaking-exported-api = false"
  },
  {
    "path": "czkawka_cli/Cargo.toml",
    "content": "[package]\nname = \"czkawka_cli\"\nversion = \"11.0.1\"\nauthors = [\"Rafał Mikrut <mikrutrafal@protonmail.com>\"]\nedition = \"2024\"\nrust-version = \"1.92.0\"\ndescription = \"CLI frontend of Czkawka\"\nlicense = \"MIT\"\nhomepage = \"https://github.com/qarmin/czkawka\"\nrepository = \"https://github.com/qarmin/czkawka\"\n\n[dependencies]\nclap = { version = \"4.5\", features = [\"derive\", \"color\"] }\n\nlog = \"0.4.22\"\nczkawka_core = { path = \"../czkawka_core\", version = \"11.0.1\", features = [] }\nindicatif = \"0.18\"\ncrossbeam-channel = { version = \"0.5\", features = [] }\nctrlc = { version = \"3.4\", features = [\"termination\"] }\nhumansize = \"2.1\"\n\n[features]\ndefault = []\nheif = [\"czkawka_core/heif\"]\nlibraw = [\"czkawka_core/libraw\"]\nlibavif = [\"czkawka_core/libavif\"]\n# Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails\n# No-op on other OSes, it is slower and provides less helpful error messages\nxdg_portal_trash = [\"czkawka_core/xdg_portal_trash\"]\n\nno_colors = []\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "czkawka_cli/LICENSE_MIT",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "czkawka_cli/README.md",
    "content": "# Czkawka CLI\n\nCLI frontend that allows you to use Czkawka from the terminal.\n\n## Requirements\n\nPrecompiled binaries should work without any additional dependencies on Linux (Ubuntu 22.04+), Windows (10+), and macOS (10.15+).\n\nOn Linux, it is even possible (with eyra) to avoid libc entirely and use a fully static Rust binary, but alternatively you can use musl for this task.\n\nIf you want to use the similar videos tool, you need to install ffmpeg (runtime dependency).  \nIf you want to use heif/libraw/libavif (build/runtime dependency), you need to install the required packages.\n\n- macOS: `brew install ffmpeg libraw libheif libavif dav1d` – [ffmpeg formula](https://formulae.brew.sh/formula/ffmpeg)\n- Linux: `sudo apt install ffmpeg libraw-dev libheif-dev libavif-dev libdav1d-dev`\n- Windows: `choco install ffmpeg` – or, if not working, download from [ffmpeg.org](https://ffmpeg.org/download.html#build-windows) and\n  unpack to the location with `czkawka_cli.exe`. Heif and libraw are not supported on Windows.\n\n## Compilation\n\nTo compile, you need to have Rust installed via [rustup](https://rustup.rs/). Then, build with:\n\n```shell\ncargo run --release --bin czkawka_cli\n```\n\nYou can enable additional features with:\n\n```shell\ncargo run --release --bin czkawka_cli --features \"heif,libraw,libavif\"\n```\n\n## How to use\n\nThe application includes concise help for each tool, which you can display by running:\n```\nczkawka_cli --help\n```\nYou can also get detailed information about the parameters of a specific tool by running, for example:\n```\nczkawka_cli dup --help\n```\n\n\nExample usage:\n```shell\nczkawka dup -d /home/rafal -e /home/rafal/Obrazy  -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo\nczkawka empty-folders -d /home/rafal/rr /home/gateway -f results.txt\nczkawka big -d /home/rafal/ /home/piszczal -e /home/rafal/Roman -n 25 -x VIDEO -f results.txt\nczkawka empty-files -d /home/rafal /home/szczekacz -e /home/rafal/Pulpit -R -f results.txt\nczkawka temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D\nczkawka music -d /home/rafal -e /home/rafal/Pulpit -z \"artist,year, ARTISTALBUM, ALBUM___tiTlE\"  -f results.txt\nczkawka symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt\n```\n\n## LICENSE\n\nMIT"
  },
  {
    "path": "czkawka_cli/src/commands.rs",
    "content": "use std::path::PathBuf;\n\n#[cfg(not(feature = \"no_colors\"))]\nuse clap::builder::Styles;\n#[cfg(not(feature = \"no_colors\"))]\nuse clap::builder::styling::AnsiColor;\nuse czkawka_core::CZKAWKA_VERSION;\nuse czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::common::tool_data::DeleteMethod;\nuse czkawka_core::re_exported::{Cropdetect, FilterType, HashAlg};\nuse czkawka_core::tools::broken_files::CheckedTypes;\nuse czkawka_core::tools::same_music::MusicSimilarity;\nuse czkawka_core::tools::similar_videos::{ALLOWED_SKIP_FORWARD_AMOUNT, ALLOWED_VID_HASH_DURATION, DEFAULT_SKIP_FORWARD_AMOUNT, crop_detect_from_str_opt};\nuse czkawka_core::tools::video_optimizer::VideoCodec;\n\n#[cfg(not(feature = \"no_colors\"))]\npub const CLAP_STYLING: Styles = Styles::styled()\n    .header(AnsiColor::Green.on_default().bold())\n    .usage(AnsiColor::Green.on_default().bold())\n    .literal(AnsiColor::Cyan.on_default().bold())\n    .placeholder(AnsiColor::Cyan.on_default().bold())\n    .error(AnsiColor::Red.on_default().bold())\n    .valid(AnsiColor::Green.on_default().bold())\n    .invalid(AnsiColor::Yellow.on_default().bold());\n\n#[derive(clap::Parser)]\n#[clap(\n    name = \"czkawka\",\n    help_template = HELP_TEMPLATE,\n    version = CZKAWKA_VERSION,\n)]\n#[cfg_attr(not(feature = \"no_colors\"), clap(styles = CLAP_STYLING))]\npub struct Args {\n    #[command(subcommand)]\n    pub command: Commands,\n}\n\n#[derive(Debug, clap::Subcommand)]\npub enum Commands {\n    #[clap(\n        name = \"dup\",\n        about = \"Finds duplicate files\",\n        after_help = \"EXAMPLE:\\n    czkawka dup -d /home/rafal -e /home/rafal/Obrazy  -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo\"\n    )]\n    Duplicates(DuplicatesArgs),\n    #[clap(\n        name = \"empty-folders\",\n        about = \"Finds empty folders\",\n        after_help = \"EXAMPLE:\\n    czkawka empty-folders -d /home/rafal/rr /home/gateway -f results.txt\"\n    )]\n    EmptyFolders(EmptyFoldersArgs),\n    #[clap(\n        name = \"big\",\n        about = \"Finds big files\",\n        after_help = \"EXAMPLE:\\n    czkawka big -d /home/rafal/ /home/piszczal -e /home/rafal/Roman -n 25 -J -x VIDEO -f results.txt\"\n    )]\n    BiggestFiles(BiggestFilesArgs),\n    #[clap(\n        name = \"empty-files\",\n        about = \"Finds empty files\",\n        after_help = \"EXAMPLE:\\n    czkawka empty-files -d /home/rafal /home/szczekacz -e /home/rafal/Pulpit -R -f results.txt\"\n    )]\n    EmptyFiles(EmptyFilesArgs),\n    #[clap(\n        name = \"temp\",\n        about = \"Finds temporary files\",\n        after_help = \"EXAMPLE:\\n    czkawka temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D\"\n    )]\n    Temporary(TemporaryArgs),\n    #[clap(\n        name = \"image\",\n        about = \"Finds similar images\",\n        after_help = \"EXAMPLE:\\n    czkawka image -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt\"\n    )]\n    SimilarImages(SimilarImagesArgs),\n    #[clap(name = \"music\", about = \"Finds same music by tags\", after_help = \"EXAMPLE:\\n    czkawka music -d /home/rafal -f results.txt\")]\n    SameMusic(SameMusicArgs),\n    #[clap(\n        name = \"symlinks\",\n        about = \"Finds invalid symlinks\",\n        after_help = \"EXAMPLE:\\n    czkawka symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt\"\n    )]\n    InvalidSymlinks(InvalidSymlinksArgs),\n    #[clap(\n        name = \"broken\",\n        about = \"Finds broken files\",\n        after_help = \"EXAMPLE:\\n    czkawka broken -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt\"\n    )]\n    BrokenFiles(BrokenFilesArgs),\n    #[clap(name = \"video\", about = \"Finds similar video files\", after_help = \"EXAMPLE:\\n    czkawka video -d /home/rafal -f results.txt\")]\n    SimilarVideos(SimilarVideosArgs),\n    #[clap(\n        name = \"ext\",\n        about = \"Finds files with invalid extensions\",\n        after_help = \"EXAMPLE:\\n    czkawka ext -d /home/czokolada/ -f results.txt\"\n    )]\n    BadExtensions(BadExtensionsArgs),\n    #[clap(\n        name = \"bad-names\",\n        about = \"Finds files with bad names\",\n        after_help = \"EXAMPLE:\\n    czkawka bad-names -d /home/rafal -f results.txt\"\n    )]\n    BadNames(BadNamesArgs),\n    #[clap(\n        name = \"video-optimizer\",\n        about = \"Optimizes video files (transcode or crop)\",\n        after_help = \"EXAMPLE:\\n    czkawka video-optimizer -d /home/rafal -f results.txt\"\n    )]\n    VideoOptimizer(VideoOptimizerArgs),\n    #[clap(\n        name = \"exif-remover\",\n        about = \"Finds and removes EXIF tags from images\",\n        after_help = \"EXAMPLE:\\n    czkawka exif-remover -d /home/rafal -f results.txt\"\n    )]\n    ExifRemover(ExifRemoverArgs),\n}\n\n#[derive(Debug, clap::Args)]\npub struct DuplicatesArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub reference_directories: ReferenceDirectories,\n    #[clap(\n        short = 'Z',\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"257144\",\n        help = \"Minimum prehash cache file size in bytes\",\n        long_help = \"Minimum size of prehash cached files in bytes\"\n    )]\n    pub minimal_prehash_cache_file_size: u64,\n    #[clap(\n        short = 'u',\n        long,\n        help = \"Use prehash cache\",\n        long_help = \"Use prehash cache to speed up the scanning process by avoiding rehashing files that have already been hashed\"\n    )]\n    pub use_prehash_cache: bool,\n    #[clap(\n        short,\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"8192\",\n        help = \"Minimum size in bytes\",\n        long_help = \"Minimum size of checked files in bytes, assigning bigger value may speed up searching\"\n    )]\n    pub minimal_file_size: u64,\n    #[clap(\n        short = 'i',\n        long,\n        value_parser = parse_maximal_file_size,\n        default_value = \"18446744073709551615\",\n        help = \"Maximum size in bytes\",\n        long_help = \"Maximum size of checked files in bytes, assigning lower value may speed up searching\"\n    )]\n    pub maximal_file_size: u64,\n    #[clap(\n        short = 'c',\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"257144\",\n        help = \"Minimum cached file size in bytes\",\n        long_help = \"Minimum size of cached files in bytes, assigning bigger value may speed up the scan but loading the cache will be slower, assigning smaller value may slow down the scan and some files may need to be hashed again but loading the cache will be faster\"\n    )]\n    pub minimal_cached_file_size: u64,\n    #[clap(\n        short,\n        long,\n        default_value = \"HASH\",\n        value_parser = parse_checking_method_duplicate,\n        help = \"Search method (NAME, SIZE, HASH)\",\n        long_help = \"Methods to search files.\\nNAME - Fast but rarely usable,\\nSIZE - Fast but not accurate, checking by the file's size,\\nHASH - The slowest method, checking by the hash of the entire file\"\n    )]\n    pub search_method: CheckingMethod,\n    #[clap(flatten)]\n    pub delete_method: DMethod,\n    #[clap(\n        short = 't',\n        long,\n        default_value = \"BLAKE3\",\n        value_parser = parse_hash_type,\n        help = \"Hash type (BLAKE3, CRC32, XXH3)\",\n        long_help = \"Hash algorithm used to calculate file hashes. BLAKE3 is recommended for most cases (fast and secure), CRC32 is faster but less reliable, XXH3 is very fast but not cryptographically secure.\"\n    )]\n    pub hash_type: HashType,\n    #[clap(flatten)]\n    pub case_sensitive_name_comparison: CaseSensitiveNameComparison,\n    #[clap(flatten)]\n    pub allow_hard_links: AllowHardLinks,\n}\n\n#[derive(Debug, clap::Args)]\npub struct EmptyFoldersArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n}\n\n#[derive(Debug, clap::Args)]\npub struct BiggestFilesArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(\n        short,\n        long,\n        default_value = \"50\",\n        help = \"Number of files to be shown\",\n        long_help = \"Number of biggest (or smallest with -J flag) files to display in results\"\n    )]\n    pub number_of_files: usize,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n    #[clap(\n        short = 'J',\n        long,\n        help = \"Finds the smallest files instead the biggest\",\n        long_help = \"Switch mode to find smallest files instead of biggest ones\"\n    )]\n    pub smallest_mode: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct EmptyFilesArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n}\n\n#[derive(Debug, clap::Args)]\npub struct TemporaryArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n}\n\n#[derive(Debug, clap::Args)]\npub struct SimilarImagesArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub reference_directories: ReferenceDirectories,\n    #[clap(\n        short,\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"16384\",\n        help = \"Minimum size in bytes\",\n        long_help = \"Minimum size of checked files in bytes, assigning bigger value may speed up searching\"\n    )]\n    pub minimal_file_size: u64,\n    #[clap(\n        short = 'i',\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"18446744073709551615\",\n        help = \"Maximum size in bytes\",\n        long_help = \"Maximum size of checked files in bytes, assigning lower value may speed up searching\"\n    )]\n    pub maximal_file_size: u64,\n    #[clap(\n        short = 's',\n        long,\n        default_value = \"5\",\n        value_parser = clap::value_parser!(u32).range(0..=40),\n        help = \"Maximum difference between images (0-40)\",\n        long_help = \"Maximum difference between images to be considered as similar (0-40). Lower values mean more strict matching. For hash_size 8, values up to 10 are recommended, for hash_size 16 up to 20 are recommended.\"\n    )]\n    pub max_difference: u32,\n    #[clap(flatten)]\n    pub delete_method: DMethod,\n    #[clap(flatten)]\n    pub allow_hard_links: AllowHardLinks,\n    #[clap(flatten)]\n    pub ignore_same_size: IgnoreSameSize,\n    #[clap(\n        short = 'g',\n        long,\n        default_value = \"Gradient\",\n        value_parser = parse_similar_hash_algorithm,\n        help = \"Hash algorithm (Mean, Gradient, Blockhash, VertGradient, DoubleGradient, Median)\",\n        long_help = \"Perceptual hash algorithm used to compare images. Gradient (default) works well for most cases, Mean is faster but less accurate, Blockhash is good for finding very similar images, VertGradient/DoubleGradient provide different matching characteristics, Median is robust against color changes.\"\n    )]\n    pub hash_alg: HashAlg,\n    #[clap(\n        short = 'z',\n        long,\n        default_value = \"Nearest\",\n        value_parser = parse_similar_image_filter,\n        help = \"Image resize filter (Lanczos3, Nearest, Triangle, Gaussian, CatmullRom)\",\n        long_help = \"Filter algorithm used when resizing images for comparison. Lanczos3 provides highest quality but is slower, Nearest is fastest but lowest quality, Triangle/Gaussian/CatmullRom offer different quality-speed tradeoffs.\"\n    )]\n    pub image_filter: FilterType,\n    #[clap(\n        short = 'c',\n        long,\n        default_value = \"16\",\n        value_parser = parse_image_hash_size,\n        help = \"Hash size (8, 16, 32, 64)\",\n        long_help = \"Size of the perceptual hash. Larger values provide more detailed comparison but require higher max_difference values. 8 is fastest and least detailed, 64 is slowest but most detailed. Recommended: 8 or 16 for typical use.\"\n    )]\n    pub hash_size: u8,\n}\n\n#[derive(Debug, clap::Args)]\npub struct SameMusicArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub reference_directories: ReferenceDirectories,\n    #[clap(flatten)]\n    pub delete_method: DMethod,\n    #[clap(\n        short,\n        long,\n        help = \"Approximate comparison of music tags\",\n        long_help = \"Use approximate comparison when comparing music tags (allows small differences in tag values)\"\n    )]\n    pub approximate_comparison: bool,\n    #[clap(\n        short,\n        long,\n        help = \"Compare fingerprints only with similar titles\",\n        long_help = \"When using audio content comparison, only compare files that have similar titles to reduce false positives and speed up the process\"\n    )]\n    pub compare_fingerprints_only_with_similar_titles: bool,\n    #[clap(\n        short = 'z',\n        long,\n        default_value = \"track_title,track_artist\",\n        value_parser = parse_music_duplicate_type,\n        help = \"Search method (track_title,track_artist,year,bitrate,genre,length)\",\n        long_help = \"Sets which rows must be equal to set these files as duplicates (may be mixed, but must be divided by commas).\"\n    )]\n    pub music_similarity: MusicSimilarity,\n    #[clap(\n        short,\n        long,\n        default_value = \"TAGS\",\n        value_parser = parse_checking_method_same_music,\n        help = \"Search method (CONTENT, TAGS)\",\n        long_help = \"Methods to search files.\\nCONTENT - finds similar audio files by content, TAGS - finds similar music by tags.\"\n    )]\n    pub search_method: CheckingMethod,\n    #[clap(\n        short,\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"8192\",\n        help = \"Minimum size in bytes\",\n        long_help = \"Minimum size of checked files in bytes, assigning bigger value may speed up searching\"\n    )]\n    pub minimal_file_size: u64,\n    #[clap(\n        short = 'i',\n        long,\n        value_parser = parse_maximal_file_size,\n        default_value = \"18446744073709551615\",\n        help = \"Maximum size in bytes\",\n        long_help = \"Maximum size of checked files in bytes, assigning lower value may speed up searching\"\n    )]\n    pub maximal_file_size: u64,\n    #[clap(\n        short = 'l',\n        long,\n        value_parser = parse_minimum_segment_duration,\n        default_value = \"10.0\",\n        help = \"Minimum segment duration in seconds\",\n        long_help = \"Minimum duration of audio segment to compare in seconds. Smaller values will find shorter similar segments but may increase false positives. Values should be between 0.0 and 3600.0\"\n    )]\n    pub minimum_segment_duration: f32,\n    #[clap(\n        short = 'Y',\n        long,\n        value_parser = parse_maximum_difference,\n        default_value = \"2.0\",\n        help = \"Maximum difference between audio segments\",\n        long_help = \"Maximum allowed difference between audio segments (0.0-10.0). Value 0.0 will find only identical segments, while 10.0 will find segments that are barely similar. Lower values mean stricter matching.\"\n    )]\n    pub maximum_difference: f64,\n}\n\nfn parse_maximum_difference(src: &str) -> Result<f64, String> {\n    match src.parse::<f64>() {\n        Ok(maximum_difference) => {\n            if maximum_difference <= 0.0 {\n                Err(\"Maximum difference must be bigger than 0\".to_string())\n            } else if maximum_difference >= 10.0 {\n                Err(\"Maximum difference must be smaller than 10.0\".to_string())\n            } else {\n                Ok(maximum_difference)\n            }\n        }\n        Err(e) => Err(e.to_string()),\n    }\n}\nfn parse_minimum_segment_duration(src: &str) -> Result<f32, String> {\n    match src.parse::<f32>() {\n        Ok(minimum_segment_duration) => {\n            if minimum_segment_duration <= 0.0 {\n                Err(\"Minimum segment duration must be bigger than 0\".to_string())\n            } else if minimum_segment_duration >= 3600.0 {\n                Err(\"Minimum segment duration must be smaller than 3600(greater values not have much sense)\".to_string())\n            } else {\n                Ok(minimum_segment_duration)\n            }\n        }\n        Err(e) => Err(e.to_string()),\n    }\n}\n\n#[derive(Debug, clap::Args)]\npub struct InvalidSymlinksArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n}\n\n#[derive(Debug, clap::Args)]\npub struct BrokenFilesArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n    #[clap(\n        short,\n        long,\n        default_value = \"PDF\",\n        value_parser = parse_broken_files,\n        help = \"Checking file types (PDF, AUDIO, IMAGE, ARCHIVE, VIDEO)\",\n        long_help = \"Methods to search files - default PDF.\\nPDF - finds broken PDF files,\\nAUDIO - finds broken audio files,\\nIMAGE - finds broken image files,\\nARCHIVE - finds broken archive files,\\nVIDEO - finds broken video files\"\n    )]\n    pub checked_types: Vec<CheckedTypes>,\n}\n\n#[derive(Debug, clap::Args)]\npub struct SimilarVideosArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub reference_directories: ReferenceDirectories,\n    #[clap(flatten)]\n    pub delete_method: DMethod,\n    #[clap(flatten)]\n    pub allow_hard_links: AllowHardLinks,\n    #[clap(flatten)]\n    pub ignore_same_size: IgnoreSameSize,\n    #[clap(\n        short,\n        long,\n        value_parser = parse_minimal_file_size,\n        default_value = \"8192\",\n        help = \"Minimum size in bytes\",\n        long_help = \"Minimum size of checked files in bytes, assigning bigger value may speed up searching\"\n    )]\n    pub minimal_file_size: u64,\n    #[clap(\n        short = 'i',\n        long,\n        value_parser = parse_maximal_file_size,\n        default_value = \"18446744073709551615\",\n        help = \"Maximum size in bytes\",\n        long_help = \"Maximum size of checked files in bytes, assigning lower value may speed up searching\"\n    )]\n    pub maximal_file_size: u64,\n    #[clap(\n        short = 't',\n        long,\n        value_parser = parse_tolerance,\n        default_value = \"10\",\n        help = \"Video maximum difference (allowed values <0,20>)\",\n        long_help = \"Maximum difference between video frames, bigger value means that videos can looks more and more different (allowed values <0,20>)\"\n    )]\n    pub tolerance: i32,\n    #[clap(\n        short = 'U',\n        long,\n        default_value_t = DEFAULT_SKIP_FORWARD_AMOUNT,\n        value_parser = parse_skip_forward_amount,\n        help = \"Skip forward amount in seconds (allowed values: 0-300, default: 15)\",\n        long_help = \"Amount of seconds to skip forward in video. Allowed values are from 0 to 300. 0 means that no skipping will be done. Default is 15.\"\n    )]\n    pub skip_forward_amount: u32,\n    #[clap(\n        short = 'B',\n        long,\n        default_value = \"letterbox\",\n        value_parser = parse_crop_detect,\n        help = \"Crop detect method (none, letterbox, motion)\",\n        long_help = \"Method to detect and crop black bars from video frames before comparison. 'none' disables cropping, 'letterbox' removes static black bars, 'motion' uses motion detection to find content area.\"\n    )]\n    pub crop_detect: Cropdetect,\n    #[clap(\n        short = 'A',\n        long,\n        default_value = \"10\",\n        value_parser = parse_scan_duration,\n        help = \"Scan duration in seconds\",\n        long_help = \"Duration of video scanning in seconds. Longer duration provides more accurate results but takes more time. Allowed values are predefined in the application.\"\n    )]\n    pub scan_duration: u32,\n}\n\n#[derive(Debug, clap::Args)]\npub struct BadExtensionsArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(\n        short = 'F',\n        long,\n        help = \"Fix bad extensions\",\n        long_help = \"Automatically rename files to use proper extensions based on their detected file type\"\n    )]\n    pub fix_extensions: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct BadNamesArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(flatten)]\n    pub delete_method: SDMethod,\n    #[clap(\n        short = 'u',\n        long,\n        help = \"Check for uppercase extensions\",\n        long_help = \"Detects files with uppercase extensions (e.g., .JPG instead of .jpg)\"\n    )]\n    pub uppercase_extension: bool,\n    #[clap(short = 'j', long, help = \"Check for emoji in filenames\", long_help = \"Detects files with emoji characters in their names\")]\n    pub emoji_used: bool,\n    #[clap(\n        short = 'w',\n        long,\n        help = \"Check for spaces at start or end\",\n        long_help = \"Detects files with spaces at the beginning or end of their names\"\n    )]\n    pub space_at_start_or_end: bool,\n    #[clap(\n        short = 'n',\n        long,\n        help = \"Check for non-ASCII characters\",\n        long_help = \"Detects files with non-ASCII graphical characters in their names\"\n    )]\n    pub non_ascii_graphical: bool,\n    #[clap(\n        short = 'r',\n        long,\n        help = \"Restricted charset (comma-separated)\",\n        long_help = \"List of allowed special characters. Any other characters will be flagged as problematic. Example: '_- .' for underscore, dash, space, and dot\"\n    )]\n    pub restricted_charset: Option<String>,\n    #[clap(\n        short = 'a',\n        long,\n        help = \"Check for duplicated non-alphanumeric characters\",\n        long_help = \"Detects files with duplicated non-alphanumeric characters (e.g., 'file__name' or 'file..txt')\"\n    )]\n    pub remove_duplicated_non_alphanumeric: bool,\n    #[clap(\n        short = 'F',\n        long,\n        help = \"Fix bad names automatically\",\n        long_help = \"Automatically rename files to fix detected naming issues\"\n    )]\n    pub fix_names: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct VideoOptimizerArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(subcommand)]\n    pub mode: VideoOptimizerMode,\n}\n\n#[derive(Debug, clap::Subcommand)]\npub enum VideoOptimizerMode {\n    #[clap(name = \"transcode\", about = \"Transcode videos to different codec\")]\n    Transcode(TranscodeArgs),\n    #[clap(name = \"crop\", about = \"Crop black bars from videos\")]\n    Crop(CropArgs),\n}\n\n#[derive(Debug, clap::Args)]\npub struct TranscodeArgs {\n    #[clap(\n        short = 'c',\n        long,\n        help = \"Excluded video codecs (comma-separated)\",\n        long_help = \"Comma-separated list of video codecs to exclude from transcoding (e.g., 'h265,av1,vp9')\"\n    )]\n    pub excluded_codecs: Option<String>,\n    #[clap(short = 't', long, help = \"Generate thumbnails\", long_help = \"Generate video thumbnails for preview\")]\n    pub generate_thumbnails: bool,\n    #[clap(\n        short = 'V',\n        long,\n        default_value = \"10\",\n        value_parser = clap::value_parser!(u8).range(1..=99),\n        help = \"Thumbnail position percentage (1-99)\",\n        long_help = \"Percentage from start of video where thumbnail should be taken (1-99%)\"\n    )]\n    pub thumbnail_percentage: u8,\n    #[clap(short = 'g', long, help = \"Generate thumbnail grid\", long_help = \"Generate a grid of thumbnails instead of single thumbnail\")]\n    pub thumbnail_grid: bool,\n    #[clap(\n        short = 'Z',\n        long,\n        default_value = \"3\",\n        value_parser = clap::value_parser!(u8).range(2..=6),\n        help = \"Thumbnail grid tiles per side (2-6)\",\n        long_help = \"Number of tiles per side for thumbnail grid (2-6). Only used if -g is enabled.\"\n    )]\n    pub thumbnail_grid_tiles_per_side: u8,\n    #[clap(short = 'F', long, help = \"Fix/optimize videos\", long_help = \"Actually perform the transcoding on found videos\")]\n    pub fix_videos: bool,\n    #[clap(\n        long,\n        default_value = \"h265\",\n        value_parser = parse_video_codec,\n        help = \"Target codec (h264, h265, av1, vp9)\",\n        long_help = \"Target video codec for transcoding (h264, h265, av1, vp9). Only used with -F flag.\"\n    )]\n    pub target_codec: VideoCodec,\n    #[clap(\n        long,\n        default_value = \"23\",\n        value_parser = clap::value_parser!(u32).range(0..=51),\n        help = \"Encoding quality (0-51)\",\n        long_help = \"Video encoding quality (0-51). Lower values mean better quality. 23 is default for h264/h265, 30 for av1/vp9.\"\n    )]\n    pub quality: u32,\n    #[clap(long, help = \"Fail if result not smaller\", long_help = \"Fail the optimization if resulting file is not smaller than original\")]\n    pub fail_if_not_smaller: bool,\n    #[clap(long, help = \"Overwrite original files\", long_help = \"Overwrite original video files with optimized versions\")]\n    pub overwrite_original: bool,\n    #[clap(long, help = \"Limit video size\", long_help = \"Limit maximum video dimensions\")]\n    pub limit_video_size: bool,\n    #[clap(\n        long,\n        default_value = \"1920\",\n        value_parser = clap::value_parser!(u32),\n        help = \"Maximum video width\",\n        long_help = \"Maximum video width in pixels when limit_video_size is enabled\"\n    )]\n    pub max_width: u32,\n    #[clap(\n        long,\n        default_value = \"1080\",\n        value_parser = clap::value_parser!(u32),\n        help = \"Maximum video height\",\n        long_help = \"Maximum video height in pixels when limit_video_size is enabled\"\n    )]\n    pub max_height: u32,\n}\n\n#[derive(Debug, clap::Args)]\npub struct CropArgs {\n    #[clap(\n        short = 'm',\n        long,\n        default_value = \"blackbars\",\n        value_parser = parse_crop_mechanism,\n        help = \"Crop detection mechanism (blackbars, staticcontent)\",\n        long_help = \"Mechanism for detecting areas to crop: 'blackbars' for removing black bars, 'staticcontent' for detecting static content areas\"\n    )]\n    pub crop_mechanism: String,\n    #[clap(\n        short = 'k',\n        long,\n        default_value = \"32\",\n        value_parser = clap::value_parser!(u8).range(0..=128),\n        help = \"Black pixel threshold (0-128)\",\n        long_help = \"Threshold for considering a pixel as black when detecting black bars (0-128). Lower values are stricter.\"\n    )]\n    pub black_pixel_threshold: u8,\n    #[clap(\n        short = 'b',\n        long,\n        default_value = \"90\",\n        value_parser = clap::value_parser!(u8).range(50..=100),\n        help = \"Black bar minimum percentage (50-100)\",\n        long_help = \"Minimum percentage of black pixels in a line to consider it a black bar (50-100%)\"\n    )]\n    pub black_bar_percentage: u8,\n    #[clap(\n        short = 's',\n        long,\n        default_value = \"20\",\n        value_parser = parse_max_samples,\n        help = \"Maximum samples (5-1000)\",\n        long_help = \"Maximum number of video frames to sample when detecting black bars (5-1000)\"\n    )]\n    pub max_samples: usize,\n    #[clap(\n        short = 'z',\n        long,\n        default_value = \"10\",\n        value_parser = parse_min_crop_size,\n        help = \"Minimum crop size (1-1000)\",\n        long_help = \"Minimum size in pixels for crop area to be considered (1-1000)\"\n    )]\n    pub min_crop_size: u32,\n    #[clap(short = 't', long, help = \"Generate thumbnails\", long_help = \"Generate video thumbnails for preview\")]\n    pub generate_thumbnails: bool,\n    #[clap(\n        short = 'V',\n        long,\n        default_value = \"10\",\n        value_parser = clap::value_parser!(u8).range(1..=99),\n        help = \"Thumbnail position percentage (1-99)\",\n        long_help = \"Percentage from start of video where thumbnail should be taken (1-99%)\"\n    )]\n    pub thumbnail_percentage: u8,\n    #[clap(short = 'g', long, help = \"Generate thumbnail grid\", long_help = \"Generate a grid of thumbnails instead of single thumbnail\")]\n    pub thumbnail_grid: bool,\n    #[clap(\n        short = 'Z',\n        long,\n        default_value = \"3\",\n        value_parser = clap::value_parser!(u8).range(2..=6),\n        help = \"Thumbnail grid tiles per side (2-6)\",\n        long_help = \"Number of tiles per side for thumbnail grid (2-6). Only used if -g is enabled.\"\n    )]\n    pub thumbnail_grid_tiles_per_side: u8,\n    #[clap(short = 'F', long, help = \"Fix/crop videos\", long_help = \"Actually perform the cropping on found videos\")]\n    pub fix_videos: bool,\n    #[clap(long, help = \"Overwrite original files\", long_help = \"Overwrite original video files with cropped versions\")]\n    pub overwrite_original: bool,\n    #[clap(\n        long,\n        value_parser = parse_video_codec,\n        help = \"Target codec (h264, h265, av1, vp9)\",\n        long_help = \"Optional: Also transcode to different codec while cropping. Only used with -F flag.\"\n    )]\n    pub target_codec: Option<VideoCodec>,\n    #[clap(\n        long,\n        value_parser = clap::value_parser!(u32).range(0..=51),\n        help = \"Encoding quality (0-51)\",\n        long_help = \"Video encoding quality when transcoding (0-51). Only used when target_codec is specified.\"\n    )]\n    pub quality: Option<u32>,\n}\n\n#[derive(Debug, clap::Args)]\npub struct ExifRemoverArgs {\n    #[clap(flatten)]\n    pub common_cli_items: CommonCliItems,\n    #[clap(\n        short = 'i',\n        long,\n        help = \"Ignored EXIF tags (comma-separated)\",\n        long_help = \"Comma-separated list of EXIF tag names to ignore (not remove). Example: 'Orientation,DateTime,Software'\"\n    )]\n    pub ignored_tags: Option<String>,\n    #[clap(short = 'F', long, help = \"Remove EXIF tags\", long_help = \"Actually remove EXIF tags from files\")]\n    pub fix_exif: bool,\n    #[clap(\n        short = 'o',\n        long,\n        help = \"Override original files\",\n        long_help = \"Override original files instead of creating backup files with '_cleaned' suffix\"\n    )]\n    pub override_file: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct CommonCliItems {\n    #[clap(\n        short = 'T',\n        long,\n        default_value = \"0\",\n        help = \"Number of threads to use (0 = all available)\",\n        long_help = \"Limits the number of threads used for scanning. Value 0 (default) will use all available CPU threads. Lower values can reduce CPU usage.\"\n    )]\n    pub thread_number: usize,\n    #[clap(\n        short,\n        long,\n        required = true,\n        help = \"Directory(ies) to search\",\n        long_help = \"List of directory(ies) to search (absolute paths). These directories will be scanned but not set as reference folders.\"\n    )]\n    pub directories: Vec<PathBuf>,\n    #[clap(\n        short,\n        long,\n        help = \"Excluded directory(ies)\",\n        long_help = \"List of directory(ies) to exclude from search (absolute paths). Files in these directories will be completely ignored.\"\n    )]\n    pub excluded_directories: Vec<PathBuf>,\n    #[clap(\n        short = 'E',\n        long,\n        help = \"Excluded item(s)\",\n        long_help = \"List of excluded items using wildcards (e.g., */temp*, *.tmp). May be slower than -e, so use -e for directories when possible.\"\n    )]\n    pub excluded_items: Vec<String>,\n    #[clap(\n        short = 'x',\n        long,\n        help = \"Allowed file extension(s)\",\n        long_help = \"List of file extensions to check. Helpful macros are available: IMAGE (jpg,kra,gif,png,bmp,tiff,hdr,svg), TEXT (txt,doc,docx,odt,rtf), VIDEO (mp4,flv,mkv,webm,vob,ogv,gifv,avi,mov,wmv,mpg,m4v,m4p,mpeg,3gp,m2ts), MUSIC (mp3,flac,ogg,tta,wma,webm)\"\n    )]\n    pub allowed_extensions: Vec<String>,\n    #[clap(short = 'P', long, help = \"Excluded file extension(s)\", long_help = \"List of file extensions to exclude from search.\")]\n    pub excluded_extensions: Vec<String>,\n    #[clap(flatten)]\n    pub file_to_save: FileToSave,\n    #[clap(flatten)]\n    pub json_compact_file_to_save: JsonCompactFileToSave,\n    #[clap(flatten)]\n    pub json_pretty_file_to_save: JsonPrettyFileToSave,\n    #[clap(\n        short = 'R',\n        long,\n        help = \"Prevents recursive check of folders\",\n        long_help = \"Disables recursive directory traversal. Only files in the top-level directories will be scanned.\"\n    )]\n    pub not_recursive: bool,\n    #[cfg(target_family = \"unix\")]\n    #[clap(\n        short = 'X',\n        long,\n        help = \"Exclude files on other filesystems\",\n        long_help = \"Prevents scanning files on different filesystems (useful to avoid scanning mounted drives, network shares, etc.)\"\n    )]\n    pub exclude_other_filesystems: bool,\n    #[clap(flatten)]\n    pub do_not_print: DoNotPrint,\n    #[clap(\n        short = 'W',\n        long,\n        help = \"Ignore error code when files are found\",\n        long_help = \"Suppresses error exit code when duplicate/similar files are found. Useful for scripts that should continue regardless of findings.\"\n    )]\n    pub ignore_error_code_on_found: bool,\n    #[clap(\n        short = 'H',\n        long,\n        help = \"Disable cache\",\n        long_help = \"Disables the cache system. This will make scanning slower but ensures fresh results without cached data.\"\n    )]\n    pub disable_cache: bool,\n}\n\n#[derive(Debug, clap::Args, Clone, Copy)]\npub struct DoNotPrint {\n    #[clap(\n        short = 'N',\n        long,\n        help = \"Do not print results to console\",\n        long_help = \"Suppresses printing of search results to the console. Useful when only saving results to files.\"\n    )]\n    pub do_not_print_results: bool,\n    #[clap(\n        short = 'M',\n        long,\n        help = \"Do not print messages to console\",\n        long_help = \"Suppresses all informational messages, warnings, and errors from being printed to console.\"\n    )]\n    pub do_not_print_messages: bool,\n}\n\n#[derive(Debug, clap::Args, Clone, Copy)]\npub struct DMethod {\n    #[clap(\n        short = 'D',\n        long,\n        default_value = \"NONE\",\n        value_parser = parse_delete_method,\n        help = \"Delete method (AEN, AEO, ON, OO, AEB, AES, OB, OS, HARD)\",\n        long_help = \"Method for selecting which files to delete from duplicate groups:\\nAEN - All files Except Newest (keeps newest)\\nAEO - All files Except Oldest (keeps oldest)\\nON - Only 1 file, the Newest (deletes all but newest)\\nOO - Only 1 file, the Oldest (deletes all but oldest)\\nAEB - All files Except Biggest (keeps biggest)\\nAES - All files Except Smallest (keeps smallest)\\nOB - Only 1 file, the Biggest (deletes all but biggest)\\nOS - Only 1 file, the Smallest (deletes all but smallest)\\nHARD - create hard links to save space\\nNONE - do not delete files (default)\"\n    )]\n    pub delete_method: DeleteMethod,\n    #[clap(\n        short = 'Q',\n        long,\n        help = \"Dry run - preview operations\",\n        long_help = \"Performs a dry run showing what operations would be performed without actually executing them.\"\n    )]\n    pub dry_run: bool,\n    #[clap(\n        short = 'y',\n        long,\n        help = \"Move items to trash\",\n        long_help = \"Instead of permanently deleting files, move them to the system trash/recycle bin where they can be recovered.\"\n    )]\n    pub move_to_trash: bool,\n}\n\n// Simple delete method - delete files or not\n#[derive(Debug, clap::Args, Clone, Copy)]\npub struct SDMethod {\n    #[clap(short = 'D', long, help = \"Delete found items\", long_help = \"Automatically delete all found items matching the criteria.\")]\n    pub delete_files: bool,\n    #[clap(\n        short = 'Q',\n        long,\n        help = \"Dry run - preview operations\",\n        long_help = \"Performs a dry run showing what operations would be performed without actually executing them.\"\n    )]\n    pub dry_run: bool,\n    #[clap(\n        short = 'y',\n        long,\n        help = \"Move items to trash\",\n        long_help = \"Instead of permanently deleting files, move them to the system trash/recycle bin where they can be recovered.\"\n    )]\n    pub move_to_trash: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct FileToSave {\n    #[clap(\n        short,\n        long,\n        value_name = \"file-name\",\n        help = \"Save results to formatted text file\",\n        long_help = \"Saves the search results into a human-readable formatted text file.\"\n    )]\n    pub file_to_save: Option<PathBuf>,\n}\n\n#[derive(Debug, clap::Args)]\npub struct ReferenceDirectories {\n    #[clap(\n        short,\n        long,\n        help = \"Reference directory(ies)\",\n        long_help = \"List of reference directory(ies) to search (absolute paths). Files in these directories will be scanned but won't appear in the results (useful for comparing against a known good set of files).\"\n    )]\n    pub reference_directories: Vec<PathBuf>,\n}\n\n#[derive(Debug, clap::Args)]\npub struct JsonCompactFileToSave {\n    #[clap(\n        short = 'C',\n        long,\n        value_name = \"json-file-name\",\n        help = \"Save results to compact JSON file\",\n        long_help = \"Saves the search results into a compact (minified) JSON file without extra whitespace.\"\n    )]\n    pub compact_file_to_save: Option<PathBuf>,\n}\n\n#[derive(Debug, clap::Args)]\npub struct JsonPrettyFileToSave {\n    #[clap(\n        short,\n        long,\n        value_name = \"pretty-json-file-name\",\n        help = \"Save results to pretty JSON file\",\n        long_help = \"Saves the search results into a pretty-printed (indented) JSON file for better readability.\"\n    )]\n    pub pretty_file_to_save: Option<PathBuf>,\n}\n\n#[derive(Debug, clap::Args)]\npub struct AllowHardLinks {\n    #[clap(\n        short = 'L',\n        long,\n        help = \"Do not ignore hard links\",\n        long_help = \"Treats hard links as separate files rather than ignoring them. By default, hard links are detected and only counted once.\"\n    )]\n    pub allow_hard_links: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct CaseSensitiveNameComparison {\n    #[clap(\n        short = 'l',\n        long,\n        help = \"Use case-sensitive name comparison\",\n        long_help = \"Enables case-sensitive file name comparison. By default, comparisons are case-insensitive (e.g., 'File.txt' equals 'file.txt').\"\n    )]\n    pub case_sensitive_name_comparison: bool,\n}\n\n#[derive(Debug, clap::Args)]\npub struct IgnoreSameSize {\n    #[clap(\n        short = 'J',\n        long,\n        help = \"Ignore files with same size\",\n        long_help = \"Groups files by size and keeps only one file from each size group, ignoring files with identical sizes (useful for quick deduplication based solely on file size).\"\n    )]\n    pub ignore_same_size: bool,\n}\n\nimpl FileToSave {\n    pub(crate) fn file_name(&self) -> Option<&str> {\n        if let Some(file_name) = &self.file_to_save {\n            return file_name.to_str();\n        }\n\n        None\n    }\n}\nimpl JsonCompactFileToSave {\n    pub(crate) fn file_name(&self) -> Option<&str> {\n        if let Some(file_name) = &self.compact_file_to_save {\n            return file_name.to_str();\n        }\n\n        None\n    }\n}\nimpl JsonPrettyFileToSave {\n    pub(crate) fn file_name(&self) -> Option<&str> {\n        if let Some(file_name) = &self.pretty_file_to_save {\n            return file_name.to_str();\n        }\n\n        None\n    }\n}\n\nfn parse_scan_duration(s: &str) -> Result<u32, String> {\n    match s.parse::<u32>() {\n        Ok(scan_duration) => {\n            if ALLOWED_VID_HASH_DURATION.contains(&scan_duration) {\n                Ok(scan_duration)\n            } else {\n                Err(format!(\"Scan duration must be one of: {ALLOWED_VID_HASH_DURATION:?}\"))\n            }\n        }\n        Err(e) => Err(e.to_string()),\n    }\n}\n\nfn parse_crop_detect(src: &str) -> Result<Cropdetect, String> {\n    match crop_detect_from_str_opt(src) {\n        Some(crop_detect) => Ok(crop_detect),\n        None => Err(format!(\"Crop detect \\\"{src}\\\" is not valid\")),\n    }\n}\n\nfn parse_skip_forward_amount(src: &str) -> Result<u32, String> {\n    match src.parse::<u32>() {\n        Ok(skip_forward_amount) => {\n            if !ALLOWED_SKIP_FORWARD_AMOUNT.contains(&skip_forward_amount) {\n                Err(format!(\"Skip forward amount must be one of: {ALLOWED_SKIP_FORWARD_AMOUNT:?}\"))\n            } else {\n                Ok(skip_forward_amount)\n            }\n        }\n        Err(e) => Err(e.to_string()),\n    }\n}\nfn parse_hash_type(src: &str) -> Result<HashType, &'static str> {\n    match src.to_ascii_lowercase().as_str() {\n        \"blake3\" => Ok(HashType::Blake3),\n        \"crc32\" => Ok(HashType::Crc32),\n        \"xxh3\" => Ok(HashType::Xxh3),\n        _ => Err(\"Couldn't parse the hash type (allowed: BLAKE3, CRC32, XXH3)\"),\n    }\n}\n\nfn parse_tolerance(src: &str) -> Result<i32, &'static str> {\n    match src.parse::<i32>() {\n        Ok(t) => {\n            if (0..=20).contains(&t) {\n                Ok(t)\n            } else {\n                Err(\"Tolerance should be in range <0,20>(Higher and lower similarity )\")\n            }\n        }\n        _ => Err(\"Failed to parse tolerance as i32 value.\"),\n    }\n}\n\nfn parse_checking_method_duplicate(src: &str) -> Result<CheckingMethod, &'static str> {\n    match src.to_ascii_lowercase().as_str() {\n        \"name\" => Ok(CheckingMethod::Name),\n        \"size\" => Ok(CheckingMethod::Size),\n        \"size_name\" => Ok(CheckingMethod::SizeName),\n        \"hash\" => Ok(CheckingMethod::Hash),\n        _ => Err(\"Couldn't parse the search method (allowed: NAME, SIZE, HASH)\"),\n    }\n}\n\nfn parse_broken_files(src: &str) -> Result<CheckedTypes, &'static str> {\n    match src.to_ascii_lowercase().as_str() {\n        \"pdf\" => Ok(CheckedTypes::PDF),\n        \"audio\" => Ok(CheckedTypes::AUDIO),\n        \"image\" => Ok(CheckedTypes::IMAGE),\n        \"archive\" => Ok(CheckedTypes::ARCHIVE),\n        \"video\" => Ok(CheckedTypes::VIDEO),\n        _ => Err(\"Couldn't parse the broken files type (allowed: PDF, AUDIO, IMAGE, ARCHIVE, VIDEO)\"),\n    }\n}\n\nfn parse_checking_method_same_music(src: &str) -> Result<CheckingMethod, &'static str> {\n    match src.to_ascii_lowercase().as_str() {\n        \"tags\" => Ok(CheckingMethod::AudioTags),\n        \"content\" => Ok(CheckingMethod::AudioContent),\n        _ => Err(\"Couldn't parse the search method (allowed: TAGS, CONTENT)\"),\n    }\n}\n\nfn parse_video_codec(src: &str) -> Result<VideoCodec, &'static str> {\n    match src.to_ascii_lowercase().as_str() {\n        \"h264\" => Ok(VideoCodec::H264),\n        \"h265\" | \"hevc\" => Ok(VideoCodec::H265),\n        \"av1\" => Ok(VideoCodec::Av1),\n        \"vp9\" => Ok(VideoCodec::Vp9),\n        _ => Err(\"Couldn't parse the video codec (allowed: h264, h265, av1, vp9)\"),\n    }\n}\n\nfn parse_max_samples(src: &str) -> Result<usize, String> {\n    match src.parse::<usize>() {\n        Ok(val) if (5..=1000).contains(&val) => Ok(val),\n        Ok(_) => Err(\"Maximum samples must be between 5 and 1000\".to_string()),\n        Err(e) => Err(e.to_string()),\n    }\n}\n\nfn parse_min_crop_size(src: &str) -> Result<u32, String> {\n    match src.parse::<u32>() {\n        Ok(val) if (1..=1000).contains(&val) => Ok(val),\n        Ok(_) => Err(\"Minimum crop size must be between 1 and 1000\".to_string()),\n        Err(e) => Err(e.to_string()),\n    }\n}\n\nfn parse_delete_method(src: &str) -> Result<DeleteMethod, &'static str> {\n    match src.to_ascii_lowercase().as_str() {\n        \"none\" => Ok(DeleteMethod::None),\n        \"aen\" => Ok(DeleteMethod::AllExceptNewest),\n        \"aeo\" => Ok(DeleteMethod::AllExceptOldest),\n        \"hard\" => Ok(DeleteMethod::HardLink),\n        \"on\" => Ok(DeleteMethod::OneNewest),\n        \"oo\" => Ok(DeleteMethod::OneOldest),\n        \"aeb\" => Ok(DeleteMethod::AllExceptBiggest),\n        \"aes\" => Ok(DeleteMethod::AllExceptSmallest),\n        \"ob\" => Ok(DeleteMethod::OneBiggest),\n        \"os\" => Ok(DeleteMethod::OneSmallest),\n        _ => Err(\"Couldn't parse the delete method (allowed: AEN, AEO, ON, OO, HARD, AEB, AES, OB, OS)\"),\n    }\n}\n\nfn parse_minimal_file_size(src: &str) -> Result<u64, String> {\n    match src.parse::<u64>() {\n        Ok(minimal_file_size) => {\n            if minimal_file_size > 0 {\n                Ok(minimal_file_size)\n            } else {\n                Err(\"Minimum file size must be at least 1 byte\".to_string())\n            }\n        }\n        Err(e) => Err(e.to_string()),\n    }\n}\n\nfn parse_maximal_file_size(src: &str) -> Result<u64, String> {\n    match src.parse::<u64>() {\n        Ok(maximal_file_size) => Ok(maximal_file_size),\n        Err(e) => Err(e.to_string()),\n    }\n}\n\nfn parse_similar_image_filter(src: &str) -> Result<FilterType, String> {\n    let filter_type = match src.to_lowercase().as_str() {\n        \"lanczos3\" => FilterType::Lanczos3,\n        \"nearest\" => FilterType::Nearest,\n        \"triangle\" => FilterType::Triangle,\n        \"gaussian\" => FilterType::Gaussian,\n        \"catmullrom\" => FilterType::CatmullRom,\n        _ => return Err(\"Couldn't parse the image resize filter (allowed: Lanczos3, Nearest, Triangle, Gaussian, Catmullrom)\".to_string()),\n    };\n    Ok(filter_type)\n}\n\nfn parse_similar_hash_algorithm(src: &str) -> Result<HashAlg, String> {\n    let algorithm = match src.to_lowercase().as_str() {\n        \"mean\" => HashAlg::Mean,\n        \"gradient\" => HashAlg::Gradient,\n        \"blockhash\" => HashAlg::Blockhash,\n        \"vertgradient\" => HashAlg::VertGradient,\n        \"doublegradient\" => HashAlg::DoubleGradient,\n        \"median\" => HashAlg::Median,\n        _ => return Err(\"Couldn't parse the hash algorithm (allowed: Mean, Gradient, Blockhash, VertGradient, DoubleGradient, Median)\".to_string()),\n    };\n    Ok(algorithm)\n}\n\nfn parse_image_hash_size(src: &str) -> Result<u8, String> {\n    let hash_size = match src.to_lowercase().as_str() {\n        \"8\" => 8,\n        \"16\" => 16,\n        \"32\" => 32,\n        \"64\" => 64,\n        _ => return Err(\"Couldn't parse the image hash size (allowed: 8, 16, 32, 64)\".to_string()),\n    };\n    Ok(hash_size)\n}\n\nfn parse_music_duplicate_type(src: &str) -> Result<MusicSimilarity, String> {\n    if src.trim().is_empty() {\n        return Ok(MusicSimilarity::NONE);\n    }\n\n    let mut similarity: MusicSimilarity = MusicSimilarity::NONE;\n\n    let parts: Vec<String> = src.split(',').map(|e| e.to_lowercase().replace('_', \"\")).collect();\n\n    if parts.contains(&\"tracktitle\".into()) {\n        similarity |= MusicSimilarity::TRACK_TITLE;\n    }\n    if parts.contains(&\"trackartist\".into()) {\n        similarity |= MusicSimilarity::TRACK_ARTIST;\n    }\n    if parts.contains(&\"year\".into()) {\n        similarity |= MusicSimilarity::YEAR;\n    }\n    if parts.contains(&\"bitrate\".into()) {\n        similarity |= MusicSimilarity::BITRATE;\n    }\n    if parts.contains(&\"genre\".into()) {\n        similarity |= MusicSimilarity::GENRE;\n    }\n    if parts.contains(&\"length\".into()) {\n        similarity |= MusicSimilarity::LENGTH;\n    }\n\n    if similarity == MusicSimilarity::NONE {\n        return Err(\"Couldn't parse the music search method (allowed: track_title,track_artist,year,bitrate,genre,length)\".to_string());\n    }\n\n    Ok(similarity)\n}\n\nfn parse_crop_mechanism(src: &str) -> Result<String, String> {\n    match src.to_lowercase().as_str() {\n        \"blackbars\" | \"staticcontent\" => Ok(src.to_lowercase()),\n        _ => Err(\"Invalid crop mechanism. Allowed values: blackbars, staticcontent\".to_string()),\n    }\n}\n\nconst HELP_TEMPLATE: &str = r#\"\n{bin} {version}\n\nUSAGE:\n    {usage} [FLAGS] [OPTIONS]\n\nOPTIONS:\n{options}\n\nCOMMANDS:\n{subcommands}\n\n    try \"{usage} -h\" to get more info about a specific tool\n\nEXAMPLES:\n    {bin} dup -d /home/rafal -e /home/rafal/Obrazy  -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo\n    {bin} empty-folders -d /home/rafal/rr /home/gateway -f results.txt\n    {bin} big -d /home/rafal/ /home/piszczal -e /home/rafal/Roman -n 25 -x VIDEO -f results.txt\n    {bin} empty-files -d /home/rafal /home/szczekacz -e /home/rafal/Pulpit -R -f results.txt\n    {bin} temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D\n    {bin} image -d /home/rafal -e /home/rafal/Pulpit -f results.txt\n    {bin} music -d /home/rafal -e /home/rafal/Pulpit -z \\\"artist,year,ARTISTALBUM,ALBUM___tiTlE\\\"  -f results.txt\n    {bin} symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt\n    {bin} broken -d /home/mikrut/ -e /home/mikrut/trakt -f results.txt\n    {bin} ext -d /home/mikrut/ -e /home/mikrut/trakt -f results.txt\n    {bin} bad-names -d /home/rafal -u -j -w -n -f results.txt\n    {bin} video-optimizer -d /home/rafal transcode -c h264 -f results.txt\n    {bin} video-optimizer -d /home/rafal crop -m blackbars -f results.txt\n    {bin} exif-remover -d /home/rafal -x IMAGE -f results.txt\"#;\n"
  },
  {
    "path": "czkawka_cli/src/main.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::thread;\n\nuse clap::Parser;\nuse commands::Commands;\nuse crossbeam_channel::{Receiver, Sender, unbounded};\nuse czkawka_core::common::config_cache_path::{print_infos_and_warnings, set_config_cache_path};\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::image::register_image_decoding_hooks;\nuse czkawka_core::common::logger::{filtering_messages, print_version_mode, setup_logger};\nuse czkawka_core::common::progress_data::ProgressData;\nuse czkawka_core::common::set_number_of_threads;\nuse czkawka_core::common::tool_data::{CommonData, DeleteMethod};\nuse czkawka_core::common::traits::{AllTraits, FixingItems, PrintResults, Search};\nuse czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsFixParams, BadExtensionsParameters};\nuse czkawka_core::tools::bad_names::{BadNames, BadNamesParameters, NameFixerParams, NameIssues};\nuse czkawka_core::tools::big_file::{BigFile, BigFileParameters, SearchMode};\nuse czkawka_core::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes};\nuse czkawka_core::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters};\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse czkawka_core::tools::empty_folder::EmptyFolder;\nuse czkawka_core::tools::exif_remover::{ExifRemover, ExifRemoverParameters, ExifTagsFixerParams};\nuse czkawka_core::tools::invalid_symlinks::InvalidSymlinks;\nuse czkawka_core::tools::same_music::{SameMusic, SameMusicParameters};\nuse czkawka_core::tools::similar_images::{SimilarImages, SimilarImagesParameters};\nuse czkawka_core::tools::similar_videos::{SimilarVideos, SimilarVideosParameters};\nuse czkawka_core::tools::temporary::Temporary;\nuse czkawka_core::tools::video_optimizer::{\n    VideoCropFixParams, VideoCropParams, VideoCroppingMechanism, VideoOptimizer, VideoOptimizerFixParams, VideoOptimizerParameters, VideoTranscodeFixParams, VideoTranscodeParams,\n};\nuse log::{debug, error, info};\n\nuse crate::commands::{\n    Args, BadExtensionsArgs, BadNamesArgs, BiggestFilesArgs, BrokenFilesArgs, CommonCliItems, DMethod, DuplicatesArgs, EmptyFilesArgs, EmptyFoldersArgs, ExifRemoverArgs,\n    InvalidSymlinksArgs, SDMethod, SameMusicArgs, SimilarImagesArgs, SimilarVideosArgs, TemporaryArgs, VideoOptimizerArgs,\n};\nuse crate::progress::connect_progress;\n\nmod commands;\nmod progress;\n\n#[derive(Debug)]\npub struct CliOutput {\n    pub found_any_files: bool,\n    pub ignored_error_code_on_found: bool,\n    pub output: String,\n}\n\nfn main() {\n    register_image_decoding_hooks();\n    if cfg!(debug_assertions) {\n        use clap::CommandFactory;\n        Args::command().debug_assert();\n    }\n    let command = Args::parse().command;\n\n    let config_cache_path_set_result = set_config_cache_path(\"Czkawka\", \"Czkawka\");\n    setup_logger(true, \"czkawka_cli\", filtering_messages);\n    print_version_mode(\"Czkawka cli\");\n    print_infos_and_warnings(config_cache_path_set_result.infos, config_cache_path_set_result.warnings);\n\n    if cfg!(debug_assertions) {\n        debug!(\"Running command - {command:?}\");\n    }\n\n    let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    let store_flag_cloned = stop_flag.clone();\n\n    let calculate_thread = thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || match command {\n            Commands::Duplicates(duplicates_args) => duplicates(duplicates_args, &stop_flag, &progress_sender),\n            Commands::EmptyFolders(empty_folders_args) => empty_folders(empty_folders_args, &stop_flag, &progress_sender),\n            Commands::BiggestFiles(biggest_files_args) => biggest_files(biggest_files_args, &stop_flag, &progress_sender),\n            Commands::EmptyFiles(empty_files_args) => empty_files(empty_files_args, &stop_flag, &progress_sender),\n            Commands::Temporary(temporary_args) => temporary(temporary_args, &stop_flag, &progress_sender),\n            Commands::SimilarImages(similar_images_args) => similar_images(similar_images_args, &stop_flag, &progress_sender),\n            Commands::SameMusic(same_music_args) => same_music(same_music_args, &stop_flag, &progress_sender),\n            Commands::InvalidSymlinks(invalid_symlinks_args) => invalid_symlinks(invalid_symlinks_args, &stop_flag, &progress_sender),\n            Commands::BrokenFiles(broken_files_args) => broken_files(broken_files_args, &stop_flag, &progress_sender),\n            Commands::SimilarVideos(similar_videos_args) => similar_videos(similar_videos_args, &stop_flag, &progress_sender),\n            Commands::BadExtensions(bad_extensions_args) => bad_extensions(bad_extensions_args, &stop_flag, &progress_sender),\n            Commands::BadNames(bad_names_args) => bad_names(bad_names_args, &stop_flag, &progress_sender),\n            Commands::VideoOptimizer(video_optimizer_args) => video_optimizer(video_optimizer_args, &stop_flag, &progress_sender),\n            Commands::ExifRemover(exif_remover_args) => exif_remover(exif_remover_args, &stop_flag, &progress_sender),\n        })\n        .expect(\"Failed to spawn calculation thread\");\n\n    ctrlc::set_handler(move || {\n        if store_flag_cloned.load(std::sync::atomic::Ordering::SeqCst) {\n            return;\n        }\n        info!(\"Got Ctrl+C signal, stopping...\");\n        store_flag_cloned.store(true, std::sync::atomic::Ordering::SeqCst);\n    })\n    .expect(\"Error setting Ctrl-C handler\");\n\n    connect_progress(&progress_receiver);\n\n    let cli_output = calculate_thread.join().expect(\"Failed to join calculation thread\");\n\n    #[expect(clippy::print_stdout)]\n    if !cli_output.output.is_empty() {\n        println!(\"{}\", cli_output.output);\n    }\n\n    if cli_output.found_any_files && !cli_output.ignored_error_code_on_found {\n        std::process::exit(11);\n    } else {\n        std::process::exit(0);\n    }\n}\n\nfn duplicates(duplicates: DuplicatesArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let DuplicatesArgs {\n        common_cli_items,\n        reference_directories,\n        minimal_file_size,\n        maximal_file_size,\n        minimal_cached_file_size,\n        search_method,\n        delete_method,\n        hash_type,\n        allow_hard_links,\n        case_sensitive_name_comparison,\n        minimal_prehash_cache_file_size,\n        use_prehash_cache,\n    } = duplicates;\n\n    let params = DuplicateFinderParameters::new(\n        search_method,\n        hash_type,\n        use_prehash_cache,\n        minimal_cached_file_size,\n        minimal_prehash_cache_file_size,\n        case_sensitive_name_comparison.case_sensitive_name_comparison,\n    );\n    let mut tool = DuplicateFinder::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));\n    tool.set_minimal_file_size(minimal_file_size);\n    tool.set_maximal_file_size(maximal_file_size);\n    tool.set_hide_hard_links(!allow_hard_links.allow_hard_links);\n    set_advanced_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn empty_folders(empty_folders: EmptyFoldersArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let EmptyFoldersArgs { common_cli_items, delete_method } = empty_folders;\n\n    let mut tool = EmptyFolder::new();\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn biggest_files(biggest_files: BiggestFilesArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let BiggestFilesArgs {\n        common_cli_items,\n        number_of_files,\n        delete_method,\n        smallest_mode,\n    } = biggest_files;\n\n    let big_files_mode = if smallest_mode { SearchMode::SmallestFiles } else { SearchMode::BiggestFiles };\n    let params = BigFileParameters::new(number_of_files, big_files_mode);\n    let mut tool = BigFile::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn empty_files(empty_files: EmptyFilesArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let EmptyFilesArgs { common_cli_items, delete_method } = empty_files;\n\n    let mut tool = EmptyFiles::new();\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn temporary(temporary: TemporaryArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let TemporaryArgs { common_cli_items, delete_method } = temporary;\n\n    let mut tool = Temporary::new();\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn similar_images(similar_images: SimilarImagesArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let SimilarImagesArgs {\n        common_cli_items,\n        reference_directories,\n        minimal_file_size,\n        maximal_file_size,\n        max_difference,\n        hash_alg,\n        image_filter,\n        hash_size,\n        delete_method,\n        allow_hard_links,\n        ignore_same_size,\n    } = similar_images;\n\n    let params = SimilarImagesParameters::new(max_difference, hash_size, hash_alg, image_filter, ignore_same_size.ignore_same_size);\n    let mut tool = SimilarImages::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));\n    tool.set_minimal_file_size(minimal_file_size);\n    tool.set_maximal_file_size(maximal_file_size);\n    tool.set_hide_hard_links(!allow_hard_links.allow_hard_links);\n    set_advanced_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn same_music(same_music: SameMusicArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let SameMusicArgs {\n        common_cli_items,\n        reference_directories,\n        delete_method,\n        minimal_file_size,\n        maximal_file_size,\n        music_similarity,\n        minimum_segment_duration,\n        maximum_difference,\n        search_method,\n        approximate_comparison,\n        compare_fingerprints_only_with_similar_titles,\n    } = same_music;\n\n    let params = SameMusicParameters::new(\n        music_similarity,\n        approximate_comparison,\n        search_method,\n        minimum_segment_duration,\n        maximum_difference,\n        compare_fingerprints_only_with_similar_titles,\n    );\n    let mut tool = SameMusic::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));\n    tool.set_minimal_file_size(minimal_file_size);\n    tool.set_maximal_file_size(maximal_file_size);\n    set_advanced_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn invalid_symlinks(invalid_symlinks: InvalidSymlinksArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let InvalidSymlinksArgs { common_cli_items, delete_method } = invalid_symlinks;\n\n    let mut tool = InvalidSymlinks::new();\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn broken_files(broken_files: BrokenFilesArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let BrokenFilesArgs {\n        common_cli_items,\n        delete_method,\n        checked_types,\n    } = broken_files;\n\n    let mut checked_type = CheckedTypes::NONE;\n    for check_type in checked_types {\n        checked_type |= check_type;\n    }\n    let params = BrokenFilesParameters::new(checked_type);\n    let mut tool = BrokenFiles::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn similar_videos(similar_videos: SimilarVideosArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let SimilarVideosArgs {\n        reference_directories,\n        common_cli_items,\n        tolerance,\n        minimal_file_size,\n        maximal_file_size,\n        delete_method,\n        allow_hard_links,\n        ignore_same_size,\n        skip_forward_amount,\n        crop_detect,\n        scan_duration,\n    } = similar_videos;\n\n    let params = SimilarVideosParameters::new(\n        tolerance,\n        ignore_same_size.ignore_same_size,\n        skip_forward_amount,\n        scan_duration,\n        crop_detect,\n        false, // creating thumbnails in CLI, makes almost no sense\n        10,    // creating thumbnails in CLI, makes almost no sense\n        false, // creating thumbnails in CLI, makes almost no sense\n        2,     // creating thumbnails in CLI, makes almost no sense\n    );\n    let mut tool = SimilarVideos::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref()));\n    tool.set_minimal_file_size(minimal_file_size);\n    tool.set_maximal_file_size(maximal_file_size);\n    tool.set_hide_hard_links(!allow_hard_links.allow_hard_links);\n    set_advanced_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn bad_extensions(bad_extensions: BadExtensionsArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let BadExtensionsArgs { common_cli_items, fix_extensions } = bad_extensions;\n\n    let params = BadExtensionsParameters::new();\n    let mut tool = BadExtensions::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    if fix_extensions {\n        let fix_params = BadExtensionsFixParams {};\n        tool.fix_items(stop_flag, Some(progress_sender), fix_params);\n    }\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn bad_names(bad_names: BadNamesArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let BadNamesArgs {\n        common_cli_items,\n        delete_method,\n        uppercase_extension,\n        emoji_used,\n        space_at_start_or_end,\n        non_ascii_graphical,\n        restricted_charset,\n        remove_duplicated_non_alphanumeric,\n        fix_names,\n    } = bad_names;\n\n    let restricted_charset_allowed = restricted_charset.and_then(|s| {\n        let mut items: Vec<_> = s.chars().collect();\n        items.sort_unstable();\n        items.dedup();\n        if items.is_empty() { None } else { Some(items) }\n    });\n\n    let name_issues = NameIssues {\n        uppercase_extension,\n        emoji_used,\n        space_at_start_or_end,\n        non_ascii_graphical,\n        restricted_charset_allowed,\n        remove_duplicated_non_alphanumeric,\n    };\n\n    let params = BadNamesParameters::new(name_issues);\n    let mut tool = BadNames::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n    set_simple_delete(&mut tool, delete_method);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    if fix_names {\n        let fix_params = NameFixerParams::default();\n        tool.fix_items(stop_flag, Some(progress_sender), fix_params);\n    }\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn video_optimizer(video_optimizer: VideoOptimizerArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    use crate::commands::{CropArgs, TranscodeArgs, VideoOptimizerMode as CliVideoOptimizerMode};\n\n    let VideoOptimizerArgs { common_cli_items, mode } = video_optimizer;\n\n    match mode {\n        CliVideoOptimizerMode::Transcode(transcode_args) => {\n            let TranscodeArgs {\n                excluded_codecs,\n                generate_thumbnails,\n                thumbnail_percentage,\n                thumbnail_grid,\n                fix_videos,\n                target_codec,\n                quality,\n                fail_if_not_smaller,\n                overwrite_original,\n                limit_video_size,\n                max_width,\n                max_height,\n                thumbnail_grid_tiles_per_side,\n            } = transcode_args;\n\n            let excluded_codecs_vec = excluded_codecs.map_or_else(\n                || vec![\"hevc\".to_string(), \"h265\".to_string(), \"av1\".to_string(), \"vp9\".to_string()],\n                |s| s.split(',').map(|c| c.trim().to_string()).collect(),\n            );\n\n            let params = VideoOptimizerParameters::VideoTranscode(VideoTranscodeParams::new(\n                excluded_codecs_vec,\n                generate_thumbnails,\n                thumbnail_percentage,\n                thumbnail_grid,\n                thumbnail_grid_tiles_per_side,\n            ));\n\n            let mut tool = VideoOptimizer::new(params);\n            set_common_settings(&mut tool, &common_cli_items, None);\n            tool.search(stop_flag, Some(progress_sender));\n\n            if fix_videos {\n                let fix_params = VideoOptimizerFixParams::VideoTranscode(VideoTranscodeFixParams {\n                    codec: target_codec,\n                    quality,\n                    fail_if_not_smaller,\n                    overwrite_original,\n                    limit_video_size,\n                    max_width,\n                    max_height,\n                });\n                tool.fix_items(stop_flag, Some(progress_sender), fix_params);\n            }\n\n            save_and_write_results_to_writer(&tool, &common_cli_items)\n        }\n        CliVideoOptimizerMode::Crop(crop_args) => {\n            let CropArgs {\n                crop_mechanism,\n                black_pixel_threshold,\n                black_bar_percentage,\n                max_samples,\n                min_crop_size,\n                generate_thumbnails,\n                thumbnail_percentage,\n                thumbnail_grid,\n                thumbnail_grid_tiles_per_side,\n                fix_videos,\n                overwrite_original,\n                target_codec,\n                quality,\n            } = crop_args;\n\n            #[expect(clippy::match_same_arms)]\n            let crop_mech = match crop_mechanism.as_str() {\n                \"blackbars\" => VideoCroppingMechanism::BlackBars,\n                \"staticcontent\" => VideoCroppingMechanism::StaticContent,\n                _ => VideoCroppingMechanism::BlackBars,\n            };\n\n            let params = VideoOptimizerParameters::VideoCrop(VideoCropParams::with_custom_params(\n                crop_mech,\n                black_pixel_threshold,\n                black_bar_percentage,\n                max_samples,\n                min_crop_size,\n                generate_thumbnails,\n                thumbnail_percentage,\n                thumbnail_grid,\n                thumbnail_grid_tiles_per_side,\n            ));\n\n            let mut tool = VideoOptimizer::new(params);\n            set_common_settings(&mut tool, &common_cli_items, None);\n            tool.search(stop_flag, Some(progress_sender));\n\n            if fix_videos {\n                let fix_params = VideoOptimizerFixParams::VideoCrop(VideoCropFixParams {\n                    overwrite_original,\n                    target_codec,\n                    quality,\n                    crop_mechanism: crop_mech,\n                });\n                tool.fix_items(stop_flag, Some(progress_sender), fix_params);\n            }\n\n            save_and_write_results_to_writer(&tool, &common_cli_items)\n        }\n    }\n}\n\nfn exif_remover(exif_remover: ExifRemoverArgs, stop_flag: &Arc<AtomicBool>, progress_sender: &Sender<ProgressData>) -> CliOutput {\n    let ExifRemoverArgs {\n        common_cli_items,\n        ignored_tags,\n        fix_exif,\n        override_file,\n    } = exif_remover;\n\n    let ignored_tags_vec = ignored_tags.map(|s| s.split(',').map(|tag| tag.trim().to_string()).collect()).unwrap_or_default();\n\n    let params = ExifRemoverParameters::new(ignored_tags_vec);\n    let mut tool = ExifRemover::new(params);\n\n    set_common_settings(&mut tool, &common_cli_items, None);\n\n    tool.search(stop_flag, Some(progress_sender));\n\n    if fix_exif {\n        let fix_params = ExifTagsFixerParams { override_file };\n        tool.fix_items(stop_flag, Some(progress_sender), fix_params);\n    }\n\n    save_and_write_results_to_writer(&tool, &common_cli_items)\n}\n\nfn save_and_write_results_to_writer<T: CommonData + PrintResults>(component: &T, common_cli_items: &CommonCliItems) -> CliOutput {\n    if let Some(file_name) = common_cli_items.file_to_save.file_name()\n        && let Err(e) = component.print_results_to_file(file_name)\n    {\n        error!(\"Failed to save results to file {e}\");\n    }\n    if let Some(file_name) = common_cli_items.json_compact_file_to_save.file_name()\n        && let Err(e) = component.save_results_to_file_as_json(file_name, false)\n    {\n        error!(\"Failed to save compact json results to file {e}\");\n    }\n    if let Some(file_name) = common_cli_items.json_pretty_file_to_save.file_name()\n        && let Err(e) = component.save_results_to_file_as_json(file_name, true)\n    {\n        error!(\"Failed to save pretty json results to file {e}\");\n    }\n\n    let mut buf_writer = std::io::BufWriter::new(Vec::new());\n    if !common_cli_items.do_not_print.do_not_print_results {\n        let _ = component.print_results_to_writer(&mut buf_writer).map_err(|e| {\n            error!(\"Failed to print results to output: {e}\");\n        });\n    }\n\n    if !common_cli_items.do_not_print.do_not_print_messages {\n        let _ = component.get_text_messages().print_messages_to_writer(&mut buf_writer).map_err(|e| {\n            error!(\"Failed to print results to output: {e}\");\n        });\n    }\n\n    let mut cli_output = CliOutput {\n        found_any_files: component.found_any_items(),\n        ignored_error_code_on_found: common_cli_items.ignore_error_code_on_found,\n        output: String::new(),\n    };\n\n    if let Ok(file_vec) = buf_writer.into_inner()\n        && let Ok(output) = String::from_utf8(file_vec)\n    {\n        cli_output.output = output;\n    }\n\n    cli_output\n}\n\nfn set_simple_delete<T>(component: &mut T, s_delete: SDMethod)\nwhere\n    T: AllTraits,\n{\n    if s_delete.delete_files {\n        component.set_delete_method(DeleteMethod::Delete);\n    }\n    component.set_dry_run(s_delete.dry_run);\n    component.set_move_to_trash(s_delete.move_to_trash);\n}\n\nfn set_advanced_delete<T>(component: &mut T, a_delete: DMethod)\nwhere\n    T: AllTraits,\n{\n    component.set_delete_method(a_delete.delete_method);\n    component.set_dry_run(a_delete.dry_run);\n    component.set_move_to_trash(a_delete.move_to_trash);\n}\n\nfn set_common_settings<T>(component: &mut T, common_cli_items: &CommonCliItems, reference_directories: Option<&Vec<PathBuf>>)\nwhere\n    T: AllTraits,\n{\n    set_number_of_threads(common_cli_items.thread_number);\n\n    let mut included_directories = common_cli_items.directories.clone();\n    if let Some(reference_directories) = reference_directories {\n        included_directories.extend_from_slice(reference_directories);\n        component.set_reference_paths(reference_directories.clone());\n    }\n\n    component.set_included_paths(included_directories);\n    component.set_excluded_paths(common_cli_items.excluded_directories.clone());\n    component.set_excluded_items(common_cli_items.excluded_items.clone());\n    component.set_recursive_search(!common_cli_items.not_recursive);\n    #[cfg(target_family = \"unix\")]\n    component.set_exclude_other_filesystems(common_cli_items.exclude_other_filesystems);\n    component.set_allowed_extensions(common_cli_items.allowed_extensions.clone());\n    component.set_excluded_extensions(common_cli_items.excluded_extensions.clone());\n    component.set_use_cache(!common_cli_items.disable_cache);\n}\n"
  },
  {
    "path": "czkawka_cli/src/progress.rs",
    "content": "use std::time::Duration;\n\nuse crossbeam_channel::Receiver;\nuse czkawka_core::common::model::ToolType;\nuse czkawka_core::common::progress_data::{CurrentStage, ProgressData};\nuse humansize::{BINARY, format_size};\nuse indicatif::{ProgressBar, ProgressStyle};\n\npub(crate) fn connect_progress(progress_receiver: &Receiver<ProgressData>) {\n    let mut pb = ProgressBar::new(1);\n    let mut latest_id = None;\n    while let Ok(progress_data) = progress_receiver.recv() {\n        // We only need to recreate progress bar if stage changed\n        if latest_id != Some(progress_data.current_stage_idx) {\n            pb.finish_and_clear();\n            if progress_data.current_stage_idx == 0 {\n                pb = get_progress_bar_for_collect_files();\n            } else if progress_data.sstage.check_if_loading_saving_cache() {\n                pb = get_progress_loading_saving_cache(progress_data.sstage.check_if_loading_cache());\n            } else if progress_data.bytes_to_check != 0 {\n                pb = get_progress_known_values(progress_data.bytes_to_check);\n            } else {\n                pb = get_progress_known_values(progress_data.entries_to_check as u64);\n            }\n            latest_id = Some(progress_data.current_stage_idx);\n        }\n\n        if progress_data.sstage == CurrentStage::CollectingFiles && progress_data.tool_type != ToolType::EmptyFolders {\n            pb.set_message(format!(\"Collecting files: {}\", progress_data.entries_checked));\n        } else if progress_data.sstage == CurrentStage::CollectingFiles {\n            pb.set_message(format!(\"Collecting folders: {}\", progress_data.entries_checked));\n        } else if !progress_data.sstage.check_if_loading_saving_cache() {\n            if progress_data.bytes_to_check != 0 {\n                pb.set_position(progress_data.bytes_checked);\n                pb.set_message(format!(\n                    \"{}: {}/{} ({}/{})\",\n                    get_progress_message(&progress_data),\n                    progress_data.entries_checked,\n                    progress_data.entries_to_check,\n                    format_size(progress_data.bytes_checked, BINARY),\n                    format_size(progress_data.bytes_to_check, BINARY)\n                ));\n            } else {\n                pb.set_position(progress_data.entries_checked as u64);\n                pb.set_message(format!(\n                    \"{}: {}/{}\",\n                    get_progress_message(&progress_data),\n                    progress_data.entries_checked,\n                    progress_data.entries_to_check\n                ));\n            }\n        }\n    }\n    pb.finish_and_clear();\n}\n\npub(crate) fn get_progress_message(progress_data: &ProgressData) -> String {\n    match progress_data.sstage {\n        CurrentStage::SameMusicReadingTags => \"Reading tags\",\n        CurrentStage::SameMusicCalculatingFingerprints => \"Calculating fingerprints\",\n        CurrentStage::SameMusicComparingTags => \"Comparing tags\",\n        CurrentStage::SameMusicComparingFingerprints => \"Comparing fingerprints\",\n        CurrentStage::DuplicatePreHashing => \"Calculating prehashes\",\n        CurrentStage::DuplicateFullHashing => \"Calculating hashes\",\n        CurrentStage::SimilarImagesCalculatingHashes => \"Calculating image hashes\",\n        CurrentStage::SimilarImagesComparingHashes => \"Comparing image hashes\",\n        CurrentStage::SimilarVideosCalculatingHashes => \"Reading similar values\",\n        CurrentStage::SimilarVideosCreatingThumbnails | CurrentStage::VideoOptimizerCreatingThumbnails => \"Creating video thumbnails\",\n        CurrentStage::BrokenFilesChecking => \"Checking broken files\",\n        CurrentStage::BadExtensionsChecking => \"Checking extensions of files\",\n        CurrentStage::DeletingFiles => \"Deleting files/folders\",\n        CurrentStage::RenamingFiles => \"Renaming files\",\n        CurrentStage::MovingFiles => \"Moving files\",\n        CurrentStage::HardlinkingFiles => \"Creating hardlinks\",\n        CurrentStage::SymlinkingFiles => \"Creating symlinks\",\n        CurrentStage::OptimizingVideos => \"Optimizing videos\",\n        CurrentStage::CleaningExif => \"Cleaning EXIF data\",\n        CurrentStage::ExifRemoverExtractingTags => \"Extracting EXIF tags\",\n        CurrentStage::VideoOptimizerProcessingVideos => \"Processing videos\",\n        CurrentStage::BadNamesChecking => \"Checking names of files\",\n\n        CurrentStage::CollectingFiles\n        | CurrentStage::DuplicateCacheSaving\n        | CurrentStage::DuplicateCacheLoading\n        | CurrentStage::DuplicatePreHashCacheSaving\n        | CurrentStage::DuplicatePreHashCacheLoading\n        | CurrentStage::DuplicateScanningName\n        | CurrentStage::DuplicateScanningSizeName\n        | CurrentStage::DuplicateScanningSize\n        | CurrentStage::SameMusicCacheSavingTags\n        | CurrentStage::SameMusicCacheLoadingTags\n        | CurrentStage::SameMusicCacheSavingFingerprints\n        | CurrentStage::SameMusicCacheLoadingFingerprints\n        | CurrentStage::ExifRemoverCacheLoading\n        | CurrentStage::ExifRemoverCacheSaving => unreachable!(\"This stages(caches, initial files scanning) should be handled somewhere else\"),\n    }\n    .to_string()\n}\n\npub(crate) fn get_progress_bar_for_collect_files() -> ProgressBar {\n    let pb = ProgressBar::new_spinner();\n    pb.enable_steady_tick(Duration::from_millis(120));\n    #[expect(clippy::literal_string_with_formatting_args)]\n    pb.set_style(\n        ProgressStyle::with_template(\"{msg} {spinner:.blue}\")\n            .expect(\"Failed to create progress bar style\")\n            .tick_strings(&[\"▹▹▹▹▹\", \"▸▹▹▹▹\", \"▹▸▹▹▹\", \"▹▹▸▹▹\", \"▹▹▹▸▹\", \"▹▹▹▹▸\", \"▪▪▪▪▪\"]),\n    );\n    pb\n}\n\npub(crate) fn get_progress_known_values(max_value: u64) -> ProgressBar {\n    let pb = ProgressBar::new(max_value);\n    pb.set_style(\n        ProgressStyle::with_template(\"{msg} [{bar}]\")\n            .expect(\"Failed to create progress bar style\")\n            .progress_chars(\"=> \"),\n    );\n    pb\n}\n\npub(crate) fn get_progress_loading_saving_cache(loading: bool) -> ProgressBar {\n    let msg = if loading { \"Loading cache\" } else { \"Saving cache\" };\n    let pb = ProgressBar::new_spinner();\n    pb.enable_steady_tick(Duration::from_millis(120));\n    pb.set_style(\n        ProgressStyle::with_template(&format!(\"{msg} {{spinner:.blue}}\"))\n            .expect(\"Failed to create progress bar style\")\n            .tick_strings(&[\"▹▹▹▹▹\", \"▸▹▹▹▹\", \"▹▸▹▹▹\", \"▹▹▸▹▹\", \"▹▹▹▸▹\", \"▹▹▹▹▸\", \"▪▪▪▪▪\"]),\n    );\n    pb\n}\n"
  },
  {
    "path": "czkawka_core/Cargo.toml",
    "content": "[package]\nname = \"czkawka_core\"\nversion = \"11.0.1\"\nauthors = [\"Rafał Mikrut <mikrutrafal@protonmail.com>\"]\nedition = \"2024\"\nrust-version = \"1.92.0\"\ndescription = \"Core of Czkawka app\"\nlicense = \"MIT\"\nhomepage = \"https://github.com/qarmin/czkawka\"\nrepository = \"https://github.com/qarmin/czkawka\"\nbuild = \"build.rs\"\n\n[dependencies]\nhumansize = \"2.1\"\nrayon = \"1.10\"\ncrossbeam-channel = \"0.5\"\n\n# For stable iteration over hashmaps.\nindexmap = \"2.11\"\n\n# For saving/loading config files to specific directories\ndirectories-next = \"2.0\"\n\n# Needed by similar images\nimage_hasher = { version = \"3.0\", features = [\"fast_resize_unstable\"] }\nbk-tree = \"0.5\"\nimage = { version = \"0.25\", default-features = false, features = [\"bmp\", \"dds\", \"exr\", \"ff\", \"gif\", \"hdr\", \"ico\", \"jpeg\", \"png\", \"pnm\", \"qoi\", \"tga\", \"tiff\", \"webp\", \"rayon\"] }\nhamming-bitwise-fast = \"1.0\"\nfast_image_resize = { version = \"6.0.0\", features = [\"image\"] }\n\n# Needed by same music\nbitflags = \"2.6\"\nlofty = \"0.23\"\n\n# Needed by broken files\nzip = { version = \"8.1\", features = [\"aes-crypto\", \"bzip2\", \"deflate\", \"time\"], default-features = false }\nlopdf = \"0.39.0\"\n\n# Needed by audio similarity feature\nrusty-chromaprint = \"0.3\"\nsymphonia = { version = \"0.5\", features = [\"all\"] }\n\n# Hashes for duplicate files\nblake3 = \"1.5\"\ncrc32fast = \"1.4\"\nxxhash-rust = { version = \"0.8\", features = [\"xxh3\"] }\n\ntempfile = \"3.13\"\n\n# Video Duplicates\nvid_dup_finder_lib = \"0.4\"\nfiletime = \"0.2.26\"\n\n# For extracting video properties using ffprobe CLI\n# https://github.com/theduke/ffprobe-rs/issues/33\n#ffprobe = \"0.4.0\"\n\n# Saving/Loading Cache\nserde = \"1.0\"\nbincode = \"<2.0\"\nserde_json = \"1.0\"\n\n# Language\ni18n-embed = { version = \"0.16\", features = [\"fluent-system\", \"desktop-requester\"] }\ni18n-embed-fl = \"0.10\"\nrust-embed = { version = \"8.5\", features = [\"debug-embed\"] }\nonce_cell = \"1.20\"\n\n# Raw image files\nrawler = \"0.7.0\"\nlibraw-rs = { version = \"0.0.4\", optional = true }\njxl-oxide = { version = \"0.12.0\", features = [\"image\"] }\n\n# Checking for invalid extensions\nmime_guess = \"2.0\"\ninfer = \"0.19\"\n\n# Newer version of libheif-rs, which is not compatible with Ubuntu 22.04, and works only o\nlibheif-rs = { version = \"2\", optional = true, default-features = false, features = [\"v1_17\", \"image\"] }\n\nnom-exif = \"2.1.0\"\n\n# EXIF data cleaning\nlittle_exif = \"0.6.20\"\n\ndunce = \"1.0.5\"\n\nos_info = { version = \"3\", default-features = false }\nlog = \"0.4.22\"\nhandsome_logger = \"0.9\"\nfun_time = { version = \"0.3\", features = [\"log\"] }\nitertools = \"0.14\"\nstatic_assertions = \"1.1.0\"\nfile-rotate = \"0.8.0\"\n\nopen = \"5.3\"\n\nlog-panics = { version = \"2.1.0\", features = [\"with-backtrace\"] }\ndeunicode = \"1.6.2\"\nglibc_musl_version = \"0.1.0\"\n\nrand = \"0.10.0\"\n\nashpd = { version = \"0.13.2\", optional = true, features = [\"trash\"] }\ntokio = { version = \"1.49.0\", optional = true }\n\n[target.'cfg(not(any(target_os = \"android\", target_os = \"ios\")))'.dependencies]\ntrash = \"5.1\"\n\n[target.'cfg(windows)'.dependencies]\nfile-id = \"0.2.2\"\n\n[build-dependencies]\nrustc_version = \"0.4\"\nglibc_musl_version = \"0.1.0\"\n\n[dev-dependencies]\ncriterion =  { version = \"0.8\", default-features = false, features = [] }\n\n[[bench]]\nname = \"hash_calculation_benchmark\"\nharness = false\n\n[features]\ndefault = []\nheif = [\"dep:libheif-rs\"]\nlibraw = [\"dep:libraw-rs\"]\nlibavif = [\"image/avif-native\", \"image/avif\"]\nblake_pure = [\"blake3/pure\"]\n# Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails\n# No-op on other OSes, it is slower and provides less helpful error messages\nxdg_portal_trash = [\"ashpd\", \"tokio\"]\n[lints]\nworkspace = true\n"
  },
  {
    "path": "czkawka_core/LICENSE_CC_BY_4_TEST_FILES",
    "content": "All icons and audio files, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).\n\nCopyright (c) 2020-2026 Rafał Mikrut\n- test_resources/*/*.png\n- test_resources/*/*.mp3 (generated by AI)\n\nLicense: CC-BY-4.0\n Creative Commons Attribution 4.0 International Public License\n .\n By exercising the Licensed Rights (defined below), You accept and agree\n to be bound by the terms and conditions of this Creative Commons\n Attribution 4.0 International Public License (\"Public\n License\"). To the extent this Public License may be interpreted as a\n contract, You are granted the Licensed Rights in consideration of Your\n acceptance of these terms and conditions, and the Licensor grants You\n such rights in consideration of benefits the Licensor receives from\n making the Licensed Material available under these terms and\n conditions.\n .\n Section 1 -- Definitions.\n .\n a. Adapted Material means material subject to Copyright and Similar\n Rights that is derived from or based upon the Licensed Material\n and in which the Licensed Material is translated, altered,\n arranged, transformed, or otherwise modified in a manner requiring\n permission under the Copyright and Similar Rights held by the\n Licensor. For purposes of this Public License, where the Licensed\n Material is a musical work, performance, or sound recording,\n Adapted Material is always produced where the Licensed Material is\n synched in timed relation with a moving image.\n .\n b. Adapter's License means the license You apply to Your Copyright\n and Similar Rights in Your contributions to Adapted Material in\n accordance with the terms and conditions of this Public License.\n .\n c. Copyright and Similar Rights means copyright and/or similar rights\n closely related to copyright including, without limitation,\n performance, broadcast, sound recording, and Sui Generis Database\n Rights, without regard to how the rights are labeled or\n categorized. For purposes of this Public License, the rights\n specified in Section 2(b)(1)-(2) are not Copyright and Similar\n Rights.\n .\n d. Effective Technological Measures means those measures that, in the\n absence of proper authority, may not be circumvented under laws\n fulfilling obligations under Article 11 of the WIPO Copyright\n Treaty adopted on December 20, 1996, and/or similar international\n agreements.\n .\n e. Exceptions and Limitations means fair use, fair dealing, and/or\n any other exception or limitation to Copyright and Similar Rights\n that applies to Your use of the Licensed Material.\n .\n f. Licensed Material means the artistic or literary work, database,\n or other material to which the Licensor applied this Public\n License.\n .\n g. Licensed Rights means the rights granted to You subject to the\n terms and conditions of this Public License, which are limited to\n all Copyright and Similar Rights that apply to Your use of the\n Licensed Material and that the Licensor has authority to license.\n .\n h. Licensor means the individual(s) or entity(ies) granting rights\n under this Public License.\n .\n i. Share means to provide material to the public by any means or\n process that requires permission under the Licensed Rights, such\n as reproduction, public display, public performance, distribution,\n dissemination, communication, or importation, and to make material\n available to the public including in ways that members of the\n public may access the material from a place and at a time\n individually chosen by them.\n .\n j. Sui Generis Database Rights means rights other than copyright\n resulting from Directive 96/9/EC of the European Parliament and of\n the Council of 11 March 1996 on the legal protection of databases,\n as amended and/or succeeded, as well as other essentially\n equivalent rights anywhere in the world.\n .\n k. You means the individual or entity exercising the Licensed Rights\n under this Public License. Your has a corresponding meaning.\n .\n Section 2 -- Scope.\n .\n a. License grant.\n .\n 1. Subject to the terms and conditions of this Public License,\n the Licensor hereby grants You a worldwide, royalty-free,\n non-sublicensable, non-exclusive, irrevocable license to\n exercise the Licensed Rights in the Licensed Material to:\n .\n a. reproduce and Share the Licensed Material, in whole or\n in part; and\n .\n b. produce, reproduce, and Share Adapted Material.\n .\n 2. Exceptions and Limitations. For the avoidance of doubt, where\n Exceptions and Limitations apply to Your use, this Public\n License does not apply, and You do not need to comply with\n its terms and conditions.\n .\n 3. Term. The term of this Public License is specified in Section\n 6(a).\n .\n 4. Media and formats; technical modifications allowed. The\n Licensor authorizes You to exercise the Licensed Rights in\n all media and formats whether now known or hereafter created,\n and to make technical modifications necessary to do so. The\n Licensor waives and/or agrees not to assert any right or\n authority to forbid You from making technical modifications\n necessary to exercise the Licensed Rights, including\n technical modifications necessary to circumvent Effective\n Technological Measures. For purposes of this Public License,\n simply making modifications authorized by this Section 2(a)\n (4) never produces Adapted Material.\n .\n 5. Downstream recipients.\n .\n a. Offer from the Licensor -- Licensed Material. Every\n recipient of the Licensed Material automatically\n receives an offer from the Licensor to exercise the\n Licensed Rights under the terms and conditions of this\n Public License.\n .\n b. No downstream restrictions. You may not offer or impose\n any additional or different terms or conditions on, or\n apply any Effective Technological Measures to, the\n Licensed Material if doing so restricts exercise of the\n Licensed Rights by any recipient of the Licensed\n Material.\n .\n 6. No endorsement. Nothing in this Public License constitutes or\n may be construed as permission to assert or imply that You\n are, or that Your use of the Licensed Material is, connected\n with, or sponsored, endorsed, or granted official status by,\n the Licensor or others designated to receive attribution as\n provided in Section 3(a)(1)(A)(i).\n .\n b. Other rights.\n .\n 1. Moral rights, such as the right of integrity, are not\n licensed under this Public License, nor are publicity,\n privacy, and/or other similar personality rights; however, to\n the extent possible, the Licensor waives and/or agrees not to\n assert any such rights held by the Licensor to the limited\n extent necessary to allow You to exercise the Licensed\n Rights, but not otherwise.\n .\n 2. Patent and trademark rights are not licensed under this\n Public License.\n .\n 3. To the extent possible, the Licensor waives any right to\n collect royalties from You for the exercise of the Licensed\n Rights, whether directly or through a collecting society\n under any voluntary or waivable statutory or compulsory\n licensing scheme. In all other cases the Licensor expressly\n reserves any right to collect such royalties.\n .\n Section 3 -- License Conditions.\n .\n Your exercise of the Licensed Rights is expressly made subject to the\n following conditions.\n .\n a. Attribution.\n .\n 1. If You Share the Licensed Material (including in modified\n form), You must:\n .\n a. retain the following if it is supplied by the Licensor\n with the Licensed Material:\n .\n i. identification of the creator(s) of the Licensed\n Material and any others designated to receive\n attribution, in any reasonable manner requested by\n the Licensor (including by pseudonym if\n designated);\n .\n ii. a copyright notice;\n .\n iii. a notice that refers to this Public License;\n .\n iv. a notice that refers to the disclaimer of\n warranties;\n .\n v. a URI or hyperlink to the Licensed Material to the\n extent reasonably practicable;\n .\n b. indicate if You modified the Licensed Material and\n retain an indication of any previous modifications; and\n .\n c. indicate the Licensed Material is licensed under this\n Public License, and include the text of, or the URI or\n hyperlink to, this Public License.\n .\n 2. You may satisfy the conditions in Section 3(a)(1) in any\n reasonable manner based on the medium, means, and context in\n which You Share the Licensed Material. For example, it may be\n reasonable to satisfy the conditions by providing a URI or\n hyperlink to a resource that includes the required\n information.\n .\n 3. If requested by the Licensor, You must remove any of the\n information required by Section 3(a)(1)(A) to the extent\n reasonably practicable.\n .\n 4. If You Share Adapted Material You produce, the Adapter's\n License You apply must not prevent recipients of the Adapted\n Material from complying with this Public License.\n .\n Section 4 -- Sui Generis Database Rights.\n .\n Where the Licensed Rights include Sui Generis Database Rights that\n apply to Your use of the Licensed Material:\n .\n a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n to extract, reuse, reproduce, and Share all or a substantial\n portion of the contents of the database;\n .\n b. if You include all or a substantial portion of the database\n contents in a database in which You have Sui Generis Database\n Rights, then the database in which You have Sui Generis Database\n Rights (but not its individual contents) is Adapted Material; and\n .\n c. You must comply with the conditions in Section 3(a) if You Share\n all or a substantial portion of the contents of the database.\n .\n For the avoidance of doubt, this Section 4 supplements and does not\n replace Your obligations under this Public License where the Licensed\n Rights include other Copyright and Similar Rights.\n .\n Section 5 -- Disclaimer of Warranties and Limitation of Liability.\n .\n a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n .\n b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n .\n c. The disclaimer of warranties and limitation of liability provided\n above shall be interpreted in a manner that, to the extent\n possible, most closely approximates an absolute disclaimer and\n waiver of all liability.\n .\n Section 6 -- Term and Termination.\n .\n a. This Public License applies for the term of the Copyright and\n Similar Rights licensed here. However, if You fail to comply with\n this Public License, then Your rights under this Public License\n terminate automatically.\n .\n b. Where Your right to use the Licensed Material has terminated under\n Section 6(a), it reinstates:\n .\n 1. automatically as of the date the violation is cured, provided\n it is cured within 30 days of Your discovery of the\n violation; or\n .\n 2. upon express reinstatement by the Licensor.\n .\n For the avoidance of doubt, this Section 6(b) does not affect any\n right the Licensor may have to seek remedies for Your violations\n of this Public License.\n .\n c. For the avoidance of doubt, the Licensor may also offer the\n Licensed Material under separate terms or conditions or stop\n distributing the Licensed Material at any time; however, doing so\n will not terminate this Public License.\n .\n d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n License.\n .\n Section 7 -- Other Terms and Conditions.\n .\n a. The Licensor shall not be bound by any additional or different\n terms or conditions communicated by You unless expressly agreed.\n .\n b. Any arrangements, understandings, or agreements regarding the\n Licensed Material not stated herein are separate from and\n independent of the terms and conditions of this Public License.\n .\n Section 8 -- Interpretation.\n .\n a. For the avoidance of doubt, this Public License does not, and\n shall not be interpreted to, reduce, limit, restrict, or impose\n conditions on any use of the Licensed Material that could lawfully\n be made without permission under this Public License.\n .\n b. To the extent possible, if any provision of this Public License is\n deemed unenforceable, it shall be automatically reformed to the\n minimum extent necessary to make it enforceable. If the provision\n cannot be reformed, it shall be severed from this Public License\n without affecting the enforceability of the remaining terms and\n conditions.\n .\n c. No term or condition of this Public License will be waived and no\n failure to comply consented to unless expressly agreed to by the\n Licensor.\n .\n d. Nothing in this Public License constitutes or may be interpreted\n as a limitation upon, or waiver of, any privileges and immunities\n that apply to the Licensor or You, including from the legal\n processes of any jurisdiction or authority.\n"
  },
  {
    "path": "czkawka_core/LICENSE_MIT",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "czkawka_core/README.md",
    "content": "# Czkawka Core\n\nCore of Czkawka GUI/CLI and Krokiet projects.\n"
  },
  {
    "path": "czkawka_core/benches/hash_calculation_benchmark.rs",
    "content": "use std::env::temp_dir;\nuse std::fs::File;\nuse std::hint::black_box;\nuse std::io::Write;\nuse std::path::PathBuf;\nuse std::sync::Arc;\n\nuse criterion::{Criterion, criterion_group, criterion_main};\nuse czkawka_core::common::model::HashType;\nuse czkawka_core::tools::duplicate::{DuplicateEntry, hash_calculation};\n\nfn setup_test_file(size: u64) -> PathBuf {\n    let path = temp_dir().join(\"test_file\");\n    let mut file = File::create(&path).expect(\"Failed to create test file\");\n    file.write_all(&vec![0u8; size as usize]).expect(\"Failed to write to test file\");\n    path\n}\n\nfn get_file_entry(size: u64) -> DuplicateEntry {\n    let path = setup_test_file(size);\n    DuplicateEntry {\n        path,\n        modified_date: 0,\n        size,\n        hash: String::new(),\n    }\n}\n\nfn benchmark_hash_calculation_vec<const FILE_SIZE: u64, const BUFFER_SIZE: usize>(c: &mut Criterion) {\n    let file_entry = get_file_entry(FILE_SIZE);\n    let function_name = format!(\"hash_calculation_vec_file_{FILE_SIZE}_buffer_{BUFFER_SIZE}\");\n\n    c.bench_function(&function_name, |b| {\n        b.iter(|| {\n            let mut buffer = vec![0u8; BUFFER_SIZE];\n            hash_calculation(\n                black_box(&mut buffer),\n                black_box(&file_entry),\n                black_box(HashType::Blake3),\n                &Arc::default(),\n                &Arc::default(),\n            )\n            .expect(\"Failed to calculate hash\");\n        });\n    });\n}\n\nfn benchmark_hash_calculation_arr<const FILE_SIZE: u64, const BUFFER_SIZE: usize>(c: &mut Criterion) {\n    let file_entry = get_file_entry(FILE_SIZE);\n    let function_name = format!(\"hash_calculation_arr_file_{FILE_SIZE}_buffer_{BUFFER_SIZE}\");\n\n    c.bench_function(&function_name, |b| {\n        b.iter(|| {\n            let mut buffer = [0u8; BUFFER_SIZE];\n            hash_calculation(\n                black_box(&mut buffer),\n                black_box(&file_entry),\n                black_box(HashType::Blake3),\n                &Arc::default(),\n                &Arc::default(),\n            )\n            .expect(\"Failed to calculate hash\");\n        });\n    });\n}\n\ncriterion_group!(benches,\n    benchmark_hash_calculation_vec<{16 * 1024 * 1024}, {16 * 1024}>,\n    benchmark_hash_calculation_vec<{16 * 1024 * 1024}, {1024 * 1024}>,\n    benchmark_hash_calculation_arr<{16 * 1024 * 1024}, {16 * 1024}>,\n    benchmark_hash_calculation_arr<{16 * 1024 * 1024}, {1024 * 1024}>,\n);\ncriterion_main!(benches);\n"
  },
  {
    "path": "czkawka_core/build.rs",
    "content": "fn main() {\n    let rust_version = match rustc_version::version_meta() {\n        Ok(meta) => {\n            let rust_v = meta.semver.to_string();\n            let rust_date = meta.commit_date.unwrap_or_default();\n            format!(\"{rust_v} ({rust_date})\")\n        }\n        Err(_) => \"<unknown>\".to_string(),\n    };\n    println!(\"cargo:rustc-env=RUST_VERSION_INTERNAL={rust_version}\");\n\n    if let Ok(encoded) = std::env::var(\"CARGO_ENCODED_RUSTFLAGS\") {\n        println!(\"cargo:rustc-env=UUSED_RUSTFLAGS={encoded}\");\n    }\n\n    // Get Git commit hash\n    let git_commit = std::process::Command::new(\"git\")\n        .args([\"rev-parse\", \"HEAD\"])\n        .output()\n        .ok()\n        .and_then(|output| if output.status.success() { String::from_utf8(output.stdout).ok() } else { None })\n        .map_or_else(|| \"<unknown>\".to_string(), |s| s.trim().to_string());\n    println!(\"cargo:rustc-env=CZKAWKA_GIT_COMMIT={git_commit}\");\n\n    // Get short Git commit hash\n    let git_commit_short = if git_commit != \"<unknown>\" && git_commit.len() >= 10 {\n        git_commit.chars().take(10).collect::<String>()\n    } else {\n        git_commit\n    };\n    println!(\"cargo:rustc-env=CZKAWKA_GIT_COMMIT_SHORT={git_commit_short}\");\n\n    // Commit date\n    let git_commit_date = std::process::Command::new(\"git\")\n        .args([\"log\", \"-1\", \"--format=%cd\", \"--date=format:%Y-%m-%d\"])\n        .output()\n        .ok()\n        .and_then(|output| if output.status.success() { String::from_utf8(output.stdout).ok() } else { None })\n        .map_or_else(|| \"<unknown>\".to_string(), |s| s.trim().to_string());\n    println!(\"cargo:rustc-env=CZKAWKA_GIT_COMMIT_DATE={git_commit_date}\");\n\n    // Official build flag\n    if std::env::var(\"CZKAWKA_OFFICIAL_BUILD\") == Ok(\"1\".to_string()) {\n        println!(\"cargo:rustc-env=CZKAWKA_OFFICIAL_BUILD=1\");\n    } else {\n        println!(\"cargo:rustc-env=CZKAWKA_OFFICIAL_BUILD=0\");\n    }\n\n    let using_cranelift =\n        std::env::var(\"CARGO_PROFILE_RELEASE_CODEGEN_UNITS\") == Ok(\"1\".to_string()) || std::env::var(\"CARGO_PROFILE_DEV_CODEGEN_BACKEND\") == Ok(\"cranelift\".to_string());\n\n    if using_cranelift {\n        println!(\"cargo:rustc-env=USING_CRANELIFT=1\");\n    }\n\n    if cfg!(target_os = \"linux\") {\n        if let Ok(ver) = glibc_musl_version::get_os_libc_versions() {\n            println!(\"cargo:rustc-env=CZKAWKA_LIBC_VERSIONS={ver}\");\n        }\n\n        if cfg!(target_env = \"gnu\") {\n            println!(\"cargo:rustc-env=CZKAWKA_LIBC=glibc\");\n        } else if cfg!(target_env = \"musl\") {\n            println!(\"cargo:rustc-env=CZKAWKA_LIBC=musl\");\n        } else {\n            println!(\"cargo:rustc-env=CZKAWKA_LIBC=unknown\");\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/i18n/ar/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = الأصل\ncore_similarity_very_high = عالية جدا\ncore_similarity_high = مرتفع\ncore_similarity_medium = متوسط\ncore_similarity_small = صغير\ncore_similarity_very_small = صغير جدا\ncore_similarity_minimal = الحد الأدنى\ncore_cannot_open_dir = لا يمكن فتح dir { $dir }، السبب { $reason }\ncore_cannot_read_entry_dir = لا يمكن قراءة الإدخال في dir { $dir }، السبب { $reason }\ncore_cannot_read_metadata_dir = لا يمكن قراءة البيانات الوصفية في dir { $dir }، السبب { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = يبدو أن الملف { $name } قد تم تعديله قبل يونكس Epoch\ncore_folder_modified_before_epoch = يبدو أن المجلد { $name } قد تم تعديله قبل يونكس Epoch\ncore_file_no_modification_date = غير قادر على الحصول على تاريخ التعديل من الملف { $name }، السبب { $reason }\ncore_folder_no_modification_date = غير قادر على الحصول على تاريخ التعديل من المجلد { $name }، السبب { $reason }\ncore_cannot_start_scan_no_included_paths = لا يمكن بدء المسح، لأن لا توجد مسارات مضمنة\ncore_skip_exist_check_all_included_paths_nonexistent = لا يمكن بدء المسح، لأن جميع المسارات المدرجة غير موجودة\ncore_missing_no_chosen_included_path = لم يتم اختيار مسار مضمن صالح (كانت المسارات المضمنة المستبعدة تستبعد جميع المسارات المضمنة)\ncore_reference_included_paths_same = لا يمكن بدء المسح حيث تكون جميع المسارات المدرجة الصالحة أيضًا مسارات مرجعية، حاول التحقق من الصحة أو تعطيل المسارات المرجعية\ncore_path_must_exists = يجب أن يكون المسار المقدم موجودًا، مع تجاهل { $path }\ncore_must_be_directory_or_file = يجب أن يشير المسار المقدم إلى دليل أو ملف صالح، مع تجاهل { $path }\ncore_excluded_paths_pointless_slash = باستثناء / لا معنى له، لأنه يعني عدم فحص الملفات\ncore_paths_unable_to_get_device_id = تعذر الحصول على معرف الجهاز من المجلد { $path }\ncore_needs_allowed_extensions_limited_by_tool = لا يمكن بدء المسح، عندما تم استبعاد جميع الإضافات المتاحة في هذا الأداة ({ $extensions }) من المسح\ncore_needs_allowed_extensions = لا يمكن بدء المسح، عندما تم استبعاد جميع الإضافات من المسح\ncore_needs_to_set_at_least_one_broken_option = لا يمكن بدء المسح، عندما لا يتم تعيين خيار \"معطل\" للمسح\ncore_needs_to_set_at_least_one_bad_name_option = لا يمكن بدء المسح، عندما لا يتم تعيين خيار الاسم السيئ للبحث عنه\ncore_ffmpeg_not_found = لا يمكن العثور على تثبيت مناسب لـ FFmpeg أو FFprobe. هذه برامج خارجية يجب تثبيتها يدويًا.\ncore_ffmpeg_not_found_windows = تأكد من أن ffmpeg.exe و ffprobe.exe متوفرتان في PATH أو يتم وضعهما مباشرة في نفس المجلد مع التطبيق القابل للتنفيذ\ncore_invalid_symlink_infinite_recursion = التكرار اللامتناهي\ncore_invalid_symlink_non_existent_destination = ملف الوجهة غير موجود\ncore_messages_limit_reached_characters = تجاوز عدد الرسائل الحد الأقصى المحدد ({ $current }/{ $limit } حرفا)، لذا تم اقتطاع الخروج. لقراءة الإخراج الكامل، قم بتعطيل خيار الحد في الإعدادات.\ncore_messages_limit_reached_lines = تجاوز عدد الرسائل الحد الأقصى المحدد ({ $current }/{ $limit } سطر)، لذا تم اقتطاع الخروج. لقراءة الإخراج الكامل، قم بتعطيل خيار الحد في الإعدادات.\ncore_error_moving_to_trash = خطأ أثناء نقل \"{ $file }\" إلى سلة المحذوفات: { $error }\ncore_error_removing = خطأ أثناء حذف \"{ $file }\": { $error }\ncore_no_similarity_method_selected = لا يمكن العثور على ملفات موسيقية مماثلة بدون طريقة تشابه محددة\ncore_failed_to_spawn_command = فشل أمر الإطلاق: { $reason }\ncore_failed_to_check_process_status = فشل التحقق من حالة العملية: { $reason }\ncore_failed_to_wait_for_process = فشل الانتظار للعملية: { $reason }\ncore_failed_to_read_video_properties = فشل في قراءة خصائص الفيديو: { $reason }\ncore_failed_to_execute_ffmpeg = فشل تنفيذ ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = فشل ffmpeg مع الحالة { $status }: { $stderr } (الأمر: { $command })\ncore_failed_to_load_image_frame = فشل تحميل إطار الصورة: { $reason }\ncore_failed_to_extract_frame = فشل استخراج الإطار في { $time } ثانية من \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = فشل حفظ썸 فين لـ \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = فشل في الحصول على الإطار في الطابع الزمني { $timestamp } من \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = فشل في الحصول على الإطار من \"{ $file }\" في الطابع الزمني { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = غير صالح مستطيل المحصول: يسار={ $left }، أعلى={ $top }، يمين={ $right }، أسفل={ $bottom }\ncore_failed_to_crop_video_file = فشل قص الفيديو \"{ $file }\": { $reason }\ncore_cropped_video_not_created = الملف المرئي المقتطع لم يتم إنشاؤه: { $temp }\ncore_unable_check_hash_of_file = تعذر التحقق من تجزئة الملف \"{ $file }\"، والسبب { $reason }\ncore_error_checking_hash_of_file = حدث خطأ عند التحقق من تجزئة الملف \"{ $file }\"، السبب { $reason }\ncore_image_zero_dimensions = الصورة لها عرض أو ارتفاع يساوي صفر \"{ $path }\"\ncore_image_open_failed = لا يمكن فتح ملف الصورة \"{ $path }\": { $reason }\ncore_not_directory_remove = محاولة إزالة المجلد \"{ $path }\" والذي ليس مجلدًا\ncore_cannot_read_directory = لا يمكن قراءة الدليل \"{ $path }\"\ncore_cannot_read_entry_from_directory = لا يمكن قراءة الإدخال من الدليل \"{ $path }\"\ncore_folder_contains_file_inside = المجلد يحتوي على الملف \"{ $entry }\" داخل \"{ $folder }\"\ncore_unknown_directory_entry = تعذر تحديد نوع الملف لإدخال الدليل \"{ $entry }\" داخل \"{ $path }\"\ncore_video_width_exceeds_limit = عرض الفيديو { $width } يتجاوز الحد الأقصى لـ { $limit }\ncore_video_height_exceeds_limit = فيديو الارتفاع { $height } يتجاوز الحد الأقصى لـ { $limit }\ncore_failed_to_process_video = فشل معالجة ملف الفيديو { $file }: { $reason }\ncore_optimized_file_larger = الملف المحسن { $optimized } (الحجم: { $new_size }) ليس أصغر من الأصلي { $original } (الحجم: { $original_size })\ncore_unknown_codec = ترميز غير معروف: { $codec }\ncore_invalid_video_optimizer_mode = وضع مُحسِّن الفيديو غير صالح: '{ $mode }'. القيم المسموح بها: transcode, crop\ncore_folder_does_not_exist = المجلد غير موجود: { $folder }\ncore_path_not_directory = المسار ليس مجلدًا: { $folder }\ncore_test_error_for_folder = خطأ في الاختبار للمجلد: { $folder }\ncore_unknown_exif_tag_group = مجموعة بيانات EXIF غير المعروفة: { $tag }\ncore_error_comparing_fingerprints = خطأ أثناء مقارنة بصمات الأصابع: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = فشل إنشاء صورة مصغرة لـ \"{ $file }\": الصور المستخرجة لها أبعاد مختلفة\ncore_failed_to_generate_thumbnail = فشل إنشاء الصورة المصغرة لـ \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = فشل استخراج الإطار في { $time } ثانية من \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = الملف المرئي غير موجود (يمكن حذفه بين المسح/الخطوات اللاحقة): \"{ $path }\"\ncore_image_too_large = الصورة كبيرة جداً ({ $width }x{ $height }) - أكثر من المدعوم { $max } بكسل\ncore_failed_to_get_video_metadata = فشل الحصول على بيانات الفيديو للملف \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = فشل الحصول على ترميز الفيديو للملف \"{ $file }\"\ncore_failed_to_get_video_duration = تعذر الحصول على مدة الفيديو للملف \"{ $file }\"\ncore_failed_to_get_video_dimensions = فشل الحصول على أبعاد الفيديو للملف \"{ $file }\"\ncore_frame_dimensions_mismatch = أبعاد الإطار لعلامة الوقت { $timestamp } لا تتطابق مع أبعاد الإطار الأول ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = تعذر تحميل البيانات من ملف التخزين المؤقت { $file }، والسبب { $reason }\ncore_failed_to_load_data_from_json_cache = تعذر تحميل البيانات من ملف ذاكرة التخزين المؤقت JSON { $file}، والسبب { $reason}\ncore_failed_to_replace_with_optimized = فشل استبدال الملف \"{ $file }\" بإصدار مُحسّن: { $reason }\ncore_failed_to_write_data_to_cache = لا يمكن كتابة البيانات إلى ملف التخزين المؤقت \"{ $file }\"، والسبب { $reason }\ncore_properly_saved_cache_entries = تم حفظها بشكل صحيح في الملف { $count } إدخالات ذاكرة تخزين مؤقت.\ncore_video_processing_stopped_by_user = تم إيقاف معالجة الفيديو بواسطة المستخدم\ncore_thumbnail_generation_stopped_by_user = إنشاء الصور المصغرة توقف بواسطة المستخدم\ncore_failed_to_optimize_video = فشل تحسين الفيديو \"{ $file }\": { $reason }\ncore_failed_to_crop_video = فشل قص الفيديو \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = فشل الحصول على بيانات التعريف للملف المحسن \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = لا يمكن إنشاء مجلد الإعدادات \"{ $folder }\"، والسبب { $reason }\ncore_cannot_create_cache_folder = لا يمكن إنشاء مجلد ذاكرة التخزين المؤقت \"{ $folder }\"، والسبب { $reason }\ncore_cannot_create_or_open_cache_file = لا يمكن إنشاء أو فتح ملف التخزين المؤقت \"{ $file }\"، والسبب { $reason }\ncore_cannot_set_config_cache_path = لا يمكن تعيين مسار التكوين/التخزين المؤقت - لن يتم استخدام التكوين والتخزين المؤقت.\ncore_invalid_extension_contains_space = { $extension } ليس امتدادًا صالحًا لأنه يحتوي على مساحة فارغة داخلية\ncore_invalid_extension_contains_dot = { $extension } ليس امتدادًا صالحًا لأنه يحتوي على نقطة داخلية\n"
  },
  {
    "path": "czkawka_core/i18n/bg/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Оригинален\ncore_similarity_very_high = Много високо\ncore_similarity_high = Високо\ncore_similarity_medium = Средно\ncore_similarity_small = Малко\ncore_similarity_very_small = Много малък\ncore_similarity_minimal = Минимално\ncore_cannot_open_dir = Не може да се отвори папка { $dir }, причината е { $reason }\ncore_cannot_read_entry_dir = Не може да се прочете папка { $dir }, причината е { $reason }\ncore_cannot_read_metadata_dir = Не могат да се прочетат мета-данните в папка { $dir }, причината е { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch\ncore_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch\ncore_file_no_modification_date = Невъзможно е да се извлече дата на промяна от файл { $name }, причината е { $reason }\ncore_folder_no_modification_date = Невъзможно е да се извлече дата на промяна от папка { $name }, причината е { $reason }\ncore_cannot_start_scan_no_included_paths = Не може да се стартира сканирането, защото няма включени пътища\ncore_skip_exist_check_all_included_paths_nonexistent = Не може да се стартира сканирането, защото всички включени пътища не съществуват\ncore_missing_no_chosen_included_path = Няма избран валиден път, включен (изключените пътища биха могли да са изключили всички включени пътища)\ncore_reference_included_paths_same = Не може да се стартира сканиране, където всички валидни включени пътища са също и препратени пътища, опитайте се да валидирате или да деактивирате препратените пътища\ncore_path_must_exists = Предоставеният път трябва да съществува, пренебрегвайки { $path }\ncore_must_be_directory_or_file = Предоставеният път трябва да сочи валиден директория или файл, пренебрегвайки { $path }\ncore_excluded_paths_pointless_slash = Изключването / е безсмислено, защото означава, че няма да бъдат сканирани файлове\ncore_paths_unable_to_get_device_id = Не може да се получи идентификатор на устройството от папката { $path }\ncore_needs_allowed_extensions_limited_by_tool = Не може да се стартира сканирането, когато всички налични разширения в този инструмент ({ $extensions }) бяха изключени от сканирането\ncore_needs_allowed_extensions = Не може да се стартира сканирането, когато всички разширения са били изключени от сканирането\ncore_needs_to_set_at_least_one_broken_option = Не може да се стартира сканиране, когато не е зададена опция за сканиране на повредени\ncore_needs_to_set_at_least_one_bad_name_option = Не може да се стартира сканирането, когато не е зададена опцията за лошо име за сканиране\ncore_ffmpeg_not_found = Не може да се намери подходяща инсталация на FFmpeg или FFprobe. Те са външни програми, които трябва да бъдат инсталирани ръчно.\ncore_ffmpeg_not_found_windows = Бъдете сигурни, че ffmpeg.exe и ffprobe.exe са налични в PATH или са разположени напрямок в същата папка като изпълнимият файл на апplикацията\ncore_invalid_symlink_infinite_recursion = Безкрайна рекурсия\ncore_invalid_symlink_non_existent_destination = Несъществуващ дестинационен файл\ncore_messages_limit_reached_characters = Number of messages exceeded the set limit ({ $current }/{ $limit } characters), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_messages_limit_reached_lines = Number of messages exceeded the set limit ({ $current }/{ $limit } lines), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_error_moving_to_trash = Грешка при преместване на \"{ $file }\" в кош: { $error }\ncore_error_removing = Огледална грешка при изтриване на \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Не можете да намерите подобри музикални файлове без избран метод за сличност\ncore_failed_to_spawn_command = Не успя да стартира командата: { $reason }\ncore_failed_to_check_process_status = Не успях да проверя статуса на процеса: { $reason }\ncore_failed_to_wait_for_process = Не успях да изчакам процеса: { $reason }\ncore_failed_to_read_video_properties = Не успях да прочета свойствата на видеото: { $reason }\ncore_failed_to_execute_ffmpeg = Не успях да изпълня ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg се провали с статус { $status }: { $stderr } (команда: { $command })\ncore_failed_to_load_image_frame = Не успях да заредя кадъра на изображението: { $reason }\ncore_failed_to_extract_frame = Не успях да извлека кадъра на { $time } секунди от \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Не успях да запазя миниатюра за \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Не успях да получа кадъра в момента { $timestamp } от \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Не успях да получа кадъра от \"{ $file }\" в момента { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Невалиден правоъгълник на отрязък: ляво={ $left }, горно={ $top }, дясно={ $right }, долно={ $bottom }\ncore_failed_to_crop_video_file = Не успях да изрежа видео файла \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Отреденият видео файл не беше създаден: { $temp }\ncore_unable_check_hash_of_file = Не може да се провери хеша на файла \"{ $file }\", причина { $reason }\ncore_error_checking_hash_of_file = Грешка възникна при проверка на хеша на файла \"{ $file }\", причина { $reason }\ncore_image_zero_dimensions = Изображението има нулева ширина или височина \"{ $path }\"\ncore_image_open_failed = Не може да се отвори файла с изображение \"{ $path }\": { $reason }\ncore_not_directory_remove = Опитвам да премахна папката \"{ $path }\" която не е директория\ncore_cannot_read_directory = Не може да се прочете директорията \"{ $path }\"\ncore_cannot_read_entry_from_directory = Не мога да прочета записа от директорията \"{ $path }\"\ncore_folder_contains_file_inside = Папка съдържа файл \"{ $entry }\" в \"{ $folder }\"\ncore_unknown_directory_entry = Не може да се определи типа на файла на входа на директорията \"{ $entry }\" в \"{ $path }\"\ncore_video_width_exceeds_limit = Видео ширина { $width } надхвърля лимита на { $limit }\ncore_video_height_exceeds_limit = Видео височина { $height } надхвърля лимита от { $limit }\ncore_failed_to_process_video = Не успях да обработя видео файла { $file }: { $reason }\ncore_optimized_file_larger = Оптимизиран файл { $optimized } (размер: { $new_size }) не е по-малък от оригиналния { $original } (размер: { $original_size })\ncore_unknown_codec = Неизвестен кодек: { $codec }\ncore_invalid_video_optimizer_mode = Невалиден режим на оптимизация на видео: '{ $mode }'. Разрешени стойности: transcode, crop\ncore_folder_does_not_exist = Папка не съществува: { $folder }\ncore_path_not_directory = Пътят не е директория: { $folder }\ncore_test_error_for_folder = Грешка при тест за папка: { $folder }\ncore_unknown_exif_tag_group = Неизвестна EXIF група таг: { $tag }\ncore_error_comparing_fingerprints = Грешка при сравняване на пръстови отпечатъци: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Не успях да генерирам миниатюра за \"{ $file }\": извлечените кадри имат различни размери\ncore_failed_to_generate_thumbnail = Не успях да генерирам миниатюра за \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Не успях да извлека кадъра на { $time } секунди от \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Видео файлът не съществува (може да бъде премахнат между сканирането/по-късните стъпки): \"{ $path }\"\ncore_image_too_large = Изображението е твърде голямо ({ $width }x{ $height }) - повече от поддържаните { $max } пиксела\ncore_failed_to_get_video_metadata = Не успях да получа метаданните на видеото за файла \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Не успях да получа видео кодек за файла \"{ $file }\"\ncore_failed_to_get_video_duration = Не успях да получа продължителността на видеото за файла \"{ $file }\"\ncore_failed_to_get_video_dimensions = Не успях да получа размерите на видеото за файла \"{ $file }\"\ncore_frame_dimensions_mismatch = Размерите на кадъра за времето { $timestamp } не съвпадат с размерите на първия кадър ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Не успях да заредя данните от кеш файла { $file }, причина { $reason }\ncore_failed_to_load_data_from_json_cache = Не успях да заредя данните от JSON кеш файла { $file }, причина { $reason }\ncore_failed_to_replace_with_optimized = Не успях да заменя файла \"{ $file }\" с оптимизираната версия: { $reason }\ncore_failed_to_write_data_to_cache = Не може да се запише данни към кеш файла \"{ $file }\", причина { $reason }\ncore_properly_saved_cache_entries = Правилно запазени в файл { $count } кеш записи.\ncore_video_processing_stopped_by_user = Видео обработката беше спряна от потребителя\ncore_thumbnail_generation_stopped_by_user = Генерирането на миниатюри беше спряно от потребителя\ncore_failed_to_optimize_video = Не успях да оптимизирам видео \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Не успя да се изреже видеото \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Не успях да получа метаданните на оптимизирания файл \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Не може да се създаде папка с конфигурация \"{ $folder }\", причина { $reason }\ncore_cannot_create_cache_folder = Не може да се създаде кешираща папка \"{ $folder }\", причина { $reason }\ncore_cannot_create_or_open_cache_file = Не може да се създаде или отвори кеширания файл \"{ $file }\", причина { $reason }\ncore_cannot_set_config_cache_path = Не може да се зададе път към config/cache - config и cache няма да бъдат използвани.\ncore_invalid_extension_contains_space = { $extension } не е валиден разширение, защото съдържа празно пространство вътре\ncore_invalid_extension_contains_dot = { $extension } не е валиден разширение, защото съдържа точка вътре\n"
  },
  {
    "path": "czkawka_core/i18n/cs/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Originál\ncore_similarity_very_high = Velmi vysoká\ncore_similarity_high = Vysoká\ncore_similarity_medium = Střední\ncore_similarity_small = Malá\ncore_similarity_very_small = Velmi malá\ncore_similarity_minimal = Minimální\ncore_cannot_open_dir = Nelze otevřít adresář { $dir }, důvod { $reason }\ncore_cannot_read_entry_dir = Nelze načíst záznam v adresáři { $dir }, důvod { $reason }\ncore_cannot_read_metadata_dir = Nelze načíst metadata v adresáři { $dir }, důvod { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Soubor { $name } se zdá být před Unix Epoch upraven\ncore_folder_modified_before_epoch = Složka { $name } se zdá být upravena před Unix Epoch\ncore_file_no_modification_date = Nelze získat datum úpravy ze souboru { $name }, důvod { $reason }\ncore_folder_no_modification_date = Nelze získat datum úpravy ze složky { $name }, důvod { $reason }\ncore_cannot_start_scan_no_included_paths = Nemožno spustit skenování, protože nejsou zahrnuty žádné cesty\ncore_skip_exist_check_all_included_paths_nonexistent = Nemožno zahájit skenování, protože všechny zahrnuté cesty neexistují\ncore_missing_no_chosen_included_path = Neosáhlý zahrnutý cíl nebyl vybrán (vyloučený cesty mohly vyloučit všechny zahrnuté cesty)\ncore_reference_included_paths_same = Nelze spustit sken, kde jsou všechny platné zahrnuté cesty také odkazované cesty, zkuste ověřit nebo vypnout odkazované cesty\ncore_must_be_directory_or_file = Zadaná cesta musí ukazovat na platný adresář nebo soubor, ignoruje { $path }\ncore_excluded_paths_pointless_slash = Vyloučení / je zbytečné, protože to znamená, že nebude naskenováno žádných souborů\ncore_paths_unable_to_get_device_id = Nemožno získat ID zařízení z adresáře { $path }\ncore_needs_allowed_extensions_limited_by_tool = Nelze spustit sken, když byly všechny dostupné rozšíření v tomto nástroji ({ $extensions }) vyloučeny ze skenu\ncore_needs_allowed_extensions = Nedaří se spustit sken, když byly všechny rozšíření vyloučeny ze skenu\ncore_needs_to_set_at_least_one_broken_option = Nemožno spustit sken, ak nie je nastavená možnosť zlomeného skenovania\ncore_needs_to_set_at_least_one_bad_name_option = Nemožné spustit sken, pokud není nastavená možnost špatného jména pro skenování\ncore_ffmpeg_not_found = Nemohu najít správnou instalaci FFmpeg nebo FFprobe. Jedná se o externí programy, které je třeba nainstalovat ručně.\ncore_ffmpeg_not_found_windows = Ujistěte se, že ffmpeg.exe a ffprobe.exe jsou k dispozici v PATH nebo jsou umístěny přímo ve stejné složce jako spustitelný soubor aplikace\ncore_invalid_symlink_infinite_recursion = Nekonečná rekurze\ncore_invalid_symlink_non_existent_destination = Neexistující cílový soubor\ncore_messages_limit_reached_characters = Počet zpráv překročil nastavený limit ({ $current }/{ $limit } znaků), takže výstup byl zkrácen. Chcete-li číst celý výstup, zakažte v nastavení omezovací možnost.\ncore_messages_limit_reached_lines = Počet zpráv překročil nastavený limit ({ $current }/{ $limit } řádky), takže výstup byl zkrácen. Chcete-li číst celý výstup, zakažte v nastavení omezovací možnost.\ncore_error_moving_to_trash = Chyba při přesouvání \"{ $file }\" do koše: { $error }\ncore_error_removing = Chyba při odstraňování \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Nemůže najít podobné hudební soubory bez vybrané metody podobnosti\ncore_failed_to_spawn_command = Selhalo spuštění příkazu: { $reason }\ncore_failed_to_wait_for_process = Selhalo čekání na proces: { $reason }\ncore_failed_to_read_video_properties = Selhalo načtení vlastností videa: { $reason }\ncore_failed_to_execute_ffmpeg = Selhalo provedení ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg selhal s stavem { $status }: { $stderr } (příkaz: { $command })\ncore_failed_to_load_image_frame = Selhalo načtení snímku obrazu: { $reason }\ncore_failed_to_extract_frame = Selhalo načtení snímku v { $time } sekundách z \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Selhalo uložení miniatury pro \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Selhalo získání snímku v časové značce { $timestamp } z \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Selhalo získání snímku z \"{ $file }\" v časové značce { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Neplatná obdélníková oblast pro zrání: levý={ $left }, horní={ $top }, pravý={ $right }, dolní={ $bottom }\ncore_failed_to_crop_video_file = Selhalo oříznutí video souboru \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Zkrácený video soubor nebyl vytvořen: { $temp }\ncore_unable_check_hash_of_file = Nemožno zkontrolovat soubor \"{ $file }\", důvod { $reason }\ncore_error_checking_hash_of_file = Chyba nastala při kontrole souhrnu souboru \"{ $file }\", důvod { $reason }\ncore_image_zero_dimensions = Obraz má nulovou šířku nebo výšku \"{ $path }\"\ncore_image_open_failed = Nemožno otevřít soubor s obrázkem \"{ $path }\": { $reason }\ncore_not_directory_remove = Pokouším se odstranit složku \"{ $path }\" která není adresář\ncore_cannot_read_directory = Nelze číst adresář \"{ $path }\"\ncore_cannot_read_entry_from_directory = Nelze přečíst záznam z adresáře \"{ $path }\"\ncore_folder_contains_file_inside = Složka obsahuje soubor \"{ $entry }\" uvnitř \"{ $folder }\"\ncore_unknown_directory_entry = Nemožno určit typ súboru záznamu adresára \"{ $entry }\" v \"{ $path }\"\ncore_video_width_exceeds_limit = Video šířka { $width } překračuje limit { $limit }\ncore_video_height_exceeds_limit = Video výška { $height } překračuje limit { $limit }\ncore_failed_to_process_video = Selhalo zpracování video souboru { $file }: { $reason }\ncore_optimized_file_larger = Optimalizovaný soubor { $optimized } (velikost: { $new_size }) není menší než originální { $original } (velikost: { $original_size })\ncore_unknown_codec = Neznámý kodek: { $codec }\ncore_invalid_video_optimizer_mode = Neplatý režim optimalizátoru videa: '{ $mode }'. Umožněné hodnoty: transkodovat, oříznout\ncore_folder_does_not_exist = Složka neexistuje: { $folder }\ncore_path_not_directory = Cesta není adresář: { $folder }\ncore_test_error_for_folder = Test chyba pro složku: { $folder }\ncore_unknown_exif_tag_group = Neznámá EXIF skupina značek: { $tag }\ncore_error_comparing_fingerprints = Chyba při porovnávání otisků prstů: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Selhalo generování miniatury pro \"{ $file }\": extrahované snímky mají různé rozměry\ncore_failed_to_generate_thumbnail = Selhalo při generování miniatury pro \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Selhalo načtení snímku v { $time } sekundách z \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Video soubor neexistuje (může být odstraněn mezi skenem/pozdějšími kroky): \"{ $path }\"\ncore_image_too_large = Obraz je příliš velký ({ $width }x{ $height }) - více než podporovaných { $max } pixelů\ncore_failed_to_get_video_metadata = Selhalo načtení metadat videa pro soubor \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Selhalo načtení video kódu pro soubor \"{ $file }\"\ncore_failed_to_get_video_duration = Selhalo načtení délky videa pro soubor \"{ $file }\"\ncore_failed_to_get_video_dimensions = Selhalo načtení rozměrů souboru \"{ $file }\"\ncore_frame_dimensions_mismatch = Rozměry snímku pro časové razítko { $timestamp } se neshodují s rozměry prvního snímku ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Nepodařilo se načíst data z mezipaměťového souboru { $file }, důvod { $reason }\ncore_failed_to_load_data_from_json_cache = Nepodařilo se načíst data z JSON mezipaměťového souboru { $file }, důvod { $reason }\ncore_failed_to_replace_with_optimized = Selhalo při nahrazení souboru \"{ $file }\" optimalizovanou verzí: { $reason }\ncore_failed_to_write_data_to_cache = Nelze zapisovat data do souboru dočasné paměti \"{ $file }\", důvod { $reason }\ncore_properly_saved_cache_entries = Správně uloženo do souboru { $count } políček mezipaměti.\ncore_video_processing_stopped_by_user = Video zpracování bylo uživatelem zastaveno\ncore_thumbnail_generation_stopped_by_user = Generování miniatur bylo zastaveno uživatelem\ncore_failed_to_optimize_video = Selhalo při optimalizaci videa \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Selhalo oříznutí videa \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Selhalo načtení metadat optimalizovaného souboru \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Nemožno vytvořit složku „{ $folder }“, důvod { $reason }\ncore_cannot_create_cache_folder = Nemůže být vytvořena složka pro ukládání \"{ $folder }\", důvod { $reason }\ncore_cannot_create_or_open_cache_file = Nemožno vytvořit nebo otevřít mezipaměťový soubor \"{ $file }\", důvod { $reason }\ncore_cannot_set_config_cache_path = Nelze nastavit cestu k souboru s konfigurací/cache - konfigurace a cache nebude použity.\ncore_invalid_extension_contains_space = { $extension } není platná přípona, protože obsahuje prázdné místo uvnitř\ncore_invalid_extension_contains_dot = { $extension } není platná přípona, protože obsahuje tečku uvnitř\n\ncore_path_must_exists = Zadaná cesta musí existovat, bez ohledu na { $path }\ncore_failed_to_check_process_status = Nepodařilo se zkontrolovat stav procesu: { $reason }"
  },
  {
    "path": "czkawka_core/i18n/de/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Original\ncore_similarity_very_high = Sehr Hoch\ncore_similarity_high = Hoch\ncore_similarity_medium = Mittel\ncore_similarity_small = Klein\ncore_similarity_very_small = Sehr klein\ncore_similarity_minimal = Minimalistisch\ncore_cannot_open_dir = Verzeichnis { $dir } kann nicht geöffnet werden, Grund { $reason }\ncore_cannot_read_entry_dir = Kann Eintrag in Verzeichnis { $dir } nicht lesen, Grund { $reason }\ncore_cannot_read_metadata_dir = Metadaten können in Verzeichnis { $dir } nicht gelesen werden, Grund { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Datei { $name } scheint vor der Unix-Epoche geändert worden zu sein\ncore_folder_modified_before_epoch = Ordner { $name } scheint vor der Unix-Epoche geändert worden zu sein\ncore_file_no_modification_date = Konnte das Änderungsdatum von Datei { $name } nicht abrufen, Grund { $reason }\ncore_folder_no_modification_date = Konnte das Änderungsdatum aus dem Ordner { $name } nicht abrufen, Grund { $reason }\ncore_cannot_start_scan_no_included_paths = Kann den Scan nicht starten, da keine enthaltenen Pfade vorhanden sind\ncore_skip_exist_check_all_included_paths_nonexistent = Kann den Scan nicht starten, da alle enthaltenen Pfade nicht existieren\ncore_missing_no_chosen_included_path = Kein gültiger einzubander Pfad ausgewählt (ausgeschlossene Pfade hätten alle einzubanderten Pfade ausschließen können)\ncore_reference_included_paths_same = Kann den Scan nicht starten, wo alle gültigen eingeschlossenen Pfade auch referenzierte Pfade sind, bitte validieren oder die referenzierten Pfade deaktivieren\ncore_excluded_paths_pointless_slash = Ausgeschlossen / ist zwecklos, weil es bedeutet, dass keine Dateien gescannt werden\ncore_needs_allowed_extensions_limited_by_tool = Kann den Scan nicht starten, wenn alle verfügbaren Erweiterungen in diesem Tool ({ $extensions }) vom Scan ausgeschlossen wurden\ncore_needs_allowed_extensions = Kann den Scan nicht starten, wenn alle Erweiterungen vom Scan ausgeschlossen wurden\ncore_needs_to_set_at_least_one_broken_option = Kann den Scan nicht starten, wenn keine Option „defektes Element“ zum Scannen festgelegt ist\ncore_needs_to_set_at_least_one_bad_name_option = Kann den Scan nicht starten, wenn keine Option „schlechter Name“ zum Scannen festgelegt ist\ncore_ffmpeg_not_found = Kann die korrekte Installation von FFmpeg oder FFprobe nicht finden. Dies sind externe Programme, die manuell installiert werden müssen.\ncore_ffmpeg_not_found_windows = Stellen Sie sicher, dass ffmpeg.exe und ffprobe.exe in PATH verfügbar sind oder direkt im selben Ordner wie die Programmdatei der App liegen\ncore_invalid_symlink_infinite_recursion = Endlose Rekursion\ncore_invalid_symlink_non_existent_destination = Nicht existierende Zieldatei\ncore_messages_limit_reached_characters = Anzahl der Nachrichten überschritten die festgelegte Grenze ({ $current }/{ $limit } Zeichen), so dass die Ausgabe abgeschnitten wurde. Um die vollständige Ausgabe zu lesen, deaktivieren Sie die Limitierungsoption in den Einstellungen.\ncore_messages_limit_reached_lines = Anzahl der Nachrichten überschritten das festgelegte Limit ({ $current }/{ $limit } Zeilen), so dass die Ausgabe abgeschnitten wurde. Um die vollständige Ausgabe zu lesen, deaktivieren Sie die Limitierungsoption in den Einstellungen.\ncore_error_moving_to_trash = Fehler beim Verschieben von \"{ $file }\" in den Papierkorb: { $error }\ncore_error_removing = Fehler beim Entfernen von \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Kann keine ähnlichen Musikdateien ohne eine ausgewählte Similarity-Methode finden\ncore_failed_to_spawn_command = Fehlgeschlagenes Spawnen des Befehls: { $reason }\ncore_failed_to_check_process_status = Fehlgeschlagen bei der Überprüfung des Prozessstatus: { $reason }\ncore_failed_to_wait_for_process = Fehlgeschlagenes Warten auf Prozess: { $reason }\ncore_failed_to_read_video_properties = Fehlgeschlagen beim Lesen der Videoeigenschaften: { $reason }\ncore_failed_to_execute_ffmpeg = Fehlgeschlagenes Ausführen von ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg fehlgeschlagen mit Status { $status }: { $stderr } (Befehl: { $command })\ncore_failed_to_load_image_frame = Fehlgeschlagenes Laden des Bildrahmens: { $reason }\ncore_failed_to_extract_frame = Fehlgeschlagenes Extrahieren des Frames bei { $time } Sekunden aus \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Fehlgeschlagen beim Speichern des Miniaturansichts für \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Fehlgeschlagenes Abrufen des Frames bei Zeitstempel { $timestamp } von \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Fehlgeschlagenes Abrufen des Frames von \"{ $file }\" zu Zeitstempel { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Ungültiges Zuschneide-Rechteck: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom }\ncore_failed_to_crop_video_file = Fehlgeschlagenes Zuschneiden der Videodatei \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Das Video-Datei wurde nicht erstellt: { $temp }\ncore_unable_check_hash_of_file = Kann Hash von Datei \"{ $file }\" nicht überprüft werden, Grund { $reason }\ncore_error_checking_hash_of_file = Fehler beim Prüfen des Hash von Datei \"{ $file }\", Grund { $reason }\ncore_image_zero_dimensions = Bild hat breite Null oder Höhe \"{ $path }\"\ncore_image_open_failed = Kann das Bildfile \"{ $path }\" nicht öffnen: { $reason }\ncore_not_directory_remove = Versuche, Ordner \"{ $path }\" zu entfernen, der kein Verzeichnis ist\ncore_cannot_read_directory = Kann den Verzeichnis \"{ $path }\" nicht lesen\ncore_cannot_read_entry_from_directory = Kann den Eintrag nicht aus dem Verzeichnis \"{ $path }\" lesen\ncore_folder_contains_file_inside = Ordner enthält Datei \"{ $entry }\" innerhalb \"{ $folder }\"\ncore_unknown_directory_entry = Kann den Dateityp des Verzeichniseintrags \"{ $entry }\" innerhalb \"{ $path }\" nicht bestimmen\ncore_video_width_exceeds_limit = Video Breite { $width } überschreitet die Grenze von { $limit }\ncore_video_height_exceeds_limit = Videohöhe { $height } überschreitet die Grenze von { $limit }\ncore_failed_to_process_video = Fehlgeschlagenes Verarbeiten der Videodatei { $file }: { $reason }\ncore_optimized_file_larger = Optimierter Datei { $optimized } (Größe: { $new_size }) ist nicht kleiner als Original { $original } (Größe: { $original_size })\ncore_unknown_codec = Unbekannter Codec: { $codec }\ncore_invalid_video_optimizer_mode = Ungültiger Video-Optimierungsmodus: '{ $mode }'. Erlaubte Werte: transkodieren, zuschneiden\ncore_folder_does_not_exist = Ordner existiert nicht: { $folder }\ncore_path_not_directory = Der Pfad ist keine Verzeichnis: { $folder }\ncore_test_error_for_folder = Testfehler für Ordner: { $folder }\ncore_unknown_exif_tag_group = Unbekanntes EXIF-Tag-Gruppen: { $tag }\ncore_error_comparing_fingerprints = Fehler beim Vergleichen von Fingerabdrücken: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Fehlgeschlagen, Miniatur für \"{ $file }\" zu generieren: extrahierte Frames haben unterschiedliche Dimensionen\ncore_failed_to_generate_thumbnail = Fehlgeschlagenes Generieren des Miniaturansichts für \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Fehlgeschlagenes Extrahieren des Frames bei { $time } Sekunden aus \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Video-Datei existiert nicht (kann zwischen Scan/späteren Schritten entfernt werden): \"{ $path }\"\ncore_image_too_large = Bild ist zu groß ({ $width }x{ $height }) - mehr als unterstützt { $max } Pixel\ncore_failed_to_get_video_metadata = Fehlgeschlagen beim Abrufen der Videodaten für Datei \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Fehlgeschlagenes Abrufen des Videocodecs für die Datei \"{ $file }\"\ncore_failed_to_get_video_duration = Fehlgeschlagen, die Video-Dauer für die Datei \"{ $file }\" zu erhalten\ncore_failed_to_get_video_dimensions = Fehlgeschlagen, Video-Abmessungen für Datei \"{ $file }\" zu erhalten\ncore_frame_dimensions_mismatch = Rahmenmaße für Zeitstempel { $timestamp } stimmen nicht mit den ersten Rahmenmaßen ({ $first_w }x{ $first_h }) überein\ncore_failed_to_load_data_from_cache = Fehler beim Laden von Daten aus der Cache-Datei { $file }, Grund { $reason }\ncore_failed_to_load_data_from_json_cache = Fehler beim Laden von Daten aus der JSON-Cache-Datei { $file }, Grund { $reason }\ncore_failed_to_replace_with_optimized = Fehlgeschlagenes Ersetzen der Datei \"{ $file }\" mit der optimierten Version: { $reason }\ncore_failed_to_write_data_to_cache = Kann keine Daten in die Cache-Datei \"{ $file }\" schreiben, Grund { $reason }\ncore_properly_saved_cache_entries = Ordentlich in Datei { $count } Cache-Einträge gespeichert.\ncore_video_processing_stopped_by_user = Video-Verarbeitung wurde durch Benutzer gestoppt\ncore_thumbnail_generation_stopped_by_user = Erstellung von Vorschaubildern wurde durch Benutzer gestoppt\ncore_failed_to_optimize_video = Fehlgeschlagenes Optimieren des Videos \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Fehlgeschlagenes Zuschneiden des Videos \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Fehlgeschlagenes Abrufen der Metadaten der optimierten Datei \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Kann die Konfigurationsordner \"{ $folder }\" nicht erstellen, Grund { $reason }\ncore_cannot_create_cache_folder = Kann den Cache-Ordner \"{ $folder }\" nicht erstellen, Grund { $reason }\ncore_cannot_create_or_open_cache_file = Kann die Cache-Datei \"{ $file }\" nicht erstellen oder öffnen, Grund { $reason }\ncore_cannot_set_config_cache_path = Kann die Konfiguration/Cache-Pfad nicht setzen - Konfiguration und Cache werden nicht verwendet.\ncore_invalid_extension_contains_space = { $extension } ist keine gültige Erweiterung, da sie Leerzeichen enthält\ncore_invalid_extension_contains_dot = { $extension } ist keine gültige Erweiterung, da sie einen Punkt enthält\n\ncore_path_must_exists = Der angegebene Pfad muss existieren, wobei { $path } ignoriert wird\ncore_must_be_directory_or_file = Der angegebene Pfad muss auf ein gültiges Verzeichnis oder eine Datei verweisen, wobei { $path } ignoriert wird\ncore_paths_unable_to_get_device_id = Gerät-ID konnte nicht aus dem Ordner { $path } abgerufen werden"
  },
  {
    "path": "czkawka_core/i18n/el/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Αρχικό\ncore_similarity_very_high = Πολύ Υψηλή\ncore_similarity_high = Υψηλή\ncore_similarity_medium = Μεσαίο\ncore_similarity_small = Μικρό\ncore_similarity_very_small = Πολύ Μικρό\ncore_similarity_minimal = Ελάχιστα\ncore_cannot_open_dir = Αδυναμία ανοίγματος dir { $dir }, λόγος { $reason }\ncore_cannot_read_entry_dir = Αδυναμία ανάγνωσης καταχώρησης στον κατάλογο { $dir }, λόγος { $reason }\ncore_cannot_read_metadata_dir = Αδύνατη η ανάγνωση μεταδεδομένων στον κατάλογο { $dir }, λόγος { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Το αρχείο { $name } φαίνεται να έχει τροποποιηθεί πριν το Unix Epoch\ncore_folder_modified_before_epoch = Φάκελος { $name } φαίνεται να έχει τροποποιηθεί πριν το Unix Epoch\ncore_file_no_modification_date = Δεν είναι δυνατή η λήψη ημερομηνίας τροποποίησης από το αρχείο { $name }, λόγος { $reason }\ncore_folder_no_modification_date = Δεν είναι δυνατή η λήψη ημερομηνίας τροποποίησης από το φάκελο { $name }, λόγος { $reason }\ncore_cannot_start_scan_no_included_paths = Δεν μπορεί να ξεκινήσει η σάρωση, επειδή δεν περιλαμβάνονται καθόλου οι διαδρομές\ncore_skip_exist_check_all_included_paths_nonexistent = Δεν μπορεί να ξεκινήσει η σάρωση, επειδή οι μισθοί διαδρομές που περιλαμβάνονται δεν υπάρχουν\ncore_missing_no_chosen_included_path = Δεν επιλέχθηκε έγκυρος συμπεριλαμβανόμενος δρόμος (οι αποκλεισμένοι δρόμοι θα μπορούσαν να έχουν αποκλείσει όλους τους συμπεριλαμβανόμενους δρόμους)\ncore_reference_included_paths_same = Δεν μπορεί να ξεκινήσει η σάρωση όπου όλα τα έγκυρα συμπεριλημμένα μονοπάτια είναι επίσης μονοπάτια αναφοράς, προσπαθήστε να επικυρώσετε ή να απενεργοποιήσετε τα μονοπάτια αναφοράς\ncore_path_must_exists = Παρασχόμενος ο δρόμος πρέπει να υπάρχει, αγνοώντας το { $path }\ncore_must_be_directory_or_file = Παρέχεται ο δρόμος πρέπει να δείχνει προς έναν έγκυρο κατάλογο ή αρχείο, αγνοώντας { $path }\ncore_excluded_paths_pointless_slash = Αποκλείοντας / είναι μάταιο, γιατί σημαίνει ότι κανένα αρχείο δεν θα σαρωθεί\ncore_paths_unable_to_get_device_id = Δεν μπορώ να λάβω το ID συσκευής από τον φάκελο { $path }\ncore_needs_allowed_extensions_limited_by_tool = Δεν μπορεί να ξεκινήσει η σάρωση, όταν όλα τα πρόσθετα διαθέσιμα σε αυτό το εργαλείο ({ $extensions }) έχουν αποκλειστεί από την σάρωση\ncore_needs_allowed_extensions = Δεν μπορεί να ξεκινήσει η σάρωση, όταν έχουν αποκλειστεί όλες οι προσθήκες από τη σάρωση\ncore_needs_to_set_at_least_one_broken_option = Δεν μπορεί να ξεκινήσει η σάρωση, όταν δεν έχει οριστεί η επιλογή \"κατεστραμμένο\" για σάρωση\ncore_needs_to_set_at_least_one_bad_name_option = Δεν μπορεί να ξεκινήσει η σάρωση, όταν δεν έχει ρυθμιστεί η επιλογή για κακό όνομα για σάρωση\ncore_ffmpeg_not_found = Δεν μπορεί να βρεθεί μια σωστή εγκατάσταση του FFmpeg ή FFprobe. Αυτά είναι εξωτερικά προγράμματα που πρέπει να εγκατασταθούν χειροκίνητα.\ncore_ffmpeg_not_found_windows = Να είστε βέβαιος ότι ffmpeg.exe και ffprobe.exe είναι διαθέσιμα σε PATH ή τοποθετούνται απευθείας στον ίδιο φάκελο με το εκτελέσιμο app\ncore_invalid_symlink_infinite_recursion = Άπειρη αναδρομή\ncore_invalid_symlink_non_existent_destination = Αρχείο ανύπαρκτου προορισμού\ncore_messages_limit_reached_characters = Ο αριθμός μηνυμάτων υπερέβη το καθορισμένο όριο ({ $current }/{ $limit } χαρακτήρες), οπότε η έξοδος περικόπηκε. Για να διαβάσετε την πλήρη έξοδο, απενεργοποιήστε την επιλογή περιορισμού στις ρυθμίσεις.\ncore_messages_limit_reached_lines = Ο αριθμός μηνυμάτων υπερέβη το καθορισμένο όριο ({ $current }/{ $limit } γραμμές), οπότε η έξοδος περικόπηκε. Για να διαβάσετε την πλήρη έξοδο, απενεργοποιήστε την επιλογή περιορισμού στις ρυθμίσεις.\ncore_error_moving_to_trash = Σφάλμα κατά μετακίνηση \"{ $file }\" στον κάλαπο”. { $error }\ncore_error_removing = Σφάλμα κατά την αφαίρεση \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Δεν μπορεί να βρεθούν παρόμοια μουσικά αρχεία χωρίς μια επιλεγμένη μέθοδο ομοιότητας\ncore_failed_to_spawn_command = Αποτυχία εκκίνησης εντολής: { $reason }\ncore_failed_to_check_process_status = Αποτυχία ελέγχου της κατάστασης της διαδικασίας: { $reason }\ncore_failed_to_wait_for_process = Αποτυχία αναμονής για τη διαδικασία: { $reason }\ncore_failed_to_read_video_properties = Αποτυχία ανάγνωσης ιδιοτήτων βίντεο: { $reason }\ncore_failed_to_execute_ffmpeg = Αποτυχία εκτέλεσης ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = έπεσε η ffmpeg με την κατάσταση { $status }: { $stderr } (εντολή: { $command })\ncore_failed_to_load_image_frame = Αποτυχία φόρτωσης πλαισίου εικόνας: { $reason }\ncore_failed_to_extract_frame = Αποτυχία εξαγωγής καρέ στις { $time } δευτερόλεπτα από το \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Αποτυχία αποθήκευσης μικρογραφίας για \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Αποτυχία ανάκτησης καρέ στην σφραγίδα χρόνου { $timestamp } από το \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Αποτυχία λήψης πλαισίου από \"{ $file }\" στην σφραγίδα χρόνου { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Μη έγκυρος ορθογώνιο καλλιέργειας: αριστερά={ $left }, άνω={ $top }, δεξιά={ $right }, κάτω={ $bottom }\ncore_failed_to_crop_video_file = Αποτυχία κοπής του αρχείου βίντεο \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Το αρχείο βίντεο που κόπηκε δεν δημιουργήθηκε: { $temp }\ncore_unable_check_hash_of_file = Δεν μπορώ να ελέγξω το hash του αρχείου \"{ $file }\", λόγος { $reason }\ncore_error_checking_hash_of_file = Σφάλμα συνέβη κατά την επαλήθευση του hash του αρχείου \"{ $file }\", λόγος { $reason }\ncore_image_zero_dimensions = Η εικόνα έχει μηδενικό πλάτος ή ύψος \"{ $path }\"\ncore_image_open_failed = Δεν μπορεί να ανοίξει το αρχείο εικόνας \"{ $path }\": { $reason }\ncore_not_directory_remove = Προσπαθώντας να διαγράψω τον φάκελο \"{ $path }\" που δεν είναι κατάλογος\ncore_cannot_read_directory = Δεν μπορώ να διαβάσω τον κατάλογο \"{ $path }\"\ncore_cannot_read_entry_from_directory = Δεν μπορώ να διαβάσω την εγγραφή από τον κατάλογο \"{ $path }\"\ncore_folder_contains_file_inside = Ο φάκελος περιέχει το αρχείο \"{ $entry }\" μέσα στο \"{ $folder }\"\ncore_unknown_directory_entry = Δεν μπορεί να προσδιοριστεί ο τύπος αρχείου της καταχώρησης καταλόγου \"{ $entry }\" μέσα στο \"{ $path }\"\ncore_video_width_exceeds_limit = Βίντεο πλάτος { $width } υπερβαίνει το όριο του { $limit }\ncore_video_height_exceeds_limit = Βίντεο ύψος { $height } υπερβαίνει το όριο των { $limit }\ncore_failed_to_process_video = Αποτυχία επεξεργασίας αρχείου βίντεο { $file }: { $reason }\ncore_optimized_file_larger = Βελτιστοποιημένο αρχείο { $optimized } (μέγεθος: { $new_size }) δεν είναι μικρότερο από το αρχικό { $original } (μέγεθος: { $original_size })\ncore_unknown_codec = Άγνωστος κωδικοποιητής: { $codec }\ncore_invalid_video_optimizer_mode = Μη έγκυρος τρόπος βελτιστοποίησης βίντεο: '{ $mode }'. Επιτρεπτές τιμές: transcode, crop\ncore_folder_does_not_exist = Ο φάκελος δεν υπάρχει: { $folder }\ncore_path_not_directory = Το μονοπάτι δεν είναι κατάλογος: { $folder }\ncore_test_error_for_folder = Σφάλμα δοκιμής για φάκελο: { $folder }\ncore_unknown_exif_tag_group = Άγνωστη ομάδα ετικετών EXIF: { $tag }\ncore_error_comparing_fingerprints = Σφάλμα κατά σύγκριση δακτυλικών αποτυπωμάτων: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Αποτυχία δημιουργίας μικρογραφίας για \"{ $file }\": τα εξωθημένα πλάνα έχουν διαφορετικές διαστάσεις\ncore_failed_to_generate_thumbnail = Αποτυχία δημιουργίας μικρογραφίας για \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Αποτυχία εξαγωγής καρέ στις { $time } δευτερόλεπτα από το \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Το αρχείο βίντεο δεν υπάρχει (μπορεί να αφαιρεθεί μεταξύ σάρωσης/μετέπειτα βημάτων): \"{ $path }\"\ncore_image_too_large = Η εικόνα είναι πολύ μεγάλη ({ $width }x{ $height }) - περισσότερο από το υποστηριζόμενο { $max } pixels\ncore_failed_to_get_video_metadata = Αποτυχία ανάκτησης μεταδεδομένων βίντεο για το αρχείο \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Αποτυχία ανάκτησης του codec βίντεο για το αρχείο \"{ $file }\"\ncore_failed_to_get_video_duration = Αποτυχία λήψης της διάρκειας βίντεο για το αρχείο \"{ $file }\"\ncore_failed_to_get_video_dimensions = Αποτυχία λήψης διαστάσεων βίντεο για το αρχείο \"{ $file }\"\ncore_frame_dimensions_mismatch = Οι διαστάσεις του πλαισίου για την σφραγίδα χρόνου { $timestamp } δεν ταιριάζουν με τις διαστάσεις του πρώτου πλαισίου ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Αποτυχία φόρτωσης δεδομένων από το αρχείο cache { $file }, λόγος { $reason }\ncore_failed_to_load_data_from_json_cache = Αποτυχία φόρτωσης δεδομένων από το αρχείο cache json { $file }, λόγος { $reason }\ncore_failed_to_replace_with_optimized = Αποτυχία αντικατάστασης του αρχείου \"{ $file }\" με την βελτιστοποιημένη έκδοση: { $reason }\ncore_failed_to_write_data_to_cache = Δεν μπορεί να γραφτεί δεδομένα στο αρχείο cache \"{ $file }\", λόγος { $reason }\ncore_properly_saved_cache_entries = Αποθηκεύτηκε σωστά στο αρχείο { $count } καταχωρήσεις cache.\ncore_video_processing_stopped_by_user = Η επεξεργασία βίντεο σταμάτησε από τον χρήστη\ncore_thumbnail_generation_stopped_by_user = Δημιουργία μικρογραφιών σταματήθηκε από χρήστη\ncore_failed_to_optimize_video = Αποτυχία βελτιστοποίησης βίντεο \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Αποτυχία κοπής βίντεο \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Αποτυχία ανάκτησης μεταδεδομένων του βελτιστοποιημένου αρχείου \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Δεν μπορεί να δημιουργηθεί ο φάκελος διαμόρφωσης \"{ $folder }\", λόγος { $reason }\ncore_cannot_create_cache_folder = Δεν μπορεί να δημιουργηθεί ο φάκελος προσωρινής αποθήκευσης \"{ $folder }\", λόγος { $reason }\ncore_cannot_create_or_open_cache_file = Δεν μπορεί να δημιουργηθεί ή να ανοίξει το αρχείο cache \"{ $file }\", λόγος { $reason }\ncore_cannot_set_config_cache_path = Δεν μπορεί να ρυθμιστεί ο/η διαδρομή config/cache - η config και η cache δεν θα χρησιμοποιηθούν.\ncore_invalid_extension_contains_space = { $extension } δεν είναι έγκυρος τύπος επέκτασης επειδή περιέχει κενό διάστημα μέσα\ncore_invalid_extension_contains_dot = Το { $extension } δεν είναι έγκυρος τύπος επέκτασης επειδή περιέχει τελεία μέσα\n"
  },
  {
    "path": "czkawka_core/i18n/en/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Original\ncore_similarity_very_high = Very High\ncore_similarity_high = High\ncore_similarity_medium = Medium\ncore_similarity_small = Small\ncore_similarity_very_small = Very Small\ncore_similarity_minimal = Minimal\n\ncore_cannot_open_dir = Cannot open dir {$dir}, reason {$reason}\ncore_cannot_read_entry_dir = Cannot read entry in dir {$dir}, reason {$reason}\ncore_cannot_read_metadata_dir = Cannot read metadata in dir {$dir}, reason {$reason}\ncore_cannot_read_metadata_file = Cannot read metadata of file {$file}, reason {$reason}\ncore_file_modified_before_epoch = File {$name} seems to have been modified before the Unix Epoch\ncore_folder_modified_before_epoch = Folder {$name} seems to have been modified before the Unix Epoch\ncore_file_no_modification_date = Unable to get modification date from file {$name}, reason {$reason}\ncore_folder_no_modification_date = Unable to get modification date from folder {$name}, reason {$reason}\n\ncore_cannot_start_scan_no_included_paths = Cannot start scan, because there are no included paths\ncore_skip_exist_check_all_included_paths_nonexistent = Cannot start scan, because all included paths do not exist\ncore_missing_no_chosen_included_path = No valid included path was chosen(excluded paths could have excluded all included paths)\ncore_reference_included_paths_same = Cannot start scan where all valid included paths are also referenced paths, try to validate or disable referenced paths\ncore_path_must_exists = Provided path must exist, ignoring { $path }\ncore_must_be_directory_or_file = Provided path must point to a vaild directory or file, ignoring { $path }\ncore_excluded_paths_pointless_slash = Excluding / is pointless, because it means no files will be scanned\ncore_paths_unable_to_get_device_id = Unable to get device id from folder { $path }\n\ncore_needs_allowed_extensions_limited_by_tool = Cannot start scan, when all extensions available in this tool ({ $extensions }) were excluded from scan\ncore_needs_allowed_extensions = Cannot start scan, when all extensions were excluded from scan\ncore_needs_to_set_at_least_one_broken_option = Cannot start scan, when there is no broken option set to scan for\ncore_needs_to_set_at_least_one_bad_name_option = Cannot start scan, when there is no bad name option set to scan for\n\ncore_ffmpeg_not_found = Cannot find a proper installation of FFmpeg or FFprobe. These are external programs that must be installed manually.\ncore_ffmpeg_not_found_windows = Be sure that ffmpeg.exe and ffprobe.exe are available in PATH or are placed directly in the same folder as the app executable\n\ncore_invalid_symlink_infinite_recursion = Infinite recursion\ncore_invalid_symlink_non_existent_destination = Non-existent destination file\n\ncore_messages_limit_reached_characters = Number of messages exceeded the set limit ({$current}/{$limit} characters), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_messages_limit_reached_lines = Number of messages exceeded the set limit ({$current}/{$limit} lines), so the output was truncated. To read the full output, disable the limiting option in settings.\n\ncore_error_moving_to_trash = Error while moving \"{ $file }\" to the trash: { $error }\ncore_error_removing = Error while removing \"{ $file }\": { $error }\n\ncore_no_similarity_method_selected = Cannot find similar music files without a selected similarity method\n\ncore_failed_to_spawn_command = Failed to spawn command: { $reason }\ncore_failed_to_check_process_status = Failed to check process status: { $reason }\ncore_failed_to_wait_for_process = Failed to wait for process: { $reason }\ncore_failed_to_read_video_properties = Failed to read video properties: { $reason }\ncore_failed_to_execute_ffmpeg = Failed to execute ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg failed with status { $status }: { $stderr } (command: { $command })\ncore_failed_to_load_image_frame = Failed to load image frame: { $reason }\ncore_failed_to_extract_frame = Failed to extract frame at { $time } seconds from \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Failed to save thumbnail for \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Failed to get frame at timestamp { $timestamp } from \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Failed to get frame from \"{ $file }\" at timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Invalid crop rectangle: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom }\ncore_failed_to_crop_video_file = Failed to crop video file \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Cropped video file was not created: { $temp }\ncore_unable_check_hash_of_file = Unable to check hash of file \"{ $file }\", reason { $reason }\ncore_error_checking_hash_of_file = Error happened when checking hash of file \"{ $file }\", reason { $reason }\ncore_image_zero_dimensions = Image has zero width or height \"{ $path }\"\ncore_image_open_failed = Cannot open image file \"{ $path }\": { $reason }\ncore_not_directory_remove = Trying to remove folder \"{ $path }\" which is not a directory\ncore_cannot_read_directory = Cannot read directory \"{ $path }\"\ncore_cannot_read_entry_from_directory = Cannot read entry from directory \"{ $path }\"\ncore_folder_contains_file_inside = Folder contains file \"{ $entry }\" inside \"{ $folder }\"\ncore_unknown_directory_entry = Unable to determine file type of directory entry \"{ $entry }\" inside \"{ $path }\"\ncore_video_width_exceeds_limit = Video width { $width } exceeds the limit of { $limit }\ncore_video_height_exceeds_limit = Video height { $height } exceeds the limit of { $limit }\ncore_failed_to_process_video = Failed to process video file { $file }: { $reason }\ncore_optimized_file_larger = Optimized file { $optimized } (size: { $new_size }) is not smaller than original { $original } (size: { $original_size })\ncore_unknown_codec = Unknown codec: { $codec }\ncore_invalid_video_optimizer_mode = Invalid video optimizer mode: '{ $mode }'. Allowed values: transcode, crop\ncore_folder_does_not_exist = Folder does not exist: { $folder }\ncore_path_not_directory = Path is not a directory: { $folder }\ncore_test_error_for_folder = Test error for folder: { $folder }\ncore_unknown_exif_tag_group = Unknown EXIF tag group: { $tag }\ncore_error_comparing_fingerprints = Error while comparing fingerprints: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Failed to generate thumbnail for \"{ $file }\": extracted frames have different dimensions\ncore_failed_to_generate_thumbnail = Failed to generate thumbnail for \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Failed to extract frame at { $time } seconds from \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Video file does not exist (could be removed between scan/later steps): \"{ $path }\"\ncore_image_too_large = Image is too large ({ $width }x{ $height }) - more than supported { $max } pixels\ncore_failed_to_get_video_metadata = Failed to get video metadata for file \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Failed to get video codec for file \"{ $file }\"\ncore_failed_to_get_video_duration = Failed to get video duration for file \"{ $file }\"\ncore_failed_to_get_video_dimensions = Failed to get video dimensions for file \"{ $file }\"\ncore_frame_dimensions_mismatch = Frame dimensions for timestamp { $timestamp } do not match the first frame dimensions ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Failed to load data from cache file { $file }, reason { $reason }\ncore_failed_to_load_data_from_json_cache = Failed to load data from json cache file { $file }, reason { $reason }\ncore_failed_to_replace_with_optimized = Failed to replace file \"{ $file }\" with optimized version: { $reason }\ncore_failed_to_write_data_to_cache = Cannot write data to cache file \"{ $file }\", reason { $reason }\ncore_properly_saved_cache_entries = Properly saved to file { $count } cache entries.\ncore_video_processing_stopped_by_user = Video processing was stopped by user\ncore_thumbnail_generation_stopped_by_user = Thumbnail generation was stopped by user\ncore_failed_to_optimize_video = Failed to optimize video \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Failed to crop video \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Failed to get metadata of optimized file \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Cannot create config folder \"{ $folder }\", reason { $reason }\ncore_cannot_create_cache_folder = Cannot create cache folder \"{ $folder }\", reason { $reason }\ncore_cannot_create_or_open_cache_file = Cannot create or open cache file \"{ $file }\", reason { $reason }\ncore_cannot_set_config_cache_path = Cannot set config/cache path - config and cache will not be used.\ncore_invalid_extension_contains_space = { $extension } is not a valid extension because it contains empty space inside\ncore_invalid_extension_contains_dot = { $extension } is not a valid extension because it contains dot inside\ncore_ffmpeg_unknown_encoder = Cannot encode { $file } using the { $encoder } encoder. The current FFmpeg build does not support this encoder. Use a different FFmpeg version with the required codec support or select another encoder.\ncore_ffmpeg_error = FFmpeg error while processing { $file }, status code { $code }, reason { $reason }"
  },
  {
    "path": "czkawka_core/i18n/es-ES/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Original\ncore_similarity_very_high = Muy alta\ncore_similarity_high = Alta\ncore_similarity_medium = Medio\ncore_similarity_small = Pequeño\ncore_similarity_very_small = Muy pequeño\ncore_similarity_minimal = Mínimo\ncore_cannot_open_dir = No se puede abrir el directorio { $dir }, razón { $reason }\ncore_cannot_read_entry_dir = No se puede leer la entrada en directorio { $dir }, razón { $reason }\ncore_cannot_read_metadata_dir = No se pueden leer metadatos en el directorio { $dir }, razón { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = El archivo { $name } parece haber sido modificado antes del Epoch Unix\ncore_folder_modified_before_epoch = La carpeta { $name } parece haber sido modificada antes del Epoch Unix\ncore_file_no_modification_date = No se puede obtener la fecha de modificación del archivo { $name }, razón { $reason }\ncore_folder_no_modification_date = No se puede obtener la fecha de modificación de la carpeta { $name }, razón { $reason }\ncore_cannot_start_scan_no_included_paths = No se puede iniciar el escaneo, porque no hay rutas incluidas\ncore_skip_exist_check_all_included_paths_nonexistent = No se puede iniciar el escaneo, porque todas las rutas incluidas no existen\ncore_missing_no_chosen_included_path = No ruta incluida válida fue elegida (las rutas excluidas podrían haber excluido todas las rutas incluidas)\ncore_reference_included_paths_same = No se puede iniciar el escaneo donde todas las rutas incluidas válidas también son rutas referenciadas, intente validar o deshabilitar las rutas referenciadas\ncore_path_must_exists = Se debe proporcionar la ruta especificada, ignorando { $path }\ncore_must_be_directory_or_file = Proporcionado el camino debe apuntar a un directorio o archivo válido, ignorando { $path }\ncore_excluded_paths_pointless_slash = Excluyendo / es inútil, porque significa que no se escanean archivos\ncore_paths_unable_to_get_device_id = Imposible obtener el id del dispositivo del directorio { $path }\ncore_needs_allowed_extensions_limited_by_tool = No se puede iniciar el escaneo, cuando todas las extensiones disponibles en esta herramienta ({ $extensions }) fueron excluidas del escaneo\ncore_needs_allowed_extensions = No se puede iniciar el escaneo, cuando todas las extensiones fueron excluidas del escaneo\ncore_needs_to_set_at_least_one_broken_option = No se puede iniciar el escaneo, cuando no está configurada la opción de roto para escanear\ncore_needs_to_set_at_least_one_bad_name_option = No se puede iniciar el escaneo, cuando no está configurada la opción de nombre incorrecto para escanear\ncore_ffmpeg_not_found = No se puede encontrar una instalación adecuada de FFmpeg o FFprobe. Estos son programas externos que deben instalarse manualmente.\ncore_ffmpeg_not_found_windows = Asegúrese de que ffmpeg.exe y ffprobe.exe están disponibles en PATH o se colocan directamente en la misma carpeta que el ejecutable de la aplicación\ncore_invalid_symlink_infinite_recursion = Recursión infinita\ncore_invalid_symlink_non_existent_destination = Archivo de destino inexistente\ncore_messages_limit_reached_characters = El número de mensajes excedió el límite establecido (caracteres{ $current }/{ $limit } ), por lo que la salida fue truncada. Para leer la salida completa, deshabilite la opción de limitación en los ajustes.\ncore_messages_limit_reached_lines = Número de mensajes excedido el límite establecido ({ $current }/{ $limit } líneas), por lo que la salida fue truncada. Para leer la salida completa, deshabilite la opción de limitación en los ajustes.\ncore_error_moving_to_trash = Error al mover \"{ $file }\" a la papelera: { $error }\ncore_error_removing = Error al eliminar \"{ $file }\": { $error }\ncore_no_similarity_method_selected = No se pueden encontrar archivos de música similares sin un método de similitud seleccionado\ncore_ffmpeg_failed_with_status = ffmpeg falló con estado { $status }: { $stderr } (comando: { $command })\ncore_failed_to_extract_frame = Falló al extraer el fotograma en { $time } segundos de \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Falló al guardar el miniagujete para \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Error al obtener el fotograma en el sello de tiempo { $timestamp } de \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = No se pudo obtener el fotograma de \"{ $file }\" en el sello de tiempo { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = ¡Rectángulo de recorte inválido: izquierda={ $left }, arriba={ $top }, derecha={ $right }, abajo={ $bottom }\ncore_failed_to_crop_video_file = El recorte del archivo de video \"{ $file }\" falló: { $reason }\ncore_cropped_video_not_created = El archivo de video recortado no fue creado: { $temp }\ncore_unable_check_hash_of_file = Imposible verificar el hash del archivo \"{ $file }\", la razón { $reason }\ncore_error_checking_hash_of_file = Error ocurrió al verificar el hash del archivo \"{ $file }\", razón { $reason }\ncore_image_zero_dimensions = La imagen tiene un ancho o alto de cero \"{ $path }\"\ncore_image_open_failed = No se puede abrir el archivo de imagen \"{ $path }\": { $reason }\ncore_not_directory_remove = Intentando eliminar la carpeta \"{ $path }\" que no es un directorio\ncore_cannot_read_directory = No se puede leer el directorio \"{ $path }\"\ncore_cannot_read_entry_from_directory = No se puede leer la entrada del directorio \"{ $path }\"\ncore_folder_contains_file_inside = La carpeta contiene el archivo \"{ $entry }\" dentro de \"{ $folder }\"\ncore_unknown_directory_entry = No se puede determinar el tipo de archivo de la entrada del directorio \"{ $entry }\" dentro de \"{ $path }\"\ncore_video_width_exceeds_limit = Video ancho { $width } excede el límite de { $limit }\ncore_video_height_exceeds_limit = Video altura { $height } excede el límite de { $limit }\ncore_failed_to_process_video = No se pudo procesar el archivo de video { $file }: { $reason }\ncore_optimized_file_larger = Archivo optimizado { $optimized } (tamaño: { $new_size }) no es más pequeño que el original { $original } (tamaño: { $original_size })\ncore_unknown_codec = Códec desconocido: { $codec }\ncore_invalid_video_optimizer_mode = El modo optimizador de video no es válido: '{ $mode }'. Los valores permitidos: transcodificar, recortar\ncore_folder_does_not_exist = La carpeta no existe: { $folder }\ncore_path_not_directory = La ruta no es un directorio: { $folder }\ncore_test_error_for_folder = Error de prueba para la carpeta: { $folder }\ncore_unknown_exif_tag_group = Grupo de etiquetas EXIF desconocido: { $tag }\ncore_error_comparing_fingerprints = Error al comparar huellas dactilares: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Falló al generar miniatura para \"{ $file }\": los fotogramas extraídos tienen diferentes dimensiones\ncore_failed_to_generate_thumbnail = No se pudo generar miniatura para \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Falló al extraer el fotograma en { $time } segundos de \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = El archivo de video no existe (puede ser eliminado entre las etapas de escaneo/más tarde): \"{ $path }\"\ncore_image_too_large = La imagen es demasiado grande ({ $width }x{ $height }) - más que los soportados { $max } píxeles\ncore_failed_to_get_video_metadata = No se pudo obtener los metadatos del video para el archivo \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = No se pudo obtener el códec de video para el archivo \"{ $file }\"\ncore_failed_to_get_video_duration = No se pudo obtener la duración del video para el archivo \"{ $file }\"\ncore_failed_to_get_video_dimensions = No se pudo obtener las dimensiones del video para el archivo \"{ $file }\"\ncore_frame_dimensions_mismatch = Las dimensiones del fotograma para la marca de tiempo { $timestamp } no coinciden con las dimensiones del primer fotograma ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = No se pudo cargar los datos del archivo de caché { $file }, la razón { $reason }\ncore_failed_to_load_data_from_json_cache = No se pudo cargar los datos del archivo de caché json { $file }, motivo { $reason }\ncore_failed_to_replace_with_optimized = No se pudo reemplazar el archivo \"{ $file }\" con la versión optimizada: { $reason }\ncore_failed_to_write_data_to_cache = No se puede escribir datos al archivo de caché \"{ $file }\", la razón { $reason }\ncore_properly_saved_cache_entries = Guardado correctamente a archivo { $count } entradas de caché.\ncore_video_processing_stopped_by_user = El procesamiento de video fue detenido por el usuario\ncore_thumbnail_generation_stopped_by_user = La generación de miniaturas fue detenida por el usuario\ncore_failed_to_optimize_video = Falló optimizar el video \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Falló al recortar el video \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = No se pudo obtener los metadatos del archivo optimizado \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = No se puede crear la carpeta de configuración \"{ $folder }\", la razón { $reason }\ncore_cannot_create_cache_folder = No se puede crear la carpeta de caché \"{ $folder }\", la razón { $reason }\ncore_cannot_create_or_open_cache_file = No se puede crear o abrir el archivo de caché \"{ $file }\", la razón { $reason }\ncore_cannot_set_config_cache_path = No se puede establecer la ruta de configuración/caché - la configuración y la caché no se utilizarán.\ncore_invalid_extension_contains_space = { $extension } no es una extensión válida porque contiene espacios en blanco dentro\ncore_invalid_extension_contains_dot = { $extension } no es una extensión válida porque contiene un punto dentro\n\ncore_failed_to_spawn_command = No se pudo ejecutar el comando: { $reason }\ncore_failed_to_check_process_status = No se pudo verificar el estado del proceso: { $reason }\ncore_failed_to_wait_for_process = No se pudo esperar a que finalizara el proceso: { $reason }\ncore_failed_to_read_video_properties = No se pudieron leer las propiedades del video: { $reason }\ncore_failed_to_execute_ffmpeg = No se pudo ejecutar ffmpeg: { $reason }\ncore_failed_to_load_image_frame = No se pudo cargar el fotograma de la imagen: { $reason }"
  },
  {
    "path": "czkawka_core/i18n/fa/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = اصولی\ncore_similarity_very_high = بسیار بلند\ncore_similarity_high = ارتفاع\ncore_similarity_medium = میانبر\ncore_similarity_small = کوچک\ncore_similarity_very_small = بسیار کوچک\ncore_similarity_minimal =\n    最少istantly converted to Persian: \n    مینیمال\ncore_cannot_open_dir = نمی‌توانم مسیر { $dir } را باز کنم، دلیل آن { $reason }\ncore_cannot_read_entry_dir = نمی‌توانید درایه‌ای از پوشه { $dir } را بخوانید، دلیل آن { $reason }\ncore_cannot_read_metadata_dir = می‌توانید مетا داده در پوشه { $dir } را خواند، با دلیل \"{ $reason }\"\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = فایل { $name } به نظر می‌رسد قبل از زمان‌بندی سیستم عکس آشامیده شده است\ncore_folder_modified_before_epoch = دایره‌نامه { $name } به نظر می‌رسد قبل از زمان‌پا‌شناخت عینک لوبیا برمدرمود شده است\ncore_file_no_modification_date = نemی‌توانم تاریخ تغییرات فایل { $name } را دریافت کنم، دلیل { $reason }\ncore_folder_no_modification_date = نemی‌توانم تاریخ بروزرسanی از پوشه { $name } را دریافته‌ام، دلیل \"{ $reason }\"\ncore_cannot_start_scan_no_included_paths = امکان شروع اسکن وجود ندارد، زیرا هیچ مسیرهای گنجانده شده‌ای وجود ندارد\ncore_skip_exist_check_all_included_paths_nonexistent = امکان شروع اسکن وجود ندارد، زیرا تمام مسیرهای گنجانیده شده وجود ندارند\ncore_missing_no_chosen_included_path = مسیر گنجانده شده معتبری انتخاب نشد (مسیرهای رد شده می‌توانستند تمام مسیرهای گنجانده شده را رد کنند)\ncore_reference_included_paths_same = امکان شروع اسکن وجود ندارد، جایی که تمام مسیرهای گنجانده شده معتبر نیز مسیرهای ارجاعی هستند، لطفاً اعتبار سنجی را انجام دهید یا مسیرهای ارجاعی را غیرفعال کنید\ncore_path_must_exists = مسیر ارائه شده باید وجود داشته باشد، نادیده گرفتن { $path }\ncore_must_be_directory_or_file = مسیر ارائه شده باید به یک دایرکتوری یا فایل معتبر اشاره کند، با نادیده گرفتن { $path }\ncore_excluded_paths_pointless_slash = исключить / бессмысленно, потому что это означает, что файлы не будут сканироваться\ncore_paths_unable_to_get_device_id = امکان دریافت شناسه دستگاه از پوشه { $path } وجود ندارد\ncore_needs_allowed_extensions_limited_by_tool = امکان شروع اسکن وجود ندارد، زمانی که تمام افزونه‌های موجود در این ابزار ({ $extensions }) از اسکن حذف شده‌اند\ncore_needs_allowed_extensions = امکان شروع اسکن وجود ندارد، زمانی که تمام افزونه‌ها از اسکن حذف شده‌اند\ncore_needs_to_set_at_least_one_broken_option = امکان شروع اسکن وجود ندارد، زمانی که گزینه \"شکسته\" برای اسکن تنظیم نشده باشد\ncore_needs_to_set_at_least_one_bad_name_option = امکان شروع اسکن وجود ندارد، زمانی که گزینه نام نامناسب تنظیم نشده باشد تا برای اسکن جستجو شود\ncore_ffmpeg_not_found = امکان یافتن یک نصب مناسب از FFmpeg یا FFprobe وجود ندارد. این‌ها برنامه‌های خارجی هستند که باید به صورت دستی نصب شوند.\ncore_ffmpeg_not_found_windows = با توجه به نگارش همان طور که در متن داده شده است، مطمئن شوید که ffmpeg.exe و ffprobe.exe در PATH موجود هستند یا در آن پوشه که شامل اجرایabled exe اپلیکیشن است قرار داده شده‌اند\ncore_invalid_symlink_infinite_recursion = بازگشت نامتناهی\ncore_invalid_symlink_non_existent_destination = فایل مقصد مفقود\ncore_messages_limit_reached_characters = تعداد پیام‌هایی که بیش از حاشیه مقرر ({ $current }/{ $limit } کاراکتر) بودند، باعث قطع شدن خروجی شد. برای مشاهده کامل خروجی، گزینه محدود سازی را در تنظیمات غیرفعال کنید.\ncore_messages_limit_reached_lines = تعداد پیام‌ها حاشیه مقرر ({ $current }/{ $limit } 行) را بیشینه کرد، بنابراین خروجی کوتاه شده است. برای مطالعه خروجی کامل، گزینه محدود کردن را در تنظیمات غیرفعال کنید.\ncore_error_moving_to_trash = خطا در منتقل کردن \"{ $file }\" به سبد حذف شد: { $error }\ncore_error_removing = خطا در حذف \"{ $file }\": { $error }\ncore_no_similarity_method_selected = فیلدهای موسیقی مشابه را بدون انتخاب روش مشابهی پیدا نمی‌توانید بیابید\ncore_failed_to_spawn_command = ناموفق بود تا دستورالعمل تولید شود: { $reason }\ncore_failed_to_check_process_status = ناموفق بودن بررسی وضعیت فرآیند: { $reason }\ncore_failed_to_wait_for_process = ناموفق بود برای منتظر ماندن از فرآیند: { $reason }\ncore_failed_to_read_video_properties = ناموفقیت در خواندن ویژگی‌های ویدیو: { $reason }\ncore_failed_to_execute_ffmpeg = ناموفق بود تا ffmpeg اجرا شود: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg با وضعیت { $status } ناموفق بود: { $stderr } (دستور: { $command })\ncore_failed_to_load_image_frame = ناموفقیت بارگذاری فریم تصویر: { $reason }\ncore_failed_to_extract_frame = ناموفق بود دریافت فریم در { $time } ثانیه از \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = ناموفق بود برای ذخیره تصویر کوچک برای \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = ناموفق برای دریافت فریم در زمان‌بندی { $timestamp } از \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = ناموفق برای دریافت فریم از \"{ $file }\" در زمان‌بندی { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = عدم معتبر بودن مستطیل کشتزار: چپ={ $left }، بالا={ $top }، راست={ $right }، پایین={ $bottom }\ncore_failed_to_crop_video_file = فشل برش فایل ویدیویی \"{ $file }\": { $reason }\ncore_cropped_video_not_created = فایل ویدیوی برش خورده ایجاد نشد: { $temp }\ncore_unable_check_hash_of_file = امکان بررسی هش فایل \"{ $file }\" وجود ندارد، دلیل { $reason }\ncore_error_checking_hash_of_file = خطای رخ داده هنگام بررسی هش فایل \"{ $file }\"، دلیل { $reason }\ncore_image_zero_dimensions = تصویر دارای عرض یا ارتفاع صفر \"{ $path }\"\ncore_image_open_failed = امکان باز کردن فایل تصویر \"{ $path }\": { $reason }\ncore_not_directory_remove = در حال حذف پوشه \"{ $path }\" که یک دایرکتوری نیست\ncore_cannot_read_directory = امکان خواندن دایرکتوری \"{ $path }\" وجود ندارد\ncore_cannot_read_entry_from_directory = Could not read entry from directory \"{ $path }\"\ncore_folder_contains_file_inside = فایل \"{ $entry }\" داخل پوشه \"{ $folder }\" وجود دارد\ncore_unknown_directory_entry = امکان تعیین نوع فایل ورودی دایرکتوری \"{ $entry }\" داخل \"{ $path }\" وجود ندارد\ncore_video_width_exceeds_limit = Video عرض { $width } از حد { $limit } تجاوز می‌کند\ncore_video_height_exceeds_limit = Video ارتفاع { $height } از حد { $limit } تجاوز می‌کند\ncore_failed_to_process_video = فشل پردازش فایل ویدیویی { $file }: { $reason }\ncore_optimized_file_larger = فایل بهینه‌شده { $optimized } (حجم: { $new_size }) کوچکتر از فایل اصلی { $original } (حجم: { $original_size }) نیست\ncore_unknown_codec = کدک ناشناخته: { $codec }\ncore_invalid_video_optimizer_mode = حالت بهینه‌سازی ویدیو نامعتبر: '{ $mode }'. مقادیر مجاز: transcode, crop\ncore_folder_does_not_exist = فোলدر وجود ندارد: { $folder }\ncore_path_not_directory = مسیر معتبر نیست: { $folder }\ncore_test_error_for_folder = خطای آزمایشی برای پوشه: { $folder }\ncore_unknown_exif_tag_group = گروه برچسب EXIF ناشناخته: { $tag }\ncore_error_comparing_fingerprints = خطای مقایسه اثر انگشت: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = ناموفق بود ایجاد پیش‌نمایه‌ی برای \"{ $file }\": فریم‌های استخراج‌شده ابعاد متفاوتی دارند\ncore_failed_to_generate_thumbnail = ناموفق بود ایجاد پیش‌نمایه‌ی \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = ناموفق بود دریافت فریم در { $time } ثانیه از \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = فایل ویدیویی وجود ندارد (می‌توان آن را بین اسکن/مراحل بعدی حذف کرد): \"{ $path }\"\ncore_image_too_large = تصویر خیلی بزرگ است ({ $width }x{ $height }) - بیش از حد مجاز { $max } پیکسل\ncore_failed_to_get_video_metadata = ناموفق بود دریافت اطلاعات ویدئویی برای فایل \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = ناموفق بود دریافت کدک ویدیویی برای فایل \"{ $file }\"\ncore_failed_to_get_video_duration = ناموفق بود دریافت مدت زمان ویدیو برای فایل \"{ $file }\"\ncore_failed_to_get_video_dimensions = ناموفق بود دریافت ابعاد ویدیو برای فایل \"{ $file }\"\ncore_frame_dimensions_mismatch = ابعاد فریم برای زمان‌بندی { $timestamp } با ابعاد فریم اول ({ $first_w }x{ $first_h }) مطابقت ندارند\ncore_failed_to_load_data_from_cache = ناموفق بود تا داده‌ها را از فایل کش { $file } بارگیری شود، دلیل { $reason }\ncore_failed_to_load_data_from_json_cache = ناموفق بود تا داده‌ها را از فایل کش JSON { $file} بارگیری شود، دلیل { $reason }\ncore_failed_to_replace_with_optimized = ناموفق بود فایل \"{ $file }\" با نسخه بهینه جایگزین شود: { $reason }\ncore_failed_to_write_data_to_cache = امکان نوشتن داده‌ها به فایل کش \"{ $file }\" وجود ندارد، دلیل { $reason }\ncore_properly_saved_cache_entries = ذخیره شده به درستی در فایل { $count } ورودی کش.\ncore_video_processing_stopped_by_user = پردازش ویدیو توسط کاربر متوقف شد\ncore_thumbnail_generation_stopped_by_user = تولید پیش‌نمایی متوقف شد توسط کاربر\ncore_failed_to_optimize_video = ناموفق بود برای بهینه‌سازی ویدیو \"{ $file }\": { $reason }\ncore_failed_to_crop_video = ناموفق بود برش ویدیو \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = ناموفق بود دریافت اطلاعات متا داده شده از فایل بهینه شده \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = امکان ایجاد پوشه تنظیمات \"{ $folder }\" وجود ندارد، دلیل { $reason }\ncore_cannot_create_cache_folder = امکان ایجاد پوشه کش \"{ $folder }\" وجود ندارد، دلیل { $reason }\ncore_cannot_create_or_open_cache_file = امکان ایجاد یا باز کردن فایل کش \"{ $file }\" وجود ندارد، دلیل { $reason }\ncore_cannot_set_config_cache_path = امکان تنظیم مسیر config/cache وجود ندارد - config و cache استفاده نخواهند شد.\ncore_invalid_extension_contains_space = { $extension } یک پسوند معتبر نیست زیرا حاوی فاصله خالی در داخل است\ncore_invalid_extension_contains_dot = { $extension } یک پسوند معتبر نیست زیرا شامل نقطه داخل آن است\n"
  },
  {
    "path": "czkawka_core/i18n/fr/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Originale\ncore_similarity_very_high = Très haute\ncore_similarity_high = Haute\ncore_similarity_medium = Moyenne\ncore_similarity_small = Basse\ncore_similarity_very_small = Très basse\ncore_similarity_minimal = Minimale\ncore_cannot_open_dir = Impossible d’ouvrir le répertoire { $dir }. Raison : { $reason }\ncore_cannot_read_entry_dir = Impossible de lire l'entrée dans le répertoire { $dir }. Raison : { $reason }\ncore_cannot_read_metadata_dir = Impossible de lire les métadonnées dans le répertoire { $dir }. Raison  : { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Le fichier { $name } semble avoir été modifié avant l'époque Unix\ncore_folder_modified_before_epoch = Le dossier { $name } semble avoir été modifié avant l'époque Unix\ncore_file_no_modification_date = Impossible d'obtenir la date de modification du fichier { $name }. Raison  : { $reason }\ncore_folder_no_modification_date = Impossible d'obtenir la date de modification du dossier { $name }. Raison : { $reason }\ncore_cannot_start_scan_no_included_paths = Impossible de démarrer l'analyse, car il n'y a pas de chemins inclus\ncore_skip_exist_check_all_included_paths_nonexistent = Impossible de démarrer l'analyse, car tous les chemins inclus n'existent pas\ncore_missing_no_chosen_included_path = Aucune voie incluse valide n'a été choisie (les voies exclues auraient pu exclure toutes les voies incluses)\ncore_reference_included_paths_same = Impossible de démarrer l'analyse où tous les chemins inclus valides sont également des chemins référencés, essayez de valider ou de désactiver les chemins référencés\ncore_path_must_exists = Le chemin fourni doit exister, en ignorant { $path }\ncore_must_be_directory_or_file = Le chemin fourni doit pointer vers un répertoire ou un fichier valide, en ignorant { $path }\ncore_excluded_paths_pointless_slash = Exclure / est inutile, car cela signifie que aucun fichier ne sera scanné\ncore_paths_unable_to_get_device_id = Impossible d’obtenir l’identifiant de l’appareil à partir du dossier { $path }\ncore_needs_allowed_extensions_limited_by_tool = Impossible de démarrer l'analyse, lorsque toutes les extensions disponibles dans cet outil ({ $extensions }) ont été exclues de l'analyse\ncore_needs_allowed_extensions = Impossible de démarrer l'analyse, lorsque toutes les extensions ont été exclues de l'analyse\ncore_needs_to_set_at_least_one_broken_option = Impossible de démarrer l'analyse, lorsqu'aucune option de détection de panne n'est définie pour l'analyse\ncore_needs_to_set_at_least_one_bad_name_option = Impossible de démarrer l'analyse, lorsqu'aucune option de mauvais nom n'est définie pour l'analyse\ncore_ffmpeg_not_found = Impossible de trouver une installation appropriée de FFmpeg ou FFprobe. Ce sont des programmes externes qui doivent être installés manuellement.\ncore_ffmpeg_not_found_windows = Assurez-vous que ffmpeg.exe et ffprobe.exe sont disponibles en PATH ou sont placés directement dans le même dossier que l'exécutable de l'application\ncore_invalid_symlink_infinite_recursion = Récursion infinie\ncore_invalid_symlink_non_existent_destination = Fichier de destination inexistant\ncore_messages_limit_reached_characters = Le nombre de messages a dépassé la limite définie ({ $current }/{ $limit } caractères), donc la sortie a été tronquée. Pour lire la sortie complète, désactivez l'option de limitation dans les paramètres.\ncore_messages_limit_reached_lines = Le nombre de messages a dépassé la limite définie (lignes{ $current }/{ $limit } ) donc la sortie a été tronquée. Pour lire la sortie complète, désactivez l'option de limitation dans les paramètres.\ncore_error_moving_to_trash = Erreur lors du déplacement de \"{ $file }\" vers la poubelle : { $error }\ncore_error_removing = Erreur lors de la suppression de \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Impossible de trouver des fichiers musicaux similaires sans une méthode de similarité sélectionnée\ncore_ffmpeg_failed_with_status = ffmpeg a échoué avec le statut { $status } : { $stderr } (commande : { $command })\ncore_failed_to_load_image_frame = Erreur de chargement du cadre d'image : { $reason }\ncore_failed_to_extract_frame = Échec de l'extraction du cadre à { $time } secondes depuis \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Échec de sauvegarde de la miniature pour \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Échec de récupération du cadre au timestamp { $timestamp } depuis \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Échec de récupération du cadre à partir de \"{ $file }\" à l'horodatage { $timestamp } : { $reason }\ncore_invalid_crop_rectangle = Rectangle de culture non valide : gauche={ $left }, haut={ $top }, droite={ $right }, bas={ $bottom }\ncore_failed_to_crop_video_file = Échec du recadrage du fichier vidéo \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Fichier vidéo coupé non créé : { $temp }\ncore_unable_check_hash_of_file = Impossible de vérifier le hachage du fichier \"{ $file }\", la raison est { $reason }\ncore_error_checking_hash_of_file = Erreur survenue lors de la vérification du haché du fichier \"{ $file }\", la raison { $reason }\ncore_image_zero_dimensions = Image a zéro largeur ou hauteur \"{ $path }\"\ncore_image_open_failed = Impossible d'ouvrir le fichier image \"{ $path }\": { $reason }\ncore_not_directory_remove = Essayer de supprimer le dossier \"{ $path }\" qui n'est pas un répertoire\ncore_cannot_read_directory = Impossible de lire le répertoire \"{ $path }\"\ncore_cannot_read_entry_from_directory = Impossible de lire l’entrée du répertoire \"{ $path }\"\ncore_folder_contains_file_inside = Le dossier contient le fichier \"{ $entry }\" à l'intérieur \"{ $folder }\"\ncore_unknown_directory_entry = Impossible de déterminer le type de fichier de l'entrée de répertoire \"{ $entry }\" dans \"{ $path }\"\ncore_video_width_exceeds_limit = La largeur de la vidéo { $width } dépasse la limite de { $limit }\ncore_video_height_exceeds_limit = Vidéo hauteur { $height } dépasse la limite de { $limit }\ncore_failed_to_process_video = Échec du traitement du fichier vidéo { $file }: { $reason }\ncore_unknown_codec = Codec inconnu : { $codec }\ncore_invalid_video_optimizer_mode = Mode d'optimisation vidéo non valide : '{ $mode }'. Valeurs autorisées : transcode, crop\ncore_folder_does_not_exist = Le dossier n’existe pas : { $folder }\ncore_path_not_directory = Le chemin n'est pas un répertoire : { $folder }\ncore_test_error_for_folder = Erreur de test pour le dossier : { $folder }\ncore_unknown_exif_tag_group = Groupe de balises EXIF inconnu : { $tag }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Échec de génération de miniature pour \"{ $file }\": les images extraites ont des dimensions différentes\ncore_failed_to_generate_thumbnail = Échec de la génération de miniature pour \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Échec de l'extraction du cadre à { $time } secondes depuis \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Fichier vidéo introuvable (peut être supprimé entre les étapes de numérisation/plus tard) : \"{ $path }\"\ncore_image_too_large = L'image est trop grande ({ $width }x{ $height }) - plus que le supporté { $max } pixels\ncore_failed_to_get_video_metadata = Échec de récupération des métadonnées vidéo pour le fichier \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Échec de récupération du codec vidéo pour le fichier \"{ $file }\"\ncore_failed_to_get_video_duration = Impossible d’obtenir la durée de la vidéo pour le fichier \"{ $file }\"\ncore_failed_to_get_video_dimensions = Impossible d'obtenir les dimensions de la vidéo pour le fichier \"{ $file }\"\ncore_frame_dimensions_mismatch = Les dimensions du cadre pour le timestamp { $timestamp } ne correspondent pas aux dimensions du premier cadre ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Échec de chargement des données depuis le fichier de cache { $file }, la raison { $reason }\ncore_failed_to_load_data_from_json_cache = Échec de chargement des données à partir du fichier de cache JSON { $file }, la raison { $reason }\ncore_failed_to_replace_with_optimized = Échec du remplacement du fichier \"{ $file }\" par la version optimisée : { $reason }\ncore_failed_to_write_data_to_cache = Impossible d'écrire des données dans le fichier de cache \"{ $file }\", la raison { $reason }\ncore_properly_saved_cache_entries = Sauvegardé correctement dans le fichier { $count } entrées de cache.\ncore_video_processing_stopped_by_user = Le traitement vidéo a été arrêté par l'utilisateur\ncore_thumbnail_generation_stopped_by_user = La génération de miniatures a été arrêtée par l'utilisateur\ncore_failed_to_optimize_video = Échec de l'optimisation de la vidéo \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Échec du recadrage de la vidéo \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Échec de récupération des métadonnées du fichier optimisé \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Impossible de créer le dossier de configuration \"{ $folder }\", la raison est { $reason }\ncore_cannot_create_cache_folder = Impossible de créer le dossier de cache \"{ $folder }\", la raison { $reason }\ncore_cannot_create_or_open_cache_file = Impossible de créer ou d'ouvrir le fichier de cache \"{ $file }\", la raison { $reason }\ncore_cannot_set_config_cache_path = Impossible de définir le chemin de config/cache - la config et le cache ne seront pas utilisés.\ncore_invalid_extension_contains_space = { $extension } n'est pas une extension valide car elle contient des espaces vides à l'intérieur\ncore_invalid_extension_contains_dot = { $extension } n'est pas une extension valide car elle contient un point à l'intérieur\n\ncore_failed_to_spawn_command = Impossible de lancer la commande : { $reason }\ncore_failed_to_check_process_status = Impossible de vérifier l'état du processus : { $reason }\ncore_failed_to_wait_for_process = Impossible d'attendre la fin du processus : { $reason }\ncore_failed_to_read_video_properties = Impossible de lire les propriétés de la vidéo : { $reason }\ncore_failed_to_execute_ffmpeg = Impossible d'exécuter ffmpeg : { $reason }\ncore_optimized_file_larger = Le fichier optimisé { $optimized } (taille : { $new_size }) n'est pas plus petit que le fichier original { $original } (taille : { $original_size })\ncore_error_comparing_fingerprints = Erreur lors de la comparaison des empreintes : { $reason }"
  },
  {
    "path": "czkawka_core/i18n/it/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Originali\ncore_similarity_very_high = Altissima\ncore_similarity_high = Alta\ncore_similarity_medium = Media\ncore_similarity_small = Piccola\ncore_similarity_very_small = Piccolissima\ncore_similarity_minimal = Minima\ncore_cannot_open_dir = Impossibile aprire cartella { $dir }, motivo { $reason }\ncore_cannot_read_entry_dir = Impossibile leggere elemento nella cartella { $dir }, ragione { $reason }\ncore_cannot_read_metadata_dir = Impossibile leggere metadati nella cartella { $dir }, ragione { $reason }\ncore_cannot_read_metadata_file = Impossibile leggere i metadati del file { $file } , ragione { $reason }\ncore_file_modified_before_epoch = Il file { $name } sembra essere stato modificato prima dell'Epoch Unix\ncore_folder_modified_before_epoch = La cartella { $name } sembra essere stata modificata prima dell'Epoch Unix\ncore_file_no_modification_date = Impossibile recuperare data di modifica dal file { $name }, ragione { $reason }\ncore_folder_no_modification_date = Impossibile recuperare data di modifica dalla cartella { $name }, ragione { $reason }\ncore_cannot_start_scan_no_included_paths = Impossibile avviare la scansione, perché non ci sono percorsi inclusi\ncore_skip_exist_check_all_included_paths_nonexistent = Impossibile avviare la scansione, perché tutti i percorsi inclusi non esistono\ncore_missing_no_chosen_included_path = Non è stato incluso nessun percorso valido (i percorsi esclusi potrebbero aver escluso tutti i percorsi inclusi)\ncore_reference_included_paths_same = Impossibile avviare la scansione dove tutti i percorsi inclusi validi sono anche percorsi di riferimento, provare a ricontrollare o a disabilitare i percorsi di riferimento\ncore_path_must_exists = Percorso fornito non esistente, ignoro { $path }\ncore_must_be_directory_or_file = Il percorso fornito deve puntare a una cartella o file validi, ignoro { $path }\ncore_excluded_paths_pointless_slash = Escludendo / è inutile, perché significa che nessun file verrà scansionato\ncore_paths_unable_to_get_device_id = Impossibile ottenere l'id del dispositivo dalla cartella { $path }\ncore_needs_allowed_extensions_limited_by_tool = Impossibile avviare la scansione, quando tutte le estensioni disponibili in questo strumento ({ $extensions }) sono state escluse dalla scansione\ncore_needs_allowed_extensions = Impossibile avviare la scansione, quando tutte le estensioni sono state escluse dalla scansione\ncore_needs_to_set_at_least_one_broken_option = Impossibile avviare la scansione, quando non è impostata l'opzione \"broken\" per la scansione\ncore_needs_to_set_at_least_one_bad_name_option = Impossibile avviare la scansione, quando non è impostata l'opzione \"nome errato\" per la scansione\ncore_ffmpeg_not_found = Non riesco a trovare un'installazione appropriata di FFmpeg o FFprobe. Questi sono programmi esterni che devono essere installati manualmente.\ncore_ffmpeg_not_found_windows = Assicurati che ffmpeg.exe e ffprobe.exe siano disponibili in PATH o siano posizionati direttamente nella stessa cartella dell'eseguibile dell'app\ncore_invalid_symlink_infinite_recursion = Ricorsione infinita\ncore_invalid_symlink_non_existent_destination = File di destinazione inesistente\ncore_messages_limit_reached_characters = Il numero di messaggi ha superato il limite impostato ( caratteri{ $current }/{ $limit } ), quindi l'output è stato troncato. Per leggere l'output completo, disabilitare l'opzione di limitazione nelle impostazioni.\ncore_messages_limit_reached_lines = Il numero di messaggi ha superato il limite impostato ( linee{ $current }/{ $limit } ), quindi l'output è stato troncato. Per leggere l'output completo, disabilitare l'opzione di limitazione nelle impostazioni.\ncore_error_moving_to_trash = Errore durante lo spostamento di \"{ $file }\" nel cestino: { $error }\ncore_error_removing = Errore durante la rimozione \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Non riesco a trovare file musicali simili senza un metodo di similarità selezionato\ncore_failed_to_spawn_command = Fallito generare comando: { $reason }\ncore_failed_to_check_process_status = Impossibile controllare lo stato del processo: { $reason }\ncore_failed_to_wait_for_process = Impossibile attendere il processo: { $reason }\ncore_failed_to_read_video_properties = Impossibile leggere le proprietà del video: { $reason }\ncore_failed_to_execute_ffmpeg = Impossibile eseguire ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg fallito con stato { $status }: { $stderr } (comando: { $command })\ncore_failed_to_load_image_frame = Impossibile caricare il frame dell'immagine: { $reason }\ncore_failed_to_extract_frame = Fallito nell'estrarre il frame a { $time } secondi da \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Impossibile salvare l'anteprima per \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Impossibile ottenere il frame al timestamp { $timestamp } da \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Impossibile ottenere il frame da \"{ $file }\" al timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Rettangolo di riempimento non valido: sinistra={ $left }, in alto={ $top }, destra={ $right }, in basso={ $bottom }\ncore_failed_to_crop_video_file = Impossibile ritagliare il file video \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Il file video ritagliato non è stato creato: { $temp }\ncore_unable_check_hash_of_file = Impossibile controllare l'hash del file \"{ $file }\", motivo { $reason }\ncore_error_checking_hash_of_file = Errore avvenuto durante il controllo dell'hash del file \"{ $file }\", motivo { $reason }\ncore_image_zero_dimensions = L'immagine ha zero larghezza o altezza \"{ $path }\"\ncore_image_open_failed = Impossibile aprire il file immagine \"{ $path }\": { $reason }\ncore_not_directory_remove = Tentativo di rimuovere la cartella \"{ $path }\" che non è una directory\ncore_cannot_read_directory = Impossibile leggere la directory \"{ $path }\"\ncore_cannot_read_entry_from_directory = Impossibile leggere l'entrata dal directory \"{ $path }\"\ncore_folder_contains_file_inside = La cartella contiene il file \"{ $entry }\" all'interno di \"{ $folder }\"\ncore_unknown_directory_entry = Impossibile determinare il tipo di file dell'inserimento della directory \"{ $entry }\" all'interno di \"{ $path }\"\ncore_video_width_exceeds_limit = Video larghezza { $width } supera il limite di { $limit }\ncore_video_height_exceeds_limit = Video altezza { $height } supera il limite di { $limit }\ncore_failed_to_process_video = Impossibile elaborare il file video { $file }: { $reason }\ncore_optimized_file_larger = File ottimizzato { $optimized } (dimensione: { $new_size }) non è più piccolo dell'originale { $original } (dimensione: { $original_size })\ncore_unknown_codec = Codec sconosciuto: { $codec }\ncore_invalid_video_optimizer_mode = Modalità ottimizzatore video non valida: '{ $mode }'. Valori ammessi: transcodifica, ritaglio\ncore_folder_does_not_exist = La cartella non esiste: { $folder }\ncore_path_not_directory = Il percorso non è una directory: { $folder }\ncore_test_error_for_folder = Errore di test per cartella: { $folder }\ncore_unknown_exif_tag_group = Gruppo di tag EXIF sconosciuto: { $tag }\ncore_error_comparing_fingerprints = Errore durante il confronto delle impronte digitali: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Impossibile generare l'anteprima per \"{ $file }\": i fotogrammi estratti hanno dimensioni diverse\ncore_failed_to_generate_thumbnail = Impossibile generare l'anteprima per \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Fallito nell'estrarre il frame a { $time } secondi da \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = File video non esistente (può essere rimosso tra le fasi di scansione/successive): \"{ $path }\"\ncore_image_too_large = L'immagine è troppo grande ({ $width }x{ $height }) - più di { $max } pixel supportati\ncore_failed_to_get_video_metadata = Impossibile ottenere i metadati video per il file \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Impossibile ottenere il codec video per il file \"{ $file }\"\ncore_failed_to_get_video_duration = Impossibile ottenere la durata del video per il file \"{ $file }\"\ncore_failed_to_get_video_dimensions = Impossibile ottenere le dimensioni del video per il file \"{ $file }\"\ncore_frame_dimensions_mismatch = Dimensioni del fotogramma per timestamp { $timestamp } non corrispondono alle dimensioni del primo fotogramma ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Impossibile caricare i dati dal file di cache { $file }, motivo { $reason }\ncore_failed_to_load_data_from_json_cache = Impossibile caricare i dati dal file di cache json { $file }, motivo { $reason }\ncore_failed_to_replace_with_optimized = Impossibile sostituire il file \"{ $file }\" con la versione ottimizzata: { $reason }\ncore_failed_to_write_data_to_cache = Impossibile scrivere i dati nel file di cache \"{ $file }\", motivo { $reason }\ncore_properly_saved_cache_entries = Salvatato correttamente nel file { $count } voci di cache.\ncore_video_processing_stopped_by_user = L'elaborazione video è stata interrotta dall'utente\ncore_thumbnail_generation_stopped_by_user = Generazione miniatura interrotta dall'utente\ncore_failed_to_optimize_video = Impossibile ottimizzare il video \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Impossibile ritagliare il video \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Impossibile ottenere i metadati del file ottimizzato \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Impossibile creare la cartella di configurazione \"{ $folder }\", motivo { $reason }\ncore_cannot_create_cache_folder = Impossibile creare la cartella di cache \"{ $folder }\", motivo { $reason }\ncore_cannot_create_or_open_cache_file = Impossibile creare o aprire il file di cache \"{ $file }\", motivo { $reason }\ncore_cannot_set_config_cache_path = Impossibile impostare il percorso config/cache - config e cache non verranno utilizzati.\ncore_invalid_extension_contains_space = { $extension } non è un'estensione valida perché contiene spazi vuoti all'interno\ncore_invalid_extension_contains_dot = { $extension } non è un'estensione valida perché contiene un punto all'interno\n"
  },
  {
    "path": "czkawka_core/i18n/ja/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = 新規に作成\ncore_similarity_very_high = 非常に高い\ncore_similarity_high = 高い\ncore_similarity_medium = ミディアム\ncore_similarity_small = 小\ncore_similarity_very_small = 非常に小さい\ncore_similarity_minimal = 最小\ncore_cannot_open_dir = ディレクトリを開くことができません { $dir }、理由 { $reason }\ncore_cannot_read_entry_dir = Dir { $dir } でエントリを読み込めません、理由 { $reason }\ncore_cannot_read_metadata_dir = Dir { $dir } でメタデータを読み込めません、理由 { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = ファイル { $name } は Unix Epoch より前に変更されているようです\ncore_folder_modified_before_epoch = フォルダ { $name } は、Unix Epoch の前に変更されているようです\ncore_file_no_modification_date = ファイル { $name } から変更日を取得できません、理由 { $reason }\ncore_folder_no_modification_date = フォルダ { $name } から変更日を取得できません、理由 { $reason }\ncore_cannot_start_scan_no_included_paths = スキャンを開始できません、含まれているパスがないため。\ncore_skip_exist_check_all_included_paths_nonexistent = スキャンを開始できません。指定されたすべてのパスが存在しません。\ncore_missing_no_chosen_included_path = 有効な含まれるパスが選択されませんでした（除外されたパスがすべての含まれるパスを除外した可能性があります）\ncore_reference_included_paths_same = スキャンを開始できません。すべての有効な含まれるパスが参照パスでもある場合、検証を試みてください、または参照パスを無効化してください。\ncore_path_must_exists = 提供されたパスが存在しなければなりません、{ $path } を無視して\ncore_must_be_directory_or_file = 提供されたパスは、有効なディレクトリまたはファイルに指し示す必要があり、{ $path } を無視します。\ncore_excluded_paths_pointless_slash = 除外 / は無意味で、ファイルがスキャンされないことを意味するからです\ncore_paths_unable_to_get_device_id = フォルダ { $path } からデバイスIDを取得できません\ncore_needs_allowed_extensions_limited_by_tool = スキャンを開始できません。このツール ({ $extensions }) に存在するすべての拡張機能を除外しても。\ncore_needs_allowed_extensions = スキャンを開始できません。すべての拡張機能をスキャンから除外したとき\ncore_needs_to_set_at_least_one_broken_option = スキャンを開始できません。破損オプションが設定されていない場合に発生します。\ncore_needs_to_set_at_least_one_bad_name_option = スキャンを開始できません。悪い名前オプションが設定されていない場合にのみスキャンします。\ncore_ffmpeg_not_found = FFmpegまたはFFprobeの適切なインストールを見つけられません。これらは外部プログラムであり、手動でインストールする必要があります。.\ncore_ffmpeg_not_found_windows = ffmpeg.exeとffprobe.exeがPATHで使用できるか、アプリ実行ファイルと同じフォルダに直接配置されていることを確認してください\ncore_invalid_symlink_infinite_recursion = 無限再帰性\ncore_invalid_symlink_non_existent_destination = 保存先ファイルが存在しません\ncore_messages_limit_reached_characters = メッセージ数が設定された制限({ $current }/{ $limit } 文字)を超えたため、出力は切り捨てられました。 フル出力を読み込むには、設定で制限オプションを無効にします。.\ncore_messages_limit_reached_lines = メッセージ数が設定された制限({ $current }/{ $limit } 行)を超えたため、出力は切り捨てられました。 フル出力を読み込むには、設定で制限オプションを無効にします。.\ncore_error_moving_to_trash = \"{ $file }\" をゴミ箱に移動中にエラーが発生しました: { $error }\ncore_error_removing = エラーを削除中に \"{ $file }\" で発生しました: { $error }\ncore_no_similarity_method_selected = 類似の音楽ファイルを見つけることができません。選択された類似性方法がない場合\ncore_failed_to_spawn_command = コマンドの生成に失敗しました：{ $reason }\ncore_failed_to_check_process_status = プロセス状態の確認に失敗しました：{ $reason }\ncore_failed_to_wait_for_process = プロセスを待機できませんでした：{ $reason }\ncore_failed_to_read_video_properties = ビデオプロパティの読み込みに失敗しました： { $reason }\ncore_failed_to_execute_ffmpeg = ffmpegの実行に失敗しました：{ $reason }\ncore_ffmpeg_failed_with_status = ffmpeg はステータス { $status } で失敗しました: { $stderr } (コマンド: { $command })\ncore_failed_to_load_image_frame = 画像フレームの読み込みに失敗しました：{ $reason }\ncore_failed_to_extract_frame = { $time }秒でフレームを抽出できませんでした。「{ $file }」から：{ $reason }\ncore_failed_to_save_thumbnail = サムネイルを \"{ $file }\" のために保存できませんでした：{ $reason }\ncore_failed_get_frame_at_timestamp = タイムスタンプ { $timestamp } から \"{ $file }\" のフレームを取得できませんでした：{ $reason }\ncore_failed_get_frame_from_file = \"{ $file }\" からフレームを取得できませんでした。タイムスタンプ { $timestamp }、理由 { $reason }。\ncore_invalid_crop_rectangle = 無効な作物矩形：左={ $left }、上={ $top }、右={ $right }、下={ $bottom }\ncore_failed_to_crop_video_file = ビデオファイル \"{ $file }\" のトリミングに失敗しました：{ $reason }\ncore_cropped_video_not_created = 切り抜かれた動画ファイルが作成されませんでした：{ $temp }\ncore_unable_check_hash_of_file = ファイル \"{ $file }\" のハッシュを確認できません。理由 { $reason }\ncore_error_checking_hash_of_file = ファイル \"{ $file }\" のハッシュチェック時にエラーが発生しました、理由 { $reason }\ncore_image_zero_dimensions = 画像はゼロの幅または高さ \"{ $path }\"\ncore_image_open_failed = 画像ファイル \"{ $path }\" を開けません：{ $reason }\ncore_not_directory_remove = フォルダ \"{ $path }\" を削除しようとしています。これはディレクトリではありません。\ncore_cannot_read_directory = \"{ $path }\" を読み取れません\ncore_cannot_read_entry_from_directory = ディレクトリ \"{ $path }\" からエントリを読み取ることができません。\ncore_folder_contains_file_inside = フォルダ内にファイル \"{ $entry }\" が \"{ $folder }\" 内に存在します。\ncore_unknown_directory_entry = ディレクトリエントリ \"{ $entry }\" のファイルタイプを \"{ $path }\" 内で判別できません。\ncore_video_width_exceeds_limit = 動画の幅 { $width } は { $limit } の制限を超えています\ncore_video_height_exceeds_limit = 動画の高さ { $height } は { $limit } の制限を超えています\ncore_failed_to_process_video = ビデオファイル { $file } の処理に失敗しました: { $reason }\ncore_optimized_file_larger = 最適化ファイル { $optimized } (サイズ: { $new_size }) は、元の { $original } (サイズ: { $original_size }) よりも小さくありません。\ncore_unknown_codec = 不明コーデック：{ $codec }\ncore_invalid_video_optimizer_mode = 無効なビデオ最適化モード：'{ $mode }'。許可される値：transcode, crop\ncore_folder_does_not_exist = フォルダが存在しません: { $folder }\ncore_path_not_directory = パスはディレクトリではありません：{ $folder }\ncore_test_error_for_folder = フォルダのテストエラー：{ $folder }\ncore_unknown_exif_tag_group = 不明EXIFタググループ：{ $tag }\ncore_error_comparing_fingerprints = 指紋の比較中にエラー：{ $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = \"{ $file }\" のサムネイルを生成できませんでした：抽出されたフレームの寸法が異なります\ncore_failed_to_generate_thumbnail = \"{ $file }\" のサムネイルの生成に失敗しました：{ $reason }\ncore_failed_to_extract_frame_at_seek_time = { $time }秒でフレームを抽出できませんでした。「{ $file }」から：{ $reason }\ncore_video_file_does_not_exist = ビデオファイルが存在しません（スキャン/後続ステップ間で削除しても構いません）：\"{ $path }\"\ncore_image_too_large = 画像が大きすぎです ({ $width }x{ $height }) - { $max }ピクセルを超えています\ncore_failed_to_get_video_metadata = ファイル \"{ $file }\" のビデオメタデータを取得できませんでした：{ $reason }\ncore_failed_to_get_video_codec = ファイル \"{ $file }\" のビデオコーデックを取得できませんでした。\ncore_failed_to_get_video_duration = ファイル \"{ $file }\" の動画の期間を取得できませんでした。\ncore_failed_to_get_video_dimensions = ファイル \"{ $file }\" のビデオ寸法を取得できませんでした。\ncore_frame_dimensions_mismatch = タイムスタンプ { $timestamp } のフレーム寸法と、最初のフレーム寸法 ({ $first_w }x{ $first_h }) が一致しません。\ncore_failed_to_load_data_from_cache = キャッシュファイル { $file } からデータ読み込みに失敗しました、理由 { $reason }\ncore_failed_to_load_data_from_json_cache = JSONキャッシュファイル { $file } からデータ読み込みに失敗しました。理由 { $reason }\ncore_failed_to_replace_with_optimized = ファイル \"{ $file }\" を最適化バージョンで置き換えに失敗しました: { $reason }\ncore_failed_to_write_data_to_cache = キャッシュファイル \"{ $file }\" へのデータ書き込みに失敗しました、理由 { $reason }\ncore_properly_saved_cache_entries = ファイルに正しく保存されました { $count } 件のキャッシュエントリ。.\ncore_video_processing_stopped_by_user = ビデオ処理はユーザーによって停止されました\ncore_thumbnail_generation_stopped_by_user = サムネイル生成はユーザーによって停止されました\ncore_failed_to_optimize_video = ビデオの最適化に失敗しました \"{ $file }\": { $reason }\ncore_failed_to_crop_video = ビデオのトリミングに失敗しました \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = 最適化されたファイル \"{ $file }\" のメタデータ取得に失敗しました：{ $reason }\ncore_cannot_create_config_folder = 設定ファイル \"{ $folder }\" を作成できません。理由 { $reason } です。\ncore_cannot_create_cache_folder = キャッシュフォルダ \"{ $folder }\" を作成できません。理由 { $reason }\ncore_cannot_create_or_open_cache_file = キャッシュファイル \"{ $file }\" を作成または開けません。理由 { $reason }\ncore_cannot_set_config_cache_path = 設定/キャッシュのパスを設定できません - 設定とキャッシュは使用されません。.\ncore_invalid_extension_contains_space = { $extension } は有効な拡張子ではありません。なぜなら、中に空白が含まれているからです。\ncore_invalid_extension_contains_dot = { $extension } は有効な拡張子ではありません。なぜなら、中にドットが含まれているからです。\n"
  },
  {
    "path": "czkawka_core/i18n/ko/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = 원본\ncore_similarity_very_high = 매우 높음\ncore_similarity_high = 높음\ncore_similarity_medium = 보통\ncore_similarity_small = 낮음\ncore_similarity_very_small = 매우 낮음\ncore_similarity_minimal = 최소\ncore_cannot_open_dir = { $dir } 디렉터리를 열 수 없습니다. 이유: { $reason }\ncore_cannot_read_entry_dir = { $dir } 디렉터리를 열 수 없습니다. 이유: { $reason }\ncore_cannot_read_metadata_dir = { $dir } 디렉터리의 메타데이터를 열 수 없습니다. 이유: { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch\ncore_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch\ncore_file_no_modification_date = { $name } 파일의 수정된 시각을 읽을 수 없습니다. 이유: { $reason }\ncore_folder_no_modification_date = { $name } 폴더의 수정된 시각을 읽을 수 없습니다. 이유: { $reason }\ncore_cannot_start_scan_no_included_paths = 스캔을 시작할 수 없습니다, 포함된 경로가 없습니다\ncore_skip_exist_check_all_included_paths_nonexistent = 스캔을 시작할 수 없습니다, 모든 포함된 경로가 존재하지 않기 때문입니다\ncore_missing_no_chosen_included_path = 유효한 포함된 경로가 선택되지 않았습니다(제외된 경로가 모든 포함된 경로를 배제했을 수 있습니다)\ncore_reference_included_paths_same = 모든 유효한 포함된 경로가 참조 경로로도 참조되는 경우 스캔을 시작할 수 없습니다. 유효성을 검사하거나 참조 경로를 비활성화하십시오\ncore_path_must_exists = 제공된 경로가 존재해야 하며, { $path } 무시합니다\ncore_must_be_directory_or_file = 제공된 경로가 유효한 디렉터리 또는 파일에 가리키도록 해야 하며, { $path } 무시합니다\ncore_excluded_paths_pointless_slash = 제외 / 는 무의미하며, 이는 파일이 스캔되지 않음을 의미하기 때문입니다\ncore_paths_unable_to_get_device_id = 폴더 { $path } 에서 장치 ID를 가져올 수 없음\ncore_needs_allowed_extensions_limited_by_tool = 스캔을 시작할 수 없습니다, 이 도구({ $extensions })에 있는 모든 확장 기능이 스캔에서 제외되었기 때문입니다\ncore_needs_allowed_extensions = 스캔 시작할 수 없습니다, 모든 확장 프로그램이 스캔에서 제외되었을 때\ncore_needs_to_set_at_least_one_broken_option = 스캔을 시작할 수 없습니다, 손상 옵션이 스캔하도록 설정되지 않았을 때\ncore_needs_to_set_at_least_one_bad_name_option = 스캔을 시작할 수 없습니다, 잘못된 이름 옵션이 스캔하도록 설정되지 않았을 때\ncore_ffmpeg_not_found = FFmpeg 또는 FFprobe의 적절한 설치 파일을 찾을 수 없습니다. 이러한 프로그램들은 수동으로 설치해야 합니다.\ncore_ffmpeg_not_found_windows = ffmpeg.exe와 ffprobe.exe가 PATH에 있거나 앱 실행 파일과 같은 폴더에 직접 배치되어 있는지 확인하세요\ncore_invalid_symlink_infinite_recursion = 무한 재귀\ncore_invalid_symlink_non_existent_destination = 목표 파일이 없음\ncore_messages_limit_reached_characters = Number of messages exceeded the set limit ({ $current }/{ $limit } characters), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_messages_limit_reached_lines = Number of messages exceeded the set limit ({ $current }/{ $limit } lines), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_error_moving_to_trash = \"{ $file }\"를 쓰레기통으로 옮길 때 오류가 발생했습니다: { $error }\ncore_error_removing = \"{ $file }\" 삭제 중 오류: { $error }\ncore_no_similarity_method_selected = 유형을 선택하지 않았기 때문에 유사한 음악 파일을 찾을 수 없습니다\ncore_failed_to_spawn_command = 명령어 생성 실패: { $reason }\ncore_failed_to_check_process_status = 프로세스 상태 확인 실패: { $reason }\ncore_failed_to_wait_for_process = 프로세스 대기 실패: { $reason }\ncore_failed_to_read_video_properties = 비디오 속성 읽기 실패: { $reason }\ncore_failed_to_execute_ffmpeg = ffmpeg 실행 실패: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg 실패했습니다 상태 { $status }: { $stderr } (명령: { $command })\ncore_failed_to_load_image_frame = 이미지 프레임을 로드하지 못했습니다: { $reason }\ncore_failed_to_extract_frame = 실패했습니다: { $time } 초에서 \"{ $file }\"에서 프레임을 추출하지 못했습니다: { $reason }\ncore_failed_to_save_thumbnail = 썸네일 { $file } 저장 실패: { $reason }\ncore_failed_get_frame_at_timestamp = 실패했습니다. 타임스탬프 { $timestamp }에서 \"{ $file }\"에서 프레임을 가져오지 못했습니다: { $reason }\ncore_failed_get_frame_from_file = \"{ $file }\"에서 프레임을 가져오지 못했습니다. 타임스탬프 { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = 유효하지 않은 래스터 영역: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom }\ncore_failed_to_crop_video_file = 비디오 파일 \"{ $file }\" 자르기 실패: { $reason }\ncore_cropped_video_not_created = 잘린 비디오 파일이 생성되지 않았습니다: { $temp }\ncore_unable_check_hash_of_file = 파일 해시 확인 불가 { $file }, 이유 { $reason }\ncore_image_zero_dimensions = 이미지의 너비 또는 높이는 0입니다 \"{ $path }\"\ncore_image_open_failed = 불가능: \"{ $path }\" 이미지 파일을 열 수 없습니다. { $reason }\ncore_not_directory_remove = 폴더 \"{ $path }\"을 제거하려고 하는데, 디렉터리가 아닙니다\ncore_cannot_read_directory = \"{ $path }\"를 읽을 수 없습니다\ncore_cannot_read_entry_from_directory = 디렉토리 \"{ $path }\"에서 항목을 읽을 수 없습니다\ncore_folder_contains_file_inside = 폴더 안에 파일 \"{ $entry }\" 가 \"{ $folder }\" 안에 있습니다\ncore_unknown_directory_entry = 디렉토리 항목 \"{ $entry }\"의 파일 유형을 \"{ $path }\" 내에서 확인할 수 없음\ncore_video_width_exceeds_limit = 비디오 너비 { $width } 가 { $limit } 의 제한을 초과합니다\ncore_video_height_exceeds_limit = 비디오 높이 { $height }는 { $limit }의 제한을 초과합니다\ncore_failed_to_process_video = 비디오 파일 { $file } 처리 실패: { $reason }\ncore_optimized_file_larger = 최적화된 파일 { $optimized } (크기: { $new_size }) 는 원본 { $original } (크기: { $original_size }) 보다 작지 않습니다\ncore_unknown_codec = 알 수 없는 코덱: { $codec }\ncore_invalid_video_optimizer_mode = 유효하지 않은 비디오 최적화 모드: '{ $mode }'. 허용 값: transcode, crop\ncore_folder_does_not_exist = 폴더가 존재하지 않습니다: { $folder }\ncore_path_not_directory = 경로는 디렉터리가 아닙니다: { $folder }\ncore_test_error_for_folder = 폴더 오류 테스트: { $folder }\ncore_unknown_exif_tag_group = 알 수 없는 EXIF 태그 그룹: { $tag }\ncore_error_comparing_fingerprints = 지문 비교 중 오류: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = \"{ $file }\"에 대한 썸네일 생성 실패: 추출된 프레임의 차원이 다릅니다\ncore_failed_to_generate_thumbnail = \"{ $file }\": { $reason } 생성을 실패했습니다\ncore_failed_to_extract_frame_at_seek_time = 실패했습니다: { $time } 초에서 \"{ $file }\"에서 프레임을 추출하지 못했습니다: { $reason }\ncore_video_file_does_not_exist = 비디오 파일이 존재하지 않습니다 (스캔/후속 단계 사이에 제거할 수 있음): \"{ $path }\"\ncore_image_too_large = 이미지가 너무 큽니다 ({ $width }x{ $height }) - { $max } 픽셀 이상을 지원하지 않습니다\ncore_failed_to_get_video_metadata = 파일 \"{ $file }\"의 비디오 메타데이터를 가져오지 못했습니다: { $reason }\ncore_failed_to_get_video_codec = 파일 \"{ $file }\"의 비디오 코덱을 가져오지 못했습니다\ncore_failed_to_get_video_duration = 파일 \"{ $file }\"의 비디오 길이 가져오기 실패\ncore_failed_to_get_video_dimensions = 파일 \"{ $file }\"의 비디오 치수를 가져오지 못했습니다\ncore_frame_dimensions_mismatch = 프레임 치수 타임스탬프 { $timestamp }와 첫 번째 프레임 치수 ({ $first_w }x{ $first_h })가 일치하지 않습니다\ncore_failed_to_load_data_from_cache = 캐시 파일 { $file } 로부터 데이터를 로드하지 못했습니다. 이유 { $reason }\ncore_failed_to_load_data_from_json_cache = JSON 캐시 파일 { $file } 로부터 데이터 로드 실패, 이유 { $reason }\ncore_failed_to_replace_with_optimized = 파일 \"{ $file }\"을 최적화된 버전으로 대체하지 못했습니다: { $reason }\ncore_failed_to_write_data_to_cache = 캐시 파일 \"{ $file }\"에 데이터를 쓸 수 없습니다, 이유 { $reason }\ncore_properly_saved_cache_entries = 파일에 제대로 저장됨 { $count } 캐시 항목.\ncore_video_processing_stopped_by_user = 사용자가 비디오 처리 중단을 중단했습니다\ncore_thumbnail_generation_stopped_by_user = 썸네일 생성 중단됨\ncore_failed_to_optimize_video = 비디오 최적화 실패 \"{ $file }\": { $reason }\ncore_failed_to_crop_video = 비디오 자르기 실패 \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = 최적화된 파일 \"{ $file }\"의 메타데이터를 가져오지 못했습니다: { $reason }\ncore_cannot_create_config_folder = 설정 폴더 \"{ $folder }\"를 생성할 수 없습니다, 이유 { $reason }\ncore_cannot_create_cache_folder = 캐시 폴더 \"{ $folder }\"를 생성할 수 없습니다, 이유 { $reason }\ncore_cannot_create_or_open_cache_file = 캐시 파일 \"{ $file }\"를 생성하거나 열 수 없습니다, 이유 { $reason }\ncore_cannot_set_config_cache_path = 설정/캐시 경로 설정 불가 - 설정 및 캐시는 사용되지 않습니다.\ncore_invalid_extension_contains_space = { $extension }는 유효하지 않은 확장자입니다. 확장자 안에 빈 공간이 있기 때문입니다\ncore_invalid_extension_contains_dot = { $extension }는 유효하지 않은 확장자입니다. 확장자 안에 점이 포함되어 있기 때문입니다\n \ncore_error_checking_hash_of_file = 파일 \"{ $file }\"의 해시 값을 확인하는 과정에서 오류가 발생했습니다. 원인: { $reason }"
  },
  {
    "path": "czkawka_core/i18n/nl/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Origineel\ncore_similarity_very_high = Zeer hoog\ncore_similarity_high = hoog\ncore_similarity_medium = Middelgroot\ncore_similarity_small = Klein\ncore_similarity_very_small = Zeer Klein\ncore_similarity_minimal = Minimaal\ncore_cannot_open_dir = Kan dir { $dir }niet openen, reden { $reason }\ncore_cannot_read_entry_dir = Kan invoer niet lezen in map { $dir }, reden { $reason }\ncore_cannot_read_metadata_dir = Kan metadata niet lezen in map { $dir }, reden { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Het bestand { $name } lijkt aangepast te zijn voor Unix Epoch\ncore_folder_modified_before_epoch = Map { $name } lijkt gewijzigd te zijn voor Unix Epoch\ncore_file_no_modification_date = Niet in staat om de datum van bestand { $name }te krijgen, reden { $reason }\ncore_folder_no_modification_date = Niet in staat om wijzigingsdatum van map { $name }te krijgen, reden { $reason }\ncore_cannot_start_scan_no_included_paths = Kan de scan niet starten, omdat er geen opgenomen paden zijn\ncore_skip_exist_check_all_included_paths_nonexistent = Kan de scan niet starten, omdat alle opgenomen paden niet bestaan\ncore_missing_no_chosen_included_path = Geen geldige opgenomen pad werd gekozen (uitgesloten paden konden alle opgenomen paden uitsluiten)\ncore_reference_included_paths_same = Kan geen scan starten waar alle geldige opgenomen paden ook naar verwijzingen paden zijn, probeer te valideren of verwijzingen paden uitschakelen\ncore_excluded_paths_pointless_slash = Uitsluiten / is zinloos, omdat het betekent dat er geen bestanden zullen worden gescand\ncore_needs_allowed_extensions_limited_by_tool = Kan de scan niet starten, wanneer alle extensies beschikbaar in dit hulpmiddel ({ $extensions }) zijn uitgesloten van de scan\ncore_needs_allowed_extensions = Kan de scan niet starten, wanneer alle extensies zijn uitgesloten van de scan\ncore_needs_to_set_at_least_one_broken_option = Kan geen scan starten, wanneer er geen gebroken optie is ingesteld om te scannen\ncore_needs_to_set_at_least_one_bad_name_option = Kan de scan niet starten, wanneer er geen optie “slecht naam” is ingesteld om naar te scannen\ncore_ffmpeg_not_found = Kan geen juiste installatie van FFmpeg of FFprobe vinden. Dit zijn externe programma's die handmatig geïnstalleerd moeten worden.\ncore_ffmpeg_not_found_windows = Zorg ervoor dat ffmpeg.exe en ffprobe.exe beschikbaar zijn in PATH of direct in dezelfde map geplaatst zijn als de app uitvoerbaar\ncore_invalid_symlink_infinite_recursion = Oneindige recursie\ncore_invalid_symlink_non_existent_destination = Niet-bestaand doelbestand\ncore_messages_limit_reached_characters = Het aantal berichten overschrijdt de ingestelde limiet ({ $current }/{ $limit } karakters), zodat de uitvoer is afgebroken. Om de volledige uitvoer te lezen, schakel de beperkende optie uit in de instellingen.\ncore_messages_limit_reached_lines = Het aantal berichten overschrijdt de ingestelde limiet ({ $current }/{ $limit } lijnen), waardoor de uitvoer is afgebrokkeld. Om de volledige uitvoer te lezen, schakel de beperkende optie uit in de instellingen.\ncore_error_moving_to_trash = Fout bij het verplaatsen van \"{ $file }\" naar de prullenbak: { $error }\ncore_error_removing = Fout bij het verwijderen van \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Kan geen soortgelijke muziekbestanden vinden zonder een geselecteerde similariteitsmethode\ncore_failed_to_spawn_command = Faillissement van het opstarten van het commando: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg faalde met status { $status }: { $stderr } (command: { $command })\ncore_failed_to_extract_frame = Faalde om frame te extraheren op { $time } seconden van \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Faillissement van miniature opslaan voor \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Faalde bij het ophalen van frame op timestamp { $timestamp } van \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Faalde bij het ophalen van frame van \"{ $file }\" op timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Ongeldige oogstrechthoek: links={ $left }, boven={ $top }, rechts={ $right }, onder={ $bottom }\ncore_failed_to_crop_video_file = Faillissement van video bestand \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Verwijderde videobestand is niet aangemaakt: { $temp }\ncore_unable_check_hash_of_file = Kan hash van bestand \"{ $file }\" niet controleren, reden { $reason }\ncore_error_checking_hash_of_file = Fout opgetreden bij het controleren van de hash van bestand \"{ $file }\", reden { $reason }\ncore_image_open_failed = Kan bestand niet openen \"{ $path }\": { $reason }\ncore_not_directory_remove = Proberen om map \"{ $path }\" te verwijderen, wat geen directory is\ncore_cannot_read_directory = Kan de directory \"{ $path }\" niet lezen\ncore_cannot_read_entry_from_directory = Kan geen vermelding lezen uit de map \"{ $path }\"\ncore_folder_contains_file_inside = Map bevat bestand \"{ $entry }\" in \"{ $folder }\"\ncore_unknown_directory_entry = Kan het type bestand van de directory-entry \"{ $entry }\" niet bepalen binnen \"{ $path }\"\ncore_video_width_exceeds_limit = Video breedte { $width } overschrijdt de limiet van { $limit }\ncore_video_height_exceeds_limit = Video hoogte { $height } overschrijdt de limiet van { $limit }\ncore_failed_to_process_video = Faalde bij het verwerken van videobestand { $file }: { $reason }\ncore_optimized_file_larger = Geoptimaliseerd bestand { $optimized } (grootte: { $new_size }) is niet kleiner dan origineel { $original } (grootte: { $original_size })\ncore_unknown_codec = Onbekend codec: { $codec }\ncore_invalid_video_optimizer_mode = Ongeldige videobesturing modus: '{ $mode }'. Toegestane waarden: transcode, crop\ncore_folder_does_not_exist = Map niet bestaan: { $folder }\ncore_path_not_directory = Pad is geen directory: { $folder }\ncore_test_error_for_folder = Test fout voor map: { $folder }\ncore_unknown_exif_tag_group = Onbekende EXIF tag groep: { $tag }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Faalde bij het genereren van de miniaturen voor \"{ $file }\": de geëxtraheerde frames hebben verschillende afmetingen\ncore_failed_to_generate_thumbnail = Faalde bij het genereren van miniaturen voor \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Faalde om frame te extraheren op { $time } seconden van \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Bestand bestaat niet (kan worden verwijderd tussen scan/latere stappen): \"{ $path }\"\ncore_image_too_large = Afbeelding is te groot ({ $width }x{ $height }) - meer dan ondersteund { $max } pixels\ncore_failed_to_get_video_metadata = Faalde bij het ophalen van videometadeta voor bestand \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Faalde bij het ophalen van de videocodec voor bestand \"{ $file }\"\ncore_failed_to_get_video_duration = Kon geen duur van het bestand \"{ $file }\" ophalen\ncore_failed_to_get_video_dimensions = Faalde bij het ophalen van de videogrootte voor bestand \"{ $file }\"\ncore_frame_dimensions_mismatch = Afmetingen van het frame voor timestamp { $timestamp } komen niet overeen met de afmetingen van het eerste frame ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Faalde bij het laden van gegevens uit cachebestand { $file }, reden { $reason }\ncore_failed_to_load_data_from_json_cache = Faalde bij het laden van gegevens uit JSON cachebestand { $file }, reden { $reason }\ncore_failed_to_replace_with_optimized = Faalde bij het vervangen van bestand \"{ $file }\" met de geoptimaliseerde versie: { $reason }\ncore_failed_to_write_data_to_cache = Kan geen gegevens schrijven naar cachebestand \"{ $file }\", reden { $reason }\ncore_properly_saved_cache_entries = Correct opgeslagen naar bestand { $count } cache-items.\ncore_video_processing_stopped_by_user = Video verwerking is gestopt door gebruiker\ncore_thumbnail_generation_stopped_by_user = Thumbnail generatie is gestopt door gebruiker\ncore_failed_to_optimize_video = Faalde het bij het optimaliseren van video \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Failliet video snijden \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Faalde bij het ophalen van metadata van de geoptimaliseerde bestand \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Kan configuratiefolder \"{ $folder }\" niet aanmaken, reden { $reason }\ncore_cannot_create_cache_folder = Kan cache map \"{ $folder }\" niet aanmaken, reden { $reason }\ncore_cannot_create_or_open_cache_file = Kan bestand { $file } niet aanmaken of openen, reden { $reason }\ncore_cannot_set_config_cache_path = Kan de config/cache pad niet instellen - config en cache zullen niet worden gebruikt.\ncore_invalid_extension_contains_space = { $extension } is geen geldige extensie omdat het lege ruimte bevat binnenin\ncore_invalid_extension_contains_dot = { $extension } is geen geldige extensie omdat het punt erin zit\n\ncore_path_must_exists = Het opgegeven pad moet bestaan, waarbij { $path } genegeerd wordt\ncore_must_be_directory_or_file = Het opgegeven pad moet verwijzen naar een geldige map of bestand, waarbij { $path } genegeerd wordt\ncore_paths_unable_to_get_device_id = Kan het apparaat-ID niet ophalen vanuit de map { $path }\ncore_failed_to_check_process_status = Het controleren van de processtatus is mislukt: { $reason }\ncore_failed_to_wait_for_process = Het wachten op het proces is mislukt: { $reason }\ncore_failed_to_read_video_properties = Het lukte niet om de videoproperties te lezen: { $reason }\ncore_failed_to_execute_ffmpeg = Het uitvoeren van ffmpeg is mislukt: { $reason }\ncore_failed_to_load_image_frame = Het laden van het afbeeldingsframe is mislukt: { $reason }\ncore_image_zero_dimensions = De afbeelding heeft een breedte of hoogte van nul: \"{ $path }\"\ncore_error_comparing_fingerprints = Fout tijdens het vergelijken van vingerafdrukken: { $reason }"
  },
  {
    "path": "czkawka_core/i18n/no/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Opprinnelig\ncore_similarity_very_high = Veldig høy\ncore_similarity_high = Høy\ncore_similarity_medium = Middels\ncore_similarity_small = Liten\ncore_similarity_very_small = Veldig liten\ncore_similarity_minimal = Minimalt\ncore_cannot_open_dir = Kan ikke åpne dir { $dir }, årsak { $reason }\ncore_cannot_read_entry_dir = Kan ikke lese oppføringen i dir { $dir }, årsak { $reason }\ncore_cannot_read_metadata_dir = Kan ikke lese metadata i dir { $dir }, årsak { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Filen { $name } ser ut til å ha blitt endret før Unix Epoch\ncore_folder_modified_before_epoch = Mappen { $name } ser ut til å ha blitt endret før Unix Epoch\ncore_file_no_modification_date = Klarte ikke å hente endringsdato fra filen { $name }. Årsak { $reason }\ncore_folder_no_modification_date = Klarte ikke å hente endringsdato fra mappen { $name }. Årsak { $reason }\ncore_cannot_start_scan_no_included_paths = Kan ikke starte skanning, fordi det ikke er inkludert stier\ncore_skip_exist_check_all_included_paths_nonexistent = Kan ikke starte skanning, fordi alle inkluderte stiene ikke eksisterer\ncore_missing_no_chosen_included_path = Ingen gyldig inkludert sti ble valgt (utelatelser kunne ha utelukket alle inkluderte stier)\ncore_reference_included_paths_same = Kan ikke starte skanning der alle gyldige inkluderte stier også er refererte stier, prøv å validere eller deaktivere refererte stier\ncore_must_be_directory_or_file = Angitt sti må peke til en gyldig mappe eller fil, og ignorere { $path }\ncore_excluded_paths_pointless_slash = Unntatt / er meningsløst, fordi det betyr at ingen filer vil bli scannet\ncore_paths_unable_to_get_device_id = Kan ikke hente enhets-ID fra mappen { $path }\ncore_needs_allowed_extensions_limited_by_tool = Kan ikke starte skanning, når alle utvidelser tilgjengelige i dette verktøyet ({ $extensions }) ble ekskludert fra skanning\ncore_needs_allowed_extensions = Kan ikke starte skanning, når alle utvidelser ble ekskludert fra skanning\ncore_needs_to_set_at_least_one_broken_option = Kan ikke starte skanning, når det ikke er satt en ødelagt alternativ for å skanne etter\ncore_needs_to_set_at_least_one_bad_name_option = Kan ikke starte skanning, når det ikke er satt en dårlig navn-alternativ for å skanne etter\ncore_ffmpeg_not_found = Kan ikke finne en passende installasjon av FFmpeg eller FFprobe. Disse er eksterne programmer som må installeres manuelt.\ncore_ffmpeg_not_found_windows = Pass på at ffmpeg.exe og ffprobe.exe er tilgjengelig i PATH eller plasseres direkte i samme mappe som appen kan utføres\ncore_invalid_symlink_infinite_recursion = Uendelig rekursjon\ncore_invalid_symlink_non_existent_destination = Ikke-eksisterende målfil\ncore_messages_limit_reached_characters = Antall meldinger overskred den angitte grensen ({ $current }/{ $limit } tegn), så resultatet ble avkortet. For å lese hele utdataen, deaktiver begrensningsalternativet i innstillinger.\ncore_messages_limit_reached_lines = Antall meldinger overskred den angitte grensen ({ $current }/{ $limit } linjer), så utgangen ble avkortet. For å lese hele utdataen, deaktiver begrensningsalternativet i innstillinger.\ncore_error_moving_to_trash = Feil ved flytting av \"{ $file }\" til papirkurven: { $error }\ncore_error_removing = Feil ved sletting av \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Kan ikke finne lignende musikkfiler uten en valgt likhet metode\ncore_failed_to_spawn_command = Krevde ikke å starte kommando: { $reason }\ncore_failed_to_check_process_status = Feilet med å sjekke prosessstatus: { $reason }\ncore_failed_to_wait_for_process = Krevde ikke å vente på prosessen: { $reason }\ncore_failed_to_read_video_properties = Kunne ikke lese videoegenskaper: { $reason }\ncore_failed_to_execute_ffmpeg = Krevde ikke utførelse av ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg feilet med status { $status }: { $stderr } (kommando: { $command })\ncore_failed_to_load_image_frame = Klarte ikke å laste inn bildefragment: { $reason }\ncore_failed_to_extract_frame = Klarte ikke å hente ut ramme ved { $time } sekunder fra \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Feilet ved å lagre miniatyrbilde for \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Feilet å hente ramme ved timestamp { $timestamp } fra \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Kunne ikke hente ramme fra \"{ $file }\" ved timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Ugyldig avlingsrektangel: venstre={ $left }, øvre={ $top }, høyre={ $right }, nedre={ $bottom }\ncore_failed_to_crop_video_file = Feilet med å beskjære videofilen \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Klippet videofil ble ikke opprettet: { $temp }\ncore_unable_check_hash_of_file = Uklart å sjekke hasj for fil \"{ $file }\", årsak { $reason }\ncore_error_checking_hash_of_file = Feil oppstod ved sjekk av hasj for fil \"{ $file }\", årsak { $reason }\ncore_image_zero_dimensions = Bildet har null bredde eller høyde \"{ $path }\"\ncore_image_open_failed = Kan ikke åpne bildefil \"{ $path }\": { $reason }\ncore_not_directory_remove = Prøver å fjerne mappen \"{ $path }\" som ikke er en mappe\ncore_cannot_read_directory = Kan ikke lese katalog \"{ $path }\"\ncore_cannot_read_entry_from_directory = Kan ikke lese oppføring fra katalog \"{ $path }\"\ncore_folder_contains_file_inside = Mappen inneholder filen \"{ $entry }\" inne i \"{ $folder }\"\ncore_unknown_directory_entry = Kan ikke fastslå filtype for directory entry \"{ $entry }\" inne i \"{ $path }\"\ncore_video_width_exceeds_limit = Video bredde { $width } overstiger grensen for { $limit }\ncore_video_height_exceeds_limit = Video høyde { $height } overstiger grensen på { $limit }\ncore_failed_to_process_video = Klarte ikke å behandle videofilen { $file }: { $reason }\ncore_unknown_codec = Ukjent kodek: { $codec }\ncore_invalid_video_optimizer_mode = Ugyldig videobestøkningsmodus: '{ $mode }'. Tillatte verdier: transkode, kutt\ncore_path_not_directory = Stien er ikke en mappe: { $folder }\ncore_test_error_for_folder = Test feil for mappe: { $folder }\ncore_unknown_exif_tag_group = Ukjent EXIF-tag gruppe: { $tag }\ncore_error_comparing_fingerprints = Feil ved sammenligning av fingeravtrykk: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Feilet med å generere miniatyrbilde for \"{ $file }\": uthentede rammer har forskjellige dimensjoner\ncore_failed_to_generate_thumbnail = Feilet med å generere miniatyrbilde for \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Klarte ikke å hente ut ramme ved { $time } sekunder fra \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Video fil finnes ikke (kan fjernes mellom skanning/senere steg): \"{ $path }\"\ncore_image_too_large = Bildet er for stort ({ $width }x{ $height }) - mer enn støttet { $max } piksler\ncore_failed_to_get_video_metadata = Klarte ikke å hente videometadata for fil \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Kunne ikke hente videocodec for fil \"{ $file }\"\ncore_failed_to_get_video_duration = Kunne ikke hente videolengde for fil \"{ $file }\"\ncore_failed_to_get_video_dimensions = Kunne ikke hente vide dimensjoner for fil \"{ $file }\"\ncore_frame_dimensions_mismatch = Ramme dimensjoner for tidsstempel { $timestamp } stemmer ikke overens med de første ramme dimensjonene ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Kunne ikke laste data fra cache-fil { $file }, årsak { $reason }\ncore_failed_to_load_data_from_json_cache = Kunne ikke laste data fra json-cachen { $file}, årsak { $reason }\ncore_failed_to_replace_with_optimized = Klarte ikke å erstatte filen \"{ $file }\" med den optimaliserte versjonen: { $reason }\ncore_failed_to_write_data_to_cache = Kan ikke skrive data til cache-fil \"{ $file }\", årsak { $reason }\ncore_properly_saved_cache_entries = Riktig lagret til fil { $count } cache-poster.\ncore_video_processing_stopped_by_user = Videobehandling ble stoppet av bruker\ncore_thumbnail_generation_stopped_by_user = Miniatyrgenerering ble stoppet av bruker\ncore_failed_to_optimize_video = Kjempetilfelte video \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Feilet å beskjære video \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Klarte ikke å hente metadata for den optimaliserte filen \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Kan ikke opprette konfigurasjonsmappe \"{ $folder }\", årsak { $reason }\ncore_cannot_create_cache_folder = Kan ikke opprette cache-mappe \"{ $folder }\", årsak { $reason }\ncore_cannot_create_or_open_cache_file = Kan ikke opprette eller åpne cachefil \"{ $file }\", årsak { $reason }\ncore_cannot_set_config_cache_path = Kan ikke sette config/cache-sti - config og cache vil ikke bli brukt.\ncore_invalid_extension_contains_space = { $extension } er ikke en gyldig filtype fordi den inneholder mellomrom\n \ncore_path_must_exists = Den angitte stien må eksistere, ignorerer { $path }\ncore_optimized_file_larger = Den optimaliserte filen { $optimized } (størrelse: { $new_size }) er ikke mindre enn den originale filen { $original } (størrelse: { $original_size })\ncore_folder_does_not_exist = Mappen finnes ikke: { $folder }\ncore_invalid_extension_contains_dot = { $extension } er ikke en gyldig filtype fordi den inneholder et punkt (.) inni seg"
  },
  {
    "path": "czkawka_core/i18n/pl/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Oryginalny\ncore_similarity_very_high = Bardzo Duże\ncore_similarity_high = Duże\ncore_similarity_medium = Średnie\ncore_similarity_small = Małe\ncore_similarity_very_small = Bardzo Małe\ncore_similarity_minimal = Minimalne\ncore_cannot_open_dir = Nie można otworzyć folderu { $dir }, powód { $reason }\ncore_cannot_read_entry_dir = Nie można odczytać danych z folderu { $dir }, powód { $reason }\ncore_cannot_read_metadata_dir = Nie można odczytać metadanych folderu \"{ $dir }\": { $reason }\ncore_cannot_read_metadata_file = Nie można odczytać metadanych pliku \"{ $file }\": { $reason }\ncore_file_modified_before_epoch = Plik \"{ $name }\" wygląda na zmodyfikowany przed epoką Unix\ncore_folder_modified_before_epoch = Folder \"{ $name }\" wygląda na zmodyfikowany przed epoką Unix\ncore_file_no_modification_date = Nie udało się pobrać daty modyfikacji z pliku { $name }, powód { $reason }\ncore_folder_no_modification_date = Nie udało się pobrać daty modyfikacji z folderu { $name }, powód { $reason }\ncore_cannot_start_scan_no_included_paths = Nie można uruchomić skanowania, ponieważ nie wybrano żadnych folderów wejściowych\ncore_skip_exist_check_all_included_paths_nonexistent = Nie można uruchomić skanowania, ponieważ wszystkie ścieżki do wyszukiwania nie istnieją\ncore_missing_no_chosen_included_path = Nie wybrano prawidłowej ścieżki do wyszukiwania (wykluczone ścieżki mogły wykluczyć wszystkie ścieżki wejściowe)\ncore_reference_included_paths_same = Nie można uruchomić skanu, gdzie wszystkie poprawne ścieżki uwzględnione są również ścieżkami odniesionymi, spróbuj zweryfikować lub wyłączyć ścieżki odniesione\ncore_path_must_exists = Podana ścieżka musi istnieć, ignorowanie { $path }\ncore_must_be_directory_or_file = Podany ścieżka musi wskazywać na ważny katalog lub plik, pomijając { $path }\ncore_excluded_paths_pointless_slash = Wykluczenie / jest bezcelowe, ponieważ oznacza to, że żadne pliki nie zostaną przeskanowane\ncore_paths_unable_to_get_device_id = Nie można uzyskać identyfikatora urządzenia z folderu { $path }\ncore_needs_allowed_extensions_limited_by_tool = Nie można uruchomić skanu, gdy wszystkie rozszerzenia dostępne w tym narzędziu ({ $extensions }) zostały wykluczone z skanu\ncore_needs_allowed_extensions = Nie można uruchomić skanu, gdy wszystkie rozszerzenia zostały wykluczone z skanu\ncore_needs_to_set_at_least_one_broken_option = Nie można uruchomić skanu, gdy nie ustawiono opcji \"uszkodzony\" do skanowania\ncore_needs_to_set_at_least_one_bad_name_option = Nie można uruchomić skanu, jeśli nie ustawiono opcji \"złe nazwy\" do skanowania\ncore_ffmpeg_not_found = Nie można znaleźć prawidłowej instalacji FFmpeg lub FFprobe. Są to programy zewnętrzne, które muszą być zainstalowane ręcznie.\ncore_ffmpeg_not_found_windows = Upewnij się, że ffmpeg.exe i ffprobe.exe są dostępne w PATH lub są umieszczone bezpośrednio w tym samym folderze co plik wykonywalny aplikacji\ncore_invalid_symlink_infinite_recursion = Nieskończona rekurencja\ncore_invalid_symlink_non_existent_destination = Nieistniejący docelowy plik\ncore_messages_limit_reached_characters = Liczba wiadomości przekroczyła ustalony limit ({ $current }/{ $limit } znaków), więc wynik został obcięty. Aby odczytać pełne wyjście, wyłącz opcję ograniczenia liczby znaków w ustawieniach.\ncore_messages_limit_reached_lines = Liczba wiadomości przekroczyła ustalony limit ({ $current }/{ $limit } linii), więc wynik został obcięty. Aby odczytać pełne wyjście, wyłącz opcję ograniczenia liczby linii w ustawieniach.\ncore_error_moving_to_trash = Błąd podczas przenoszenia \"{ $file }\" do kosza: { $error }\ncore_error_removing = Błąd podczas usuwania \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Nie można znaleźć podobnych plików muzycznych bez wybranego sposobu podobieństwa\ncore_failed_to_spawn_command = Nie udało się wygenerować polecenia: { $reason }\ncore_failed_to_check_process_status = Błąd podczas sprawdzania statusu procesu: { $reason }\ncore_failed_to_wait_for_process = Nie udało się oczekiwać na proces: { $reason }\ncore_failed_to_read_video_properties = Nie udało się odczytać właściwości wideo: { $reason }\ncore_failed_to_execute_ffmpeg = Nie udało się wykonać ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg nie powiodło się z kodem { $status }: { $stderr } (polecenie: { $command })\ncore_failed_to_load_image_frame = Błąd ładowania ramy obrazu: { $reason }\ncore_failed_to_extract_frame = Nie udało się wyodrębnić klatki o { $time } sekundach z \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Nie udało się zapisać miniaturki dla \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Błąd podczas pobierania klatki o znaczniku { $timestamp } z \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Nie udało się uzyskać ramy z \"{ $file }\" o znaczniku czasu { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Nieprawidłowy prostokąt uprawnień: lewy={ $left }, górny={ $top }, prawy={ $right }, dolny={ $bottom }\ncore_failed_to_crop_video_file = Nie udało się przyciąć pliku wideo \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Przekrojony plik wideo nie został utworzony: { $temp }\ncore_unable_check_hash_of_file = Nie można sprawdzić sumy kontrolnej pliku \"{ $file }\", powód { $reason }\ncore_error_checking_hash_of_file = Błąd wystąpił podczas sprawdzania sumy kontrolnej pliku \"{ $file }\", powód { $reason }\ncore_image_zero_dimensions = Obraz ma zerową szerokość lub wysokość \"{ $path }\"\ncore_image_open_failed = Nie można otworzyć pliku obrazu \"{ $path }\": { $reason }\ncore_not_directory_remove = Próba usunięcia folderu \"{ $path }\" który nie jest katalogiem\ncore_cannot_read_directory = Nie można odczytać katalogu \"{ $path }\"\ncore_cannot_read_entry_from_directory = Nie można odczytać wpisu z katalogu \"{ $path }\"\ncore_folder_contains_file_inside = Folder zawiera plik \"{ $entry }\" wewnątrz \"{ $folder }\"\ncore_unknown_directory_entry = Nie można określić typu pliku wpisu katalogowego \"{ $entry }\" wewnątrz \"{ $path }\"\ncore_video_width_exceeds_limit = Szerokość wideo { $width } przekracza limit { $limit }\ncore_video_height_exceeds_limit = Wysokość wideo { $height } przekracza limit { $limit }\ncore_failed_to_process_video = Nie udało się przetworzyć pliku wideo { $file }: { $reason }\ncore_optimized_file_larger = Zoptymalizowany plik { $optimized } (rozmiar: { $new_size }) nie jest mniejszy niż oryginalny { $original } (rozmiar: { $original_size })\ncore_unknown_codec = Nieznany kodek: { $codec }\ncore_invalid_video_optimizer_mode = Nieprawidłowy tryb optymalizacji wideo: '{ $mode }'. Dopuszczalne wartości: transkoduj, przycinaj\ncore_folder_does_not_exist = Folder nie istnieje: { $folder }\ncore_path_not_directory = Ścieżka nie jest katalogiem: { $folder }\ncore_test_error_for_folder = Test błąd dla folderu: { $folder }\ncore_unknown_exif_tag_group = Nieznana grupa tagów EXIF: { $tag }\ncore_error_comparing_fingerprints = Błąd podczas porównywania odcisków palców: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Nie udało się wygenerować miniaturki dla \"{ $file }\": wyekstrahowane klatki mają różne wymiary\ncore_failed_to_generate_thumbnail = Nie udało się wygenerować miniaturki dla \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Nie udało się wyodrębnić klatki o { $time } sekundach z \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Plik wideo nie istnieje (można usunąć między skanowaniem/późniejszymi krokami): \"{ $path }\"\ncore_image_too_large = Obraz jest zbyt duży ({ $width }x{ $height }) - więcej niż obsługiwane { $max } pikseli\ncore_failed_to_get_video_metadata = Nie udało się uzyskać metadanych wideo dla pliku \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Nie udało się uzyskać kodeka wideo dla pliku \"{ $file }\"\ncore_failed_to_get_video_duration = Nie udało się uzyskać długości wideo dla pliku \"{ $file }\"\ncore_failed_to_get_video_dimensions = Nie udało się uzyskać wymiarów wideo dla pliku \"{ $file }\"\ncore_frame_dimensions_mismatch = Wymiary klatki dla znacznika czasu { $timestamp } nie pasują do wymiarów pierwszej klatki ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Nie udało się załadować danych z pliku pamięci podręcznej \"{ $file }\": { $reason }\ncore_failed_to_load_data_from_json_cache = Nie udało się załadować danych z pliku pamięci podręcznej JSON \"{ $file }\": { $reason }\ncore_failed_to_replace_with_optimized = Nie udało się zastąpić pliku \"{ $file }\" zoptymalizowaną wersją: { $reason }\ncore_failed_to_write_data_to_cache = Nie można zapisać danych do pliku pamięci podręcznej \"{ $file }\": { $reason }\ncore_properly_saved_cache_entries = Prawidłowo zapisano w pamięci podręcznej { $count } wpisów do pliku.\ncore_video_processing_stopped_by_user = Przetwarzanie wideo zostało przerwane przez użytkownika\ncore_thumbnail_generation_stopped_by_user = Generowanie miniatur zostało przerwane przez użytkownika\ncore_failed_to_optimize_video = Pominięto optymalizację wideo \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Nie udało się przyciąć wideo \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Nie udało się pobrać metadanych z zoptymalizowanego pliku \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Nie można utworzyć folderu konfiguracyjnego \"{ $folder }\": { $reason }\ncore_cannot_create_cache_folder = Nie można utworzyć folderu pamięci podręcznej \"{ $folder }\": { $reason }\ncore_cannot_create_or_open_cache_file = Nie można utworzyć ani otworzyć pliku pamięci podręcznej \"{ $file }\": { $reason }\ncore_cannot_set_config_cache_path = Nie można ustawić ścieżki do konfiguracji/pamięci podręcznej — konfiguracja i pamięć podręczna nie zostaną użyte.\ncore_invalid_extension_contains_space = \"{ $extension }\" nie jest prawidłowym rozszerzeniem, ponieważ zawiera spację\ncore_invalid_extension_contains_dot = \"{ $extension }\" nie jest prawidłowym rozszerzeniem, ponieważ zawiera kropkę\n"
  },
  {
    "path": "czkawka_core/i18n/pt-BR/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Please provide the text to translate\ncore_similarity_very_high = Muito grande\ncore_similarity_high = Grande\ncore_similarity_medium = Médio\ncore_similarity_small = Pequeno\ncore_similarity_very_small = Muito pequeno\ncore_similarity_minimal = Mínimo\ncore_cannot_open_dir = Não foi possível abrir o diretório ‘{ $dir }’, por causa de ‘{ $reason }’\ncore_cannot_read_entry_dir = Não foi possível ler os dados do diretório ‘{ $dir }’, por causa de ‘{ $reason }’\ncore_cannot_read_metadata_dir = Não foi possível ler os metadados do diretório ‘{ $dir }’, por causa de ‘{ $reason }’\ncore_cannot_read_metadata_file = Não foi possível ler os metadados no arquivo ‘{ $file }’, por causa de ‘{ $reason }’\ncore_file_modified_before_epoch = O arquivo { $name } parece ser modificado antes do Epoch Unix\ncore_folder_modified_before_epoch = A pasta ‘{ $name }’ parece ter sido modificada antes do ‘Epoch’ do Unix \ncore_file_no_modification_date = Não foi possível obter a data da modificação do arquivo ‘{ $name }’, por causa de ‘{ $reason }’\ncore_folder_no_modification_date = Não foi possível obter a data da modificação da pasta ‘{ $name }’, por causa de ‘{ $reason }’\ncore_cannot_start_scan_no_included_paths = Não é possível iniciar a varredura, porque não há caminhos incluídos\ncore_skip_exist_check_all_included_paths_nonexistent = Não é possível iniciar a varredura, porque todos os caminhos incluídos não existem\ncore_missing_no_chosen_included_path = Nenhum caminho válido foi selecionado (os caminhos que foram excluídos poderiam ter excluído todos os caminhos que haviam sido incluídos)\ncore_reference_included_paths_same = Não foi possível iniciar a verificação porque todos os caminhos válidos que foram incluídos também são caminhos referenciados. Tente ativar ou desativar os caminhos referenciados\ncore_path_must_exists = O caminho que foi fornecido tem que apontar para um diretório, por tanto, o caminho ‘{ $path }’ será ignorado\ncore_must_be_directory_or_file = O caminho que foi fornecido tem que apontar para um diretório ou para um arquivo válido, por tanto, o caminho ‘{ $path }’ será ignorado\ncore_excluded_paths_pointless_slash = Se você excluir a barra ‘ / ’, significa que nenhum arquivo será verificado\ncore_paths_unable_to_get_device_id = Não foi possível obter o ID (identificador) do dispositivo da pasta ‘{ $path }’\ncore_needs_allowed_extensions_limited_by_tool = Não foi possível iniciar a verificação porque todas as extensões ‘{ $extensions }’ que estão disponíveis nesta ferramenta foram excluídas da verificação\ncore_needs_allowed_extensions = Não foi possível iniciar a verificação porque todas as extensões foram excluídas da verificação\ncore_needs_to_set_at_least_one_broken_option = Não foi possível iniciar a verificação porque nenhuma opção do nome corrompido foi definida para ser verificada\ncore_needs_to_set_at_least_one_bad_name_option = Não foi possível iniciar a verificação porque nenhuma opção do nome incorreto foi definida para ser verificada\ncore_ffmpeg_not_found = Não foi possível encontrar a instalação dos programas ‘FFmpeg’ ou ‘FFprobe’. Estes programas são externos e você tem que ser instalá-los manualmente.\ncore_ffmpeg_not_found_windows = Certifique-se de que o ‘ffmpeg.exe’ e ‘ffprobe.exe’ estejam disponíveis no caminho ou sejam colocados diretamente na mesma pasta onde está o executável do programa\ncore_invalid_symlink_infinite_recursion = Ocorreu um erro de execução na recursão infinita\ncore_invalid_symlink_non_existent_destination = O arquivo de destino não existe\ncore_messages_limit_reached_characters = A quantidade de ‘{ $current }’ caracteres na mensagem excedeu o limite de ‘{ $limit }’ que foi definido, portanto, a saída foi truncada. Para ler a saída completa, desative a opção do limite de caracteres nas configurações.\ncore_messages_limit_reached_lines = A quantidade de ‘{ $current }’ linhas na mensagem excedeu o limite de ‘{ $limit }’ que foi definido, portanto, a saída foi truncada. Para ler a saída completa, desative a opção do limite de linhas nas configurações.\ncore_error_moving_to_trash = Ocorreu o erro ‘{ $error }’ ao tentar mover o arquivo ‘{ $file }’ para a lixeira\ncore_error_removing = Ocorreu o erro ‘{ $error }’ ao tentar remover o arquivo ‘{ $file }’\ncore_no_similarity_method_selected = Não foi possível encontrar os arquivos de música equivalentes porque um método de equivalência não foi definido\ncore_failed_to_spawn_command = Falha ao gerar comando: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg falhou com o status { $status }: { $stderr } (comando: { $command })\ncore_failed_to_extract_frame = Falha ao extrair quadro em { $time } segundos de \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Falha ao salvar miniatura para \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Falha ao obter frame no timestamp { $timestamp } de \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Falhou ao obter o quadro de \"{ $file }\" no timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Retângulo de cultivo inválido: esquerda={ $left }, superior={ $top }, direita={ $right }, inferior={ $bottom }\ncore_failed_to_crop_video_file = Falha ao recortar o arquivo de vídeo \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Arquivo de vídeo cortado não foi criado: { $temp }\ncore_unable_check_hash_of_file = Não foi possível verificar o hash do arquivo \"{ $file }\", motivo { $reason }\ncore_error_checking_hash_of_file = Erro ocorrido ao verificar o hash do arquivo \"{ $file }\", motivo { $reason }\ncore_image_zero_dimensions = A imagem tem largura ou altura zero \"{ $path }\"\ncore_image_open_failed = Não é possível abrir o arquivo de imagem \"{ $path }\": { $reason }\ncore_not_directory_remove = Tentando remover a pasta \"{ $path }\" que não é um diretório\ncore_cannot_read_directory = Não é possível ler o diretório \"{ $path }\"\ncore_cannot_read_entry_from_directory = Não é possível ler entrada do diretório \"{ $path }\"\ncore_folder_contains_file_inside = A pasta contém o arquivo \"{ $entry }\" dentro de \"{ $folder }\"\ncore_unknown_directory_entry = Não foi possível determinar o tipo de arquivo da entrada de diretório \"{ $entry }\" dentro de \"{ $path }\"\ncore_video_height_exceeds_limit = Vídeo altura { $height } excede o limite de { $limit }\ncore_failed_to_process_video = Falha ao processar o arquivo de vídeo { $file }: { $reason }\ncore_unknown_codec = Codec desconhecido: { $codec }\ncore_invalid_video_optimizer_mode = Modo otimizador de vídeo inválido: '{ $mode }'. Valores permitidos: transcodar, cortar\ncore_folder_does_not_exist = A pasta não existe: { $folder }\ncore_path_not_directory = O caminho não é um diretório: { $folder }\ncore_test_error_for_folder = Erro de teste para a pasta: { $folder }\ncore_unknown_exif_tag_group = Grupo EXIF desconhecido: { $tag }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Falha ao gerar miniatura para \"{ $file }\": os quadros extraídos têm dimensões diferentes\ncore_failed_to_generate_thumbnail = Falha ao gerar miniatura para \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Falha ao extrair quadro em { $time } segundos de \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Arquivo de vídeo não existe (pode ser removido entre as etapas de digitalização/mais tarde): \"{ $path }\"\ncore_failed_to_get_video_metadata = Falha ao obter metadados de vídeo para o arquivo \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Falha ao obter codec de vídeo para o arquivo \"{ $file }\"\ncore_failed_to_get_video_duration = Falhou ao obter a duração do vídeo para o arquivo \"{ $file }\"\ncore_failed_to_get_video_dimensions = Falha ao obter as dimensões do vídeo para o arquivo \"{ $file }\"\ncore_frame_dimensions_mismatch = Dimensões do quadro para timestamp { $timestamp } não correspondem às dimensões do primeiro quadro ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Falha ao carregar dados do arquivo de cache { $file }, motivo { $reason }\ncore_failed_to_load_data_from_json_cache = Falha ao carregar dados do arquivo de cache JSON { $file }, motivo { $reason }\ncore_failed_to_replace_with_optimized = Falha ao substituir o arquivo \"{ $file }\" pela versão otimizada: { $reason }\ncore_failed_to_write_data_to_cache = Não é possível escrever dados para o arquivo de cache \"{ $file }\", motivo { $reason }\ncore_properly_saved_cache_entries = Salvado corretamente para o arquivo { $count } entradas de cache.\ncore_video_processing_stopped_by_user = O processamento de vídeo foi interrompido pelo usuário\ncore_thumbnail_generation_stopped_by_user = Geração de miniaturas foi interrompida pelo usuário\ncore_failed_to_optimize_video = Falha ao otimizar o vídeo \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Falha ao recortar o vídeo \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Falha ao obter metadados do arquivo otimizado \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Não é possível criar a pasta de configuração \"{ $folder }\", a razão é { $reason }\ncore_cannot_create_cache_folder = Não é possível criar a pasta de cache \"{ $folder }\", motivo { $reason }\ncore_cannot_create_or_open_cache_file = Não é possível criar ou abrir o arquivo de cache \"{ $file }\", a razão { $reason }\ncore_cannot_set_config_cache_path = Não é possível definir o caminho de configuração/cache - a configuração e o cache não serão utilizados.\ncore_invalid_extension_contains_space = { $extension } não é uma extensão válida porque contém espaço em branco dentro\ncore_invalid_extension_contains_dot = { $extension } não é uma extensão válida porque contém ponto dentro\n\ncore_failed_to_check_process_status = Falha ao verificar o status do processo: { $reason }\ncore_failed_to_wait_for_process = Falha ao aguardar o processo: { $reason }\ncore_failed_to_read_video_properties = Falha ao ler as propriedades do vídeo: { $reason }\ncore_failed_to_execute_ffmpeg = Falha ao executar o ffmpeg: { $reason }\ncore_failed_to_load_image_frame = Falha ao carregar o quadro da imagem: { $reason }\ncore_video_width_exceeds_limit = Largura do vídeo { $width } excede o limite de { $limit }\ncore_optimized_file_larger = O arquivo otimizado { $optimized } (tamanho: { $new_size }) não é menor que o arquivo original { $original } (tamanho: { $original_size })\ncore_error_comparing_fingerprints = Erro ao comparar impressões digitais: { $reason }\ncore_image_too_large = A imagem é muito grande ({$width}x{$height}) - excede o limite de { $max } pixels suportados"
  },
  {
    "path": "czkawka_core/i18n/pt-PT/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Original\ncore_similarity_very_high = Muito alto\ncore_similarity_high = Alto\ncore_similarity_medium = Média\ncore_similarity_small = Pequeno\ncore_similarity_very_small = Muito Pequeno\ncore_similarity_minimal = Mínimo\ncore_cannot_open_dir = Não é possível abrir o diretório { $dir }, razão { $reason }\ncore_cannot_read_entry_dir = Não é possível ler a entrada no diretório { $dir }, razão { $reason }\ncore_cannot_read_metadata_dir = Não é possível ler os metadados no diretório { $dir }, razão { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = O arquivo { $name } parece ter sido modificado antes do Epoch Unix\ncore_folder_modified_before_epoch = A pasta { $name } parece ter sido modificada antes do Epoch Unix\ncore_file_no_modification_date = Não foi possível obter a data de modificação do arquivo { $name }, motivo { $reason }\ncore_folder_no_modification_date = Não foi possível obter a data de modificação da pasta { $name }, motivo { $reason }\ncore_cannot_start_scan_no_included_paths = Não é possível iniciar a varredura, porque não há caminhos incluídos\ncore_skip_exist_check_all_included_paths_nonexistent = Não é possível iniciar a varredura, porque todos os caminhos incluídos não existem\ncore_missing_no_chosen_included_path = O caminho incluído não válido foi escolhido (os caminhos excluídos poderiam ter excluído todos os caminhos incluídos)\ncore_reference_included_paths_same = Não é possível iniciar a varredura onde todos os caminhos incluídos válidos também são caminhos referenciados, tente validar ou desabilitar os caminhos referenciados\ncore_path_must_exists = O caminho fornecido deve existir, ignorando { $path }\ncore_must_be_directory_or_file = O caminho fornecido deve apontar para um diretório ou arquivo válido, ignorando { $path }\ncore_excluded_paths_pointless_slash = Excluindo / é inútil, porque significa que nenhum arquivo será escaneado\ncore_paths_unable_to_get_device_id = Impossível obter o ID do dispositivo da pasta { $path }\ncore_needs_allowed_extensions_limited_by_tool = Não é possível iniciar a varredura, quando todas as extensões disponíveis nesta ferramenta ({ $extensions }) foram excluídas da varredura\ncore_needs_allowed_extensions = Não é possível iniciar a varredura, quando todas as extensões foram excluídas da varredura\ncore_needs_to_set_at_least_one_broken_option = Não é possível iniciar a varredura, quando não há opção de quebrado definida para varrer\ncore_needs_to_set_at_least_one_bad_name_option = Não é possível iniciar a varredura, quando não há opção de nome inválido definida para varrer\ncore_ffmpeg_not_found = Não é possível encontrar uma instalação adequada de FFmpeg ou FFprobe. Estes são programas externos que devem ser instalados manualmente.\ncore_ffmpeg_not_found_windows = Certifique-se de que o ffmpeg.exe e ffprobe.exe estejam disponíveis no PATH ou estejam diretamente na mesma pasta que o app executável\ncore_invalid_symlink_infinite_recursion = Recursão infinita\ncore_invalid_symlink_non_existent_destination = Arquivo de destino não existe\ncore_messages_limit_reached_characters = Número de mensagens excedido o limite definido ({ $current }/{ $limit } caracteres), então a saída foi truncada. Para ler a saída completa, desative a opção de limitação nas configurações.\ncore_messages_limit_reached_lines = Número de mensagens excedido o limite definido ({ $current }/{ $limit } linhas), então a saída foi truncada. Para ler a saída completa, desative a opção de limitação nas configurações.\ncore_error_moving_to_trash = Erro ao mover \"{ $file }\" para a lixeira: { $error }\ncore_error_removing = Erro ao remover \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Não é possível encontrar arquivos de música semelhantes sem um método de similaridade selecionado\ncore_failed_to_spawn_command = Falhou em spawn comando: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg falhou com o status { $status }: { $stderr } (comando: { $command })\ncore_failed_to_load_image_frame = Falha ao carregar o quadro de imagem: { $reason }\ncore_failed_to_extract_frame = Falhou em extrair o quadro em { $time } segundos de \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Falhou ao salvar a miniatura para \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Falhou em obter o quadro no timestamp { $timestamp } de \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Falhou ao obter o quadro de \"{ $file }\" no timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Retângulo de cultivo inválido: esquerda={ $left }, superior={ $top }, direita={ $right }, inferior={ $bottom }\ncore_failed_to_crop_video_file = Falha ao cortar o arquivo de vídeo \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Arquivo de vídeo cortado não foi criado: { $temp }\ncore_unable_check_hash_of_file = Não foi possível verificar o hash do arquivo \"{ $file }\", motivo { $reason }\ncore_error_checking_hash_of_file = Erro ocorreu ao verificar o hash do arquivo \"{ $file }\", motivo { $reason }\ncore_image_zero_dimensions = A imagem tem largura ou altura zero \"{ $path }\"\ncore_image_open_failed = Não é possível abrir o arquivo de imagem \"{ $path }\": { $reason }\ncore_not_directory_remove = Tentando remover a pasta \"{ $path }\" que não é um diretório\ncore_cannot_read_directory = Não é possível ler o diretório \"{ $path }\"\ncore_cannot_read_entry_from_directory = Não é possível ler entrada do diretório \"{ $path }\"\ncore_folder_contains_file_inside = A pasta contém o arquivo \"{ $entry }\" dentro de \"{ $folder }\"\ncore_unknown_directory_entry = Não foi possível determinar o tipo de arquivo da entrada de diretório \"{ $entry }\" dentro de \"{ $path }\"\ncore_video_height_exceeds_limit = Vídeo altura { $height } excede o limite de { $limit }\ncore_failed_to_process_video = Falha ao processar o arquivo de vídeo { $file }: { $reason }\ncore_unknown_codec = Codec desconhecido: { $codec }\ncore_invalid_video_optimizer_mode = Modo otimizador de vídeo inválido: '{ $mode }'. Valores permitidos: transcodar, cortar\ncore_path_not_directory = O caminho não é um diretório: { $folder }\ncore_test_error_for_folder = Erro de teste para a pasta: { $folder }\ncore_unknown_exif_tag_group = Grupo EXIF desconhecido: { $tag }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Falhou ao gerar miniatura para \"{ $file }\": os quadros extraídos têm dimensões diferentes\ncore_failed_to_generate_thumbnail = Falhou ao gerar miniatura para \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Falhou em extrair o quadro em { $time } segundos de \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = O arquivo de vídeo não existe (pode ser removido entre as etapas de digitalização/mais tarde): \"{ $path }\"\ncore_failed_to_get_video_metadata = Falhou ao obter os metadados de vídeo para o arquivo \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Falhou ao obter o codec de vídeo para o arquivo \"{ $file }\"\ncore_failed_to_get_video_duration = Falhou ao obter a duração do vídeo para o arquivo \"{ $file }\"\ncore_failed_to_get_video_dimensions = Falhou ao obter as dimensões do vídeo para o arquivo \"{ $file }\"\ncore_frame_dimensions_mismatch = Dimensões do quadro para timestamp { $timestamp } não correspondem às dimensões do primeiro quadro ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Falha ao carregar dados do ficheiro de cache { $file }, motivo { $reason }\ncore_failed_to_load_data_from_json_cache = Falha ao carregar dados do ficheiro de cache json { $file }, motivo { $reason }\ncore_failed_to_replace_with_optimized = Falhou ao substituir o arquivo \"{ $file }\" pela versão otimizada: { $reason }\ncore_failed_to_write_data_to_cache = Não é possível escrever dados para o ficheiro de cache \"{ $file }\", motivo { $reason }\ncore_properly_saved_cache_entries = Salvado corretamente para o arquivo { $count } entradas de cache.\ncore_video_processing_stopped_by_user = O processamento de vídeo foi interrompido pelo utilizador\ncore_thumbnail_generation_stopped_by_user = Geração de miniaturas foi interrompida pelo usuário\ncore_failed_to_optimize_video = Falha ao otimizar o vídeo \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Falha ao cortar o vídeo \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Falhou ao obter metadados do arquivo otimizado \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Não é possível criar a pasta de configuração \"{ $folder }\", a razão { $reason }\ncore_cannot_create_cache_folder = Não é possível criar a pasta de cache \"{ $folder }\", a razão { $reason }\ncore_cannot_create_or_open_cache_file = Não é possível criar ou abrir o arquivo de cache \"{ $file }\", motivo { $reason }\ncore_cannot_set_config_cache_path = Não é possível definir o caminho de configuração/cache - a configuração e o cache não serão utilizados.\ncore_invalid_extension_contains_space = { $extension } não é uma extensão válida porque contém espaço em branco no interior\ncore_invalid_extension_contains_dot = { $extension } não é uma extensão válida porque contém ponto dentro\n\ncore_failed_to_check_process_status = Falha ao verificar o status do processo: { $reason }\ncore_failed_to_wait_for_process = Falha ao aguardar a conclusão do processo: { $reason }\ncore_failed_to_read_video_properties = Falha ao ler as propriedades do vídeo: { $reason }\ncore_failed_to_execute_ffmpeg = Falha ao executar o ffmpeg: { $reason }\ncore_video_width_exceeds_limit = Largura do vídeo { $width } excede o limite de { $limit }\ncore_optimized_file_larger = O arquivo otimizado { $optimized } (tamanho: { $new_size }) não é menor que o arquivo original { $original } (tamanho: { $original_size })\ncore_folder_does_not_exist = Pasta não encontrada: { $folder }\ncore_error_comparing_fingerprints = Erro ao comparar impressões digitais: { $reason }"
  },
  {
    "path": "czkawka_core/i18n/ro/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Originală\ncore_similarity_very_high = Foarte Mare\ncore_similarity_high = Ridicat\ncore_similarity_medium = Medie\ncore_similarity_small = Mică\ncore_similarity_very_small = Foarte mic\ncore_similarity_minimal = Minimă\ncore_cannot_open_dir = Nu se poate deschide dir { $dir }, motiv { $reason }\ncore_cannot_read_entry_dir = Nu se poate citi intrarea în dir { $dir }, motivul { $reason }\ncore_cannot_read_metadata_dir = Metadatele nu pot fi citite în dir { $dir }, motivul { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Fișierul { $name } pare să fi fost modificat înainte de Epocul Unix\ncore_folder_modified_before_epoch = Dosarul { $name } pare să fi fost modificat înainte de Epocul Unix\ncore_file_no_modification_date = Imposibil de obținut data modificării din fișierul { $name }, motivul { $reason }\ncore_folder_no_modification_date = Imposibil de obținut data modificării din dosarul { $name }, motivul { $reason }\ncore_cannot_start_scan_no_included_paths = Nu se poate începe scanarea, deoarece nu există căi incluse\ncore_skip_exist_check_all_included_paths_nonexistent = Nu se poate începe scanarea, deoarece toate căile incluse nu există\ncore_missing_no_chosen_included_path = Nu a fost selectat niciun drum inclus valid (drumurile excluse ar fi putut exclude toate drumurile incluse)\ncore_reference_included_paths_same = Nu se poate începe scanarea unde toate căile incluse valide sunt și căi referite, încercați să validați sau să dezactivați căile referite\ncore_path_must_exists = Calea furnizată trebuie să existe, ignorând { $path }\ncore_must_be_directory_or_file = Fiul indicat trebuie să indice un director sau fișier valid, ignorând { $path }\ncore_excluded_paths_pointless_slash = Excluderea / este inutilă, deoarece înseamnă că niciun fișier nu va fi scanat\ncore_paths_unable_to_get_device_id = Nu se poate obține ID-ul dispozitivului din folder { $path }\ncore_needs_allowed_extensions_limited_by_tool = Nu se poate începe scanarea, când toate extensiile disponibile în acest instrument ({ $extensions }) au fost excluse din scan\ncore_needs_allowed_extensions = Nu se poate începe scanarea, când toate extensiile au fost excluse din scan\ncore_needs_to_set_at_least_one_broken_option = Nu se poate începe scanarea, când nu este setată opțiunea de a scana pentru elemente rupte\ncore_needs_to_set_at_least_one_bad_name_option = Nu se poate începe scanarea, când nu este setată opțiunea de nume invalid pentru scanare\ncore_ffmpeg_not_found = Nu se poate găsi o instalare adecvată a FFmpeg sau FFprobe. Acestea sunt programe externe care trebuie instalate manual.\ncore_ffmpeg_not_found_windows = Asigurați-vă că ffmpeg.exe și ffprobe.exe sunt disponibile în PATH sau sunt plasate direct în același folder cu aplicația executabilă\ncore_invalid_symlink_infinite_recursion = Recepţie infinită\ncore_invalid_symlink_non_existent_destination = Fișier destinație inexistent\ncore_messages_limit_reached_characters = Numărul de mesaje a depășit limita setată ({ $current }/{ $limit } caractere), deci rezultatul a fost trunchiat. Pentru a citi ieșirea completă, dezactivați opțiunea de limitare din setări.\ncore_messages_limit_reached_lines = Numărul de mesaje a depășit limita setată ({ $current }/{ $limit } linii), astfel rezultatul a fost trunchiat. Pentru a citi ieșirea completă, dezactivați opțiunea de limitare din setări.\ncore_error_moving_to_trash = Eroare la mutarea \"{ $file }\" în coșul de gunoi: { $error }\ncore_error_removing = Eroare la eliminarea \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Nu pot găsi fișiere muzicale similare fără o metodă de similaritate selectată\ncore_failed_to_spawn_command = Eșuare la generarea comenzii: { $reason }\ncore_failed_to_check_process_status = Eșec la verificarea stării procesului: { $reason }\ncore_failed_to_wait_for_process = Eșec la așteptarea procesului: { $reason }\ncore_failed_to_read_video_properties = Eșec la citirea proprietăților videoclipului: { $reason }\ncore_failed_to_execute_ffmpeg = Eșec la execuția ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg a eșuat cu status { $status }: { $stderr } (comanda: { $command })\ncore_failed_to_load_image_frame = Nu s-a putut încărca cadrul imaginii: { $reason }\ncore_failed_to_extract_frame = Eșec la extragerea cadrului la { $time } secunde din \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Nu s-a putut salva miniatură pentru \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Eșec la obținerea cadrului la timestamp { $timestamp } din \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Nu s-a putut obține cadrul din \"{ $file }\" la timestamp { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Dreptunghiul de selecție este invalid: stânga={ $left }, sus={ $top }, dreapta={ $right }, jos={ $bottom }\ncore_failed_to_crop_video_file = Eșec la tăierea fișierului video \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Fișierul video tăiat nu a fost creat: { $temp }\ncore_unable_check_hash_of_file = Nu se poate verifica hash-ul fișierului \"{ $file }\", motivul { $reason }\ncore_error_checking_hash_of_file = Eroare a apărut la verificarea hash-ului fișierului \"{ $file }\", motivul { $reason }\ncore_image_zero_dimensions = Imaginea are o lățime sau înălțime zero \"{ $path }\"\ncore_image_open_failed = Nu se poate deschide fișierul de imagine \"{ $path }\": { $reason }\ncore_not_directory_remove = Încercarea de a elimina folderul \"{ $path }\" care nu este un director\ncore_cannot_read_directory = Nu pot citi directorul \"{ $path }\"\ncore_cannot_read_entry_from_directory = Nu pot citi intrarea din directorul \"{ $path }\"\ncore_folder_contains_file_inside = Folder conține fișierul \"{ $entry }\" în interiorul \"{ $folder }\"\ncore_unknown_directory_entry = Nu se poate determina tipul fișierului intrării din director \"{ $entry }\" în \"{ $path }\"\ncore_video_width_exceeds_limit = Video lățime { $width } depășește limita de { $limit }\ncore_video_height_exceeds_limit = Video înălțime { $height } depășește limita de { $limit }\ncore_failed_to_process_video = Fișierul video { $file } nu a putut fi procesat: { $reason }\ncore_optimized_file_larger = Fișier optimizat { $optimized } (dimensiune: { $new_size }) nu este mai mic decât originalul { $original } (dimensiune: { $original_size })\ncore_unknown_codec = Codc necunoscut: { $codec }\ncore_invalid_video_optimizer_mode = Mod de optimizare video invalid: '{ $mode }'. Valorile permise: transcode, crop\ncore_folder_does_not_exist = Dosar nu există: { $folder }\ncore_path_not_directory = Calele nu este un director: { $folder }\ncore_test_error_for_folder = Eroare test pentru folder: { $folder }\ncore_unknown_exif_tag_group = Grupul EXIF necunoscut: { $tag }\ncore_error_comparing_fingerprints = Eroare la compararea amprentelor: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Eșec la generarea miniaturii pentru \"{ $file }\": cadrele extrase au dimensiuni diferite\ncore_failed_to_generate_thumbnail = Eșec la generarea miniaturii pentru \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Eșec la extragerea cadrului la { $time } secunde din \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Fișier video inexistent (poate fi eliminat între scanare/pașii ulteriori): \"{ $path }\"\ncore_image_too_large = Imaginea este prea mare ({ $width }x{ $height }) - mai mult decât suportat { $max } pixeli\ncore_failed_to_get_video_metadata = Eșec la obținerea metadatelor video pentru fișierul \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Eșec la obținerea codec-ului video pentru fișierul \"{ $file }\"\ncore_failed_to_get_video_duration = Nu s-a putut obține durata video pentru fișierul \"{ $file }\"\ncore_failed_to_get_video_dimensions = Eșec la obținerea dimensiunilor video pentru fișierul \"{ $file }\"\ncore_frame_dimensions_mismatch = Dimensiunile cadrului pentru marcajul de timp { $timestamp } nu corespund cu dimensiunile primului cadru ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Nu s-a putut încărca date din fișierul cache { $file }, motivul { $reason }\ncore_failed_to_load_data_from_json_cache = Nu s-a putut încărca date din fișierul cache JSON { $file }, motiv { $reason }\ncore_failed_to_replace_with_optimized = Eșec la înlocuirea fișierului \"{ $file }\" cu versiunea optimizată: { $reason }\ncore_failed_to_write_data_to_cache = Nu se poate scrie date în fișierul cache \"{ $file }\", motiv { $reason }\ncore_properly_saved_cache_entries = Salvat corect în fișier { $count } intrări în cache.\ncore_video_processing_stopped_by_user = Procesarea video a fost oprită de utilizator\ncore_thumbnail_generation_stopped_by_user = Generarea miniaturilor a fost oprită de utilizator\ncore_failed_to_optimize_video = Eșec la optimizarea videoclipului \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Eșec la tăierea videoclipului \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Eșec la obținerea metadatelor fișierului optimizat \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Nu se poate crea folderul de configurare \"{ $folder }\", motivul { $reason }\ncore_cannot_create_cache_folder = Nu se poate crea folderul de cache \"{ $folder }\", motiv { $reason }\ncore_cannot_create_or_open_cache_file = Nu se poate crea sau deschide fișierul de cache \"{ $file }\", motiv { $reason }\ncore_cannot_set_config_cache_path = Nu se poate seta calea de configurare/cache - configurarea și cache-ul nu vor fi utilizate.\ncore_invalid_extension_contains_space = { $extension } nu este o extensie validă deoarece conține spații goale în interior\ncore_invalid_extension_contains_dot = { $extension } nu este o extensie validă deoarece conține punct în interior\n"
  },
  {
    "path": "czkawka_core/i18n/ru/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Оригинальное\ncore_similarity_very_high = Очень высокое\ncore_similarity_high = Высокое\ncore_similarity_medium = Среднее\ncore_similarity_small = Низкое\ncore_similarity_very_small = Очень низкое\ncore_similarity_minimal = Минимальное\ncore_cannot_open_dir = Невозможно открыть каталог { $dir }, причина: { $reason }\ncore_cannot_read_entry_dir = Невозможно прочитать запись в директории { $dir }, причина: { $reason }\ncore_cannot_read_metadata_dir = Невозможно прочитать метаданные в директории { $dir }, причина: { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Файл { $name } был изменен до эпохи Unix\ncore_folder_modified_before_epoch = Папка { $name } была изменена до начала Unix Epoch\ncore_file_no_modification_date = Не удаётся получить дату изменения из файла { $name }, причина: { $reason }\ncore_folder_no_modification_date = Не удаётся получить дату изменения из папки { $name }, причина: { $reason }\ncore_cannot_start_scan_no_included_paths = Невозможно начать сканирование, так как не указаны пути\ncore_skip_exist_check_all_included_paths_nonexistent = Невозможно начать сканирование, потому что все включенные пути не существуют\ncore_missing_no_chosen_included_path = Не был выбран действительный путь, который был включен (исключающие пути могли исключить все включенные пути)\ncore_reference_included_paths_same = Невозможно начать сканирование, где все допустимые включенные пути также являются ссылочными путями, попробуйте проверить или отключить ссылочные пути\ncore_path_must_exists = Предоставленный путь должен существовать, игнорируя { $path }\ncore_must_be_directory_or_file = Предоставленный путь должен указывать на действительную директорию или файл, игнорируя { $path }\ncore_excluded_paths_pointless_slash = Исключать / бессмысленно, потому что это означает, что файлы не будут сканированы\ncore_paths_unable_to_get_device_id = Невозможно получить идентификатор устройства из папки { $path }\ncore_needs_allowed_extensions_limited_by_tool = Невозможно начать сканирование, когда все расширения, доступные в этом инструменте ({ $extensions }), были исключены из сканирования\ncore_needs_allowed_extensions = Невозможно начать сканирование, когда все расширения исключены из сканирования\ncore_needs_to_set_at_least_one_broken_option = Невозможно начать сканирование, когда не указана опция «поврежденный» для сканирования\ncore_needs_to_set_at_least_one_bad_name_option = Невозможно начать сканирование, когда опция «плохое имя» не установлена для сканирования\ncore_ffmpeg_not_found = Не удается найти надлежащую установку FFmpeg или FFprobe. Это внешние программы, которые должны быть установлены вручную.\ncore_ffmpeg_not_found_windows = Убедитесь, что ffmpeg.exe и ffprobe.exe доступны в PATH или находятся непосредственно в той же папке, что и приложение\ncore_invalid_symlink_infinite_recursion = Бесконечная рекурсия\ncore_invalid_symlink_non_existent_destination = Не найден конечный файл\ncore_messages_limit_reached_characters = Количество сообщений превысило установленный лимит ({ $current }/{ $limit } символов), поэтому вывод был усечен. Чтобы прочитать весь вывод, отключите опцию ограничения в настройках.\ncore_messages_limit_reached_lines = Количество сообщений превысило установленный лимит ({ $current }/{ $limit } строк), поэтому вывод был усечен. Чтобы прочитать весь вывод, отключите опцию ограничения в настройках.\ncore_error_moving_to_trash = Ошибка при перемещении \"{ $file }\" в корзину: { $error }\ncore_error_removing = Ошибка при удалении \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Не удается найти похожие музыкальные файлы без выбранного метода сходства\ncore_failed_to_spawn_command = Не удалось запустить команду: { $reason }\ncore_failed_to_check_process_status = Не удалось проверить статус процесса: { $reason }\ncore_failed_to_wait_for_process = Не удалось дождаться процесса: { $reason }\ncore_failed_to_read_video_properties = Не удалось прочитать свойства видео: { $reason }\ncore_failed_to_execute_ffmpeg = Не удалось выполнить ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg завершился с кодом ошибки { $status }: { $stderr } (команда: { $command })\ncore_failed_to_load_image_frame = Не удалось загрузить кадр изображения: { $reason }\ncore_failed_to_extract_frame = Не удалось извлечь кадр в { $time } секундах из \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Не удалось сохранить миниатюру для \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Не удалось получить кадр в метке времени { $timestamp } из \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Не удалось получить кадр из \"{ $file }\" в метке времени { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Неверный прямоугольник обрезки: лево={ $left }, сверху={ $top }, право={ $right }, снизу={ $bottom }\ncore_failed_to_crop_video_file = Не удалось обрезать видеофайл \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Обрезённый видеофайл не был создан: { $temp }\ncore_unable_check_hash_of_file = Невозможно проверить хэш файла \"{ $file }\", причина { $reason }\ncore_error_checking_hash_of_file = Ошибка при проверке хэша файла \"{ $file }\", причина { $reason }\ncore_image_zero_dimensions = Изображение имеет нулевую ширину или высоту \"{ $path }\"\ncore_image_open_failed = Невозможно открыть файл изображения \"{ $path }\": { $reason }\ncore_not_directory_remove = Попытка удалить папку \"{ $path }\" которая не является директорией\ncore_cannot_read_directory = Невозможно прочитать каталог \"{ $path }\"\ncore_cannot_read_entry_from_directory = Невозможно прочитать запись из каталога \"{ $path }\"\ncore_folder_contains_file_inside = Папка содержит файл \"{ $entry }\" внутри \"{ $folder }\"\ncore_unknown_directory_entry = Невозможно определить тип файла для записи каталога \"{ $entry }\" внутри \"{ $path }\"\ncore_video_width_exceeds_limit = Видео ширина { $width } превышает лимит { $limit }\ncore_video_height_exceeds_limit = Видео высота { $height } превышает лимит { $limit }\ncore_failed_to_process_video = Не удалось обработать видеофайл { $file }: { $reason }\ncore_optimized_file_larger = Оптимизированный файл { $optimized } (размер: { $new_size }) не меньше, чем исходный { $original } (размер: { $original_size })\ncore_unknown_codec = Неизвестный кодек: { $codec }\ncore_invalid_video_optimizer_mode = Недопустимый режим оптимизации видео: '{ $mode }'. Разрешенные значения: transcode, crop\ncore_folder_does_not_exist = Папка не существует: { $folder }\ncore_path_not_directory = Путь не является каталогом: { $folder }\ncore_test_error_for_folder = Ошибка теста для папки: { $folder }\ncore_unknown_exif_tag_group = Неизвестная группа EXIF тегов: { $tag }\ncore_error_comparing_fingerprints = Ошибка при сравнении отпечатков пальцев: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Не удалось сгенерировать миниатюру для \"{ $file }\": извлеченные кадры имеют разные размеры\ncore_failed_to_generate_thumbnail = Не удалось сгенерировать миниатюру для \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Не удалось извлечь кадр в { $time } секундах из \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Файл видео не существует (может быть удален между сканированием/поздними шагами): \"{ $path }\"\ncore_image_too_large = Изображение слишком большое ({ $width }x{ $height }) - больше, чем поддерживаемые { $max } пикселей\ncore_failed_to_get_video_metadata = Не удалось получить метаданные видео для файла \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Не удалось получить видеокодек для файла \"{ $file }\"\ncore_failed_to_get_video_duration = Не удалось получить продолжительность видео для файла \"{ $file }\"\ncore_failed_to_get_video_dimensions = Не удалось получить размеры видео для файла \"{ $file }\"\ncore_frame_dimensions_mismatch = Размеры кадра для метки времени { $timestamp } не совпадают с размерами первого кадра ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Не удалось загрузить данные из кэш-файла { $file }, причина { $reason }\ncore_failed_to_load_data_from_json_cache = Не удалось загрузить данные из файла кэша json { $file }, причина { $reason }\ncore_failed_to_replace_with_optimized = Не удалось заменить файл \"{ $file }\" на оптимизированную версию: { $reason }\ncore_failed_to_write_data_to_cache = Невозможно записать данные в кэш-файл \"{ $file }\", причина { $reason }\ncore_properly_saved_cache_entries = Правильно сохранено в файл { $count } кэш-записей.\ncore_video_processing_stopped_by_user = Обработка видео была остановлена пользователем\ncore_thumbnail_generation_stopped_by_user = Генерация превью была остановлена пользователем\ncore_failed_to_optimize_video = Не удалось оптимизировать видео \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Не удалось обрезать видео \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Не удалось получить метаданные оптимизированного файла \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Невозможно создать папку конфигурации \"{ $folder }\", причина { $reason }\ncore_cannot_create_cache_folder = Невозможно создать папку кэша \"{ $folder }\", причина { $reason }\ncore_cannot_create_or_open_cache_file = Невозможно создать или открыть кэш-файл \"{ $file }\", причина { $reason }\ncore_cannot_set_config_cache_path = Невозможно установить путь к config/cache - config и cache не будут использоваться.\ncore_invalid_extension_contains_space = { $extension } не является допустимым расширением, поскольку оно содержит пробел внутри\ncore_invalid_extension_contains_dot = { $extension } не является допустимым расширением, так как оно содержит точку внутри\n"
  },
  {
    "path": "czkawka_core/i18n/sv-SE/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Ursprunglig\ncore_similarity_very_high = Mycket Hög\ncore_similarity_high = Hög\ncore_similarity_medium = Mellan\ncore_similarity_small = Litet\ncore_similarity_very_small = Väldigt Liten\ncore_similarity_minimal = Minimalt\ncore_cannot_open_dir = Kan inte öppna dir { $dir }anledning { $reason }\ncore_cannot_read_entry_dir = Kan inte läsa post i dir { $dir }, anledning { $reason }\ncore_cannot_read_metadata_dir = Kan inte läsa metadata i dir { $dir }, anledning { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Filen { $name } verkar ha ändrats innan Unix Epoch\ncore_folder_modified_before_epoch = Folder { $name } verkar ha ändrats innan Unix Epoch\ncore_file_no_modification_date = Det går inte att hämta ändringsdatum från filen { $name }, anledning { $reason }\ncore_folder_no_modification_date = Det går inte att hämta ändringsdatum från mappen { $name }, anledning { $reason }\ncore_cannot_start_scan_no_included_paths = Kan inte starta skanning, eftersom det inte finns några inkluderade sökvägar\ncore_skip_exist_check_all_included_paths_nonexistent = Kan inte starta skanningen, eftersom alla inkluderade sökvägar inte finns\ncore_missing_no_chosen_included_path = Ingen giltande inkluderad sökväg valdes (uteslutna sökvägar kunde ha uteslutit alla inkluderade sökvägar)\ncore_reference_included_paths_same = Kan inte starta skanning där alla giltiga inkluderade sökvägar också är refererade sökvägar, försök validera eller inaktivera refererade sökvägar\ncore_path_must_exists = Angiven sökväg måste existera, ignorera { $path }\ncore_must_be_directory_or_file = Angiven sökväg måste peka på en giltig katalog eller fil, och ignorera { $path }\ncore_excluded_paths_pointless_slash = Exkludera / är meningslöst, eftersom det innebär att inga filer kommer att skannas\ncore_paths_unable_to_get_device_id = Kan inte hämta enhets-ID från mappen { $path }\ncore_needs_allowed_extensions_limited_by_tool = Kan inte starta skanning, när alla tillgängliga tillägg i detta verktyg ({ $extensions }) var exkluderade från skanning\ncore_needs_allowed_extensions = Kan inte starta skanning, när alla tillägg har tagits ur skanning\ncore_needs_to_set_at_least_one_broken_option = Kan inte starta skanning, när inget alternativ för trasiga är inställt för att skanna för\ncore_needs_to_set_at_least_one_bad_name_option = Kan inte starta skanning, när ingen alternativ för dåliga namn är inställt för att skanna för\ncore_ffmpeg_not_found = Kan inte hitta en korrekt installation av FFmpeg eller FFprobe. Dessa är externa program som måste installeras manuellt.\ncore_ffmpeg_not_found_windows = Se till att ffmpeg.exe och ffprobe.exe finns tillgängliga i PATH eller placeras direkt i samma mapp som appen körbar\ncore_invalid_symlink_infinite_recursion = Oändlig recursion\ncore_invalid_symlink_non_existent_destination = Icke-existerande målfil\ncore_messages_limit_reached_characters = Antalet meddelanden överskred den inställda gränsen ({ $current }/{ $limit } tecken), så utmatningen trunkterades. För att läsa hela utmatningen, inaktivera begränsande alternativ i inställningarna.\ncore_messages_limit_reached_lines = Antalet meddelanden överskred den inställda gränsen ({ $current }/{ $limit } rader), så utmatningen blev trunkerad. För att läsa hela utmatningen, inaktivera begränsande alternativ i inställningarna.\ncore_error_moving_to_trash = Fel vid flyttning av \"{ $file }\" till papperskorgen: { $error }\ncore_error_removing = Fel vid borttagning av \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Kan inte hitta liknande musikfiler utan en vald likhetsmetod\ncore_failed_to_spawn_command = Misslyckades med att generera kommando: { $reason }\ncore_failed_to_check_process_status = Misslyckades med att kontrollera processstatus: { $reason }\ncore_failed_to_wait_for_process = Misslyckades med att vänta på processen: { $reason }\ncore_failed_to_read_video_properties = Kunde inte läsa videobegränsningar: { $reason }\ncore_failed_to_execute_ffmpeg = Kunde inte köra ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg misslyckades med status { $status }: { $stderr } (kommando: { $command })\ncore_failed_to_load_image_frame = Misslyckades med att ladda bildram: { $reason }\ncore_failed_to_extract_frame = Misslyckades med att extrahera bildruta vid { $time } sekunder från \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Misslyckades med att spara miniatyrbild för \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Misslyckades med att hämta bildrutor vid tidstämpling { $timestamp } från \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Misslyckades med att hämta bildrutor från \"{ $file }\" vid tidstämplen { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Ogiltig odlingsrektangel: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom }\ncore_failed_to_crop_video_file = Misslyckades med att beskära videofilen \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Skuren videofil var inte skapad: { $temp }\ncore_unable_check_hash_of_file = Kunde inte kontrollera hash för fil \"{ $file }\", anledning { $reason }\ncore_error_checking_hash_of_file = Fel inträffade vid kontroll av hash för filen \"{ $file }\", anledning { $reason }\ncore_image_zero_dimensions = Bilden har noll bredd eller höjd \"{ $path }\"\ncore_image_open_failed = Kan inte öppna bildfilen \"{ $path }\": { $reason }\ncore_not_directory_remove = Försöker ta bort mappen \"{ $path }\" vilket inte är en katalog\ncore_cannot_read_directory = Kan inte läsa katalogen \"{ $path }\"\ncore_cannot_read_entry_from_directory = Kan inte läsa inlägg från katalogen \"{ $path }\"\ncore_folder_contains_file_inside = Mappen innehåller filen \"{ $entry }\" inuti \"{ $folder }\"\ncore_unknown_directory_entry = Kan inte fastställa filtyp för katalogposten \"{ $entry }\" inuti \"{ $path }\"\ncore_video_width_exceeds_limit = Video bredd { $width } överskrider gränsen för { $limit }\ncore_video_height_exceeds_limit = Video höjd { $height } överskrider gränsen för { $limit }\ncore_optimized_file_larger = Optimerad fil { $optimized } (storlek: { $new_size }) är inte mindre än original { $original } (storlek: { $original_size })\ncore_unknown_codec = Okänt codec: { $codec }\ncore_folder_does_not_exist = Mappexisterar inte: { $folder }\ncore_path_not_directory = Sökvägen är inte en katalog: { $folder }\ncore_test_error_for_folder = Feltest för mapp: { $folder }\ncore_unknown_exif_tag_group = Okänt EXIF-tag grupp: { $tag }\ncore_error_comparing_fingerprints = Fel vid jämförelse av fingeravtryck: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Misslyckades med att generera miniatyrbild för \"{ $file }\": extraherade ramar har olika dimensioner\ncore_failed_to_generate_thumbnail = Misslyckades med att generera miniatyrbild för \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Misslyckades med att extrahera bildruta vid { $time } sekunder från \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Videofilen finns inte (kan tas bort mellan skanning/senare steg): \"{ $path }\"\ncore_image_too_large = Bilden är för stor ({ $width }x{ $height }) - mer än stödda { $max } pixlar\ncore_failed_to_get_video_metadata = Misslyckades med att hämta videodata för fil \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Misslyckades med att hämta videocodec för filen \"{ $file }\"\ncore_failed_to_get_video_duration = Kunde inte få videolängd för fil \"{ $file }\"\ncore_failed_to_get_video_dimensions = Misslyckades med att få videons dimensioner för filen \"{ $file }\"\ncore_frame_dimensions_mismatch = Bildens mått för tidstämplar { $timestamp } stämmer inte överens med bildens mått ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Misslyckades med att ladda data från cachefil { $file }, anledning { $reason }\ncore_failed_to_load_data_from_json_cache = Kunde inte ladda data från json-cache-fil { $file}, anledning { $reason }\ncore_failed_to_replace_with_optimized = Misslyckades med att ersätta filen \"{ $file }\" med den optimerade versionen: { $reason }\ncore_failed_to_write_data_to_cache = Kan inte skriva data till cachefil \"{ $file }\", anledning { $reason }\ncore_properly_saved_cache_entries = Spara korrekt till fil { $count } cacheposter.\ncore_video_processing_stopped_by_user = Videobearbetningen stoppades av användaren\ncore_thumbnail_generation_stopped_by_user = Miniatyrgenerering stoppades av användare\ncore_failed_to_optimize_video = Misslyckades med att optimera video \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Misslyckades med att beskära videon \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Misslyckades med att hämta metadata för den optimerade filen \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Kan inte skapa konfigurationsmappen \"{ $folder }\", anledningen är { $reason }\ncore_cannot_create_cache_folder = Kan inte skapa cachemappen \"{ $folder }\", anledningen { $reason }\ncore_cannot_create_or_open_cache_file = Kan inte skapa eller öppna cachefil \"{ $file }\", anledning { $reason }\ncore_cannot_set_config_cache_path = Kan inte ställa in config/cache-sökväg - config och cache kommer inte att användas.\ncore_invalid_extension_contains_space = { $extension } är inte en giltig filändelse eftersom den innehåller tomma utrymmen däri\ncore_invalid_extension_contains_dot = { $extension } är inte en giltig filändelse eftersom den innehåller en punkt inuti\n\ncore_failed_to_process_video = Kunde inte bearbeta videofil { $file }: { $reason }\ncore_invalid_video_optimizer_mode = Ogiltigt optimeringsläge för video: '{ $mode }'. Tillåtna värden: transcode, crop"
  },
  {
    "path": "czkawka_core/i18n/tr/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Asıl\ncore_similarity_very_high = Çok Yüksek\ncore_similarity_high = Yüksek\ncore_similarity_medium = Orta\ncore_similarity_small = Düşük\ncore_similarity_very_small = Çok Düşük\ncore_similarity_minimal = Aşırı Düşük\ncore_cannot_open_dir = { $dir } dizini açılamıyor, nedeni: { $reason }\ncore_cannot_read_entry_dir = { $dir } dizinindeki girdi okunamıyor, nedeni: { $reason }\ncore_cannot_read_metadata_dir = { $dir } dizinindeki metaveri okunamıyor, nedei: { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch\ncore_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch\ncore_file_no_modification_date = { $name } dosyasının değişiklik tarihine erişilemiyor, nedeni: { $reason }\ncore_folder_no_modification_date = { $name } klasörünün değişiklik tarihine erişilemiyor, nedeni: { $reason }\ncore_cannot_start_scan_no_included_paths = Tarama başlatılamıyor, çünkü hiçbir dahil yol yok\ncore_skip_exist_check_all_included_paths_nonexistent = Tarama başlatılamıyor, çünkü tüm dahil yollar mevcut değil\ncore_missing_no_chosen_included_path = Geçerli bir dahil yol seçilemedi (hariç tutulan yollar tüm dahil yolları hariç bırakmış olabilir)\ncore_reference_included_paths_same = Geçerli dahil yolların tamamının başvurulan yollar olarak da referanslandırıldığı bir tarama başlatılamaz, lütfen doğrulamayı deneyin veya başvurulan yolları devre dışı bırakın\ncore_must_be_directory_or_file = Verilen yol geçer bir dizine veya dosyaya işaret etmelidir, { $path }'i göz ardı ederek\ncore_excluded_paths_pointless_slash = Hariç tutmak / anlamsızdır, çünkü bu, hiçbir dosyanın taranmayacağı anlamına gelir\ncore_paths_unable_to_get_device_id = Cihaz kimliğini { $path } klasöründen elde edilemiyor\ncore_needs_allowed_extensions_limited_by_tool = Tarama başlatılamıyor, tüm uzantılar bu araçta ({ $extensions }) mevcutken taramadan hariç tutulduğunda\ncore_needs_allowed_extensions = Tarama başlatılamıyor, tüm uzantılar taramadan hariç tutulduğunda\ncore_needs_to_set_at_least_one_broken_option = Tarama başlatılamıyor, kırık seçenek aranırken ayarlanmamışsa\ncore_needs_to_set_at_least_one_bad_name_option = Tarama başlatılamıyor, kötü bir isim seçeneği ayarlanmamışken tarama için\ncore_ffmpeg_not_found = FFmpeg veya FFprobe için uygun bir kurulum bulunamıyor. Bunlar, manuel olarak kurulması gereken harici programlardır.\ncore_ffmpeg_not_found_windows = Emin olun ki, ffmpeg.exe ve ffprobe.exe yoluna eklenmiş veya uygulama yürütülebilir dosyasının aynı klasöründe yer almıştır\ncore_invalid_symlink_infinite_recursion = Sonsuz özyineleme\ncore_invalid_symlink_non_existent_destination = Var olmayan hedef dosya\ncore_messages_limit_reached_characters = Number of messages exceeded the set limit ({ $current }/{ $limit } characters), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_messages_limit_reached_lines = Number of messages exceeded the set limit ({ $current }/{ $limit } lines), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_error_moving_to_trash = { $file }'yi atlaşına taşıma sırasında hata: { $error }\ncore_error_removing = \"{ $file }\" kaldırılırken hata: { $error }\ncore_no_similarity_method_selected = Seçilen benzerlik metoduna olmadan benzer müzik dosyası bulamıyor\ncore_failed_to_spawn_command = Komutun oluşturulması başarısız oldu: { $reason }\ncore_failed_to_check_process_status = İşlem durumunu kontrol edemedi: { $reason }\ncore_failed_to_wait_for_process = İşlem beklemede başarısız oldu: { $reason }\ncore_failed_to_read_video_properties = Video özelliklerini okuyamıyor: { $reason }\ncore_failed_to_execute_ffmpeg = ffmpeg'i çalıştırmada başarısız: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg başarısız oldu durum { $status }: { $stderr } (komut: { $command })\ncore_failed_to_load_image_frame = Resim çerçevesi yüklenemedi: { $reason }\ncore_failed_to_extract_frame = { $time } saniyede kareyi \"{ $file }\" dosyasından çıkarılamadı: { $reason }\ncore_failed_to_save_thumbnail = \"{ $file }\" için önizlemeyi kaydedemedi: { $reason }\ncore_failed_get_frame_at_timestamp = { $timestamp } zaman damgası üzerinde çerçeve alınamadı \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = \"{ $file }\" adlı dosyadaki çerçeve alınamadı zaman damgası { $timestamp }'te: { $reason }\ncore_invalid_crop_rectangle = Geçersiz tarım dikdörtgeni: sol={ $left }, üst={ $top }, sağ={ $right }, alt={ $bottom }\ncore_cropped_video_not_created = Kesilmiş video dosyası oluşturulmadı: { $temp }\ncore_unable_check_hash_of_file = Dosyanın hash'ini \"{ $file }\" kontrol edilemedi, nedeni { $reason }\ncore_error_checking_hash_of_file = Dosya \"{ $file }\" için hash kontrolünde hata oluştu, nedeni { $reason }\ncore_image_zero_dimensions = Görüntü sıfır genişliğe veya yüksekliğe sahiptir \"{ $path }\"\ncore_image_open_failed = \"{ $path }\" adlı görüntü dosyasını açamıyoruz: { $reason }\ncore_not_directory_remove = \"{ $path }$\" adlı klasörü silmeye çalışıyor, bu bir dizin değil\ncore_cannot_read_directory = \"{ $path }\" dizinini okuyamıyor\ncore_cannot_read_entry_from_directory = \"{ $path }\" dizininden girişi okuyamaz\ncore_folder_contains_file_inside = Klasör içinde \"{ $entry }\" dosyası \"{ $folder }\" içinde bulunmaktadır\ncore_unknown_directory_entry = Dosya türünü \"{ $entry }\" girişine ait \"{ $path }\" içinde belirleyemiyorum\ncore_video_width_exceeds_limit = Video genişliği { $width } limiti { $limit } değerini aşmaktadır\ncore_video_height_exceeds_limit = Video yüksekliği { $height } limiti { $limit }'i aşmaktadır\ncore_optimized_file_larger = Optimize edilmiş dosya { $optimized } (boyut: { $new_size }) orijinal { $original } (boyut: { $original_size })'den daha küçük değil\ncore_unknown_codec = Bilinmeyen codec: { $codec }\ncore_invalid_video_optimizer_mode = Geçersiz video optimizasyon modu: '{ $mode }'. İzin verilen değerler: transcode, crop\ncore_folder_does_not_exist = Klasör bulunamadı: { $folder }\ncore_path_not_directory = Yol bir dizin değildir: { $folder }\ncore_test_error_for_folder = Dosya hatası için klasör: { $folder }\ncore_unknown_exif_tag_group = Bilinmeyen EXIF etiket grubu: { $tag }\ncore_error_comparing_fingerprints = Parmak izlerini karşılaştırmada hata: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = \"{ $file }\" için önizlemeyi oluşturulamadı: Çıkarılan kareler farklı boyutlarda\ncore_failed_to_generate_thumbnail = \"{ $file }\" için önizlemeyi oluşturulamadı: { $reason }\ncore_failed_to_extract_frame_at_seek_time = { $time } saniyede kareyi \"{ $file }\" dosyasından çıkarılamadı: { $reason }\ncore_video_file_does_not_exist = Video dosyası mevcut değil (tarama/daha sonra adımları arasında kaldırılabilir): \"{ $path }\"\ncore_image_too_large = Görüntü çok büyü ({$width}x{$height}) - desteklenmeyen { $max } pikselden fazla\ncore_failed_to_get_video_metadata = Video meta verilerini dosyayı \"{ $file }\" için elde edilemedi: { $reason }\ncore_failed_to_get_video_codec = Dosya \"{ $file }\" için video codec'i alınamadı\ncore_failed_to_get_video_duration = Video süresini \"{ $file }\" dosyası için elde edilemedi\ncore_failed_to_get_video_dimensions = Video boyutlarını dosya için \"{ $file }\" alınamadı\ncore_frame_dimensions_mismatch = Çerçeve boyutları { $timestamp } zaman damgası için ilk çerçeve boyutlarıyla (%{$first_w}x%{$first_h}) uyuşmuyor\ncore_failed_to_load_data_from_cache = Verilen dosyadaki {$file} verisi yüklenemedi, nedeni { $reason }\ncore_failed_to_replace_with_optimized = Dosya \"{ $file }\" optimize edilmiş versiyon ile değiştirilemedi: { $reason }\ncore_failed_to_write_data_to_cache = Katalog dosyasına \"{ $file }\" yazamıyor, nedeni { $reason }\ncore_properly_saved_cache_entries = Doğru şekilde dosyaya { $count } önbellek girişi kaydedildi.\ncore_video_processing_stopped_by_user = Video işleme kullanıcının tarafından durduruldu\ncore_thumbnail_generation_stopped_by_user = Miniature oluşturma durduruldu kullanıcı tarafından\ncore_failed_to_optimize_video = Video \"{ $file }\" optimizasyonu başarısız: { $reason }\ncore_failed_to_crop_video = Video kırpma başarısız: \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Optimizasyonlu dosya \"{ $file }\": { $reason } meta verisi alınamadı\ncore_cannot_create_config_folder = Konfigürasyon klasörü \"{ $folder }\" oluşturulamazdı, nedeni { $reason }\ncore_cannot_create_cache_folder = \"{ $folder }\" önbellek klasörü oluşturulamaz, nedeni { $reason }\ncore_cannot_set_config_cache_path = Config/cache yolu yapılamadı - config ve cache kullanılmayacak.\ncore_invalid_extension_contains_space = { $extension } geçerli bir uzantı değildir çünkü içinde boşluk içermektedir\ncore_invalid_extension_contains_dot = { $extension } geçerli bir uzantı değildir çünkü içinde nokta içeriyor\n\ncore_path_must_exists = Verilen yolun mevcut olması gerekmektedir, ancak {$path} kısmını dikkate almayınız\ncore_failed_to_crop_video_file = \"{ $file }\" video dosyasını kırpmakta bir sorun oluştu: { $reason }\ncore_failed_to_process_video = Video dosyasını işleme sırasında bir hata oluştu: { $file } - Neden: { $reason }\ncore_failed_to_load_data_from_json_cache = JSON önbellek dosyasından veri yüklenemedi: { $file }, nedeni: { $reason }\ncore_cannot_create_or_open_cache_file = \"{ $file }\" adlı geçici dosyayı oluşturamadı veya açamadı, nedeni: { $reason }"
  },
  {
    "path": "czkawka_core/i18n/uk/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = Оригінал\ncore_similarity_very_high = Дуже висока\ncore_similarity_high = Висока\ncore_similarity_medium = Середня\ncore_similarity_small = Низька\ncore_similarity_very_small = Дуже низька\ncore_similarity_minimal = Мінімальна\ncore_cannot_open_dir = Не вдалося відкрити каталог { $dir }, причина: { $reason }\ncore_cannot_read_entry_dir = Не вдалося прочитати запис в каталозі { $dir }, причина: { $reason }\ncore_cannot_read_metadata_dir = Не вдалося прочитати метадані в каталозі { $dir }, причина: { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = Файл { $name } здається змінено до Unix Epoch\ncore_folder_modified_before_epoch = Папка { $name } здається була змінена до Unix Epoch\ncore_file_no_modification_date = Не вдалося отримати дату модифікації з файлу { $name }, причина: { $reason }\ncore_folder_no_modification_date = Не вдалося отримати дату модифікації з каталогу { $name }, причина: { $reason }\ncore_cannot_start_scan_no_included_paths = Не вдається запустити сканування, оскільки відсутні включені шляхи\ncore_skip_exist_check_all_included_paths_nonexistent = Не вдається запустити сканування, оскільки всі включені шляхи не існують\ncore_missing_no_chosen_included_path = Не було вибрано жодного валідного включеного шляху (виключені шляхи могли виключити всі включені шляхи)\ncore_reference_included_paths_same = Не вдається запустити сканування, де всі валідні включені шляхи також є шляхами, на які посилаються, спробуйте перевірити їх або вимкнути шляхи, на які посилаються\ncore_path_must_exists = Наданий шлях повинен існувати, ігноруючи { $path }\ncore_must_be_directory_or_file = Наданий шлях повинен вказувати на дійсну директорію або файл, ігноруючи { $path }\ncore_excluded_paths_pointless_slash = Виключення / марне, оскільки це означає, що жодні файли не будуть скановані\ncore_paths_unable_to_get_device_id = Не вдається отримати ідентифікатор пристрою з папки { $path }\ncore_needs_allowed_extensions_limited_by_tool = Не вдається запустити сканування, коли всі розширення, доступні в цьому інструменті ({ $extensions }), були виключені зі скану\ncore_needs_allowed_extensions = Не вдається запустити сканування, коли всі розширення були виключені зі скану\ncore_needs_to_set_at_least_one_broken_option = Не вдається запустити сканування, коли відсутній встановлений опція для сканування пошкоджених\ncore_needs_to_set_at_least_one_bad_name_option = Не вдається запустити сканування, коли опція \"поганого імені\" не встановлена для сканування\ncore_ffmpeg_not_found = Не вдається знайти налесну установку FFmpeg або FFprobe. Це зовнішні програми, які необхідно встановити вручну.\ncore_ffmpeg_not_found_windows = Переконайтеся, що ffmpeg.exe і ffprobe.exe доступні в PATH або розташовані безпосередньо в тій же папці, що і виконуваний додаток\ncore_invalid_symlink_infinite_recursion = Нескінченна рекурсія\ncore_invalid_symlink_non_existent_destination = Неіснуючий файл призначення\ncore_messages_limit_reached_characters = Кількість повідомлень перевищило встановлене обмеження ({ $current }/{ $limit } символів), тому результат обрізано. Щоб прочитати весь вихід, вимкніть обмежувальну опцію в налаштуваннях.\ncore_messages_limit_reached_lines = Кількість повідомлень перевищило встановлене обмеження ({ $current }/{ $limit } рядка), тому вихід було скорочено. Щоб прочитати весь вихід, вимкніть обмежувальну опцію в налаштуваннях.\ncore_error_moving_to_trash = Помилка при переміщенні \"{ $file }\" у кошик: { $error }\ncore_error_removing = Помилка при видаленні \"{ $file }\": { $error }\ncore_no_similarity_method_selected = Не вдається знайти подібні музичні файли без обраного методу подібності\ncore_failed_to_spawn_command = Не вдалося запустити команду: { $reason }\ncore_failed_to_check_process_status = Не вдалося перевірити статус процесу: { $reason }\ncore_failed_to_wait_for_process = Не вдалося дочекатися процесу: { $reason }\ncore_failed_to_read_video_properties = Не вдалося прочитати властивості відео: { $reason }\ncore_failed_to_execute_ffmpeg = Не вдалося виконати ffmpeg: { $reason }\ncore_ffmpeg_failed_with_status = ffmpeg не вдалося виконатися зі статусом { $status }: { $stderr } (команда: { $command })\ncore_failed_to_load_image_frame = Не вдалося завантажити кадр зображення: { $reason }\ncore_failed_to_extract_frame = Не вдалося витягти кадр на { $time } секундах з \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = Не вдалося зберегти мініатюру для \"{ $file }\": { $reason }\ncore_failed_get_frame_at_timestamp = Не вдалося отримати кадр о мітки часу { $timestamp } з \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = Не вдалося отримати кадр з \"{ $file }\" у відбитку часу { $timestamp }: { $reason }\ncore_invalid_crop_rectangle = Недійсний прямокутник обрізки: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom }\ncore_failed_to_crop_video_file = Не вдалося обрізати відеофайл \"{ $file }\": { $reason }\ncore_cropped_video_not_created = Обрізаний відеофайл не був створений: { $temp }\ncore_unable_check_hash_of_file = Не вдалося перевірити хеш файлу \"{ $file }\", причина { $reason }\ncore_error_checking_hash_of_file = Помилка сталася під час перевірки хешу файлу \"{ $file }\", причина { $reason }\ncore_image_zero_dimensions = Зображення має нульову ширину або висоту \"{ $path }\"\ncore_image_open_failed = Не вдається відкрити файл зображення \"{ $path }\": { $reason }\ncore_not_directory_remove = Намагаюся видалити папку \"{ $path }\" яка не є директорією\ncore_cannot_read_directory = Не вдається прочитати каталог \"{ $path }\"\ncore_cannot_read_entry_from_directory = Не вдається прочитати запис із каталогу \"{ $path }\"\ncore_folder_contains_file_inside = Папка містить файл \"{ $entry }\" всередині \"{ $folder }\"\ncore_unknown_directory_entry = Не вдається визначити тип файлу запису каталогу \"{ $entry }\" всередині \"{ $path }\"\ncore_video_width_exceeds_limit = Відео ширина { $width } перевищує ліміт { $limit }\ncore_video_height_exceeds_limit = Відео висота { $height } перевищує ліміт { $limit }\ncore_failed_to_process_video = Не вдалося обробити файл відео { $file }: { $reason }\ncore_optimized_file_larger = Оптимізований файл { $optimized } (розмір: { $new_size }) не менший за оригінальний { $original } (розмір: { $original_size })\ncore_unknown_codec = Невідомий кодек: { $codec }\ncore_invalid_video_optimizer_mode = Недійсний режим оптимізатора відео: '{ $mode }'. Допустимі значення: transcode, crop\ncore_folder_does_not_exist = Папка не існує: { $folder }\ncore_path_not_directory = Шлях не є директорією: { $folder }\ncore_test_error_for_folder = Тест помилка для папки: { $folder }\ncore_unknown_exif_tag_group = Невідомий EXIF тег група: { $tag }\ncore_error_comparing_fingerprints = Помилка при порівнянні відбитків пальців: { $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = Не вдалося згенерувати мініатюру для \"{ $file }\": витягнуті кадри мають різні розміри\ncore_failed_to_generate_thumbnail = Не вдалося згенерувати мініатюру для \"{ $file }\": { $reason }\ncore_failed_to_extract_frame_at_seek_time = Не вдалося витягти кадр на { $time } секундах з \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = Файл відео не існує (може бути видалено між скануванням/пізнішими кроками): \"{ $path }\"\ncore_image_too_large = Зображення занадто велике ({ $width }x{ $height }) - більше ніж підтримується { $max } пікселів\ncore_failed_to_get_video_metadata = Не вдалося отримати метадані відео для файлу \"{ $file }\": { $reason }\ncore_failed_to_get_video_codec = Не вдалося отримати відеокодек для файлу \"{ $file }\"\ncore_failed_to_get_video_duration = Не вдалося отримати тривалість відео для файлу \"{ $file }\"\ncore_failed_to_get_video_dimensions = Не вдалося отримати розміри відео для файлу \"{ $file }\"\ncore_frame_dimensions_mismatch = Розміри кадру для відмітку часу { $timestamp } не відповідають розмірам першого кадру ({ $first_w }x{ $first_h })\ncore_failed_to_load_data_from_cache = Не вдалося завантажити дані з файлу кешу { $file }, причина { $reason }\ncore_failed_to_load_data_from_json_cache = Не вдалося завантажити дані з JSON файлу кешу { $file}, причина { $reason }\ncore_failed_to_replace_with_optimized = Не вдалося замінити файл \"{ $file }\" на оптимізовану версію: { $reason }\ncore_failed_to_write_data_to_cache = Не вдається записати дані до кешованого файлу \"{ $file }\", причина { $reason }\ncore_properly_saved_cache_entries = Правильно збережено у файл { $count } записів кешу.\ncore_video_processing_stopped_by_user = Обробка відео була зупинена користувачем\ncore_thumbnail_generation_stopped_by_user = Генерація превью була зупинена користувачем\ncore_failed_to_optimize_video = Не вдалося оптимізувати відео \"{ $file }\": { $reason }\ncore_failed_to_crop_video = Не вдалося обрізати відео \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = Не вдалося отримати метадані оптимізованого файлу \"{ $file }\": { $reason }\ncore_cannot_create_config_folder = Не вдається створити папку конфігурації \"{ $folder }\", причина { $reason }\ncore_cannot_create_cache_folder = Не вдається створити кешовий каталог \"{ $folder }\", причина { $reason }\ncore_cannot_create_or_open_cache_file = Не вдається створити або відкрити файл кешу \"{ $file }\", причина { $reason }\ncore_cannot_set_config_cache_path = Не вдається встановити шлях до конфігурації/кешу - конфігурація та кеш не будуть використані.\ncore_invalid_extension_contains_space = { $extension } не є допустимим розширенням, оскільки воно містить порожній простір всередині\ncore_invalid_extension_contains_dot = { $extension } не є допустимим розширенням, оскільки воно містить крапку всередині\n"
  },
  {
    "path": "czkawka_core/i18n/zh-CN/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = 原版\ncore_similarity_very_high = 非常高\ncore_similarity_high = 高\ncore_similarity_medium = 中\ncore_similarity_small = 小的\ncore_similarity_very_small = 非常小\ncore_similarity_minimal = 最小化\ncore_cannot_open_dir = 无法打开目录 { $dir }，因为 { $reason }\ncore_cannot_read_entry_dir = 无法在目录 { $dir } 中读取条目，因为 { $reason }\ncore_cannot_read_metadata_dir = 无法读取目录 { $dir } 中的元数据，因为 { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = 文件 { $name } 似乎在Unix Epoch前被修改\ncore_folder_modified_before_epoch = 文件夹 { $name } 似乎已在Unix Epoch前被修改\ncore_file_no_modification_date = 无法从文件 { $name } 获取修改日期，因为 { $reason }\ncore_folder_no_modification_date = 无法从文件夹 { $name } 获取修改日期，因为 { $reason }\ncore_cannot_start_scan_no_included_paths = 无法启动扫描，因为没有包含的路径\ncore_skip_exist_check_all_included_paths_nonexistent = 无法启动扫描，因为所有包含的路径都不存在\ncore_missing_no_chosen_included_path = 未选择有效的包含路径（排除路径可能排除了所有包含路径）\ncore_reference_included_paths_same = 无法在所有有效包含路径也同时是引用路径的位置开始扫描，请尝试验证或禁用引用路径\ncore_path_must_exists = 提供的路径必须存在，忽略 { $path }\ncore_must_be_directory_or_file = 提供的路径必须指向一个有效的目录或文件，忽略 { $path }\ncore_excluded_paths_pointless_slash = 排除/毫无意义，因为这意味着不会扫描任何文件\ncore_paths_unable_to_get_device_id = 无法从文件夹 { $path } 获取设备ID\ncore_needs_allowed_extensions_limited_by_tool = 无法开始扫描，当此工具中所有可用扩展程序 ({ $extensions }) 都已从扫描中排除时\ncore_needs_allowed_extensions = 无法开始扫描，当所有扩展都已从扫描中排除时\ncore_needs_to_set_at_least_one_broken_option = 无法开始扫描，当未设置任何损坏选项以进行扫描\ncore_needs_to_set_at_least_one_bad_name_option = 无法启动扫描，当未设置“坏名称”选项进行扫描\ncore_ffmpeg_not_found = 无法找到合适的 FFmpeg 或 FFprobe 安装。这些是必须手动安装的外部程序。.\ncore_ffmpeg_not_found_windows = 请确保ffmpeg.exe 和 ffprobe.exe 在 PATH 中可用或直接放入与应用可执行文件相同的文件夹\ncore_invalid_symlink_infinite_recursion = 无限递归性\ncore_invalid_symlink_non_existent_destination = 目标文件不存在\ncore_messages_limit_reached_characters = 消息数量超过了设置的限制 ({ $current }/{ $limit } 字符)，所以输出被截断。 要读取全部输出，在设置中禁用限制选项。.\ncore_messages_limit_reached_lines = 消息数量超过了设置的限制 ({ $current }/{ $limit } 行)，所以输出被截断。 要读取全部输出，在设置中禁用限制选项。.\ncore_error_moving_to_trash = 移动 \"{ $file }\" 到回收站时出错：{ $error }\ncore_error_removing = 删除 \"{ $file }\" 时出错：{ $error }\ncore_no_similarity_method_selected = 无法找到没有选择相似方法相似的音乐文件\ncore_failed_to_spawn_command = 未能生成命令：{ $reason }\ncore_failed_to_check_process_status = 未能检查进程状态：{ $reason }\ncore_failed_to_wait_for_process = 未能等待进程：{ $reason }\ncore_failed_to_read_video_properties = 读取视频属性失败：{ $reason }\ncore_failed_to_execute_ffmpeg = 未能执行 ffmpeg：{ $reason }\ncore_ffmpeg_failed_with_status = ffmpeg 失败，状态 { $status }：{ $stderr } (命令：{ $command })\ncore_failed_to_load_image_frame = 加载图像帧失败：{ $reason }\ncore_failed_to_extract_frame = 未能从 { $time } 秒的“{ $file }”中提取帧：{ $reason }\ncore_failed_to_save_thumbnail = 缩略图保存失败“{ $file }”: { $reason }\ncore_failed_get_frame_at_timestamp = 未能获取帧于时间戳 { $timestamp } 来自 \"{ $file }\": { $reason }\ncore_failed_get_frame_from_file = 未能从 \"{ $file }\" 获取帧于 { $timestamp }：{ $reason }\ncore_invalid_crop_rectangle = 无效作物矩形：左={ $left }，上={ $top }，右={ $right }，下={ $bottom }\ncore_failed_to_crop_video_file = 视频文件 \"{ $file }\" 裁剪失败：{ $reason }\ncore_cropped_video_not_created = 裁剪视频文件未创建：{ $temp }\ncore_unable_check_hash_of_file = 无法检查文件 \"{ $file }\" 的哈希值，原因 { $reason }\ncore_error_checking_hash_of_file = 检查文件“{ $file }”的哈希时发生错误，原因 { $reason }\ncore_image_zero_dimensions = 图片宽度或高度为零 \"{ $path }\"\ncore_image_open_failed = 无法打开图像文件 \"{ $path }\": { $reason }\ncore_not_directory_remove = 尝试删除文件夹 \"{ $path }\"，它不是一个目录\ncore_cannot_read_directory = 无法读取目录 \"{ $path }\"\ncore_cannot_read_entry_from_directory = 无法从目录 \"{ $path }\" 读取条目\ncore_folder_contains_file_inside = 文件夹包含文件 \"{ $entry }\" 在内 \"{ $folder }\"\ncore_unknown_directory_entry = 无法确定目录条目 \"{ $entry }\" 在 \"{ $path }\" 内部的文件类型\ncore_video_width_exceeds_limit = 视频宽度 { $width } 超出 { $limit } 的限制\ncore_video_height_exceeds_limit = 视频高度 { $height } 超出 { $limit } 的限制\ncore_failed_to_process_video = 未能处理视频文件 { $file }: { $reason }\ncore_optimized_file_larger = 优化文件 { $optimized } (大小：{ $new_size }) 未小于原始 { $original } (大小：{ $original_size })\ncore_unknown_codec = 未知编解码器：{ $codec }\ncore_invalid_video_optimizer_mode = 无效视频优化模式：'{ $mode }'。允许的值：transcode, crop\ncore_folder_does_not_exist = 文件夹不存在：{ $folder }\ncore_path_not_directory = 路径不是一个目录：{ $folder }\ncore_test_error_for_folder = 文件夹测试错误：{ $folder }\ncore_unknown_exif_tag_group = 未知EXIF标签组：{ $tag }\ncore_error_comparing_fingerprints = 比较指纹时出错：{ $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = 未能生成缩略图“{ $file }”: 提取的帧具有不同的尺寸\ncore_failed_to_generate_thumbnail = 未能生成缩略图“{ $file }”: { $reason }\ncore_failed_to_extract_frame_at_seek_time = 未能从 { $time } 秒的“{ $file }”中提取帧：{ $reason }\ncore_video_file_does_not_exist = 视频文件不存在（可删除在扫描/后续步骤之间）：\"{ $path }\"\ncore_image_too_large = 图片太大 ({ $width }x{ $height }) - 超过支持的 { $max } 像素\ncore_failed_to_get_video_metadata = 获取文件 \"{ $file }\" 的视频元数据失败：{ $reason }\ncore_failed_to_get_video_codec = 无法获取文件“{ $file }”的视频编解码器\ncore_failed_to_get_video_duration = 无法获取文件 \"{ $file }\" 的视频时长\ncore_failed_to_get_video_dimensions = 无法获取文件 \"{ $file }\" 的视频尺寸\ncore_frame_dimensions_mismatch = 帧尺寸对于时间戳 { $timestamp } 不与第一帧尺寸 ({ $first_w }x{ $first_h }) 匹配\ncore_failed_to_load_data_from_cache = 无法从缓存文件 { $file } 加载数据，原因 { $reason }\ncore_failed_to_load_data_from_json_cache = 未能从 json 缓存文件 { $file } 加载数据，原因 { $reason }\ncore_failed_to_replace_with_optimized = 未能将文件“{ $file }”替换为优化版本：{ $reason }\ncore_failed_to_write_data_to_cache = 无法将数据写入缓存文件 \"{ $file }\", 原因 { $reason }\ncore_properly_saved_cache_entries = 正确保存到文件 { $count } 个缓存条目。.\ncore_video_processing_stopped_by_user = 用户已停止视频处理\ncore_thumbnail_generation_stopped_by_user = 缩略图生成已由用户停止\ncore_failed_to_optimize_video = 视频优化失败 \"{ $file }\": { $reason }\ncore_failed_to_crop_video = 视频裁剪失败 \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = 获取优化文件 \"{ $file }\" 的元数据失败：{ $reason }\ncore_cannot_create_config_folder = 无法创建配置文件夹 \"{ $folder }\"，原因 { $reason }\ncore_cannot_create_cache_folder = 无法创建缓存文件夹 \"{ $folder }\", 原因 { $reason }\ncore_cannot_create_or_open_cache_file = 无法创建或打开缓存文件 \"{ $file }\", 原因 { $reason }\ncore_cannot_set_config_cache_path = 无法设置配置/缓存路径 - 配置和缓存将不会被使用。.\ncore_invalid_extension_contains_space = { $extension } 不是一个有效的扩展名，因为它包含内部的空格\ncore_invalid_extension_contains_dot = { $extension } 不是一个有效的扩展名，因为它包含在点内\n"
  },
  {
    "path": "czkawka_core/i18n/zh-TW/czkawka_core.ftl",
    "content": "# Core\ncore_similarity_original = 原始\ncore_similarity_very_high = 極高\ncore_similarity_high = 高\ncore_similarity_medium = 中等\ncore_similarity_small = 小\ncore_similarity_very_small = 非常小\ncore_similarity_minimal = 最小\ncore_cannot_open_dir = 無法開啟目錄 { $dir }，原因是 { $reason }\ncore_cannot_read_entry_dir = 無法讀取目錄 { $dir } 中的項目，原因是 { $reason }\ncore_cannot_read_metadata_dir = 無法讀取目錄 { $dir } 的中繼資料，原因是 { $reason }\ncore_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason }\ncore_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch\ncore_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch\ncore_file_no_modification_date = 無法取得檔案 { $name } 的修改日期，原因是 { $reason }\ncore_folder_no_modification_date = 無法取得資料夾 { $name } 的修改日期，原因是 { $reason }\ncore_cannot_start_scan_no_included_paths = 無法開始掃描，因為沒有包含的路徑\ncore_skip_exist_check_all_included_paths_nonexistent = 無法開始掃描，因為所有包含的路徑都不存在\ncore_missing_no_chosen_included_path = 無有效包含路徑被選擇 (排除路徑可能排除所有包含路徑)\ncore_reference_included_paths_same = 無法在所有有效包含路徑同時也是參照路徑處開始掃描，請嘗試驗證或停用參照路徑\ncore_path_must_exists = 提供的路徑必須存在，忽略 { $path }\ncore_must_be_directory_or_file = 提供的路徑必須指向一個有效的目錄或檔案，忽略 { $path }\ncore_excluded_paths_pointless_slash = 排除 / 是沒有用的，因為它意味著不會掃描任何檔案\ncore_paths_unable_to_get_device_id = 無法從資料夾 { $path } 取得裝置 ID\ncore_needs_allowed_extensions_limited_by_tool = 無法開始掃描，當此工具 ({ $extensions }) 中所有可用的擴充功能都已從掃描中排除時\ncore_needs_allowed_extensions = 無法開始掃描，當所有擴展功能已從掃描中排除\ncore_needs_to_set_at_least_one_broken_option = 無法開始掃描，當沒有將「損壞選項」設定為掃描時\ncore_needs_to_set_at_least_one_bad_name_option = 無法開始掃描，當沒有將「不良名稱」選項設定為掃描時\ncore_ffmpeg_not_found = 無法找到正確的 FFmpeg 或 FFprobe 安裝。這些是必須手動安裝的外部程式。.\ncore_ffmpeg_not_found_windows = 請確保ffmpeg.exe和ffprobe.exe可用於PATH，或直接放置在與應用程式執行檔同一資料夾中。\ncore_invalid_symlink_infinite_recursion = 無限遞迴\ncore_invalid_symlink_non_existent_destination = 目標檔案不存在\ncore_messages_limit_reached_characters = Number of messages exceeded the set limit ({ $current }/{ $limit } characters), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_messages_limit_reached_lines = Number of messages exceeded the set limit ({ $current }/{ $limit } lines), so the output was truncated. To read the full output, disable the limiting option in settings.\ncore_error_moving_to_trash = 移動\"{ $file }\"到垃圾桶時出現錯誤：{ $error }\ncore_error_removing = 移除\"{ $file }\"時發生錯誤：{ $error }\ncore_no_similarity_method_selected = 無法找到沒有選擇相似度方法的類似音樂檔案\ncore_failed_to_spawn_command = 未能生成命令：{ $reason }\ncore_failed_to_check_process_status = 無法檢查進程狀態：{ $reason }\ncore_failed_to_wait_for_process = 未能等待處理：{ $reason }\ncore_failed_to_read_video_properties = 無法讀取影片屬性：{ $reason }\ncore_failed_to_execute_ffmpeg = 無法執行 ffmpeg：{ $reason }\ncore_ffmpeg_failed_with_status = ffmpeg 失敗，狀態為 { $status }：{ $stderr } (命令：{ $command })\ncore_failed_to_load_image_frame = 無法載入圖片畫面：{ $reason }\ncore_failed_to_extract_frame = 在 { $time } 秒處未能提取幀來自 \"{ $file }\": { $reason }\ncore_failed_to_save_thumbnail = 無法儲存 \"{ $file }\" 的縮圖：{ $reason }\ncore_failed_get_frame_at_timestamp = 未能於時間戳 { $timestamp } 從 \"{ $file }\" 取得畫面：{ $reason }\ncore_failed_get_frame_from_file = 從 \"{ $file }\" 在 { $timestamp } 標記時間取得畫面失敗：{ $reason }\ncore_invalid_crop_rectangle = 無效的作物矩形：左={ $left }，上={ $top }，右={ $right }，下={ $bottom }\ncore_failed_to_crop_video_file = 無法裁剪影片檔案 \"{ $file }\": { $reason }\ncore_cropped_video_not_created = 裁剪後的影片檔案未建立：{ $temp }\ncore_unable_check_hash_of_file = 無法檢查檔案 \"{ $file }\" 的雜項，原因 { $reason }\ncore_error_checking_hash_of_file = 檢查檔案 \"{ $file }\" 的雜項時發生錯誤，原因 { $reason }\ncore_image_zero_dimensions = 圖片具有零寬度或高度 \"{ $path }\"\ncore_image_open_failed = 無法開啟圖片檔案 \"{ $path }\": { $reason }\ncore_not_directory_remove = 嘗試移除檔案夾 \"{ $path }\"，它不是一個目錄\ncore_cannot_read_directory = 無法讀取目錄 \"{ $path }\"\ncore_cannot_read_entry_from_directory = 無法從目錄 \"{ $path }\" 讀取資料\ncore_folder_contains_file_inside = 資料夾內包含檔案 \"{ $entry }\" 位於 \"{ $folder }\"\ncore_unknown_directory_entry = 無法判斷目錄入口 \"{ $entry }\" 內部的檔案類型 inside \"{ $path }\"\ncore_video_width_exceeds_limit = 影片寬度 { $width } 超過 { $limit } 的限制\ncore_video_height_exceeds_limit = 影片高度 { $height } 超過 { $limit } 的限制\ncore_failed_to_process_video = 無法處理影片檔案 { $file }: { $reason }\ncore_optimized_file_larger = 優化檔案 { $optimized } (大小：{ $new_size }) 未超過原始檔案 { $original } (大小：{ $original_size })\ncore_unknown_codec = 未知編碼格式：{ $codec }\ncore_invalid_video_optimizer_mode = 無效的影片優化模式：'{ $mode }'。允許的值：transcode, crop\ncore_folder_does_not_exist = 資料夾不存在：{ $folder }\ncore_path_not_directory = 路徑不是一個目錄：{ $folder }\ncore_test_error_for_folder = 測試資料夾錯誤：{ $folder }\ncore_unknown_exif_tag_group = 未知的 EXIF 標籤群組：{ $tag }\ncore_error_comparing_fingerprints = 比較指紋時發生錯誤：{ $reason }\ncore_failed_to_generate_thumbnail_frames_different_dimensions = 未能為 \"{ $file }\" 產生縮圖：提取的幀有不同尺寸\ncore_failed_to_generate_thumbnail = 未能為 \"{ $file }\" 產生縮圖：{ $reason }\ncore_failed_to_extract_frame_at_seek_time = 在 { $time } 秒處未能提取幀來自 \"{ $file }\": { $reason }\ncore_video_file_does_not_exist = 影片檔案不存在 (可移除於掃描/後續步驟中)：\"{ $path }\"\ncore_image_too_large = 圖片太大 ({ $width }x{ $height }) - 超過支援 { $max } 像素\ncore_failed_to_get_video_metadata = 無法取得檔案 \"{ $file }\" 的影片元資料：{ $reason }\ncore_failed_to_get_video_codec = 無法取得檔案 \"{ $file }\" 的影片碼通\ncore_failed_to_get_video_duration = 無法取得檔案 \"{ $file }\" 的影片長度\ncore_failed_to_get_video_dimensions = 無法取得檔案 \"{ $file }\" 的影片尺寸\ncore_frame_dimensions_mismatch = 時間戳 { $timestamp } 的畫框尺寸與第一個畫框尺寸 ({ $first_w }x{ $first_h }) 不符\ncore_failed_to_load_data_from_cache = 無法從快取檔案 { $file } 加載資料，原因 { $reason }\ncore_failed_to_load_data_from_json_cache = 未能從 JSON 緩存檔案 { $file } 加載資料，原因 { $reason }\ncore_failed_to_replace_with_optimized = 未能將檔案 \"{ $file }\" 替換為優化版本：{ $reason }\ncore_failed_to_write_data_to_cache = 無法將資料寫入快取檔案 \"{ $file }\", 原因 { $reason }\ncore_properly_saved_cache_entries = 正確儲存至檔案 { $count } 個緩存條目。.\ncore_video_processing_stopped_by_user = 使用者已停止影片處理\ncore_thumbnail_generation_stopped_by_user = 使用者已停止生成縮圖\ncore_failed_to_optimize_video = 無法優化影片 \"{ $file }\": { $reason }\ncore_failed_to_crop_video = 視頻裁剪失敗 \"{ $file }\": { $reason }\ncore_failed_to_get_metadata_of_optimized_file = 無法取得優化檔案 \"{ $file }\" 的元資料：{ $reason }\ncore_cannot_create_config_folder = 無法建立配置資料夾 \"{ $folder }\"，原因 { $reason }\ncore_cannot_create_cache_folder = 無法建立快取資料夾 \"{ $folder }\"，原因 { $reason }\ncore_cannot_create_or_open_cache_file = 無法建立或開啟快取檔案 \"{ $file }\"，原因 { $reason }\ncore_cannot_set_config_cache_path = 無法設定 config/cache 路径 - config 和 cache 将不会被使用。.\ncore_invalid_extension_contains_space = { $extension } 不是一個有效的擴充類型，因為它包含內部的空白\ncore_invalid_extension_contains_dot = { $extension } 不是一個有效的擴充類型，因為它包含內部的點。\n"
  },
  {
    "path": "czkawka_core/i18n.toml",
    "content": "# (Required) The language identifier of the language used in the\n# source code for gettext system, and the primary fallback language\n# (for which all strings must be present) when using the fluent\n# system.\nfallback_language = \"en\"\n\n# Use the fluent localization system.\n[fluent]\n# (Required) The path to the assets directory.\n# The paths inside the assets directory should be structured like so:\n# `assets_dir/{language}/{domain}.ftl`\nassets_dir = \"i18n\"\n\n"
  },
  {
    "path": "czkawka_core/src/common/basic_gui_cli.rs",
    "content": "use std::process;\n\nuse log::{error, warn};\n\nuse crate::common::config_cache_path::get_config_cache_path;\nuse crate::{CZKAWKA_VERSION, flc};\n\n#[derive(Clone, Debug)]\npub struct CliResult {\n    pub included_items: Vec<String>,\n    pub excluded_items: Vec<String>,\n    pub referenced_items: Vec<String>,\n}\n\nenum ExpectedArgs {\n    Include,\n    Exclude,\n    Referenced,\n}\n\n// Manual processing of CLI arguments, because Clap would be too heavy for this simple task\n\n#[expect(clippy::print_stdout)]\n#[expect(clippy::print_stderr)]\npub fn process_cli_args(app_display: &str, app_exec: &str, args: Vec<String>) -> Option<CliResult> {\n    if [\"--help\", \"-h\"].iter().any(|&arg| args.contains(&arg.to_string())) {\n        println!(\"{app_display}\");\n        println!(\"{app_display} allows you to specify folders to search for files via the CLI, and also to exclude or reference folders.\");\n        println!(\"If used, it will automatically apply the last preset and load its options.\");\n        println!(\"Running the app without arguments will launch the {app_display} with default or saved options.\");\n        println!(\"Usage: {app_exec} [OPTIONS] [FOLDERS...]\");\n        println!(\"Options:\");\n        println!(\"  FOLDER                Include a folder in the search\");\n        println!(\"  -e FOLDER, --exclude FOLDER      Exclude a folder from the search\");\n        println!(\"  -r FOLDER, --referenced FOLDER   Include a folder and set it as referenced\");\n        println!(\"  --cache, -c           Opens the cache folder\");\n        println!(\"  --config, -C          Opens the config folder\");\n        println!(\"  --help, -h            Show this help message\");\n        println!(\"  --version, -v         Show version information\");\n        println!(\"Examples:\");\n        println!(\"  {app_exec} /path/absolute/to/folder -e relative_path/2 -r /path/to/referenced\");\n        println!(\"  {app_exec} . folder2 folder3\");\n        println!(\"If no folders are specified, the program will exit without doing anything.\");\n        process::exit(0);\n    }\n    if [\"--version\", \"-v\"].iter().any(|&arg| args.contains(&arg.to_string())) {\n        let git_commit = env!(\"CZKAWKA_GIT_COMMIT_SHORT\");\n        let official_build = if env!(\"CZKAWKA_OFFICIAL_BUILD\") == \"1\" {\n            \"O\" // Official build\n        } else {\n            \"U\" // Unofficial build\n        };\n        let git_date = env!(\"CZKAWKA_GIT_COMMIT_DATE\");\n        println!(\"{app_display} version {CZKAWKA_VERSION}({git_commit} {official_build} {git_date})\");\n        process::exit(0);\n    }\n\n    let mut expected_arg = ExpectedArgs::Include;\n    let mut cli_result = CliResult {\n        included_items: Vec::new(),\n        excluded_items: Vec::new(),\n        referenced_items: Vec::new(),\n    };\n    let mut errors = Vec::new();\n\n    for arg in args {\n        if arg.starts_with(\"-\") {\n            match arg.as_str() {\n                \"-e\" | \"--exclude\" => expected_arg = ExpectedArgs::Exclude,\n                \"-r\" | \"--referenced\" => expected_arg = ExpectedArgs::Referenced,\n                \"-c\" | \"--cache\" => {\n                    if let Some(cfg) = get_config_cache_path() {\n                        if let Err(e) = open::that(&cfg.cache_folder) {\n                            error!(\"Failed to open cache folder \\\"{}\\\": {e}\", cfg.cache_folder.to_string_lossy());\n                            process::exit(1);\n                        }\n                        process::exit(0);\n                    } else {\n                        error!(\"Failed to get cache folder path\");\n                        process::exit(1);\n                    }\n                }\n                \"-C\" | \"--config\" => {\n                    if let Some(cfg) = get_config_cache_path() {\n                        if let Err(e) = open::that(&cfg.config_folder) {\n                            error!(\"Failed to open config folder \\\"{}\\\": {e}\", cfg.config_folder.to_string_lossy());\n                            process::exit(1);\n                        }\n                        process::exit(0);\n                    } else {\n                        error!(\"Failed to get config folder path\");\n                        process::exit(1);\n                    }\n                }\n                _ => {\n                    eprintln!(\"Unknown option: {arg}\");\n                    process::exit(1);\n                }\n            }\n        } else {\n            match expected_arg {\n                ExpectedArgs::Include => match check_if_folder_is_valid(&arg) {\n                    Ok(folder) => cli_result.included_items.push(folder),\n                    Err(e) => errors.push(e),\n                },\n                ExpectedArgs::Exclude => match check_if_folder_is_valid(&arg) {\n                    Ok(folder) => cli_result.excluded_items.push(folder),\n                    Err(e) => errors.push(e),\n                },\n                ExpectedArgs::Referenced => match check_if_folder_is_valid(&arg) {\n                    Ok(folder) => {\n                        cli_result.included_items.push(folder.clone());\n                        cli_result.referenced_items.push(folder);\n                    }\n                    Err(e) => errors.push(e),\n                },\n            }\n            expected_arg = ExpectedArgs::Include;\n        }\n    }\n\n    deduplicate_folders(&mut cli_result.included_items);\n    deduplicate_folders(&mut cli_result.excluded_items);\n    deduplicate_folders(&mut cli_result.referenced_items);\n\n    if !errors.is_empty() {\n        warn!(\"Errors encountered while processing CLI arguments:\");\n    }\n    for error in &errors {\n        warn!(\"{error}\");\n    }\n\n    if cli_result.included_items.is_empty() && cli_result.excluded_items.is_empty() && cli_result.referenced_items.is_empty() {\n        None\n    } else {\n        Some(cli_result)\n    }\n}\n\nfn deduplicate_folders(folder_list: &mut Vec<String>) {\n    folder_list.sort();\n    folder_list.dedup();\n}\n\n#[cfg(not(test))]\nfn check_if_folder_is_valid(folder: &str) -> Result<String, String> {\n    let path = std::path::Path::new(folder);\n    if !path.exists() {\n        return Err(flc!(\"core_folder_does_not_exist\", folder = folder));\n    }\n    if !path.is_dir() {\n        return Err(flc!(\"core_path_not_directory\", folder = folder));\n    }\n    let canonical_path = dunce::canonicalize(path).map_err(|e| format!(\"Failed to canonicalize path: {folder}. Error: {e}\"))?;\n\n    Ok(canonical_path.to_string_lossy().to_string())\n}\n\n#[cfg(test)]\nfn check_if_folder_is_valid(folder: &str) -> Result<String, String> {\n    if folder.contains(\"test_error\") {\n        return Err(flc!(\"core_test_error_for_folder\", folder = folder));\n    }\n    Ok(folder.to_string())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn processes_include_folder() {\n        let args = vec![\"/valid/folder\".to_string()];\n        let result = process_cli_args(\"A\", \"B\", args).expect(\"TEST\");\n        assert_eq!(result.included_items, vec![\"/valid/folder\".to_string()]);\n        assert!(result.excluded_items.is_empty());\n        assert!(result.referenced_items.is_empty());\n    }\n\n    #[test]\n    fn processes_exclude_folder() {\n        let args = vec![\"-e\".to_string(), \"/valid/folder\".to_string()];\n        let result = process_cli_args(\"A\", \"B\", args).expect(\"TEST\");\n        assert!(result.included_items.is_empty());\n        assert_eq!(result.excluded_items, vec![\"/valid/folder\".to_string()]);\n        assert!(result.referenced_items.is_empty());\n    }\n\n    #[test]\n    fn processes_referenced_folder() {\n        let args = vec![\"-r\".to_string(), \"/valid/folder\".to_string()];\n        let result = process_cli_args(\"A\", \"B\", args).expect(\"TEST\");\n        assert_eq!(result.included_items, vec![\"/valid/folder\".to_string()]);\n        assert!(result.excluded_items.is_empty());\n        assert_eq!(result.referenced_items, vec![\"/valid/folder\".to_string()]);\n    }\n\n    #[test]\n    fn processes_multiple_same_folder() {\n        let args = [\n            \"-r\",\n            \"/valid/folder\",\n            \"-r\",\n            \"/valid/folder\",\n            \"normal_folder\",\n            \"abcd\",\n            \"abcd\",\n            \"-e\",\n            \"/exclu\",\n            \"normal_folder\",\n        ]\n        .iter()\n        .map(|s| s.to_string())\n        .collect();\n        let result = process_cli_args(\"A\", \"B\", args).expect(\"TEST\");\n        assert_eq!(result.included_items, vec![\"/valid/folder\".to_string(), \"abcd\".to_string(), \"normal_folder\".to_string()]);\n        assert_eq!(result.excluded_items, vec![\"/exclu\".to_string()]);\n        assert_eq!(result.referenced_items, vec![\"/valid/folder\".to_string()]);\n    }\n\n    #[test]\n    fn handles_invalid_folder() {\n        let args = vec![\"/invalid/test_error\".to_string()];\n        let result = process_cli_args(\"A\", \"B\", args);\n        assert!(result.is_none());\n    }\n\n    #[test]\n    fn handles_no_arguments() {\n        let args = Vec::new();\n        let result = process_cli_args(\"A\", \"B\", args);\n        assert!(result.is_none());\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/cache/cleaning.rs",
    "content": "use std::fs;\nuse std::io::{BufReader, BufWriter};\nuse std::path::Path;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\n\nuse bincode::Options;\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::{debug, error};\nuse rayon::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::cache::{\n    CACHE_BROKEN_FILES_VERSION, CACHE_CLEANING_INTERVAL_SECONDS, CACHE_DUPLICATE_VERSION, CACHE_IMAGE_VERSION, CACHE_VERSION, CACHE_VIDEO_OPTIMIZE_VERSION, CACHE_VIDEO_VERSION,\n    CLEANING_TIMESTAMPS_FILE, MEMORY_LIMIT,\n};\nuse crate::common::config_cache_path::get_config_cache_path;\nuse crate::common::traits::ResultEntry;\nuse crate::tools::broken_files::BrokenEntry;\nuse crate::tools::duplicate::DuplicateEntry;\nuse crate::tools::exif_remover::ExifEntry;\nuse crate::tools::same_music::MusicEntry;\nuse crate::tools::similar_images::ImagesEntry;\nuse crate::tools::similar_videos::VideosEntry;\nuse crate::tools::video_optimizer::{VideoCropEntry, VideoTranscodeEntry};\n\n#[derive(Debug, Clone, Default)]\npub struct CacheCleaningStatistics {\n    pub total_files_found: usize,\n    pub successfully_cleaned: usize,\n    pub files_with_errors: usize,\n    pub total_entries_before: usize,\n    pub total_entries_removed: usize,\n    pub total_entries_left: usize,\n    pub total_size_before: u64,\n    pub total_size_after: u64,\n    pub errors: Vec<String>,\n}\n\n#[derive(Debug, Clone, Default)]\npub struct CacheProgressCleaning {\n    pub current_cache_file: usize,\n    pub total_cache_files: usize,\n    pub current_file_name: String,\n    pub checked_entries: usize,\n    pub all_entries: usize,\n}\n\n#[derive(Deserialize, Serialize, Debug)]\nstruct CleaningTimestamps {\n    timestamps: Vec<SingleCleaningTimestamp>,\n}\n#[derive(Deserialize, Serialize, Debug)]\nstruct SingleCleaningTimestamp {\n    cache_file_name: String,\n    last_cleaned_timestamp: u64,\n}\n\nfn get_timestamps_file_path() -> Option<std::path::PathBuf> {\n    get_config_cache_path().map(|config| config.cache_folder.join(CLEANING_TIMESTAMPS_FILE))\n}\n\npub(crate) fn should_clean_cache(cache_file_name: &str) -> bool {\n    let Some(timestamps_file) = get_timestamps_file_path() else {\n        return true;\n    };\n\n    let Ok(content) = fs::read_to_string(&timestamps_file) else {\n        return true;\n    };\n\n    let cleaning_timestamps = match serde_json::from_str::<CleaningTimestamps>(&content) {\n        Ok(t) => t,\n        Err(e) => {\n            error!(\n                \"Failed to parse cleaning timestamps file \\\"{}\\\" while processing cache file \\\"{cache_file_name}\\\" - {e:?}\",\n                timestamps_file.to_string_lossy()\n            );\n            return true;\n        }\n    };\n\n    let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();\n\n    if let Some(timestamp) = cleaning_timestamps.timestamps.iter().find(|t| t.cache_file_name == cache_file_name) {\n        let time_since_last_cleaning = current_time.saturating_sub(timestamp.last_cleaned_timestamp);\n        if time_since_last_cleaning < *CACHE_CLEANING_INTERVAL_SECONDS {\n            debug!(\n                \"Last cleaning for {} was {} seconds ago, which is less than the configured interval of {} seconds. Skipping cleaning.\",\n                cache_file_name, time_since_last_cleaning, *CACHE_CLEANING_INTERVAL_SECONDS\n            );\n            return false;\n        }\n        debug!(\n            \"Last cleaning for {} was {} seconds ago, which exceeds the configured interval of {} seconds. Proceeding with cleaning.\",\n            cache_file_name, time_since_last_cleaning, *CACHE_CLEANING_INTERVAL_SECONDS\n        );\n        return true;\n    }\n\n    debug!(\"No cleaning timestamp found for {cache_file_name}, cache cleaning should run\");\n    true\n}\n\npub(crate) fn update_cleaning_timestamp(cache_file_name: &str) {\n    let Some(timestamps_file) = get_timestamps_file_path() else {\n        return;\n    };\n\n    let mut cleaning_timestamps = if let Ok(content) = fs::read_to_string(&timestamps_file) {\n        serde_json::from_str::<CleaningTimestamps>(&content).unwrap_or_else(|e| {\n            error!(\"Failed to parse cleaning timestamps file \\\"{}\\\" content - {e:?}\", timestamps_file.to_string_lossy());\n            CleaningTimestamps { timestamps: Vec::new() }\n        })\n    } else {\n        CleaningTimestamps { timestamps: Vec::new() }\n    };\n\n    let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();\n\n    if let Some(timestamp) = cleaning_timestamps.timestamps.iter_mut().find(|t| t.cache_file_name == cache_file_name) {\n        timestamp.last_cleaned_timestamp = current_time;\n    } else {\n        cleaning_timestamps.timestamps.push(SingleCleaningTimestamp {\n            cache_file_name: cache_file_name.to_string(),\n            last_cleaned_timestamp: current_time,\n        });\n    }\n\n    if let Ok(serialized) = serde_json::to_string_pretty(&cleaning_timestamps) {\n        if let Err(e) = fs::write(&timestamps_file, serialized) {\n            error!(\"Failed to write cleaning timestamps to file {}: {e}\", timestamps_file.to_string_lossy());\n        }\n    } else {\n        error!(\"Failed to serialize cleaning timestamps\");\n    }\n}\n\n#[derive(Debug)]\nenum CacheType {\n    Duplicates,\n    MusicTags,\n    MusicFingerprints,\n    SimilarImages,\n    SimilarVideos,\n    BrokenFiles,\n    ExifRemover,\n    VideoTranscode,\n    VideoCrop,\n}\n\nimpl CacheType {\n    fn from_filename(filename: &str) -> Option<Self> {\n        if filename.starts_with(\"cache_duplicates_\") && filename.ends_with(&format!(\"_{CACHE_DUPLICATE_VERSION}.bin\")) {\n            Some(Self::Duplicates)\n        } else if filename == format!(\"cache_same_music_tags_{CACHE_VERSION}.bin\") {\n            Some(Self::MusicTags)\n        } else if filename == format!(\"cache_same_music_fingerprints_{CACHE_VERSION}.bin\") {\n            Some(Self::MusicFingerprints)\n        } else if filename.starts_with(\"cache_similar_images_\") && filename.ends_with(&format!(\"_{CACHE_IMAGE_VERSION}.bin\")) {\n            Some(Self::SimilarImages)\n        } else if filename.starts_with(&format!(\"cache_similar_videos_{CACHE_VIDEO_VERSION}__\")) && filename.ends_with(\".bin\") {\n            Some(Self::SimilarVideos)\n        } else if filename == format!(\"cache_broken_files_{CACHE_BROKEN_FILES_VERSION}.bin\") {\n            Some(Self::BrokenFiles)\n        } else if filename == format!(\"cache_exif_remover_{CACHE_VERSION}.bin\") {\n            Some(Self::ExifRemover)\n        } else if filename == format!(\"cache_video_transcode_{CACHE_VIDEO_OPTIMIZE_VERSION}.bin\") {\n            Some(Self::VideoTranscode)\n        } else if filename.starts_with(&format!(\"cache_video_crop_{CACHE_VIDEO_OPTIMIZE_VERSION}_\")) && filename.ends_with(\".bin\") {\n            Some(Self::VideoCrop)\n        } else {\n            None\n        }\n    }\n}\n\n#[fun_time(message = \"clean_all_cache_files\", level = \"debug\")]\npub fn clean_all_cache_files(stop_flag: &Arc<AtomicBool>, cache_progress_sender: Option<&Sender<CacheProgressCleaning>>) -> Result<CacheCleaningStatistics, String> {\n    let mut stats = CacheCleaningStatistics::default();\n\n    let Some(config_cache_path) = get_config_cache_path() else {\n        return Err(\"Cannot get cache folder path\".to_string());\n    };\n\n    let cache_folder = &config_cache_path.cache_folder;\n\n    let entries = fs::read_dir(cache_folder).map_err(|e| format!(\"Cannot read cache folder \\\"{}\\\": {}\", cache_folder.to_string_lossy(), e))?;\n\n    let cache_files: Vec<_> = entries\n        .flatten()\n        .filter_map(|entry| {\n            let path = entry.path();\n            if !path.is_file() {\n                return None;\n            }\n            let file_name = path.file_name()?.to_str()?.to_string();\n            let cache_type = CacheType::from_filename(&file_name)?;\n            Some((path, file_name, cache_type))\n        })\n        .collect();\n\n    let total_files = cache_files.len();\n\n    let current_file = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n    let current_file_name = Arc::new(std::sync::Mutex::new(String::new()));\n    let checked_entries = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n    let all_entries = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n    let progress_thread = cache_progress_sender.map(|sender| {\n        let sender = sender.clone();\n        let stop_flag = stop_flag.clone();\n        let current_file = current_file.clone();\n        let current_file_name = current_file_name.clone();\n        let checked_entries = checked_entries.clone();\n        let all_entries = all_entries.clone();\n\n        std::thread::spawn(move || {\n            while !stop_flag.load(Ordering::Relaxed) {\n                std::thread::sleep(std::time::Duration::from_millis(100));\n\n                let current = current_file.load(Ordering::Relaxed);\n                let name = current_file_name.lock().expect(\"Mutex poisoned\").clone();\n                let checked = checked_entries.load(Ordering::Relaxed);\n                let all = all_entries.load(Ordering::Relaxed);\n\n                if current > 0 {\n                    let _ = sender.send(CacheProgressCleaning {\n                        current_cache_file: current,\n                        total_cache_files: total_files,\n                        current_file_name: name,\n                        checked_entries: checked,\n                        all_entries: all,\n                    });\n                }\n            }\n        })\n    });\n\n    for (current_file_idx, (path, file_name, cache_type)) in cache_files.into_iter().enumerate() {\n        if stop_flag.load(Ordering::Relaxed) {\n            return Err(\"Operation stopped by user\".to_string());\n        }\n\n        stats.total_files_found += 1;\n        debug!(\"Found cache file to clean: {file_name} (type: {cache_type:?})\");\n\n        current_file.store(current_file_idx + 1, Ordering::Relaxed);\n        *current_file_name.lock().expect(\"Lock poisoned\") = file_name.clone();\n\n        checked_entries.store(0, Ordering::Relaxed);\n        all_entries.store(0, Ordering::Relaxed);\n\n        let result = match cache_type {\n            CacheType::Duplicates => clean_cache_file_typed::<DuplicateEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::MusicTags | CacheType::MusicFingerprints => clean_cache_file_typed::<MusicEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::SimilarImages => clean_cache_file_typed::<ImagesEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::SimilarVideos => clean_cache_file_typed::<VideosEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::BrokenFiles => clean_cache_file_typed::<BrokenEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::ExifRemover => clean_cache_file_typed::<ExifEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::VideoTranscode => clean_cache_file_typed::<VideoTranscodeEntry>(&path, stop_flag, &checked_entries, &all_entries),\n            CacheType::VideoCrop => clean_cache_file_typed::<VideoCropEntry>(&path, stop_flag, &checked_entries, &all_entries),\n        };\n\n        match result {\n            Ok(Some((before, after, size_before, size_after))) => {\n                stats.successfully_cleaned += 1;\n                stats.total_entries_before += before;\n                stats.total_entries_left += after;\n                stats.total_entries_removed += before - after;\n                stats.total_size_before += size_before;\n                stats.total_size_after += size_after;\n\n                update_cleaning_timestamp(&file_name);\n            }\n            Ok(None) => {\n                debug!(\"Cleaning of cache file {file_name} was skipped due to stop flag\");\n                return Err(\"Operation stopped by user\".to_string());\n            }\n            Err(e) => {\n                stats.files_with_errors += 1;\n                stats.errors.push(format!(\"{file_name}: {e}\"));\n            }\n        }\n    }\n    stop_flag.store(true, Ordering::Relaxed);\n    if let Some(handle) = progress_thread {\n        let _ = handle.join();\n    }\n\n    Ok(stats)\n}\n\nfn clean_cache_file_typed<T>(\n    cache_path: &Path,\n    stop_flag: &Arc<AtomicBool>,\n    checked_entries: &Arc<std::sync::atomic::AtomicUsize>,\n    all_entries: &Arc<std::sync::atomic::AtomicUsize>,\n) -> Result<Option<(usize, usize, u64, u64)>, String>\nwhere\n    for<'a> T: Deserialize<'a> + ResultEntry + Serialize + Clone + Send,\n{\n    let size_before = fs::metadata(cache_path).map(|m| m.len()).unwrap_or(0);\n\n    let file = fs::File::open(cache_path).map_err(|e| format!(\"Cannot open file: {e}\"))?;\n    let reader = BufReader::new(file);\n\n    let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n    let entries: Vec<T> = options.deserialize_from(reader).map_err(|e| format!(\"Cannot deserialize file: {e}\"))?;\n\n    let original_count = entries.len();\n\n    all_entries.store(original_count, Ordering::Relaxed);\n\n    let checked_entries_clone = checked_entries.clone();\n\n    let filtered_entries: Vec<T> = entries\n        .into_par_iter()\n        .map(|cached_entry| {\n            if stop_flag.load(Ordering::Relaxed) {\n                return None;\n            }\n\n            checked_entries_clone.fetch_add(1, Ordering::Relaxed);\n\n            let Ok(metadata) = fs::metadata(cached_entry.get_path()) else {\n                return Some(None);\n            };\n            if metadata.len() != cached_entry.get_size() {\n                return Some(None);\n            }\n            if let Ok(modified_time) = metadata.modified() {\n                if let Ok(duration_since_epoch) = modified_time.duration_since(std::time::UNIX_EPOCH) {\n                    if duration_since_epoch.as_secs() != cached_entry.get_modified_date() {\n                        return Some(None);\n                    }\n                } else {\n                    return Some(None);\n                }\n            }\n\n            Some(Some(cached_entry))\n        })\n        .while_some()\n        .flatten()\n        .collect();\n\n    if stop_flag.load(Ordering::Relaxed) {\n        return Ok(None);\n    }\n\n    let remaining_count = filtered_entries.len();\n    let removed_count = original_count - remaining_count;\n\n    let size_after = if removed_count > 0 {\n        let tmp_file_path = cache_path.with_extension(\"tmp\");\n\n        let tmp_file = fs::File::create(&tmp_file_path).map_err(|e| format!(\"Cannot create temporary file: {e}\"))?;\n        let writer = BufWriter::new(tmp_file);\n        let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n        options\n            .serialize_into(writer, &filtered_entries)\n            .map_err(|e| format!(\"Cannot serialize cleaned data to temporary file: {e}\"))?;\n\n        let new_size = fs::metadata(&tmp_file_path).map(|m| m.len()).unwrap_or(size_before);\n\n        fs::rename(&tmp_file_path, cache_path).map_err(|e| format!(\"Cannot replace original cache file: {e}\"))?;\n\n        debug!(\n            \"Cleaned cache file \\\"{}\\\": removed {} entries, {} remaining, size reduced from {} to {} bytes\",\n            cache_path.to_string_lossy(),\n            removed_count,\n            filtered_entries.len(),\n            size_before,\n            new_size\n        );\n\n        new_size\n    } else {\n        size_before\n    };\n\n    Ok(Some((original_count, remaining_count, size_before, size_after)))\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::PathBuf;\n    use std::sync::Arc;\n    use std::sync::atomic::AtomicBool;\n    use std::time::UNIX_EPOCH;\n\n    use bincode::Options;\n    use serde::{Deserialize, Serialize};\n    use tempfile::TempDir;\n\n    use super::*;\n    use crate::common::cache::tests::setup_cache_path;\n\n    #[derive(Clone, Debug, Serialize, Deserialize)]\n    struct TestCacheEntry {\n        path: PathBuf,\n        size: u64,\n        modified_date: u64,\n        data: String,\n    }\n\n    impl ResultEntry for TestCacheEntry {\n        fn get_path(&self) -> &Path {\n            &self.path\n        }\n        fn get_size(&self) -> u64 {\n            self.size\n        }\n        fn get_modified_date(&self) -> u64 {\n            self.modified_date\n        }\n    }\n\n    fn setup_test_env() -> (PathBuf, PathBuf) {\n        setup_cache_path();\n        let config_cache = get_config_cache_path().unwrap();\n        (config_cache.cache_folder.clone(), config_cache.config_folder)\n    }\n\n    fn create_test_file(dir: &Path, name: &str, content: &str) -> (PathBuf, u64, u64) {\n        let path = dir.join(name);\n        fs::write(&path, content).unwrap();\n        let metadata = fs::metadata(&path).unwrap();\n        let modified = metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs();\n        (path, metadata.len(), modified)\n    }\n\n    fn create_cache_file(cache_dir: &Path, name: &str, entries: &[TestCacheEntry]) -> PathBuf {\n        let cache_path = cache_dir.join(name);\n        let file = fs::File::create(&cache_path).unwrap();\n        let writer = BufWriter::new(file);\n        let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n        options.serialize_into(writer, entries).unwrap();\n        cache_path\n    }\n\n    #[test]\n    fn test_timestamp_operations_and_should_clean() {\n        let (_cache_dir, _config_dir) = setup_test_env();\n        let cache_name = format!(\"test_cache_{}\", std::process::id());\n\n        assert!(should_clean_cache(&cache_name));\n\n        update_cleaning_timestamp(&cache_name);\n        assert!(!should_clean_cache(&cache_name));\n\n        update_cleaning_timestamp(&cache_name);\n        assert!(!should_clean_cache(&cache_name));\n\n        let different_cache = format!(\"different_cache_{}\", std::process::id());\n        assert!(should_clean_cache(&different_cache));\n\n        update_cleaning_timestamp(&different_cache);\n        assert!(!should_clean_cache(&different_cache));\n        assert!(!should_clean_cache(&cache_name));\n    }\n\n    #[test]\n    fn test_clean_cache_file_typed_mixed_scenarios() {\n        let (cache_dir, _config_dir) = setup_test_env();\n        let data_dir = TempDir::new().unwrap();\n\n        let (valid_path, valid_size, valid_modified) = create_test_file(data_dir.path(), \"valid.txt\", \"valid content\");\n        let (modified_path, _, old_modified) = create_test_file(data_dir.path(), \"modified.txt\", \"old content\");\n        std::thread::sleep(std::time::Duration::from_millis(100));\n        fs::write(&modified_path, \"new content with different size\").unwrap();\n        let (deleted_path, deleted_size, deleted_modified) = create_test_file(data_dir.path(), \"deleted.txt\", \"to be deleted\");\n        fs::remove_file(&deleted_path).unwrap();\n\n        let entries = vec![\n            TestCacheEntry {\n                path: valid_path.clone(),\n                size: valid_size,\n                modified_date: valid_modified,\n                data: \"valid\".to_string(),\n            },\n            TestCacheEntry {\n                path: modified_path,\n                size: 11,\n                modified_date: old_modified,\n                data: \"modified\".to_string(),\n            },\n            TestCacheEntry {\n                path: deleted_path,\n                size: deleted_size,\n                modified_date: deleted_modified,\n                data: \"deleted\".to_string(),\n            },\n        ];\n\n        let cache_path = create_cache_file(cache_dir.as_path(), \"test_cache.bin\", &entries);\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n        let all = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n        let result = clean_cache_file_typed::<TestCacheEntry>(&cache_path, &stop_flag, &checked, &all).unwrap();\n\n        assert!(result.is_some());\n        let (original, remaining, _, _) = result.unwrap();\n        assert_eq!(original, 3);\n        assert_eq!(remaining, 1);\n        assert_eq!(checked.load(Ordering::Relaxed), 3);\n        assert_eq!(all.load(Ordering::Relaxed), 3);\n\n        let file = fs::File::open(&cache_path).unwrap();\n        let reader = BufReader::new(file);\n        let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n        let cleaned_entries: Vec<TestCacheEntry> = options.deserialize_from(reader).unwrap();\n        assert_eq!(cleaned_entries.len(), 1);\n        assert_eq!(cleaned_entries[0].path, valid_path);\n    }\n\n    #[test]\n    fn test_clean_cache_file_with_stop_flag() {\n        let (cache_dir, _config_dir) = setup_test_env();\n        let data_dir = TempDir::new().unwrap();\n\n        const ENTRIES_NUMBER: usize = 100;\n\n        let mut entries = Vec::new();\n        for i in 0..ENTRIES_NUMBER {\n            let (path, size, modified) = create_test_file(data_dir.path(), &format!(\"file_{i}.txt\"), &format!(\"content {i}\"));\n            entries.push(TestCacheEntry {\n                path,\n                size,\n                modified_date: modified,\n                data: format!(\"data {i}\"),\n            });\n        }\n\n        let cache_path = create_cache_file(cache_dir.as_path(), \"test_stop.bin\", &entries);\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n        let all = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n        let stop_flag_clone = stop_flag.clone();\n        std::thread::spawn(move || {\n            std::thread::sleep(std::time::Duration::from_millis(1));\n            stop_flag_clone.store(true, Ordering::Relaxed);\n        });\n\n        // Well - it may fail in any place, so we just cannot check exact number of checked entries\n        let result = clean_cache_file_typed::<TestCacheEntry>(&cache_path, &stop_flag, &checked, &all).unwrap();\n        if result.is_some() {\n            assert!(checked.load(Ordering::Relaxed) <= ENTRIES_NUMBER);\n        }\n    }\n\n    #[test]\n    fn test_cache_type_from_filename_all_variants() {\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_duplicates_hash_{CACHE_DUPLICATE_VERSION}.bin\")),\n            Some(CacheType::Duplicates)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_duplicates_size_{CACHE_DUPLICATE_VERSION}.bin\")),\n            Some(CacheType::Duplicates)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_same_music_tags_{CACHE_VERSION}.bin\")),\n            Some(CacheType::MusicTags)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_same_music_fingerprints_{CACHE_VERSION}.bin\")),\n            Some(CacheType::MusicFingerprints)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_similar_images_8_{CACHE_IMAGE_VERSION}.bin\")),\n            Some(CacheType::SimilarImages)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_similar_videos_{CACHE_VIDEO_VERSION}__10.bin\")),\n            Some(CacheType::SimilarVideos)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_broken_files_{CACHE_BROKEN_FILES_VERSION}.bin\")),\n            Some(CacheType::BrokenFiles)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_exif_remover_{CACHE_VERSION}.bin\")),\n            Some(CacheType::ExifRemover)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_video_transcode_{CACHE_VIDEO_OPTIMIZE_VERSION}.bin\")),\n            Some(CacheType::VideoTranscode)\n        ));\n        assert!(matches!(\n            CacheType::from_filename(&format!(\"cache_video_crop_{CACHE_VIDEO_OPTIMIZE_VERSION}_test.bin\")),\n            Some(CacheType::VideoCrop)\n        ));\n\n        assert!(CacheType::from_filename(\"invalid_cache.bin\").is_none());\n        assert!(CacheType::from_filename(\"cache_duplicates_99.bin\").is_none());\n        assert!(CacheType::from_filename(\"random_file.txt\").is_none());\n    }\n\n    #[test]\n    fn test_clean_cache_file_no_changes_needed() {\n        let (cache_dir, _config_dir) = setup_test_env();\n        let data_dir = TempDir::new().unwrap();\n\n        let mut entries = Vec::new();\n        for i in 0..5 {\n            let (path, size, modified) = create_test_file(data_dir.path(), &format!(\"valid_{i}.txt\"), &format!(\"valid content {i}\"));\n            entries.push(TestCacheEntry {\n                path,\n                size,\n                modified_date: modified,\n                data: format!(\"data {i}\"),\n            });\n        }\n\n        let cache_path = create_cache_file(cache_dir.as_path(), \"test_no_changes.bin\", &entries);\n        let size_before = fs::metadata(&cache_path).unwrap().len();\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n        let all = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n        let result = clean_cache_file_typed::<TestCacheEntry>(&cache_path, &stop_flag, &checked, &all).unwrap();\n\n        assert!(result.is_some());\n        let (original, remaining, size_before_result, size_after) = result.unwrap();\n        assert_eq!(original, 5);\n        assert_eq!(remaining, 5);\n        assert_eq!(size_before_result, size_before);\n        assert_eq!(size_after, size_before);\n    }\n\n    #[test]\n    fn test_clean_cache_file_all_entries_invalid() {\n        let (cache_dir, _config_dir) = setup_test_env();\n        let data_dir = TempDir::new().unwrap();\n\n        let (deleted1, size1, mod1) = create_test_file(data_dir.path(), \"del1.txt\", \"content 1\");\n        let (deleted2, size2, mod2) = create_test_file(data_dir.path(), \"del2.txt\", \"content 2\");\n        let (deleted3, size3, mod3) = create_test_file(data_dir.path(), \"del3.txt\", \"content 3\");\n\n        fs::remove_file(&deleted1).unwrap();\n        fs::remove_file(&deleted2).unwrap();\n        fs::remove_file(&deleted3).unwrap();\n\n        let entries = vec![\n            TestCacheEntry {\n                path: deleted1,\n                size: size1,\n                modified_date: mod1,\n                data: \"1\".to_string(),\n            },\n            TestCacheEntry {\n                path: deleted2,\n                size: size2,\n                modified_date: mod2,\n                data: \"2\".to_string(),\n            },\n            TestCacheEntry {\n                path: deleted3,\n                size: size3,\n                modified_date: mod3,\n                data: \"3\".to_string(),\n            },\n        ];\n\n        let cache_path = create_cache_file(cache_dir.as_path(), \"test_all_invalid.bin\", &entries);\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n        let all = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n        let result = clean_cache_file_typed::<TestCacheEntry>(&cache_path, &stop_flag, &checked, &all).unwrap();\n\n        assert!(result.is_some());\n        let (original, remaining, _, _) = result.unwrap();\n        assert_eq!(original, 3);\n        assert_eq!(remaining, 0);\n\n        let file = fs::File::open(&cache_path).unwrap();\n        let reader = BufReader::new(file);\n        let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n        let cleaned_entries: Vec<TestCacheEntry> = options.deserialize_from(reader).unwrap();\n        assert_eq!(cleaned_entries.len(), 0);\n    }\n\n    #[test]\n    fn test_cache_progress_cleaning_struct() {\n        let progress = CacheProgressCleaning {\n            current_cache_file: 3,\n            total_cache_files: 10,\n            current_file_name: \"test_cache.bin\".to_string(),\n            checked_entries: 50,\n            all_entries: 100,\n        };\n\n        assert_eq!(progress.current_cache_file, 3);\n        assert_eq!(progress.total_cache_files, 10);\n        assert_eq!(progress.current_file_name, \"test_cache.bin\");\n        assert_eq!(progress.checked_entries, 50);\n        assert_eq!(progress.all_entries, 100);\n    }\n\n    #[test]\n    fn test_cleaning_timestamps_serialization() {\n        let timestamps = CleaningTimestamps {\n            timestamps: vec![\n                SingleCleaningTimestamp {\n                    cache_file_name: \"cache1.bin\".to_string(),\n                    last_cleaned_timestamp: 1000,\n                },\n                SingleCleaningTimestamp {\n                    cache_file_name: \"cache2.bin\".to_string(),\n                    last_cleaned_timestamp: 2000,\n                },\n            ],\n        };\n\n        let serialized = serde_json::to_string(&timestamps).unwrap();\n        let deserialized: CleaningTimestamps = serde_json::from_str(&serialized).unwrap();\n\n        assert_eq!(deserialized.timestamps.len(), 2);\n        assert_eq!(deserialized.timestamps[0].cache_file_name, \"cache1.bin\");\n        assert_eq!(deserialized.timestamps[0].last_cleaned_timestamp, 1000);\n        assert_eq!(deserialized.timestamps[1].cache_file_name, \"cache2.bin\");\n        assert_eq!(deserialized.timestamps[1].last_cleaned_timestamp, 2000);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/cache.rs",
    "content": "#![allow(clippy::useless_let_if_seq)]\n\nmod cleaning;\n\nuse std::collections::BTreeMap;\nuse std::io::{BufReader, BufWriter};\nuse std::path::Path;\nuse std::{fs, mem};\n\nuse bincode::Options;\npub use cleaning::{CacheCleaningStatistics, CacheProgressCleaning, clean_all_cache_files};\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\nuse indexmap::IndexMap;\nuse log::{debug, error};\nuse once_cell::sync::Lazy;\nuse rayon::iter::{IntoParallelIterator, ParallelIterator};\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::cache::cleaning::{should_clean_cache, update_cleaning_timestamp};\nuse crate::common::config_cache_path::open_cache_folder;\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\nuse crate::helpers::messages::Messages;\n\npub(crate) const CACHE_VERSION: u8 = 100;\npub(crate) const CACHE_DUPLICATE_VERSION: u8 = 100;\npub(crate) const CACHE_IMAGE_VERSION: u8 = 100;\npub(crate) const CACHE_VIDEO_VERSION: u8 = 110;\npub(crate) const CACHE_BROKEN_FILES_VERSION: u8 = 110;\npub(crate) const CACHE_VIDEO_OPTIMIZE_VERSION: u8 = 110;\n\nconst MEMORY_LIMIT: u64 = 8 * 1024 * 1024 * 1024;\nconst CLEANING_TIMESTAMPS_FILE: &str = \"cleaning_timestamps.json\";\n\nstatic CACHE_CLEANING_INTERVAL_SECONDS: Lazy<u64> = Lazy::new(|| {\n    option_env!(\"CZKAWKA_CACHE_CLEANING_INTERVAL_SECONDS\")\n        .and_then(|s| s.parse::<u64>().ok())\n        .unwrap_or(7 * 24 * 60 * 60)\n});\n\nfn get_cache_size(file_name: &Path) -> String {\n    fs::metadata(file_name).map_or_else(|_| \"<unknown size>\".to_string(), |metadata| format_size(metadata.len(), BINARY))\n}\n\n#[fun_time(message = \"save_cache_to_file_generalized\", level = \"debug\")]\npub fn save_cache_to_file_generalized<T>(cache_file_name: &str, hashmap: &BTreeMap<String, T>, save_also_as_json: bool, minimum_file_size: u64) -> Messages\nwhere\n    T: Serialize + ResultEntry + Sized + Send + Sync,\n{\n    let mut text_messages = Messages::new();\n    if let Some(((file_handler, cache_file), (file_handler_json, cache_file_json))) = open_cache_folder(cache_file_name, true, save_also_as_json, &mut text_messages.warnings) {\n        let hashmap_to_save = hashmap.values().filter(|t| t.get_size() >= minimum_file_size).collect::<Vec<_>>();\n\n        {\n            let writer = BufWriter::new(file_handler.expect(\"Cannot fail, because for saving, this always exists\"));\n            let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n            if let Err(e) = options.serialize_into(writer, &hashmap_to_save) {\n                text_messages\n                    .warnings\n                    .push(flc!(\"core_failed_to_write_data_to_cache\", file = cache_file.to_string_lossy(), reason = e.to_string()));\n                debug!(\"Failed to save cache to file \\\"{}\\\" - {e}\", cache_file.to_string_lossy());\n                return text_messages;\n            }\n            debug!(\"Saved cache to binary file \\\"{}\\\" with size {}\", cache_file.to_string_lossy(), get_cache_size(&cache_file));\n        }\n        if save_also_as_json && let Some(file_handler_json) = file_handler_json {\n            let writer = BufWriter::new(file_handler_json);\n            if let Err(e) = serde_json::to_writer(writer, &hashmap_to_save) {\n                text_messages\n                    .warnings\n                    .push(flc!(\"core_failed_to_write_data_to_cache\", file = cache_file_json.to_string_lossy(), reason = e.to_string()));\n                debug!(\"Failed to save cache to file \\\"{}\\\" - {e}\", cache_file_json.to_string_lossy());\n                return text_messages;\n            }\n            debug!(\n                \"Saved cache to json file \\\"{}\\\" with size {}\",\n                cache_file_json.to_string_lossy(),\n                get_cache_size(&cache_file_json)\n            );\n        }\n\n        text_messages.messages.push(flc!(\"core_properly_saved_cache_entries\", count = hashmap.len()));\n        debug!(\"Properly saved to file {} cache entries.\", hashmap.len());\n    } else {\n        debug!(\"Failed to save cache to file {cache_file_name} because not exists\");\n    }\n    text_messages\n}\n\npub(crate) fn extract_loaded_cache<T>(\n    loaded_hash_map: &BTreeMap<String, T>,\n    files_to_check: BTreeMap<String, T>,\n    records_already_cached: &mut BTreeMap<String, T>,\n    non_cached_files_to_check: &mut BTreeMap<String, T>,\n) where\n    T: Clone,\n{\n    for (name, file_entry) in files_to_check {\n        if let Some(cached_file_entry) = loaded_hash_map.get(&name) {\n            records_already_cached.insert(name, cached_file_entry.clone());\n        } else {\n            non_cached_files_to_check.insert(name, file_entry);\n        }\n    }\n}\n\n#[fun_time(message = \"load_cache_from_file_generalized_by_path\", level = \"debug\")]\npub fn load_cache_from_file_generalized_by_path<T>(cache_file_name: &str, delete_outdated_cache: bool, used_files: &BTreeMap<String, T>) -> (Messages, Option<BTreeMap<String, T>>)\nwhere\n    for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone,\n{\n    let check_file = |file_entry: &T| {\n        let file_entry_path_str = file_entry.get_path().to_string_lossy();\n        let key: &str = file_entry_path_str.as_ref();\n        if let Some(used_file) = used_files.get(key) {\n            if file_entry.get_size() != used_file.get_size() {\n                return false;\n            }\n            if file_entry.get_modified_date() != used_file.get_modified_date() {\n                return false;\n            }\n        }\n        true\n    };\n\n    let (text_messages, vec_loaded_cache) = load_cache_from_file_generalized(cache_file_name, delete_outdated_cache, check_file);\n    let Some(vec_loaded_entries) = vec_loaded_cache else {\n        return (text_messages, None);\n    };\n\n    debug!(\"Converting cache Vec<T> into BTreeMap<String, T>\");\n    let number_of_entries = vec_loaded_entries.len();\n    let start_time = std::time::Instant::now();\n    let map_loaded_entries: BTreeMap<String, T> = vec_loaded_entries\n        .into_iter()\n        .map(|file_entry| (file_entry.get_path().to_string_lossy().into_owned(), file_entry))\n        .collect();\n    debug!(\"Converted cache Vec<T>({number_of_entries} results) into BTreeMap<String, T> in {:?}\", start_time.elapsed());\n\n    (text_messages, Some(map_loaded_entries))\n}\n\n#[fun_time(message = \"load_cache_from_file_generalized_by_size\", level = \"debug\")]\npub fn load_cache_from_file_generalized_by_size<T>(\n    cache_file_name: &str,\n    delete_outdated_cache: bool,\n    cache_not_converted: &BTreeMap<u64, Vec<T>>,\n) -> (Messages, Option<BTreeMap<u64, Vec<T>>>)\nwhere\n    for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone,\n{\n    debug!(\"Converting cache BtreeMap<u64, Vec<T>> into IndexMap<String, (u64, u64)>\");\n    let used_files: IndexMap<String, (u64, u64)> = cache_not_converted\n        .iter()\n        .flat_map(|(size, vec)| {\n            vec.iter()\n                .map(move |file_entry| (file_entry.get_path().to_string_lossy().into_owned(), (*size, file_entry.get_modified_date())))\n        })\n        .collect();\n    debug!(\"Converted cache BtreeMap<u64, Vec<T>> into IndexMap<String, (u64, u64)>\");\n\n    let check_file = |file_entry: &T| {\n        let file_entry_path_str = file_entry.get_path().to_string_lossy();\n        let key: &str = file_entry_path_str.as_ref();\n        if let Some((size, modification_date)) = used_files.get(key) {\n            if file_entry.get_size() != *size {\n                return false;\n            }\n            if file_entry.get_modified_date() != *modification_date {\n                return false;\n            }\n        }\n        true\n    };\n\n    let (text_messages, vec_loaded_cache) = load_cache_from_file_generalized(cache_file_name, delete_outdated_cache, check_file);\n    let Some(vec_loaded_entries) = vec_loaded_cache else {\n        return (text_messages, None);\n    };\n\n    debug!(\"Converting cache Vec<T> into BTreeMap<u64, Vec<T>>\");\n    let number_of_entries = vec_loaded_entries.len();\n    let start_time = std::time::Instant::now();\n    let mut map_loaded_entries: BTreeMap<u64, Vec<T>> = Default::default();\n    for file_entry in vec_loaded_entries {\n        map_loaded_entries.entry(file_entry.get_size()).or_default().push(file_entry);\n    }\n    debug!(\n        \"Converted cache Vec<T>({number_of_entries} results) into BTreeMap<u64, Vec<T>> in {:?}\",\n        start_time.elapsed()\n    );\n\n    (text_messages, Some(map_loaded_entries))\n}\n\n#[fun_time(message = \"load_cache_from_file_generalized\", level = \"debug\")]\nfn load_cache_from_file_generalized<T, F>(cache_file_name: &str, delete_outdated_cache: bool, check_func: F) -> (Messages, Option<Vec<T>>)\nwhere\n    for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone,\n    F: Fn(&T) -> bool + Send + Sync,\n{\n    let mut text_messages = Messages::new();\n\n    if let Some(((file_handler, cache_file), (file_handler_json, cache_file_json))) = open_cache_folder(cache_file_name, false, true, &mut text_messages.warnings) {\n        let cache_full_name;\n        let mut vec_loaded_entries: Vec<T>;\n        if let Some(file_handler) = file_handler {\n            cache_full_name = cache_file.clone();\n            let reader = BufReader::new(file_handler);\n\n            let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);\n            vec_loaded_entries = match options.deserialize_from(reader) {\n                Ok(t) => t,\n                Err(e) => {\n                    text_messages\n                        .warnings\n                        .push(flc!(\"core_failed_to_load_data_from_cache\", file = cache_file.to_string_lossy(), reason = e.to_string()));\n                    error!(\"Failed to load cache from file {} - {e}\", cache_file.to_string_lossy());\n                    return (text_messages, None);\n                }\n            };\n        } else {\n            cache_full_name = cache_file_json.clone();\n            let reader = BufReader::new(file_handler_json.expect(\"This cannot fail, because if file_handler is None, then this cannot be None\"));\n            vec_loaded_entries = match serde_json::from_reader(reader) {\n                Ok(t) => t,\n                Err(e) => {\n                    text_messages.warnings.push(flc!(\n                        \"core_failed_to_load_data_from_json_cache\",\n                        file = cache_file_json.to_string_lossy(),\n                        reason = e.to_string()\n                    ));\n                    debug!(\"Failed to load cache from file {} - {e}\", cache_file_json.to_string_lossy());\n                    return (text_messages, None);\n                }\n            };\n        }\n\n        let should_clean = should_clean_cache(cache_file_name);\n        debug!(\n            \"Starting removing outdated cache entries (removing non existent files from cache - {delete_outdated_cache}, should_clean - {should_clean}, entries number - {})\",\n            vec_loaded_entries.len()\n        );\n        let initial_number_of_entries = vec_loaded_entries.len();\n        let deleting_start_time = std::time::Instant::now();\n\n        let effective_delete_outdated = delete_outdated_cache && should_clean;\n\n        vec_loaded_entries = vec_loaded_entries\n            .into_par_iter()\n            .filter(|file_entry| {\n                if !check_func(file_entry) {\n                    return false;\n                }\n\n                if effective_delete_outdated && !file_entry.get_path().exists() {\n                    return false;\n                }\n\n                true\n            })\n            .collect();\n\n        if effective_delete_outdated {\n            update_cleaning_timestamp(cache_file_name);\n        }\n\n        debug!(\n            \"Completed removing outdated cache entries, removed {} out of all {} entries in {:?}\",\n            initial_number_of_entries - vec_loaded_entries.len(),\n            initial_number_of_entries,\n            deleting_start_time.elapsed()\n        );\n\n        text_messages.messages.push(format!(\"Properly loaded {} cache entries.\", vec_loaded_entries.len()));\n\n        debug!(\n            \"Loaded cache from file {cache_file_name} (or json alternative) - {} results - size {}\",\n            vec_loaded_entries.len(),\n            get_cache_size(&cache_full_name)\n        );\n        return (text_messages, Some(vec_loaded_entries));\n    }\n    debug!(\"Failed to load cache from file {cache_file_name} because not exists\");\n    (text_messages, None)\n}\n\npub(crate) fn load_and_split_cache_generalized_by_path<C: CommonData, K>(\n    cache_file_name: &str,\n    mut items_to_check: BTreeMap<String, K>,\n    common_data: &mut C,\n) -> (BTreeMap<String, K>, BTreeMap<String, K>, BTreeMap<String, K>)\nwhere\n    for<'a> K: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone,\n{\n    if !common_data.get_use_cache() {\n        return (Default::default(), Default::default(), items_to_check);\n    }\n\n    let loaded_hash_map;\n\n    let mut records_already_cached: BTreeMap<String, K> = Default::default();\n    let mut non_cached_files_to_check: BTreeMap<String, K> = Default::default();\n\n    let (messages, loaded_items) = load_cache_from_file_generalized_by_path::<K>(cache_file_name, common_data.get_delete_outdated_cache(), &items_to_check);\n    common_data.get_text_messages_mut().extend_with_another_messages(messages);\n    loaded_hash_map = loaded_items.unwrap_or_default();\n\n    debug!(\"load_cache - Starting to check for differences\");\n    extract_loaded_cache(\n        &loaded_hash_map,\n        mem::take(&mut items_to_check),\n        &mut records_already_cached,\n        &mut non_cached_files_to_check,\n    );\n    debug!(\n        \"load_cache - completed diff between loaded and prechecked files, {}({}) - non cached, {}({}) - already cached\",\n        non_cached_files_to_check.len(),\n        format_size(non_cached_files_to_check.values().map(|e| e.get_size()).sum::<u64>(), BINARY),\n        records_already_cached.len(),\n        format_size(records_already_cached.values().map(|e| e.get_size()).sum::<u64>(), BINARY),\n    );\n    (loaded_hash_map, records_already_cached, non_cached_files_to_check)\n}\n\npub(crate) fn save_and_connect_cache_generalized_by_path<C: CommonData, K>(cache_file_name: &str, vec_file_entry: &[K], loaded_hash_map: BTreeMap<String, K>, common_data: &mut C)\nwhere\n    K: Serialize + ResultEntry + Sized + Send + Sync + Clone,\n{\n    if !common_data.get_use_cache() {\n        return;\n    }\n    let mut all_results: BTreeMap<String, K> = Default::default();\n\n    for file_entry in vec_file_entry.iter().cloned() {\n        all_results.insert(file_entry.get_path().to_string_lossy().to_string(), file_entry);\n    }\n    for (name, file_entry) in loaded_hash_map {\n        all_results.insert(name, file_entry);\n    }\n\n    let messages = save_cache_to_file_generalized(cache_file_name, &all_results, common_data.get_save_also_as_json(), 0);\n    common_data.get_text_messages_mut().extend_with_another_messages(messages);\n}\n\n#[cfg(test)]\nmod tests {\n    use std::collections::BTreeMap;\n    use std::fs;\n    use std::path::PathBuf;\n    use std::sync::Once;\n\n    use tempfile::TempDir;\n\n    use super::*;\n    use crate::common::config_cache_path::set_config_cache_path_test;\n\n    static INIT: Once = Once::new();\n\n    pub(crate) fn setup_cache_path() {\n        INIT.call_once(|| {\n            let temp_cache_dir = TempDir::new().expect(\"Failed to create temp cache dir\");\n            let temp_config_dir = TempDir::new().expect(\"Failed to create temp config dir\");\n\n            let cache_path = temp_cache_dir.path().to_path_buf();\n            let config_path = temp_config_dir.path().to_path_buf();\n\n            set_config_cache_path_test(cache_path, config_path);\n\n            // Leak the TempDir to keep directories alive for the duration of tests\n            std::mem::forget(temp_cache_dir);\n            std::mem::forget(temp_config_dir);\n        });\n    }\n\n    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\n    struct TestEntry {\n        path: PathBuf,\n        size: u64,\n        modified_date: u64,\n        value: u32,\n    }\n\n    impl ResultEntry for TestEntry {\n        fn get_path(&self) -> &Path {\n            &self.path\n        }\n        fn get_modified_date(&self) -> u64 {\n            self.modified_date\n        }\n        fn get_size(&self) -> u64 {\n            self.size\n        }\n    }\n\n    impl TestEntry {\n        fn new(path: &str, size: u64, modified_date: u64, value: u32) -> Self {\n            Self {\n                path: PathBuf::from(path),\n                size,\n                modified_date,\n                value,\n            }\n        }\n    }\n\n    #[test]\n    fn test_extract_loaded_cache() {\n        let mut loaded_cache = BTreeMap::new();\n        loaded_cache.insert(\"file1\".to_string(), TestEntry::new(\"/tmp/file1\", 100, 1000, 10));\n        loaded_cache.insert(\"file2\".to_string(), TestEntry::new(\"/tmp/file2\", 200, 2000, 20));\n\n        let mut files_to_check = BTreeMap::new();\n        files_to_check.insert(\"file1\".to_string(), TestEntry::new(\"/tmp/file1\", 100, 1000, 10));\n        files_to_check.insert(\"file3\".to_string(), TestEntry::new(\"/tmp/file3\", 300, 3000, 30));\n        files_to_check.insert(\"file2\".to_string(), TestEntry::new(\"/tmp/file2\", 200, 2000, 20));\n\n        let mut records_already_cached = BTreeMap::new();\n        let mut non_cached_files_to_check = BTreeMap::new();\n\n        extract_loaded_cache(&loaded_cache, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check);\n\n        assert_eq!(records_already_cached.len(), 2);\n        assert_eq!(non_cached_files_to_check.len(), 1);\n        assert!(records_already_cached.contains_key(\"file1\"));\n        assert!(records_already_cached.contains_key(\"file2\"));\n        assert!(non_cached_files_to_check.contains_key(\"file3\"));\n        assert_eq!(records_already_cached.get(\"file1\").unwrap().value, 10);\n        assert_eq!(non_cached_files_to_check.get(\"file3\").unwrap().value, 30);\n    }\n\n    #[test]\n    fn test_extract_loaded_cache_empty() {\n        let loaded_cache: BTreeMap<String, TestEntry> = BTreeMap::new();\n        let mut files_to_check = BTreeMap::new();\n        files_to_check.insert(\"file1\".to_string(), TestEntry::new(\"/tmp/file1\", 100, 1000, 10));\n        files_to_check.insert(\"file2\".to_string(), TestEntry::new(\"/tmp/file2\", 200, 2000, 20));\n\n        let mut records_already_cached = BTreeMap::new();\n        let mut non_cached_files_to_check = BTreeMap::new();\n\n        extract_loaded_cache(&loaded_cache, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check);\n\n        assert_eq!(records_already_cached.len(), 0, \"No entries should be cached\");\n        assert_eq!(non_cached_files_to_check.len(), 2, \"All entries should be non-cached\");\n    }\n\n    #[test]\n    fn test_extract_loaded_cache_all_cached() {\n        let mut loaded_cache = BTreeMap::new();\n        loaded_cache.insert(\"file1\".to_string(), TestEntry::new(\"/tmp/file1\", 100, 1000, 10));\n        loaded_cache.insert(\"file2\".to_string(), TestEntry::new(\"/tmp/file2\", 200, 2000, 20));\n\n        let mut files_to_check = BTreeMap::new();\n        files_to_check.insert(\"file1\".to_string(), TestEntry::new(\"/tmp/file1\", 100, 1000, 10));\n        files_to_check.insert(\"file2\".to_string(), TestEntry::new(\"/tmp/file2\", 200, 2000, 20));\n\n        let mut records_already_cached = BTreeMap::new();\n        let mut non_cached_files_to_check = BTreeMap::new();\n\n        extract_loaded_cache(&loaded_cache, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check);\n\n        assert_eq!(records_already_cached.len(), 2, \"All entries should be cached\");\n        assert_eq!(non_cached_files_to_check.len(), 0, \"No entries should be non-cached\");\n    }\n\n    #[test]\n    fn test_save_and_load_cache_by_path() {\n        setup_cache_path();\n        let temp_dir = TempDir::new().unwrap();\n        let temp_file = temp_dir.path().join(\"test_file.txt\");\n        fs::write(&temp_file, \"test content\").unwrap();\n        let metadata = fs::metadata(&temp_file).unwrap();\n\n        let mut cache_to_save = BTreeMap::new();\n        cache_to_save.insert(\n            temp_file.to_string_lossy().to_string(),\n            TestEntry::new(temp_file.to_str().unwrap(), metadata.len(), metadata.modified().unwrap().elapsed().unwrap().as_secs(), 42),\n        );\n\n        // Save cache\n        let cache_name = format!(\"test_cache_by_path_{}\", std::process::id());\n        let messages = save_cache_to_file_generalized(&cache_name, &cache_to_save, false, 0);\n        assert!(messages.warnings.is_empty(), \"Should not have warnings when saving\");\n        assert!(!messages.messages.is_empty(), \"Should have success messages when saving\");\n\n        // Load cache\n        let (load_messages, loaded_cache) = load_cache_from_file_generalized_by_path::<TestEntry>(&cache_name, false, &cache_to_save);\n        assert!(load_messages.warnings.is_empty(), \"Should not have warnings when loading\");\n        assert!(!load_messages.messages.is_empty(), \"Should have success messages when loading\");\n        assert!(loaded_cache.is_some(), \"Should load cache successfully\");\n\n        let loaded = loaded_cache.unwrap();\n        assert_eq!(loaded.len(), 1, \"Should load 1 entry\");\n        assert!(loaded.contains_key(temp_file.to_str().unwrap()), \"Should contain the test file\");\n    }\n\n    #[test]\n    fn test_save_and_load_cache_by_size() {\n        setup_cache_path();\n        let temp_dir = TempDir::new().unwrap();\n        let temp_file1 = temp_dir.path().join(\"test_file1.txt\");\n        let temp_file2 = temp_dir.path().join(\"test_file2.txt\");\n        fs::write(&temp_file1, \"test content 1\").unwrap();\n        fs::write(&temp_file2, \"test content 2\").unwrap();\n\n        let metadata1 = fs::metadata(&temp_file1).unwrap();\n        let metadata2 = fs::metadata(&temp_file2).unwrap();\n\n        let mut cache_to_save: BTreeMap<u64, Vec<TestEntry>> = BTreeMap::new();\n        cache_to_save.entry(metadata1.len()).or_default().push(TestEntry::new(\n            temp_file1.to_str().unwrap(),\n            metadata1.len(),\n            metadata1.modified().unwrap().elapsed().unwrap().as_secs(),\n            10,\n        ));\n        cache_to_save.entry(metadata2.len()).or_default().push(TestEntry::new(\n            temp_file2.to_str().unwrap(),\n            metadata2.len(),\n            metadata2.modified().unwrap().elapsed().unwrap().as_secs(),\n            20,\n        ));\n\n        // Convert to flat map for saving\n        let mut flat_cache = BTreeMap::new();\n        for entries in cache_to_save.values() {\n            for entry in entries {\n                flat_cache.insert(entry.path.to_string_lossy().to_string(), entry.clone());\n            }\n        }\n\n        // Save cache\n        let cache_name = format!(\"test_cache_by_size_{}\", std::process::id());\n        let messages = save_cache_to_file_generalized(&cache_name, &flat_cache, false, 0);\n        assert!(messages.warnings.is_empty(), \"Should not have warnings when saving\");\n\n        // Load cache\n        let (load_messages, loaded_cache) = load_cache_from_file_generalized_by_size::<TestEntry>(&cache_name, false, &cache_to_save);\n        assert!(load_messages.warnings.is_empty(), \"Should not have warnings when loading\");\n        assert!(loaded_cache.is_some(), \"Should load cache successfully\");\n\n        let loaded = loaded_cache.unwrap();\n        assert!(!loaded.is_empty(), \"Should load entries\");\n    }\n\n    #[test]\n    fn test_save_cache_with_minimum_file_size() {\n        setup_cache_path();\n        let temp_dir = TempDir::new().unwrap();\n        let temp_file = temp_dir.path().join(\"test_file.txt\");\n        fs::write(&temp_file, \"test\").unwrap();\n\n        let mut cache_to_save = BTreeMap::new();\n        cache_to_save.insert(\"small_file\".to_string(), TestEntry::new(\"/tmp/small\", 10, 1000, 1));\n        cache_to_save.insert(\"large_file\".to_string(), TestEntry::new(\"/tmp/large\", 1000, 2000, 2));\n\n        // Save cache with minimum file size of 100 bytes\n        let cache_name = format!(\"test_cache_min_size_{}\", std::process::id());\n        let messages = save_cache_to_file_generalized(&cache_name, &cache_to_save, false, 100);\n        assert!(messages.warnings.is_empty(), \"Should not have warnings\");\n\n        // Load cache - should only contain large file\n        let files_to_check = cache_to_save.clone();\n        let (_, loaded_cache) = load_cache_from_file_generalized_by_path::<TestEntry>(&cache_name, false, &files_to_check);\n\n        if let Some(loaded) = loaded_cache {\n            // Only the large file should be saved (size >= 100)\n            for (_, entry) in loaded {\n                assert!(entry.size >= 100, \"All loaded entries should be >= minimum size\");\n            }\n        }\n    }\n\n    #[test]\n    fn test_load_cache_with_outdated_entries() {\n        setup_cache_path();\n        let temp_dir = TempDir::new().unwrap();\n        let temp_file = temp_dir.path().join(\"test_file.txt\");\n        fs::write(&temp_file, \"test content\").unwrap();\n        let metadata = fs::metadata(&temp_file).unwrap();\n\n        let mut cache_to_save = BTreeMap::new();\n        cache_to_save.insert(\n            temp_file.to_string_lossy().to_string(),\n            TestEntry::new(temp_file.to_str().unwrap(), metadata.len(), metadata.modified().unwrap().elapsed().unwrap().as_secs(), 42),\n        );\n\n        // Save cache\n        let cache_name = format!(\"test_cache_outdated_{}\", std::process::id());\n        save_cache_to_file_generalized(&cache_name, &cache_to_save, false, 0);\n\n        // Modify the file\n        std::thread::sleep(std::time::Duration::from_millis(100));\n        fs::write(&temp_file, \"modified content\").unwrap();\n\n        // Create new files_to_check with updated metadata\n        let new_metadata = fs::metadata(&temp_file).unwrap();\n        let mut files_to_check = BTreeMap::new();\n        files_to_check.insert(\n            temp_file.to_string_lossy().to_string(),\n            TestEntry::new(\n                temp_file.to_str().unwrap(),\n                new_metadata.len(),\n                new_metadata.modified().unwrap().elapsed().unwrap().as_secs(),\n                42,\n            ),\n        );\n\n        // Load cache - should filter out the outdated entry\n        let (_, loaded_cache) = load_cache_from_file_generalized_by_path::<TestEntry>(&cache_name, false, &files_to_check);\n\n        if let Some(loaded) = loaded_cache {\n            // Should be empty because size/modified date changed\n            assert!(loaded.is_empty() || loaded.len() < cache_to_save.len(), \"Outdated entries should be filtered\");\n        }\n    }\n\n    #[test]\n    fn test_load_nonexistent_cache() {\n        setup_cache_path();\n        let cache_name = format!(\"nonexistent_cache_{}\", std::process::id());\n        let files_to_check: BTreeMap<String, TestEntry> = BTreeMap::new();\n\n        let (messages, loaded_cache) = load_cache_from_file_generalized_by_path::<TestEntry>(&cache_name, false, &files_to_check);\n\n        assert!(loaded_cache.is_none(), \"Should return None for nonexistent cache\");\n        assert!(messages.warnings.is_empty(), \"Should not have warnings for nonexistent cache\");\n    }\n\n    #[test]\n    fn test_save_cache_with_json() {\n        setup_cache_path();\n        let temp_dir = TempDir::new().unwrap();\n        let temp_file = temp_dir.path().join(\"test_file.txt\");\n        fs::write(&temp_file, \"test content\").unwrap();\n\n        let mut cache_to_save = BTreeMap::new();\n        cache_to_save.insert(\"test_key\".to_string(), TestEntry::new(\"/tmp/test\", 100, 1000, 42));\n\n        // Save cache with JSON enabled\n        let cache_name = format!(\"test_cache_json_{}\", std::process::id());\n        let messages = save_cache_to_file_generalized(&cache_name, &cache_to_save, true, 0);\n        assert!(messages.warnings.is_empty(), \"Should not have warnings when saving with JSON\");\n    }\n\n    #[test]\n    fn test_get_cache_size_nonexistent() {\n        let nonexistent_path = Path::new(\"/nonexistent/path/to/cache.bin\");\n        let size_str = get_cache_size(nonexistent_path);\n        assert_eq!(size_str, \"<unknown size>\", \"Should return unknown size for nonexistent file\");\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/config_cache_path.rs",
    "content": "use std::fs::{File, OpenOptions};\nuse std::path::PathBuf;\nuse std::{env, fs};\n\nuse directories_next::ProjectDirs;\nuse log::{info, warn};\nuse once_cell::sync::OnceCell;\n\nuse crate::flc;\n\nstatic CONFIG_CACHE_PATH: OnceCell<Option<ConfigCachePath>> = OnceCell::new();\n\n#[derive(Debug, Clone)]\npub struct ConfigCachePath {\n    pub config_folder: PathBuf,\n    pub cache_folder: PathBuf,\n}\n\npub fn get_config_cache_path() -> Option<ConfigCachePath> {\n    CONFIG_CACHE_PATH.get().expect(\"Cannot fail if set_config_cache_path was called before\").clone()\n}\n\n/// On Android `ProjectDirs` always returns `None` because there is no concept of a home\n/// directory accessible via standard UNIX paths.  Instead we use the app-private data\n/// directory exposed by the Android runtime through the `DATA_DIR` or `HOME` env variable.\n/// If neither variable is set, `None` is returned and the caller should handle the missing\n/// path (e.g. via the `CZKAWKA_CACHE_PATH` / `CZKAWKA_CONFIG_PATH` env overrides).\n///\n/// The base directory is expected to be set by the host application (e.g. cedinia) before\n/// calling `set_config_cache_path`, so that `czkawka_core` stays package-agnostic.\n///\n/// Android paths used:\n///   cache  – $DATA_DIR/cache/<name>\n///   config – $DATA_DIR/files/<name>\n#[cfg(target_os = \"android\")]\nfn android_default_dirs(cache_name: &str, config_name: &str) -> (Option<PathBuf>, Option<PathBuf>) {\n    let base = match env::var(\"DATA_DIR\").or_else(|_| env::var(\"HOME\")) {\n        Ok(path) => PathBuf::from(path),\n        Err(_) => return (None, None),\n    };\n\n    let cache_folder = Some(base.join(\"cache\").join(cache_name));\n    let config_folder = Some(base.join(\"files\").join(config_name));\n    (cache_folder, config_folder)\n}\n\n#[cfg(not(target_os = \"android\"))]\nfn android_default_dirs(_cache_name: &str, _config_name: &str) -> (Option<PathBuf>, Option<PathBuf>) {\n    (None, None)\n}\n\nfn resolve_folder(env_var: &str, default_folder: Option<PathBuf>, name: &'static str, warnings: &mut Vec<String>) -> Option<PathBuf> {\n    let default_folder_str = default_folder.as_ref().map_or(\"<not available>\".to_string(), |t| t.to_string_lossy().to_string());\n\n    if env_var.is_empty() {\n        default_folder\n    } else {\n        let folder_path = PathBuf::from(env_var);\n        let _ = fs::create_dir_all(&folder_path);\n        if !folder_path.exists() {\n            warnings.push(format!(\n                \"{name} folder \\\"{}\\\" does not exist, using default folder \\\"{}\\\"\",\n                folder_path.to_string_lossy(),\n                default_folder_str\n            ));\n            return default_folder;\n        }\n        if !folder_path.is_dir() {\n            warnings.push(format!(\n                \"{name} folder \\\"{}\\\" is not a directory, using default folder \\\"{}\\\"\",\n                folder_path.to_string_lossy(),\n                default_folder_str\n            ));\n            return default_folder;\n        }\n\n        match dunce::canonicalize(folder_path) {\n            Ok(t) => Some(t),\n            Err(_e) => {\n                warnings.push(format!(\n                    \"Cannot canonicalize {} folder \\\"{}\\\", using default folder \\\"{}\\\"\",\n                    name.to_ascii_lowercase(),\n                    env_var,\n                    default_folder_str\n                ));\n                default_folder\n            }\n        }\n    }\n}\n#[cfg(test)]\npub fn set_config_cache_path_test(cache_path: PathBuf, config_path: PathBuf) {\n    CONFIG_CACHE_PATH\n        .set(Some(ConfigCachePath {\n            cache_folder: cache_path,\n            config_folder: config_path,\n        }))\n        .expect(\"Cannot set config cache path\");\n}\n\npub struct ConfigCachePathSetResult {\n    pub infos: Vec<String>,\n    pub warnings: Vec<String>,\n    pub config_env_set: bool,\n    pub cache_env_set: bool,\n    pub default_cache_path_exists: bool,\n    pub default_config_path_exists: bool,\n}\n\n// This function must be executed, to not crash, when gathering config/cache path\npub fn set_config_cache_path(cache_name: &'static str, config_name: &'static str) -> ConfigCachePathSetResult {\n    // By default, such folders are used:\n    // Lin: /home/username/.config/czkawka\n    // LinFlatpak: /home/username/.var/app/com.github.qarmin.czkawka/config/czkawka\n    // Win: C:\\Users\\Username\\AppData\\Roaming\\Qarmin\\Czkawka\\config\n    // Mac: /Users/Username/Library/Application Support/pl.Qarmin.Czkawka\n\n    let mut infos = Vec::new();\n    let mut warnings = Vec::new();\n\n    let config_folder_env = env::var(\"CZKAWKA_CONFIG_PATH\").unwrap_or_default().trim().to_string();\n    let cache_folder_env = env::var(\"CZKAWKA_CACHE_PATH\").unwrap_or_default().trim().to_string();\n\n    let (android_cache_folder, android_config_folder) = android_default_dirs(cache_name, config_name);\n    let default_cache_folder = ProjectDirs::from(\"pl\", \"Qarmin\", cache_name)\n        .map(|proj_dirs| proj_dirs.cache_dir().to_path_buf())\n        .or(android_cache_folder);\n    let default_config_folder = ProjectDirs::from(\"pl\", \"Qarmin\", config_name)\n        .map(|proj_dirs| proj_dirs.config_dir().to_path_buf())\n        .or(android_config_folder);\n\n    let default_config_path_exists = default_config_folder.as_ref().is_some_and(|t| t.exists());\n    let default_cache_path_exists = default_cache_folder.as_ref().is_some_and(|t| t.exists());\n\n    let config_folder = resolve_folder(&config_folder_env, default_config_folder, \"Config\", &mut warnings);\n    let cache_folder = resolve_folder(&cache_folder_env, default_cache_folder, \"Cache\", &mut warnings);\n\n    let config_cache_path = if let (Some(config_folder), Some(cache_folder)) = (config_folder, cache_folder) {\n        infos.push(format!(\n            \"Config folder set to \\\"{}\\\" and cache folder set to \\\"{}\\\"\",\n            config_folder.to_string_lossy(),\n            cache_folder.to_string_lossy()\n        ));\n        if !config_folder.exists()\n            && let Err(e) = fs::create_dir_all(&config_folder)\n        {\n            warnings.push(flc!(\"core_cannot_create_config_folder\", folder = config_folder.to_string_lossy(), reason = e.to_string()));\n        }\n        if !cache_folder.exists()\n            && let Err(e) = fs::create_dir_all(&cache_folder)\n        {\n            warnings.push(flc!(\"core_cannot_create_cache_folder\", folder = cache_folder.to_string_lossy(), reason = e.to_string()));\n        }\n        Some(ConfigCachePath { config_folder, cache_folder })\n    } else {\n        warnings.push(flc!(\"core_cannot_set_config_cache_path\"));\n        None\n    };\n\n    CONFIG_CACHE_PATH.set(config_cache_path).expect(\"Cannot set config/cache path twice\");\n\n    ConfigCachePathSetResult {\n        infos,\n        warnings,\n        config_env_set: !config_folder_env.is_empty(),\n        cache_env_set: !cache_folder_env.is_empty(),\n        default_cache_path_exists,\n        default_config_path_exists,\n    }\n}\n\npub(crate) fn open_cache_folder(\n    cache_file_name: &str,\n    save_to_cache: bool,\n    use_json: bool,\n    warnings: &mut Vec<String>,\n) -> Option<((Option<File>, PathBuf), (Option<File>, PathBuf))> {\n    let cache_dir = get_config_cache_path()?.cache_folder;\n    let cache_file = cache_dir.join(cache_file_name);\n    let cache_file_json = cache_dir.join(cache_file_name.replace(\".bin\", \".json\"));\n\n    let mut file_handler_default = None;\n    let mut file_handler_json = None;\n\n    if save_to_cache {\n        file_handler_default = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file) {\n            Ok(t) => t,\n            Err(e) => {\n                warnings.push(flc!(\"core_cannot_create_or_open_cache_file\", file = cache_file.to_string_lossy(), reason = e.to_string()));\n                return None;\n            }\n        });\n        if use_json {\n            file_handler_json = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file_json) {\n                Ok(t) => t,\n                Err(e) => {\n                    warnings.push(flc!(\n                        \"core_cannot_create_or_open_cache_file\",\n                        file = cache_file_json.to_string_lossy(),\n                        reason = e.to_string()\n                    ));\n                    return None;\n                }\n            });\n        }\n    } else if let Ok(t) = OpenOptions::new().read(true).open(&cache_file) {\n        file_handler_default = Some(t);\n    } else if use_json {\n        file_handler_json = Some(OpenOptions::new().read(true).open(&cache_file_json).ok()?);\n    } else {\n        return None;\n    }\n    Some(((file_handler_default, cache_file), (file_handler_json, cache_file_json)))\n}\n\n// When initializing logger or settings config/cache folders, logger is not yet initialized,\n// so we need to delay them until logger is initialized\npub fn print_infos_and_warnings(infos: Vec<String>, warnings: Vec<String>) {\n    for info in infos {\n        info!(\"{info}\");\n    }\n    for warning in warnings {\n        warn!(\"{warning}\");\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/consts.rs",
    "content": "pub const DEFAULT_THREAD_SIZE: usize = 8 * 1024 * 1024; // 8 MB\npub const DEFAULT_WORKER_THREAD_SIZE: usize = 4 * 1024 * 1024; // 4 MB\npub const VIDEO_RESOLUTION_LIMIT: u32 = 16 * 1024; // Not processing is a problem, but overflows, when width * height overflows u64 in gui, so with such limit can i32 can be used safely\n\npub const RAW_IMAGE_EXTENSIONS: &[&str] = &[\n    \"ari\", \"cr3\", \"cr2\", \"crw\", \"erf\", \"raf\", \"3fr\", \"kdc\", \"dcs\", \"dcr\", \"iiq\", \"mos\", \"mef\", \"mrw\", \"nef\", \"nrw\", \"orf\", \"rw2\", \"pef\", \"srw\", \"arw\", \"srf\", \"sr2\",\n];\n#[cfg(feature = \"libavif\")]\npub const IMAGE_RS_EXTENSIONS: &[&str] = &[\n    \"jpg\", \"jpeg\", \"png\", \"bmp\", \"tiff\", \"tif\", \"tga\", \"ff\", \"jif\", \"jfi\", \"webp\", \"gif\", \"ico\", \"exr\", \"qoi\", \"jxl\", \"avif\",\n];\n#[cfg(not(feature = \"libavif\"))]\npub const IMAGE_RS_EXTENSIONS: &[&str] = &[\n    \"jpg\", \"jpeg\", \"png\", \"bmp\", \"tiff\", \"tif\", \"tga\", \"ff\", \"jif\", \"jfi\", \"webp\", \"gif\", \"ico\", \"exr\", \"qoi\", \"jxl\",\n];\n#[cfg(feature = \"libavif\")]\npub const IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS: &[&str] = &[\"jpg\", \"jpeg\", \"png\", \"tiff\", \"tif\", \"tga\", \"ff\", \"jif\", \"jfi\", \"bmp\", \"webp\", \"exr\", \"qoi\", \"jxl\", \"avif\"];\n#[cfg(not(feature = \"libavif\"))]\npub const IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS: &[&str] = &[\"jpg\", \"jpeg\", \"png\", \"tiff\", \"tif\", \"tga\", \"ff\", \"jif\", \"jfi\", \"bmp\", \"webp\", \"exr\", \"qoi\", \"jxl\"];\n#[cfg(feature = \"libavif\")]\npub const IMAGE_RS_BROKEN_FILES_EXTENSIONS: &[&str] = &[\n    \"jpg\", \"jpeg\", \"png\", \"tiff\", \"tif\", \"tga\", \"ff\", \"jif\", \"jfi\", \"gif\", \"bmp\", \"ico\", \"jfif\", \"jpe\", \"pnz\", \"dib\", \"webp\", \"exr\", \"avif\", \"jxl\",\n];\n#[cfg(not(feature = \"libavif\"))]\npub const IMAGE_RS_BROKEN_FILES_EXTENSIONS: &[&str] = &[\n    \"jpg\", \"jpeg\", \"png\", \"tiff\", \"tif\", \"tga\", \"ff\", \"jif\", \"jfi\", \"gif\", \"bmp\", \"ico\", \"jfif\", \"jpe\", \"pnz\", \"dib\", \"webp\", \"exr\", \"jxl\",\n];\npub const HEIC_EXTENSIONS: &[&str] = &[\"heif\", \"heifs\", \"heic\", \"heics\", \"avci\", \"avcs\", \"hif\"];\npub const ZIP_FILES_EXTENSIONS: &[&str] = &[\"zip\", \"jar\"];\npub const PDF_FILES_EXTENSIONS: &[&str] = &[\"pdf\"];\npub const AUDIO_FILES_EXTENSIONS: &[&str] = &[\n    \"mp3\", \"flac\", \"wav\", \"ogg\", \"m4a\", \"aac\", \"aiff\", \"pcm\", \"aif\", \"aiff\", \"aifc\", \"m3a\", \"mp2\", \"mp4a\", \"mp2a\", \"mpga\", \"wave\", \"weba\", \"wma\", \"oga\",\n];\npub const VIDEO_FILES_EXTENSIONS: &[&str] = &[\n    \"mp4\", \"m4v\", \"mkv\", \"avi\", \"mov\", \"webm\", \"flv\", \"wmv\", // Popular\n    \"mpeg\", \"mpg\", \"mp2\", \"mpe\", \"m2ts\", \"vob\", \"evo\", // MPEG / broadcast, \"ts\"\n    \"3gp\", \"3g2\", \"f4v\", \"f4p\", \"f4a\", \"f4b\", // Mobile / legacy\n    \"qt\", \"m4p\", \"mpv\", // Apple / ISO BMFF\n    \"ogv\", \"rm\", \"rmvb\", \"asf\", // Streaming / recording\n    \"dv\", \"mxf\", \"roq\", \"nsv\", \"yuv\", // Professional\n    \"y4m\", \"h264\", \"h265\", \"hevc\", \"av1\", \"vp8\", \"vp9\", // Raw / uncompressed\n    \"amv\", \"drc\", \"gifv\", \"smk\", \"bik\", // Older / games\n];\n\npub const TEXT_FILES_EXTENSIONS: &[&str] = &[\"txt\", \"md\", \"csv\", \"log\", \"ini\", \"json\", \"xml\", \"yaml\", \"yml\", \"toml\", \"doc\", \"docx\", \"rtf\", \"odt\"];\n\n// \"dng\" - is theoretically a tiff file, but little_exif have problem with saving metadata to it\npub const EXIF_FILES_EXTENSIONS: &[&str] = &[\"jpg\", \"jpeg\", \"jfif\", \"png\", \"tiff\", \"tif\", \"avif\", \"jxl\", \"webp\", \"heic\", \"heif\"];\n"
  },
  {
    "path": "czkawka_core/src/common/dir_traversal.rs",
    "content": "use std::collections::BTreeMap;\nuse std::fs;\nuse std::fs::{DirEntry, FileType, Metadata};\n#[cfg(target_family = \"unix\")]\nuse std::os::unix::fs::MetadataExt;\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::UNIX_EPOCH;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::debug;\nuse rayon::prelude::*;\n\nuse crate::common::directories::Directories;\nuse crate::common::extensions::Extensions;\nuse crate::common::items::ExcludedItems;\nuse crate::common::model::{CheckingMethod, FileEntry, ToolType};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::CommonToolData;\nuse crate::flc;\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum Collect {\n    InvalidSymlinks,\n    Files,\n}\n\n#[derive(Eq, PartialEq, Copy, Clone, Debug)]\nenum EntryType {\n    File,\n    Dir,\n    Symlink,\n    Other,\n}\n\npub struct DirTraversalBuilder<'b, F> {\n    group_by: Option<F>,\n    root_dirs: Vec<PathBuf>,\n    root_files: Vec<PathBuf>,\n    stop_flag: Option<Arc<AtomicBool>>,\n    progress_sender: Option<&'b Sender<ProgressData>>,\n    minimal_file_size: Option<u64>,\n    maximal_file_size: Option<u64>,\n    checking_method: CheckingMethod,\n    collect: Collect,\n    recursive_search: bool,\n    directories: Option<Directories>,\n    excluded_items: Option<ExcludedItems>,\n    extensions: Option<Extensions>,\n    tool_type: ToolType,\n}\n\n#[derive(Debug)]\npub struct DirTraversal<'b, F> {\n    group_by: F,\n    root_dirs: Vec<PathBuf>,\n    root_files: Vec<PathBuf>,\n    stop_flag: Arc<AtomicBool>,\n    progress_sender: Option<&'b Sender<ProgressData>>,\n    recursive_search: bool,\n    directories: Directories,\n    excluded_items: ExcludedItems,\n    extensions: Extensions,\n    minimal_file_size: u64,\n    maximal_file_size: u64,\n    checking_method: CheckingMethod,\n    tool_type: ToolType,\n    collect: Collect,\n}\n\nimpl Default for DirTraversalBuilder<'_, ()> {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl DirTraversalBuilder<'_, ()> {\n    pub fn new() -> Self {\n        DirTraversalBuilder {\n            group_by: None,\n            root_dirs: Vec::new(),\n            root_files: Vec::new(),\n            stop_flag: None,\n            progress_sender: None,\n            checking_method: CheckingMethod::None,\n            minimal_file_size: None,\n            maximal_file_size: None,\n            collect: Collect::Files,\n            recursive_search: false,\n            directories: None,\n            extensions: None,\n            excluded_items: None,\n            tool_type: ToolType::None,\n        }\n    }\n}\n\nimpl<'b, F> DirTraversalBuilder<'b, F> {\n    pub(crate) fn common_data(mut self, common_tool_data: &CommonToolData) -> Self {\n        self.root_dirs = common_tool_data.directories.included_directories.clone();\n        self.root_files = common_tool_data.directories.included_files.clone();\n        self.extensions = Some(common_tool_data.extensions.clone());\n        self.excluded_items = Some(common_tool_data.excluded_items.clone());\n        self.recursive_search = common_tool_data.recursive_search;\n        self.minimal_file_size = Some(common_tool_data.minimal_file_size);\n        self.maximal_file_size = Some(common_tool_data.maximal_file_size);\n        self.tool_type = common_tool_data.tool_type;\n        self.directories = Some(common_tool_data.directories.clone());\n        self\n    }\n\n    pub(crate) fn stop_flag(mut self, stop_flag: &Arc<AtomicBool>) -> Self {\n        self.stop_flag = Some(stop_flag.clone());\n        self\n    }\n\n    pub(crate) fn progress_sender(mut self, progress_sender: Option<&'b Sender<ProgressData>>) -> Self {\n        self.progress_sender = progress_sender;\n        self\n    }\n\n    pub(crate) fn checking_method(mut self, checking_method: CheckingMethod) -> Self {\n        self.checking_method = checking_method;\n        self\n    }\n\n    pub(crate) fn minimal_file_size(mut self, minimal_file_size: u64) -> Self {\n        self.minimal_file_size = Some(minimal_file_size);\n        self\n    }\n\n    pub(crate) fn maximal_file_size(mut self, maximal_file_size: u64) -> Self {\n        self.maximal_file_size = Some(maximal_file_size);\n        self\n    }\n\n    pub(crate) fn collect(mut self, collect: Collect) -> Self {\n        self.collect = collect;\n        self\n    }\n\n    pub(crate) fn group_by<G, T>(self, group_by: G) -> DirTraversalBuilder<'b, G>\n    where\n        G: Fn(&FileEntry) -> T,\n    {\n        DirTraversalBuilder {\n            group_by: Some(group_by),\n            root_dirs: self.root_dirs,\n            root_files: self.root_files,\n            stop_flag: self.stop_flag,\n            progress_sender: self.progress_sender,\n            directories: self.directories,\n            extensions: self.extensions,\n            excluded_items: self.excluded_items,\n            recursive_search: self.recursive_search,\n            maximal_file_size: self.maximal_file_size,\n            minimal_file_size: self.minimal_file_size,\n            collect: self.collect,\n            checking_method: self.checking_method,\n            tool_type: self.tool_type,\n        }\n    }\n\n    pub(crate) fn build(self) -> DirTraversal<'b, F> {\n        DirTraversal {\n            group_by: self.group_by.expect(\"could not build\"),\n            root_dirs: self.root_dirs,\n            root_files: self.root_files,\n            stop_flag: self.stop_flag.expect(\"Stop flag must be always initialized\"),\n            progress_sender: self.progress_sender,\n            checking_method: self.checking_method,\n            minimal_file_size: self.minimal_file_size.unwrap_or(0),\n            maximal_file_size: self.maximal_file_size.unwrap_or(u64::MAX),\n            collect: self.collect,\n            directories: self.directories.expect(\"could not build\"),\n            excluded_items: self.excluded_items.expect(\"could not build\"),\n            extensions: self.extensions.unwrap_or_default(),\n            recursive_search: self.recursive_search,\n            tool_type: self.tool_type,\n        }\n    }\n}\n\npub enum DirTraversalResult<T: Ord + PartialOrd> {\n    SuccessFiles {\n        warnings: Vec<String>,\n        grouped_file_entries: BTreeMap<T, Vec<FileEntry>>,\n    },\n    Stopped,\n}\n\nfn entry_type(file_type: FileType) -> EntryType {\n    if file_type.is_dir() {\n        EntryType::Dir\n    } else if file_type.is_symlink() {\n        EntryType::Symlink\n    } else if file_type.is_file() {\n        EntryType::File\n    } else {\n        EntryType::Other\n    }\n}\n\nimpl<F, T> DirTraversal<'_, F>\nwhere\n    F: Fn(&FileEntry) -> T,\n    T: Ord + PartialOrd,\n{\n    #[fun_time(message = \"run(collecting files/dirs)\", level = \"debug\")]\n    pub(crate) fn run(self) -> DirTraversalResult<T> {\n        assert_ne!(self.tool_type, ToolType::None, \"Tool type cannot be None\");\n\n        let mut all_warnings = Vec::new();\n        let mut grouped_file_entries: BTreeMap<T, Vec<FileEntry>> = BTreeMap::new();\n\n        // Add root folders and files for finding\n        let mut folders_to_check: Vec<PathBuf> = self.root_dirs.clone();\n        let mut files_to_check: Vec<PathBuf> = self.root_files.clone();\n\n        let progress_handler = prepare_thread_handler_common(self.progress_sender, CurrentStage::CollectingFiles, 0, (self.tool_type, self.checking_method), 0);\n\n        let DirTraversal {\n            collect,\n            directories,\n            excluded_items,\n            extensions,\n            recursive_search,\n            minimal_file_size,\n            maximal_file_size,\n            stop_flag,\n            ..\n        } = self;\n\n        let mut file_results = Vec::new();\n        // File traversal\n        while let Some(current_file) = files_to_check.pop() {\n            let Some(metadata) = common_get_metadata_from_path(&current_file, &mut all_warnings) else {\n                continue;\n            };\n            let file_type = metadata.file_type();\n            match (entry_type(file_type), collect) {\n                (EntryType::File, Collect::Files) => {\n                    progress_handler.increase_items(1);\n                    process_file_in_file_mode_path_check(\n                        &current_file,\n                        &metadata,\n                        &mut all_warnings,\n                        &mut file_results,\n                        &extensions,\n                        &excluded_items,\n                        &directories,\n                        minimal_file_size,\n                        maximal_file_size,\n                    );\n                }\n                (EntryType::File, Collect::InvalidSymlinks) => {\n                    progress_handler.increase_items(1);\n                }\n                (EntryType::Symlink, Collect::InvalidSymlinks) => {\n                    progress_handler.increase_items(1);\n                    process_symlink_in_symlink_mode_path_check(&current_file, &metadata, &mut all_warnings, &mut file_results, &extensions, &excluded_items);\n                }\n                (EntryType::Symlink | EntryType::Dir | EntryType::Other, _) => {\n                    // nothing to do\n                }\n            }\n        }\n        file_results.sort_by_cached_key(|fe| fe.path.to_string_lossy().to_string());\n        for fe in file_results {\n            let key = (self.group_by)(&fe);\n            grouped_file_entries.entry(key).or_default().push(fe);\n        }\n\n        // Folder traversal\n        while !folders_to_check.is_empty() {\n            if check_if_stop_received(&stop_flag) {\n                progress_handler.join_thread();\n                return DirTraversalResult::Stopped;\n            }\n\n            let segments: Vec<_> = folders_to_check\n                .into_par_iter()\n                .with_max_len(2) // Avoiding checking too many folders in batch\n                .map(|current_folder| {\n                    let mut dir_result = Vec::new();\n                    let mut warnings = Vec::new();\n                    let mut fe_result = Vec::new();\n\n                    let Some(read_dir) = common_read_dir(&current_folder, &mut warnings) else {\n                        return Some((dir_result, warnings, fe_result));\n                    };\n\n                    let mut counter = 0;\n                    // Check every sub folder/file/link etc.\n                    for entry in read_dir {\n                        if check_if_stop_received(&stop_flag) {\n                            return None;\n                        }\n\n                        let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, &current_folder) else {\n                            continue;\n                        };\n                        let Ok(file_type) = entry_data.file_type() else { continue };\n\n                        match (entry_type(file_type), collect) {\n                            (EntryType::Dir, Collect::Files | Collect::InvalidSymlinks) => {\n                                process_dir_in_file_symlink_mode(recursive_search, entry_data, &directories, &mut dir_result, &mut warnings, &excluded_items);\n                            }\n                            (EntryType::File, Collect::Files) => {\n                                counter += 1;\n                                process_file_in_file_mode(\n                                    entry_data,\n                                    &mut warnings,\n                                    &mut fe_result,\n                                    &extensions,\n                                    &directories,\n                                    &excluded_items,\n                                    minimal_file_size,\n                                    maximal_file_size,\n                                );\n                            }\n                            (EntryType::File, Collect::InvalidSymlinks) => {\n                                counter += 1;\n                            }\n                            (EntryType::Symlink, Collect::InvalidSymlinks) => {\n                                counter += 1;\n                                process_symlink_in_symlink_mode(entry_data, &mut warnings, &mut fe_result, &extensions, &directories, &excluded_items);\n                            }\n                            (EntryType::Symlink, Collect::Files) | (EntryType::Other, _) => {\n                                // nothing to do\n                            }\n                        }\n                    }\n                    if counter > 0 {\n                        // Increase counter in batch, because usually it may be slow to add multiple times atomic value\n                        progress_handler.increase_items(counter);\n                    }\n                    Some((dir_result, warnings, fe_result))\n                })\n                .while_some()\n                .collect();\n\n            let required_size = segments.iter().map(|(segment, _, _)| segment.len()).sum::<usize>();\n            folders_to_check = Vec::with_capacity(required_size);\n\n            // Process collected data\n            for (segment, warnings, mut fe_result) in segments {\n                folders_to_check.extend(segment);\n                all_warnings.extend(warnings);\n                fe_result.sort_by_cached_key(|fe| fe.path.to_string_lossy().to_string());\n                for fe in fe_result {\n                    let key = (self.group_by)(&fe);\n                    grouped_file_entries.entry(key).or_default().push(fe);\n                }\n            }\n        }\n\n        progress_handler.join_thread();\n\n        debug!(\"Collected {} files\", grouped_file_entries.values().map(Vec::len).sum::<usize>());\n\n        match collect {\n            Collect::Files | Collect::InvalidSymlinks => DirTraversalResult::SuccessFiles {\n                grouped_file_entries,\n                warnings: all_warnings,\n            },\n        }\n    }\n}\n\nfn process_file_in_file_mode(\n    entry_data: &DirEntry,\n    warnings: &mut Vec<String>,\n    fe_result: &mut Vec<FileEntry>,\n    extensions: &Extensions,\n    directories: &Directories,\n    excluded_items: &ExcludedItems,\n    minimal_file_size: u64,\n    maximal_file_size: u64,\n) {\n    if !extensions.check_if_entry_have_valid_extension(&entry_data.file_name()) {\n        return;\n    }\n\n    let current_file_name = entry_data.path();\n    if excluded_items.is_excluded(&current_file_name) {\n        return;\n    }\n\n    if directories.is_excluded_file(&current_file_name) {\n        return;\n    }\n\n    #[cfg(target_family = \"unix\")]\n    if directories.exclude_other_filesystems() {\n        match directories.is_on_other_filesystems(&current_file_name) {\n            Ok(true) => return,\n            Err(e) => warnings.push(e),\n            _ => (),\n        }\n    }\n\n    #[cfg(windows)]\n    let _ = directories; // Silence unused variable warning on Windows\n\n    let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &current_file_name) else {\n        return;\n    };\n\n    if (minimal_file_size..=maximal_file_size).contains(&metadata.len()) {\n        // Creating new file entry\n        let fe: FileEntry = FileEntry {\n            size: metadata.len(),\n            modified_date: get_modified_time(&metadata, warnings, &current_file_name, false),\n            path: current_file_name,\n        };\n\n        fe_result.push(fe);\n    }\n}\n// Same as above, but working with Path instead of DirEntry\n// Sadly this cannot be merged, due to a little crazy optimizations done in this functions\nfn process_file_in_file_mode_path_check(\n    path: &Path,\n    metadata: &Metadata,\n    warnings: &mut Vec<String>,\n    fe_result: &mut Vec<FileEntry>,\n    extensions: &Extensions,\n    excluded_items: &ExcludedItems,\n    directories: &Directories,\n    minimal_file_size: u64,\n    maximal_file_size: u64,\n) {\n    let Some(file_name) = path.file_name() else {\n        return;\n    };\n    if !extensions.check_if_entry_have_valid_extension(file_name) {\n        return;\n    }\n\n    if directories.is_excluded_file(path) {\n        return;\n    }\n    if directories.is_excluded_item_in_dir(path) {\n        return;\n    }\n\n    if excluded_items.is_excluded(path) {\n        return;\n    }\n\n    if (minimal_file_size..=maximal_file_size).contains(&metadata.len()) {\n        // Creating new file entry\n        let fe: FileEntry = FileEntry {\n            size: metadata.len(),\n            modified_date: get_modified_time(metadata, warnings, path, false),\n            path: path.to_path_buf(),\n        };\n\n        fe_result.push(fe);\n    }\n}\n\nfn process_dir_in_file_symlink_mode(\n    recursive_search: bool,\n    entry_data: &DirEntry,\n    directories: &Directories,\n    dir_result: &mut Vec<PathBuf>,\n    warnings: &mut Vec<String>,\n    excluded_items: &ExcludedItems,\n) {\n    if !recursive_search {\n        return;\n    }\n\n    let dir_path = entry_data.path();\n    if directories.is_excluded_dir(&dir_path) {\n        return;\n    }\n\n    if excluded_items.is_excluded(&dir_path) {\n        return;\n    }\n\n    #[cfg(target_family = \"unix\")]\n    if directories.exclude_other_filesystems() {\n        match directories.is_on_other_filesystems(&dir_path) {\n            Ok(true) => return,\n            Err(e) => warnings.push(e),\n            _ => (),\n        }\n    }\n\n    #[cfg(windows)]\n    let _ = warnings; // Silence unused variable warning on Windows\n\n    dir_result.push(dir_path);\n}\n\nfn process_symlink_in_symlink_mode(\n    entry_data: &DirEntry,\n    warnings: &mut Vec<String>,\n    fe_result: &mut Vec<FileEntry>,\n    extensions: &Extensions,\n    directories: &Directories,\n    excluded_items: &ExcludedItems,\n) {\n    if !extensions.check_if_entry_have_valid_extension(&entry_data.file_name()) {\n        return;\n    }\n\n    let current_file_name = entry_data.path();\n    if excluded_items.is_excluded(&current_file_name) {\n        return;\n    }\n\n    #[cfg(target_family = \"unix\")]\n    if directories.exclude_other_filesystems() {\n        match directories.is_on_other_filesystems(&current_file_name) {\n            Ok(true) => return,\n            Err(e) => warnings.push(e),\n            _ => (),\n        }\n    }\n\n    #[cfg(windows)]\n    let _ = directories; // Silence unused variable warning on Windows\n\n    let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &current_file_name) else {\n        return;\n    };\n\n    // Creating new file entry\n    let fe: FileEntry = FileEntry {\n        size: metadata.len(),\n        modified_date: get_modified_time(&metadata, warnings, &current_file_name, false),\n        path: current_file_name,\n    };\n\n    fe_result.push(fe);\n}\nfn process_symlink_in_symlink_mode_path_check(\n    path: &Path,\n    metadata: &Metadata,\n    warnings: &mut Vec<String>,\n    fe_result: &mut Vec<FileEntry>,\n    extensions: &Extensions,\n    excluded_items: &ExcludedItems,\n) {\n    let Some(file_name) = path.file_name() else {\n        return;\n    };\n    if !extensions.check_if_entry_have_valid_extension(file_name) {\n        return;\n    }\n\n    if excluded_items.is_excluded(path) {\n        return;\n    }\n\n    // Creating new file entry\n    let fe: FileEntry = FileEntry {\n        size: metadata.len(),\n        modified_date: get_modified_time(metadata, warnings, path, false),\n        path: path.to_path_buf(),\n    };\n\n    fe_result.push(fe);\n}\n\npub(crate) fn common_read_dir(current_folder: &Path, warnings: &mut Vec<String>) -> Option<Vec<Result<DirEntry, std::io::Error>>> {\n    match fs::read_dir(current_folder) {\n        Ok(t) => Some(t.collect()),\n        Err(e) => {\n            warnings.push(flc!(\"core_cannot_open_dir\", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string()));\n            None\n        }\n    }\n}\npub(crate) fn common_get_entry_data<'a>(entry: &'a Result<DirEntry, std::io::Error>, warnings: &mut Vec<String>, current_folder: &Path) -> Option<&'a DirEntry> {\n    let entry_data = match entry {\n        Ok(t) => t,\n        Err(e) => {\n            warnings.push(flc!(\n                \"core_cannot_read_entry_dir\",\n                dir = current_folder.to_string_lossy().to_string(),\n                reason = e.to_string()\n            ));\n            return None;\n        }\n    };\n    Some(entry_data)\n}\npub(crate) fn common_get_metadata_dir(entry_data: &DirEntry, warnings: &mut Vec<String>, current_folder: &Path) -> Option<Metadata> {\n    let metadata: Metadata = match entry_data.metadata() {\n        Ok(t) => t,\n        Err(e) => {\n            warnings.push(flc!(\n                \"core_cannot_read_metadata_dir\",\n                dir = current_folder.to_string_lossy().to_string(),\n                reason = e.to_string()\n            ));\n            return None;\n        }\n    };\n    Some(metadata)\n}\n\npub(crate) fn common_get_metadata_from_path(path: &Path, warnings: &mut Vec<String>) -> Option<Metadata> {\n    let metadata: Metadata = match fs::metadata(path) {\n        Ok(t) => t,\n        Err(e) => {\n            warnings.push(flc!(\"core_cannot_read_metadata_file\", file = path.to_string_lossy().to_string(), reason = e.to_string()));\n            return None;\n        }\n    };\n    Some(metadata)\n}\n\npub(crate) fn get_modified_time(metadata: &Metadata, warnings: &mut Vec<String>, current_file_name: &Path, is_folder: bool) -> u64 {\n    match metadata.modified() {\n        Ok(t) => match t.duration_since(UNIX_EPOCH) {\n            Ok(d) => d.as_secs(),\n            Err(_inspected) => {\n                if is_folder {\n                    warnings.push(flc!(\"core_folder_modified_before_epoch\", name = current_file_name.to_string_lossy().to_string()));\n                } else {\n                    warnings.push(flc!(\"core_file_modified_before_epoch\", name = current_file_name.to_string_lossy().to_string()));\n                }\n                0\n            }\n        },\n        Err(e) => {\n            if is_folder {\n                warnings.push(flc!(\n                    \"core_folder_no_modification_date\",\n                    name = current_file_name.to_string_lossy().to_string(),\n                    reason = e.to_string()\n                ));\n            } else {\n                warnings.push(flc!(\n                    \"core_file_no_modification_date\",\n                    name = current_file_name.to_string_lossy().to_string(),\n                    reason = e.to_string()\n                ));\n            }\n            0\n        }\n    }\n}\n\n#[cfg(target_family = \"windows\")]\npub(crate) fn inode(_fe: &FileEntry) -> Option<u64> {\n    None\n}\n\n#[cfg(target_family = \"unix\")]\npub(crate) fn inode(fe: &FileEntry) -> Option<u64> {\n    if let Ok(meta) = fs::metadata(&fe.path) { Some(meta.ino()) } else { None }\n}\n\npub(crate) fn take_1_per_inode((k, mut v): (Option<u64>, Vec<FileEntry>)) -> Vec<FileEntry> {\n    if k.is_some() {\n        v.drain(1..);\n    }\n    v\n}\n\n#[cfg(test)]\nmod tests {\n    use std::fs::File;\n    use std::io::prelude::*;\n    use std::time::{Duration, SystemTime};\n    use std::{fs, io};\n\n    use indexmap::IndexSet;\n\n    use super::*;\n    use crate::common::tool_data::*;\n\n    impl CommonData for CommonToolData {\n        type Info = ();\n        type Parameters = ();\n        fn get_information(&self) -> Self::Info {}\n        fn get_params(&self) -> Self::Parameters {}\n        fn get_cd(&self) -> &CommonToolData {\n            self\n        }\n        fn get_cd_mut(&mut self) -> &mut CommonToolData {\n            self\n        }\n        fn found_any_items(&self) -> bool {\n            false\n        }\n    }\n\n    static NOW: std::sync::LazyLock<SystemTime> = std::sync::LazyLock::new(|| SystemTime::UNIX_EPOCH + Duration::new(100, 0));\n    const CONTENT: &[u8; 1] = b\"a\";\n\n    fn normalize_path(item: &Path) -> PathBuf {\n        let canonicalized = if cfg!(windows) {\n            // Only canonicalize if it's not a network path\n            // This can be done by checking if path starts with \\\\?\\UNC\\\n            if let Ok(dir_can) = item.canonicalize()\n                && let Some(dir_can_str) = dir_can.to_string_lossy().strip_prefix(r\"\\\\?\\\")\n                && dir_can_str.chars().nth(1) == Some(':')\n            {\n                PathBuf::from(dir_can_str)\n            } else {\n                item.to_path_buf()\n            }\n        } else {\n            if let Ok(dir) = item.canonicalize() { dir } else { item.to_path_buf() }\n        };\n\n        #[cfg(target_family = \"windows\")]\n        return crate::common::normalize_windows_path(&canonicalized);\n        #[cfg(not(target_family = \"windows\"))]\n        return canonicalized;\n    }\n\n    fn create_files(dir: &Path) -> io::Result<(PathBuf, PathBuf, PathBuf)> {\n        let (src, hard, other_file) = (dir.join(\"a\"), dir.join(\"b\"), dir.join(\"c\"));\n\n        let mut file = File::create(&src)?;\n        file.write_all(CONTENT)?;\n        fs::hard_link(&src, &hard)?;\n        file.set_modified(*NOW)?;\n\n        let mut file = File::create(&other_file)?;\n        file.write_all(CONTENT)?;\n        file.set_modified(*NOW)?;\n\n        Ok((normalize_path(&src), normalize_path(&hard), normalize_path(&other_file)))\n    }\n\n    #[test]\n    fn test_traversal() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let dir_path = normalize_path(dir.path());\n        let (src, hard, other_file) = create_files(&dir_path)?;\n        let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect(\"Cannot fail calculating duration since epoch\").as_secs();\n\n        let mut common_data = CommonToolData::new(ToolType::SimilarImages);\n        common_data.directories.set_included_paths([dir.path().to_owned()].to_vec());\n        common_data.set_minimal_file_size(0);\n\n        match DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(&Arc::default())\n            .common_data(&common_data)\n            .build()\n            .run()\n        {\n            DirTraversalResult::SuccessFiles {\n                warnings: _,\n                grouped_file_entries,\n            } => {\n                let actual: IndexSet<_> = grouped_file_entries.into_values().flatten().collect();\n                assert_eq!(\n                    IndexSet::from([\n                        FileEntry {\n                            path: normalize_path(&src),\n                            size: 1,\n                            modified_date: secs,\n                        },\n                        FileEntry {\n                            path: normalize_path(&hard),\n                            size: 1,\n                            modified_date: secs,\n                        },\n                        FileEntry {\n                            path: normalize_path(&other_file),\n                            size: 1,\n                            modified_date: secs,\n                        },\n                    ]),\n                    actual\n                );\n            }\n            DirTraversalResult::Stopped => {\n                panic!(\"Expect SuccessFiles.\");\n            }\n        }\n        Ok(())\n    }\n\n    fn create_temp_structure(dir: &Path) -> io::Result<(PathBuf, PathBuf, PathBuf)> {\n        let global_file = dir.join(\"global_file.txt\");\n        let other_dir = dir.join(\"other_file\");\n        fs::create_dir_all(&other_dir)?;\n        let other_file = other_dir.join(\"other_file.txt\");\n\n        let mut f = File::create(&global_file)?;\n        f.write_all(b\"global_file\")?;\n        f.set_modified(*NOW)?;\n\n        let mut f2 = File::create(&other_file)?;\n        f2.write_all(b\"other_file\")?;\n        f2.set_modified(*NOW)?;\n\n        let global_file = normalize_path(&global_file);\n        let other_file = normalize_path(&other_file);\n        let other_dir = normalize_path(&other_dir);\n\n        Ok((global_file, other_file, other_dir))\n    }\n\n    fn run_traversal(common_data: &CommonToolData) -> Vec<FileEntry> {\n        match DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(&Arc::default())\n            .common_data(common_data)\n            .build()\n            .run()\n        {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, .. } => grouped_file_entries.into_values().flatten().collect(),\n            DirTraversalResult::Stopped => panic!(\"Expect SuccessFiles.\"),\n        }\n    }\n\n    #[test]\n    #[expect(clippy::needless_for_each)]\n    fn test_traversal_with_and_without_excluded_dir() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let dir_path = dir.path().to_path_buf();\n        let dir_path = normalize_path(&dir_path);\n        let (global_file, other_file, other_dir) = create_temp_structure(&dir_path)?;\n        let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect(\"Cannot fail calculating duration since epoch\").as_secs();\n\n        let mut common_data = CommonToolData::new(ToolType::SimilarImages);\n        common_data.directories.set_included_paths([dir.path().to_owned()].to_vec());\n        common_data.set_minimal_file_size(0);\n\n        let mut actual: Vec<_> = run_traversal(&common_data);\n        actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path));\n        assert_eq!(2, actual.len());\n        assert!(actual.contains(&FileEntry {\n            path: global_file.clone(),\n            size: 11,\n            modified_date: secs\n        }));\n        assert!(actual.contains(&FileEntry {\n            path: other_file.clone(),\n            size: 10,\n            modified_date: secs\n        }));\n\n        let mut common_data2 = CommonToolData::new(ToolType::SimilarImages);\n        common_data2.directories.set_included_paths([dir.path().to_owned()].to_vec());\n        common_data2.directories.set_excluded_paths([other_dir].to_vec());\n        common_data2.set_minimal_file_size(0);\n\n        let mut actual: Vec<_> = run_traversal(&common_data2);\n        actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path));\n        assert_eq!(1, actual.len());\n        assert!(actual.contains(&FileEntry {\n            path: global_file.clone(),\n            size: 11,\n            modified_date: secs\n        }));\n\n        let mut common_data3 = CommonToolData::new(ToolType::SimilarImages);\n        common_data3.directories.set_included_paths([dir.path().to_owned()].to_vec());\n        common_data3.directories.set_excluded_paths([other_file.clone()].to_vec());\n        common_data3.set_minimal_file_size(0);\n\n        let mut actual: Vec<_> = run_traversal(&common_data3);\n        actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path));\n        assert_eq!(1, actual.len());\n        assert!(actual.contains(&FileEntry {\n            path: global_file.clone(),\n            size: 11,\n            modified_date: secs\n        }));\n\n        let mut common_data4 = CommonToolData::new(ToolType::SimilarImages);\n        common_data4.directories.set_included_paths([global_file.clone()].to_vec());\n        common_data4.set_minimal_file_size(0);\n\n        let mut actual: Vec<_> = run_traversal(&common_data4);\n        actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path));\n        assert_eq!(1, actual.len());\n        assert!(actual.contains(&FileEntry {\n            path: global_file.clone(),\n            size: 11,\n            modified_date: secs\n        }));\n\n        let mut common_data5 = CommonToolData::new(ToolType::SimilarImages);\n        common_data5.directories.set_included_paths([global_file.clone(), other_file.clone()].to_vec());\n        common_data5.set_minimal_file_size(0);\n\n        let mut actual: Vec<_> = run_traversal(&common_data5);\n        actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path));\n        assert_eq!(2, actual.len());\n        assert!(actual.contains(&FileEntry {\n            path: global_file.clone(),\n            size: 11,\n            modified_date: secs\n        }));\n        assert!(actual.contains(&FileEntry {\n            path: other_file.clone(),\n            size: 10,\n            modified_date: secs\n        }));\n\n        // Other file should be excluded by optimizer, but it works even without it, so we can keep this test, but can be removed if it will start to fail\n        let mut common_data6 = CommonToolData::new(ToolType::SimilarImages);\n        common_data6.directories.set_included_paths([global_file.clone(), other_file.clone()].to_vec());\n        common_data6.directories.set_excluded_paths([other_file].to_vec());\n        common_data6.set_minimal_file_size(0);\n\n        let mut actual: Vec<_> = run_traversal(&common_data6);\n        actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path));\n        assert_eq!(1, actual.len());\n        assert!(actual.contains(&FileEntry {\n            path: global_file,\n            size: 11,\n            modified_date: secs\n        }));\n\n        // This test is invalid - other dir should be removed by optimizer\n        // let mut common_data7 = CommonToolData::new(ToolType::SimilarImages);\n        // common_data7.directories.set_included_paths([other_file.clone()].to_vec());\n        // common_data7.directories.set_excluded_paths([other_dir.clone()].to_vec());\n        // common_data7.set_minimal_file_size(0);\n        //\n        // let actual: IndexSet<_> = run_traversal(&common_data7).into_iter().collect();\n        // assert_eq!(0, actual.len());\n\n        Ok(())\n    }\n\n    #[cfg(target_family = \"unix\")]\n    #[test]\n    fn test_traversal_group_by_inode() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let dir_path = normalize_path(dir.path());\n        let (src, _, other) = create_files(&dir_path)?;\n        let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect(\"Cannot fail calculating duration since epoch\").as_secs();\n\n        let mut common_data = CommonToolData::new(ToolType::SimilarImages);\n        common_data.directories.set_included_paths([dir.path().to_owned()].to_vec());\n        common_data.set_minimal_file_size(0);\n\n        match DirTraversalBuilder::new()\n            .group_by(inode)\n            .stop_flag(&Arc::default())\n            .common_data(&common_data)\n            .build()\n            .run()\n        {\n            DirTraversalResult::SuccessFiles {\n                warnings: _,\n                grouped_file_entries,\n            } => {\n                let actual: IndexSet<_> = grouped_file_entries.into_iter().flat_map(take_1_per_inode).collect();\n                assert_eq!(\n                    IndexSet::from([\n                        FileEntry {\n                            path: normalize_path(&src),\n                            size: 1,\n                            modified_date: secs,\n                        },\n                        FileEntry {\n                            path: normalize_path(&other),\n                            size: 1,\n                            modified_date: secs,\n                        },\n                    ]),\n                    actual\n                );\n            }\n            DirTraversalResult::Stopped => {\n                panic!(\"Expect SuccessFiles.\");\n            }\n        }\n        Ok(())\n    }\n\n    #[cfg(target_family = \"windows\")]\n    #[test]\n    fn test_traversal_group_by_inode() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let dir_path = normalize_path(&dir.path());\n        let (src, hard, other) = create_files(&dir_path)?;\n        let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect(\"Cannot fail duration from epoch\").as_secs();\n\n        let mut common_data = CommonToolData::new(ToolType::SimilarImages);\n        common_data.directories.set_included_paths([dir_path.to_owned()].to_vec());\n        common_data.set_minimal_file_size(0);\n\n        match DirTraversalBuilder::new()\n            .group_by(inode)\n            .stop_flag(&Arc::default())\n            .common_data(&common_data)\n            .build()\n            .run()\n        {\n            DirTraversalResult::SuccessFiles {\n                warnings: _,\n                grouped_file_entries,\n            } => {\n                let actual: IndexSet<_> = grouped_file_entries.into_iter().flat_map(take_1_per_inode).collect();\n                assert_eq!(\n                    IndexSet::from([\n                        FileEntry {\n                            path: src,\n                            size: 1,\n                            modified_date: secs,\n                        },\n                        FileEntry {\n                            path: hard,\n                            size: 1,\n                            modified_date: secs,\n                        },\n                        FileEntry {\n                            path: other,\n                            size: 1,\n                            modified_date: secs,\n                        },\n                    ]),\n                    actual\n                );\n            }\n            _ => {\n                panic!(\"Expect SuccessFiles.\");\n            }\n        };\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/directories.rs",
    "content": "use std::path::{Path, PathBuf};\n#[cfg(target_family = \"unix\")]\nuse std::{fs, os::unix::fs::MetadataExt};\n\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\nuse crate::helpers::messages::Messages;\n\n#[derive(Debug, Clone, Default)]\npub struct Directories {\n    pub(crate) included_directories: Vec<PathBuf>,\n    pub(crate) excluded_directories: Vec<PathBuf>,\n    pub(crate) reference_directories: Vec<PathBuf>,\n    pub(crate) included_files: Vec<PathBuf>,\n    pub(crate) excluded_files: Vec<PathBuf>,\n    pub(crate) reference_files: Vec<PathBuf>,\n\n    pub(crate) original_included_paths: Vec<PathBuf>,\n    pub(crate) original_excluded_paths: Vec<PathBuf>,\n    pub(crate) original_reference_paths: Vec<PathBuf>,\n\n    pub(crate) exclude_other_filesystems: Option<bool>,\n    #[cfg(target_family = \"unix\")]\n    pub(crate) included_dev_ids: Vec<u64>,\n}\n\nimpl Directories {\n    pub fn new() -> Self {\n        Default::default()\n    }\n\n    pub(crate) fn set_reference_paths(&mut self, reference_paths: Vec<PathBuf>) -> Messages {\n        self.reference_files = Vec::new();\n        self.reference_directories = Vec::new();\n        self.original_reference_paths = reference_paths.clone();\n        self.process_paths(reference_paths, true, false)\n    }\n\n    pub(crate) fn set_included_paths(&mut self, included_paths: Vec<PathBuf>) -> Messages {\n        self.included_files = Vec::new();\n        self.included_directories = Vec::new();\n        self.original_included_paths = included_paths.clone();\n        self.process_paths(included_paths, false, false)\n    }\n\n    pub(crate) fn set_excluded_paths(&mut self, excluded_paths: Vec<PathBuf>) -> Messages {\n        self.excluded_files = Vec::new();\n        self.excluded_directories = Vec::new();\n        self.original_excluded_paths = excluded_paths.clone();\n        self.process_paths(excluded_paths, false, true)\n    }\n\n    fn process_paths(&mut self, paths: Vec<PathBuf>, is_reference: bool, is_excluded: bool) -> Messages {\n        let mut messages: Messages = Messages::new();\n\n        if paths.is_empty() {\n            return messages;\n        }\n\n        for path in paths {\n            if is_excluded && path.to_string_lossy() == \"/\" {\n                messages.errors.push(flc!(\"core_excluded_paths_pointless_slash\"));\n                break;\n            }\n\n            let (dir, msg) = Self::canonicalize_and_clear_path(&path, is_excluded);\n\n            messages.extend_with_another_messages(msg);\n\n            if let Some(dir) = dir {\n                #[cfg(target_family = \"windows\")]\n                let dir = crate::common::normalize_windows_path(&dir);\n\n                match (dir.is_file(), is_reference, is_excluded) {\n                    (false, true, false) => self.reference_directories.push(dir),\n                    (false, false, false) => self.included_directories.push(dir),\n                    (false, false, true) => self.excluded_directories.push(dir),\n\n                    (true, true, false) => self.reference_files.push(dir),\n                    (true, false, false) => self.included_files.push(dir),\n                    (true, false, true) => self.excluded_files.push(dir),\n                    _ => unreachable!(\"Invalid combination of parameters in process_paths\"),\n                }\n            }\n        }\n\n        messages\n    }\n\n    fn canonicalize_and_clear_path(path: &Path, is_excluded: bool) -> (Option<PathBuf>, Messages) {\n        let mut messages = Messages::new();\n        let mut path = path.to_path_buf();\n        if !path.exists() {\n            if !is_excluded {\n                messages.warnings.push(flc!(\"core_path_must_exists\", path = path.to_string_lossy().to_string()));\n            }\n            return (None, messages);\n        }\n\n        if !path.is_dir() && !path.is_file() {\n            messages.warnings.push(flc!(\"core_must_be_directory_or_file\", path = path.to_string_lossy().to_string()));\n            return (None, messages);\n        }\n\n        // Try to canonicalize them\n        if cfg!(windows) {\n            // Only canonicalize if it's not a network path\n            // This can be done by checking if path starts with \\\\?\\UNC\\\n            if let Ok(dir_can) = path.canonicalize()\n                && let Some(dir_can_str) = dir_can.to_string_lossy().strip_prefix(r\"\\\\?\\\")\n                && dir_can_str.chars().nth(1) == Some(':')\n            {\n                path = PathBuf::from(dir_can_str);\n            }\n        } else {\n            if let Ok(dir) = path.canonicalize() {\n                path = dir;\n            }\n        }\n\n        #[cfg(target_family = \"windows\")]\n        let path = crate::common::normalize_windows_path(&path);\n\n        (Some(path), messages)\n    }\n\n    #[cfg(target_family = \"unix\")]\n    pub(crate) fn set_exclude_other_filesystems(&mut self, exclude_other_filesystems: bool) {\n        self.exclude_other_filesystems = Some(exclude_other_filesystems);\n    }\n\n    pub(crate) fn optimize_directories(&mut self, recursive_search: bool, skip_exist_check: bool) -> Result<Messages, Messages> {\n        let mut messages: Messages = Messages::new();\n\n        if self.original_included_paths.is_empty() {\n            messages.critical = Some(flc!(\"core_cannot_start_scan_no_included_paths\"));\n            return Err(messages);\n        }\n\n        if self.included_directories.is_empty() && self.included_files.is_empty() {\n            messages.critical = Some(flc!(\"core_skip_exist_check_all_included_paths_nonexistent\"));\n            return Err(messages);\n        }\n\n        // Remove duplicated entries like: \"/\", \"/\"\n        for items in &mut [\n            &mut self.included_directories,\n            &mut self.excluded_directories,\n            &mut self.reference_directories,\n            &mut self.included_files,\n            &mut self.excluded_files,\n            &mut self.reference_files,\n        ] {\n            items.sort_unstable();\n            items.dedup();\n        }\n\n        // Optimize for duplicated included directories - \"/\", \"/home\". \"/home/Pulpit\" to \"/\"\n        // Do not use when not using recursive search\n        if recursive_search && !self.exclude_other_filesystems.unwrap_or(false) {\n            for kk in [&mut self.included_directories, &mut self.excluded_directories] {\n                let cloned = kk.clone();\n                kk.retain(|item| !cloned.iter().any(|other_item| item != other_item && item.starts_with(other_item)));\n            }\n        }\n\n        // Remove included directories which are inside any excluded directory\n        // Same with included files\n        for kk in [&mut self.included_directories, &mut self.included_files] {\n            kk.retain(|id| !self.excluded_directories.iter().any(|ed| id.starts_with(ed)));\n        }\n\n        // Remove included files inside included directories\n        {\n            let kk = &mut self.included_files;\n            kk.retain(|id| !self.included_directories.iter().any(|ed| id.starts_with(ed)));\n        }\n\n        // Also check if files are not excluded directly\n        {\n            let kk = &mut self.included_files;\n            kk.retain(|id| !self.excluded_directories.iter().any(|ed| id == ed));\n        }\n\n        // Remove non existed directories and files\n        if !skip_exist_check {\n            for kk in [\n                &mut self.excluded_files,\n                &mut self.excluded_directories,\n                &mut self.included_files,\n                &mut self.included_directories,\n            ] {\n                kk.retain(|path| path.exists());\n            }\n        }\n\n        // Excluded paths must are inside included path, because otherwise they are pointless\n        // So first, removing included files, that are inside excluded directories\n        // So this will allow to remove excluded directories outside included directories\n        self.included_files.retain(|ifile| !self.excluded_directories.iter().any(|ed| ifile.starts_with(ed)));\n        self.excluded_directories.retain(|ed| self.included_directories.iter().any(|id| ed.starts_with(id)));\n\n        // Selecting Reference folders\n        {\n            self.reference_directories.retain(|folder| self.included_directories.iter().any(|e| folder.starts_with(e)));\n            self.reference_files\n                .retain(|file| self.included_directories.iter().any(|e| file.starts_with(e)) || self.included_files.iter().any(|f| file == f));\n        }\n\n        // Not needed, but better is to have sorted everything\n        for items in &mut [\n            &mut self.included_directories,\n            &mut self.excluded_directories,\n            &mut self.reference_directories,\n            &mut self.included_files,\n            &mut self.excluded_files,\n            &mut self.reference_files,\n        ] {\n            items.sort_unstable();\n        }\n\n        // Get device IDs for included directories, probably ther better solution would be to get one id per directory, but this is faster, but a little less precise\n        #[cfg(target_family = \"unix\")]\n        if self.exclude_other_filesystems() {\n            for d in &self.included_directories {\n                match fs::metadata(d) {\n                    Ok(m) => self.included_dev_ids.push(m.dev()),\n                    Err(_) => messages.errors.push(flc!(\"core_paths_unable_to_get_device_id\", path = d.to_string_lossy().to_string())),\n                }\n            }\n        }\n\n        if self.included_directories.is_empty() && self.included_files.is_empty() {\n            messages.critical = Some(flc!(\"core_missing_no_chosen_included_path\"));\n            return Err(messages);\n        }\n\n        if self.reference_directories == self.included_directories && self.included_files == self.reference_files {\n            messages.critical = Some(flc!(\"core_reference_included_paths_same\"));\n            return Err(messages);\n        }\n\n        Ok(messages)\n    }\n\n    pub(crate) fn is_in_referenced_directory(&self, path: &Path) -> bool {\n        self.reference_directories.iter().any(|e| path.starts_with(e));\n        self.reference_files.iter().any(|e| e.as_path() == path);\n        self.reference_directories.iter().any(|e| path.starts_with(e)) || self.reference_files.iter().any(|e| e.as_path() == path)\n    }\n\n    pub(crate) fn is_excluded_dir(&self, path: &Path) -> bool {\n        #[cfg(target_family = \"windows\")]\n        let path = crate::common::normalize_windows_path(path);\n        // We're assuming that `excluded_directories` are already normalized\n        self.excluded_directories.iter().any(|p| p.as_path() == path)\n    }\n\n    pub(crate) fn is_excluded_file(&self, path: &Path) -> bool {\n        #[cfg(target_family = \"windows\")]\n        let path = crate::common::normalize_windows_path(path);\n        // We're assuming that `excluded_files` are already normalized\n        self.excluded_files.iter().any(|p| p.as_path() == path)\n    }\n\n    // Usually it is not required, because if main directory is excluded, then we don't run check on\n    // every single children, different situation is with excluded single file\n    pub(crate) fn is_excluded_item_in_dir(&self, path: &Path) -> bool {\n        #[cfg(target_family = \"windows\")]\n        let path = crate::common::normalize_windows_path(path);\n        #[cfg(target_family = \"windows\")]\n        let path = &path;\n        // We're assuming that `excluded_directories` are already normalized\n\n        self.excluded_directories.iter().any(|p| path.starts_with(p))\n    }\n\n    #[cfg(target_family = \"unix\")]\n    pub(crate) fn exclude_other_filesystems(&self) -> bool {\n        self.exclude_other_filesystems.unwrap_or(false)\n    }\n\n    #[cfg(target_family = \"unix\")]\n    pub(crate) fn is_on_other_filesystems<P: AsRef<Path>>(&self, path: P) -> Result<bool, String> {\n        let path = path.as_ref();\n        match fs::metadata(path) {\n            Ok(m) => {\n                if m.file_type().is_file() && !self.included_files.is_empty() && self.included_files.contains(&path.to_path_buf()) {\n                    return Ok(false); // Exact equality for included files is always allowed\n                }\n                Ok(!self.included_dev_ids.iter().any(|&id| id == m.dev()))\n            }\n            Err(_) => Err(flc!(\"core_paths_unable_to_get_device_id\", path = path.to_string_lossy().to_string())),\n        }\n    }\n\n    pub(crate) fn filter_reference_folders<T>(&self, entries_to_check: Vec<Vec<T>>) -> Vec<(T, Vec<T>)>\n    where\n        T: ResultEntry,\n    {\n        entries_to_check\n            .into_iter()\n            .filter_map(|vec_file_entry| {\n                let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry.into_iter().partition(|e| self.is_in_referenced_directory(e.get_path()));\n\n                if normal_files.is_empty() {\n                    None\n                } else {\n                    files_from_referenced_folders.pop().map(|file| (file, normal_files))\n                }\n            })\n            .collect::<Vec<(T, Vec<T>)>>()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::PathBuf;\n\n    use super::*;\n\n    #[test]\n    fn test_no_included_paths_errors() {\n        let mut d = Directories::new();\n        let msgs = d.optimize_directories(true, true).unwrap_err();\n        assert!(msgs.critical.is_some());\n    }\n\n    #[test]\n    fn test_dedup_included_directories() {\n        let p = PathBuf::from(\"/this/path/does/not/exist/dedup\");\n        let mut d = Directories::new();\n        d.included_directories.push(p.clone());\n        d.included_directories.push(p.clone());\n        d.original_included_paths.push(p.clone());\n        d.original_included_paths.push(p.clone());\n        let _msgs = d.optimize_directories(true, true).unwrap();\n        assert_eq!(d.included_directories, vec![p]);\n    }\n\n    #[test]\n    fn test_excluded_removes_included_inside() {\n        let base = PathBuf::from(\"/this/base/does/not/exist\");\n        let sub = base.join(\"sub\");\n        let mut d = Directories::new();\n        d.included_directories.push(sub.clone());\n        d.original_included_paths.push(sub);\n        d.excluded_directories.push(base);\n        let _msgs = d.optimize_directories(true, true).unwrap_err();\n        assert_eq!(d.included_directories, Vec::<PathBuf>::new());\n    }\n\n    #[test]\n    fn test_optimize_nested_included_directories_dedup() {\n        let mut d = Directories::new();\n        d.included_directories.push(PathBuf::from(\"/\"));\n        d.original_included_paths.push(PathBuf::from(\"/\"));\n        d.included_directories.push(PathBuf::from(\"/home\"));\n        d.original_included_paths.push(PathBuf::from(\"/home\"));\n        d.included_directories.push(PathBuf::from(\"/home/Pulpit\"));\n        d.original_included_paths.push(PathBuf::from(\"/home/Pulpit\"));\n\n        // use recursive_search = true and skip_exist_check = true as requested\n        let msgs = d.optimize_directories(true, true).unwrap();\n        // only root should remain after dedup\n        assert_eq!(d.included_directories, vec![PathBuf::from(\"/\")]);\n        assert!(msgs.critical.is_none());\n    }\n\n    #[test]\n    fn test_excluded_directories_pruned_to_inside_included() {\n        let mut d = Directories::new();\n        d.included_directories.push(PathBuf::from(\"/this/include\"));\n        d.original_included_paths.push(PathBuf::from(\"/this/include\"));\n        d.excluded_directories.push(PathBuf::from(\"/this/include/sub\"));\n        d.excluded_directories.push(PathBuf::from(\"/other/place\"));\n        d.original_excluded_paths.push(PathBuf::from(\"/this/include/sub\"));\n        d.original_excluded_paths.push(PathBuf::from(\"/other/place\"));\n\n        let _msgs = d.optimize_directories(true, true).unwrap();\n        assert_eq!(d.included_directories, vec![PathBuf::from(\"/this/include\")]);\n        assert_eq!(d.excluded_directories, vec![PathBuf::from(\"/this/include/sub\")]);\n    }\n\n    #[test]\n    fn test_reference_dirs_and_files_retained_correctly() {\n        let mut d = Directories::new();\n        d.included_directories.push(PathBuf::from(\"/a\"));\n        d.original_included_paths.push(PathBuf::from(\"/a\"));\n        d.included_files.push(PathBuf::from(\"/a/included_file.txt\"));\n        d.original_included_paths.push(PathBuf::from(\"/a/included_file.txt\"));\n\n        d.reference_directories.push(PathBuf::from(\"/a/sub\"));\n        d.reference_directories.push(PathBuf::from(\"/other\"));\n\n        d.reference_files.push(PathBuf::from(\"/a/included_file.txt\"));\n        d.reference_files.push(PathBuf::from(\"/other/file2.txt\"));\n\n        let _msgs = d.optimize_directories(true, true).unwrap();\n\n        assert_eq!(d.included_directories, vec![PathBuf::from(\"/a\")]);\n        assert_eq!(d.excluded_directories, Vec::<PathBuf>::new());\n        assert_eq!(d.included_files, Vec::<PathBuf>::new());\n        assert_eq!(d.excluded_files, Vec::<PathBuf>::new());\n        assert_eq!(d.reference_directories, vec![PathBuf::from(\"/a/sub\")]);\n        assert_eq!(d.reference_files, vec![PathBuf::from(\"/a/included_file.txt\")]);\n    }\n\n    #[test]\n    fn test_reference_equals_included_error() {\n        let mut d = Directories::new();\n        d.included_directories.push(PathBuf::from(\"/same\"));\n        d.reference_directories.push(PathBuf::from(\"/same\"));\n        d.included_files = Vec::new();\n        d.reference_files = Vec::new();\n\n        let msgs = d.optimize_directories(true, true).unwrap_err();\n        assert!(msgs.critical.is_some());\n    }\n\n    #[test]\n    fn test_included_files_removed_when_equal_to_excluded_directory() {\n        let mut d = Directories::new();\n        d.included_directories.push(PathBuf::from(\"/base\"));\n        d.original_included_paths.push(PathBuf::from(\"/base\"));\n        d.included_files.push(PathBuf::from(\"/base/file\"));\n        d.original_included_paths.push(PathBuf::from(\"/base/file\"));\n\n        // excluded directory equals included file path\n        d.excluded_directories.push(PathBuf::from(\"/base/file\"));\n        d.original_excluded_paths.push(PathBuf::from(\"/base/file\"));\n\n        let _msgs = d.optimize_directories(true, true).unwrap();\n        // included_files should be removed because it equals an excluded directory\n        assert!(d.included_files.is_empty());\n        // excluded_directories should be retained as it's inside included_directories\n        assert_eq!(d.excluded_directories, vec![PathBuf::from(\"/base/file\")]);\n        assert_eq!(d.included_directories, vec![PathBuf::from(\"/base\")]);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/extensions.rs",
    "content": "use std::ffi::OsStr;\n\nuse indexmap::IndexSet;\n\nuse crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_EXTENSIONS, TEXT_FILES_EXTENSIONS, VIDEO_FILES_EXTENSIONS};\nuse crate::flc;\nuse crate::helpers::messages::Messages;\n\n#[derive(Debug, Clone, Default)]\npub struct Extensions {\n    allowed_extensions_hashset: IndexSet<String>,\n    excluded_extensions_hashset: IndexSet<String>,\n}\n\nimpl Extensions {\n    pub fn new() -> Self {\n        Default::default()\n    }\n\n    pub(crate) fn filter_extensions(file_extensions: Vec<String>) -> (IndexSet<String>, Messages) {\n        let mut messages = Messages::new();\n\n        let extensions_hashset: IndexSet<String> = file_extensions\n            .into_iter()\n            .flat_map(|e| match e.trim().trim_start_matches(\".\").to_lowercase().as_str() {\n                \"image\" => IMAGE_RS_EXTENSIONS.iter().map(|s| s.to_string()).collect(),\n                \"video\" => VIDEO_FILES_EXTENSIONS.iter().map(|s| s.to_string()).collect(),\n                \"music\" => AUDIO_FILES_EXTENSIONS.iter().map(|s| s.to_string()).collect(),\n                \"text\" => TEXT_FILES_EXTENSIONS.iter().map(|s| s.to_string()).collect(),\n                _ => vec![e],\n            })\n            .filter_map(|extension| {\n                let e = extension.trim().trim_start_matches(\".\").to_lowercase();\n                if e.is_empty() {\n                    return None;\n                }\n\n                if e.contains(' ') {\n                    messages.warnings.push(flc!(\"core_invalid_extension_contains_space\", extension = extension));\n                    return None;\n                }\n                if e.contains('.') {\n                    messages.warnings.push(flc!(\"core_invalid_extension_contains_dot\", extension = extension));\n                    return None;\n                }\n                Some(e)\n            })\n            .collect();\n\n        (extensions_hashset, messages)\n    }\n\n    pub(crate) fn set_allowed_extensions(&mut self, allowed_extensions: Vec<String>) -> Messages {\n        let (extensions, messages) = Self::filter_extensions(allowed_extensions);\n\n        self.allowed_extensions_hashset = extensions;\n        messages\n    }\n\n    pub(crate) fn set_excluded_extensions(&mut self, excluded_extensions: Vec<String>) -> Messages {\n        let (extensions, messages) = Self::filter_extensions(excluded_extensions);\n\n        self.excluded_extensions_hashset = extensions;\n        messages\n    }\n\n    #[expect(clippy::string_slice)] // Valid, because we address go to dot, which is known ascii character\n    pub(crate) fn check_if_entry_have_valid_extension(&self, file_name: &OsStr) -> bool {\n        if self.allowed_extensions_hashset.is_empty() && self.excluded_extensions_hashset.is_empty() {\n            return true;\n        }\n\n        // Using entry_data.path().extension() is a lot of slower, even 5 times\n        let Some(file_name_str) = file_name.to_str() else { return false };\n        let Some(extension_idx) = file_name_str.rfind('.') else { return false };\n        let extension = &file_name_str[extension_idx + 1..];\n\n        if !self.allowed_extensions_hashset.is_empty() {\n            if extension.chars().all(|c| c.is_ascii_lowercase()) {\n                self.allowed_extensions_hashset.contains(extension)\n            } else {\n                self.allowed_extensions_hashset.contains(&extension.to_lowercase())\n            }\n        } else if extension.chars().all(|c| c.is_ascii_lowercase()) {\n            !self.excluded_extensions_hashset.contains(extension)\n        } else {\n            !self.excluded_extensions_hashset.contains(&extension.to_lowercase())\n        }\n    }\n\n    // E.g. when using similar videos, user can provide extensions like \"mp4,flv\", but if user provide \"mp4,jpg\" then\n    // it will be only \"mp4\" because \"jpg\" is not valid extension for videos\n    fn intersection_allowed_extensions(&mut self, file_extensions: &[&str]) {\n        self.allowed_extensions_hashset.retain(|ext| file_extensions.contains(&ext.as_str()));\n    }\n\n    // Tool extensions may be set by the tool itself, e.g. similar images may only use image extensions\n    pub(crate) fn set_and_validate_extensions(&mut self, tool_extensions: Option<&[&str]>) -> Result<(), String> {\n        let user_set_any_allowed_extensions = !self.allowed_extensions_hashset.is_empty();\n        let tool_have_any_extensions = tool_extensions.is_some();\n\n        // If user not set any extensions and tool not have any allowed extension, it is fine\n        if !user_set_any_allowed_extensions && !tool_have_any_extensions {\n            return Ok(());\n        }\n\n        if let Some(tool_extensions) = tool_extensions {\n            // If there is no selected allowed extensions, that means that are all allowed\n            // If there are some allowed extensions, we need to do intersection with tool extensions\n            if user_set_any_allowed_extensions {\n                self.intersection_allowed_extensions(tool_extensions);\n            } else {\n                self.allowed_extensions_hashset = tool_extensions.iter().map(|ext| ext.trim_start_matches('.').to_string()).collect();\n            }\n        }\n\n        let both_extensions = self.allowed_extensions_hashset.intersection(&self.excluded_extensions_hashset).cloned().collect::<Vec<_>>();\n        self.allowed_extensions_hashset.retain(|ext| !both_extensions.contains(ext));\n        self.excluded_extensions_hashset.retain(|ext| !both_extensions.contains(ext));\n\n        if self.allowed_extensions_hashset.is_empty() {\n            if let Some(tool_extensions) = tool_extensions {\n                Err(flc!(\"core_needs_allowed_extensions_limited_by_tool\", extensions = tool_extensions.join(\", \")))\n            } else {\n                Err(flc!(\"core_needs_allowed_extensions\"))\n            }\n        } else {\n            Ok(())\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::fs;\n\n    use super::*;\n\n    #[test]\n    fn test_filter_extensions_basic_and_replacements() {\n        // Empty string\n        let (exts, msgs) = Extensions::filter_extensions(Vec::new());\n        assert!(exts.is_empty());\n        assert!(msgs.messages.is_empty() && msgs.warnings.is_empty() && msgs.errors.is_empty());\n\n        // Basic extensions\n        let (exts, msgs) = Extensions::filter_extensions(vec![\"jpg\".to_string(), \"png\".to_string(), \"gif\".to_string()]);\n        assert_eq!(exts.len(), 3);\n        assert!(exts.contains(\"jpg\") && exts.contains(\"png\") && exts.contains(\"gif\"));\n        assert!(msgs.warnings.is_empty());\n\n        // With dots\n        let (exts, _) = Extensions::filter_extensions(vec![\".jpg\".to_string(), \".png\".to_string()]);\n        assert_eq!(exts.len(), 2);\n        assert!(exts.contains(\"jpg\") && exts.contains(\"png\"));\n\n        // IMAGE replacement\n        let (exts, _) = Extensions::filter_extensions(vec![\"IMAGE\".to_string()]);\n        assert!(exts.contains(\"jpg\") && exts.contains(\"png\") && exts.contains(\"bmp\"));\n\n        // VIDEO replacement\n        let (exts, _) = Extensions::filter_extensions(vec![\"VIDEO\".to_string()]);\n        assert!(exts.contains(\"mp4\") && exts.contains(\"mkv\") && exts.contains(\"avi\"));\n\n        // Invalid extensions with dot inside\n        let (exts, msgs) = Extensions::filter_extensions(vec![\"jpg\".to_string(), \"test.bad\".to_string(), \"png\".to_string()]);\n        assert_eq!(exts.len(), 2);\n        assert!(!exts.contains(\"test.bad\"));\n        assert!(msgs.warnings.iter().any(|w| w.contains(\"test.bad\")));\n\n        // Invalid extensions with space\n        let (exts, msgs) = Extensions::filter_extensions(vec![\"jpg\".to_string(), \"bad ext\".to_string(), \"png\".to_string()]);\n        assert!(!exts.contains(\"bad ext\"));\n        assert!(msgs.warnings.iter().any(|w| w.contains(\"bad ext\")));\n    }\n\n    #[test]\n    fn test_check_if_entry_have_valid_extension() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let file_jpg = temp_dir.path().join(\"test.jpg\");\n        let file_png = temp_dir.path().join(\"test.PNG\");\n        let file_gif = temp_dir.path().join(\"test.gif\");\n        let file_txt = temp_dir.path().join(\"test.txt\");\n        let file_no_ext = temp_dir.path().join(\"noext\");\n\n        fs::write(&file_jpg, \"test\").unwrap();\n        fs::write(&file_png, \"test\").unwrap();\n        fs::write(&file_gif, \"test\").unwrap();\n        fs::write(&file_txt, \"test\").unwrap();\n        fs::write(&file_no_ext, \"test\").unwrap();\n\n        // No extensions set - all should pass\n        let ext = Extensions::new();\n        assert!(\n            ext.check_if_entry_have_valid_extension(\n                &fs::read_dir(&temp_dir)\n                    .unwrap()\n                    .find(|e| e.as_ref().unwrap().file_name() == \"test.jpg\")\n                    .unwrap()\n                    .unwrap()\n                    .file_name()\n            )\n        );\n\n        // Allowed extensions\n        let mut ext = Extensions::new();\n        ext.set_allowed_extensions(vec![\"jpg\".to_string(), \"png\".to_string()]);\n        let entries: Vec<_> = fs::read_dir(&temp_dir).unwrap().map(|e| e.unwrap()).collect();\n        assert!(ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == \"test.jpg\").unwrap().file_name()));\n        assert!(ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == \"test.PNG\").unwrap().file_name())); // case insensitive\n        assert!(!ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == \"test.gif\").unwrap().file_name()));\n        assert!(!ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == \"noext\").unwrap().file_name()));\n\n        // Excluded extensions\n        let mut ext = Extensions::new();\n        ext.set_excluded_extensions(vec![\"txt\".to_string()]);\n        assert!(ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == \"test.jpg\").unwrap().file_name()));\n        assert!(!ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == \"test.txt\").unwrap().file_name()));\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/ffmpeg_utils.rs",
    "content": "use std::process::{Command, Stdio};\n\nuse crate::common::process_utils::disable_windows_console_window;\n\npub fn check_if_ffprobe_ffmpeg_exists() -> bool {\n    let mut ffmpeg_command = Command::new(\"ffmpeg\");\n    disable_windows_console_window(&mut ffmpeg_command);\n    let ffmpeg_ok = ffmpeg_command\n        .arg(\"-version\")\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false);\n\n    let mut ffprobe_command = Command::new(\"ffprobe\");\n    disable_windows_console_window(&mut ffprobe_command);\n    let ffprobe_ok = ffprobe_command\n        .arg(\"-version\")\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false);\n\n    ffprobe_ok && ffmpeg_ok\n}\n"
  },
  {
    "path": "czkawka_core/src/common/image.rs",
    "content": "use std::fmt::Debug;\nuse std::fs::File;\nuse std::panic;\nuse std::path::Path;\n\nuse fast_image_resize::{FilterType as FirFilterType, ResizeAlg, ResizeOptions as FirResizeOptions, Resizer};\nuse image::{DynamicImage, ImageReader};\nuse log::{error, trace};\nuse nom_exif::{ExifIter, ExifTag, MediaParser, MediaSource};\n\nuse crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS};\nuse crate::common::create_crash_message;\nuse crate::flc;\n\nconst MAXIMUM_IMAGE_PIXELS: u32 = 2_000_000_000;\n\npub fn register_image_decoding_hooks() {\n    #[cfg(feature = \"heif\")]\n    libheif_rs::integration::image::register_all_decoding_hooks();\n    jxl_oxide::integration::register_image_decoding_hook();\n}\n\n// Using this instead of image::open because image::open only reads content of files if extension matches content\n// This is not really helpful when trying to show preview of files with wrong extensions\npub(crate) fn decode_normal_image(path: &str) -> Result<DynamicImage, String> {\n    let file = File::open(path).map_err(|e| e.to_string())?;\n    let reader = ImageReader::new(std::io::BufReader::new(file)).with_guessed_format().map_err(|e| e.to_string())?;\n    let img = reader.decode().map_err(|e| e.to_string())?;\n\n    Ok(img)\n}\n\npub struct LoadedImage {\n    pub image: DynamicImage,\n    pub original_width: u32,\n    pub original_height: u32,\n}\nimpl Debug for LoadedImage {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"LoadedImage\")\n            .field(\"original_width\", &self.original_width)\n            .field(\"original_height\", &self.original_height)\n            .field(\n                \"image\",\n                &format!(\n                    \"DynamicImage of type {:?} with dimensions {}x{}\",\n                    self.image.color(),\n                    self.image.width(),\n                    self.image.height()\n                ),\n            )\n            .finish()\n    }\n}\n\npub fn get_dynamic_image_from_path(path: &str, opts: Option<ImgResizeOptions>) -> Result<LoadedImage, String> {\n    let path_lower = Path::new(path).extension().unwrap_or_default().to_string_lossy().to_lowercase();\n\n    trace!(\"decoding file \\\"{path}\\\"\");\n    let res = panic::catch_unwind(|| {\n        let img = if RAW_IMAGE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext)) {\n            get_raw_image(path).map_err(|e| flc!(\"core_image_open_failed\", path = path, reason = e))\n        } else {\n            // Heic files must be registered in image-rs\n            decode_normal_image(path).map_err(|e| flc!(\"core_image_open_failed\", path = path, reason = e))\n        }?;\n\n        if img.width() == 0 || img.height() == 0 {\n            return Err(flc!(\"core_image_zero_dimensions\", path = path));\n        }\n        if img.width() as u64 * img.height() as u64 > MAXIMUM_IMAGE_PIXELS as u64 {\n            return Err(flc!(\"core_image_too_large\", width = img.width(), height = img.height(), max = MAXIMUM_IMAGE_PIXELS));\n        }\n\n        let original_width = img.width();\n        let original_height = img.height();\n\n        if let Some(opts) = opts {\n            Ok((resize_image(img, opts), original_width, original_height))\n        } else {\n            Ok((img, original_width, original_height))\n        }\n    });\n\n    if let Ok(res) = res {\n        match res {\n            Ok((img, w, h)) => {\n                let rotation = get_rotation_from_exif(path).unwrap_or(None);\n                let img_rotated = match rotation {\n                    Some(ExifOrientation::Normal) | None => img,\n                    Some(ExifOrientation::MirrorHorizontal) => img.fliph(),\n                    Some(ExifOrientation::Rotate180) => img.rotate180(),\n                    Some(ExifOrientation::MirrorVertical) => img.flipv(),\n                    Some(ExifOrientation::MirrorHorizontalAndRotate270CW) => img.fliph().rotate270(),\n                    Some(ExifOrientation::Rotate90CW) => img.rotate90(),\n                    Some(ExifOrientation::MirrorHorizontalAndRotate90CW) => img.fliph().rotate90(),\n                    Some(ExifOrientation::Rotate270CW) => img.rotate270(),\n                };\n\n                Ok(LoadedImage {\n                    image: img_rotated,\n                    original_width: w,\n                    original_height: h,\n                })\n            }\n            Err(e) => Err(flc!(\"core_image_open_failed\", path = path, reason = e)),\n        }\n    } else {\n        let message = create_crash_message(\"Image-rs or libraw-rs or jxl-oxide\", path, \"https://github.com/image-rs/image/issues\");\n        error!(\"{message}\");\n        Err(message)\n    }\n}\n\n#[derive(Debug, Clone, Copy)]\npub struct ImgResizeOptions {\n    pub max_width: u32,\n    pub max_height: u32,\n    pub filter: FirFilterType,\n}\n\nfn resize_image(img: DynamicImage, opts: ImgResizeOptions) -> DynamicImage {\n    let orig_w = img.width();\n    let orig_h = img.height();\n\n    if orig_w <= opts.max_width && orig_h <= opts.max_height {\n        return img;\n    }\n\n    let scale = f32::min(opts.max_width as f32 / orig_w as f32, opts.max_height as f32 / orig_h as f32);\n    let new_w = ((orig_w as f32 * scale) as u32).max(1).min(img.width());\n    let new_h = ((orig_h as f32 * scale) as u32).max(1).min(img.height());\n\n    let mut dst = DynamicImage::new(new_w, new_h, img.color());\n    let fir_opts = FirResizeOptions::new().resize_alg(ResizeAlg::Interpolation(opts.filter));\n\n    match Resizer::new().resize(&img, &mut dst, Some(&fir_opts)) {\n        Ok(()) => dst,\n        Err(_) => {\n            // Fall back to the image-rs built-in resizer if fast_image_resize fails, quite unlikely\n            img.resize(new_w, new_h, image::imageops::FilterType::Lanczos3)\n        }\n    }\n}\n\n#[cfg(feature = \"libraw\")]\npub(crate) fn get_raw_image<P: AsRef<Path>>(path: P) -> Result<DynamicImage, String> {\n    let buf = std::fs::read(path.as_ref()).map_err(|e| format!(\"Error reading image: {e}\"))?;\n\n    let processor = libraw::Processor::new();\n    let processed = processor.process_8bit(&buf).map_err(|e| format!(\"Error processing RAW image: {e}\"))?;\n\n    let width = processed.width();\n    let height = processed.height();\n\n    let data = processed.to_vec();\n    let data_len = data.len();\n\n    let buffer = image::ImageBuffer::from_raw(width, height, data).ok_or(format!(\n        \"Cannot create ImageBuffer from raw image with width: {width} and height: {height} and data length: {data_len}\",\n    ))?;\n\n    Ok(DynamicImage::ImageRgb8(buffer))\n}\n\n#[cfg(not(feature = \"libraw\"))]\npub(crate) fn get_raw_image<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<DynamicImage, String> {\n    use rawler::decoders::RawDecodeParams;\n    use rawler::imgop::develop::RawDevelop;\n    use rawler::rawsource::RawSource;\n\n    let mut timer = crate::helpers::debug_timer::Timer::new(\"Rawler\");\n\n    let raw_source = RawSource::new(path.as_ref()).map_err(|err| format!(\"Failed to create RawSource from path {}: {err}\", path.as_ref().to_string_lossy()))?;\n\n    timer.checkpoint(\"Created RawSource\");\n\n    let decoder = rawler::get_decoder(&raw_source).map_err(|e| e.to_string())?;\n\n    timer.checkpoint(\"Got decoder\");\n\n    let params = RawDecodeParams::default();\n\n    // TODO - Nef currently disabled, due really bad quality of some extracted images https://github.com/dnglab/dnglab/issues/638, waiting for new release\n    if !path.as_ref().to_string_lossy().to_ascii_lowercase().ends_with(\".nef\")\n        && let Some(extracted_dynamic_image) = decoder.full_image(&raw_source, &params).ok().flatten()\n    {\n        timer.checkpoint(\"Decoded full image\");\n\n        trace!(\"{}\", timer.report(\"Everything\", false));\n\n        return Ok(extracted_dynamic_image);\n    }\n\n    let raw_image = decoder.raw_image(&raw_source, &params, false).map_err(|e| e.to_string())?;\n\n    timer.checkpoint(\"Decoded raw image\");\n\n    let developer = RawDevelop::default();\n    let developed_image = developer.develop_intermediate(&raw_image).map_err(|e| e.to_string())?;\n\n    timer.checkpoint(\"Developed raw image\");\n\n    let dynamic_image = developed_image.to_dynamic_image().ok_or(\"Failed to convert image to DynamicImage\".to_string())?;\n\n    timer.checkpoint(\"Converted to DynamicImage\");\n\n    trace!(\"{}\", timer.report(\"Everything\", false));\n\n    Ok(dynamic_image)\n}\n\npub fn check_if_can_display_image(path: &str) -> bool {\n    let Some(extension) = Path::new(path).extension() else {\n        return false;\n    };\n    let extension_str = extension.to_string_lossy().to_lowercase();\n    #[cfg(feature = \"heif\")]\n    let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat();\n\n    #[cfg(not(feature = \"heif\"))]\n    let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat();\n\n    allowed_extensions.iter().any(|ext| &extension_str == ext)\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ExifOrientation {\n    Normal,\n    MirrorHorizontal,\n    Rotate180,\n    MirrorVertical,\n    MirrorHorizontalAndRotate270CW,\n    Rotate90CW,\n    MirrorHorizontalAndRotate90CW,\n    Rotate270CW,\n}\n\npub(crate) fn get_rotation_from_exif(path: &str) -> Result<Option<ExifOrientation>, nom_exif::Error> {\n    if let Some(extension) = Path::new(path).extension()\n        && HEIC_EXTENSIONS.contains(&extension.to_string_lossy().to_lowercase().as_str())\n    {\n        return Ok(None); // libheif already applies orientation\n    }\n\n    let res = panic::catch_unwind(|| {\n        let mut parser = MediaParser::new();\n        let ms = MediaSource::file_path(path)?;\n        if !ms.has_exif() {\n            return Ok(None);\n        }\n        let exif_iter: ExifIter = parser.parse(ms)?;\n        for exif_entry in exif_iter {\n            if exif_entry.tag() == Some(ExifTag::Orientation)\n                && let Some(value) = exif_entry.get_value()\n            {\n                return match value.to_string().as_str() {\n                    \"1\" => Ok(Some(ExifOrientation::Normal)),\n                    \"2\" => Ok(Some(ExifOrientation::MirrorHorizontal)),\n                    \"3\" => Ok(Some(ExifOrientation::Rotate180)),\n                    \"4\" => Ok(Some(ExifOrientation::MirrorVertical)),\n                    \"5\" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate270CW)),\n                    \"6\" => Ok(Some(ExifOrientation::Rotate90CW)),\n                    \"7\" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate90CW)),\n                    \"8\" => Ok(Some(ExifOrientation::Rotate270CW)),\n                    _ => Ok(None),\n                };\n            }\n        }\n        Ok(None)\n    });\n\n    res.unwrap_or_else(|_| {\n        let message = create_crash_message(\"nom-exif\", path, \"https://github.com/mindeng/nom-exif\");\n        error!(\"{message}\");\n        Err(nom_exif::Error::IOError(std::io::Error::other(\"Panic in get_rotation_from_exif\")))\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    const TEST_NORMAL_IMAGE: &str = \"test_resources/images/normal.jpg\";\n    const TEST_ROTATED_IMAGE: &str = \"test_resources/images/rotated.jpg\";\n\n    #[test]\n    fn test_image_loading_and_exif_rotation() {\n        let normal_img = get_dynamic_image_from_path(TEST_NORMAL_IMAGE, None).unwrap().image;\n        let rotated_img = get_dynamic_image_from_path(TEST_ROTATED_IMAGE, None).unwrap().image;\n\n        assert!(normal_img.width() > 0 && normal_img.height() > 0);\n        assert!(rotated_img.width() > 0 && rotated_img.height() > 0);\n\n        let normal_exif = get_rotation_from_exif(TEST_NORMAL_IMAGE).ok();\n        let rotated_exif = get_rotation_from_exif(TEST_ROTATED_IMAGE).ok();\n\n        if let Some(normal_orientation) = normal_exif {\n            assert!(normal_orientation == Some(ExifOrientation::Normal) || normal_orientation.is_none());\n        }\n\n        if let Some(rotated_orientation) = rotated_exif\n            && rotated_orientation.is_some()\n        {\n            let raw_rotated = decode_normal_image(TEST_ROTATED_IMAGE).unwrap();\n            if rotated_orientation == Some(ExifOrientation::Rotate90CW) || rotated_orientation == Some(ExifOrientation::Rotate270CW) {\n                assert_eq!(rotated_img.width(), raw_rotated.height());\n                assert_eq!(rotated_img.height(), raw_rotated.width());\n            }\n        }\n    }\n\n    #[test]\n    fn test_check_if_can_display_image() {\n        assert!(check_if_can_display_image(\"test.jpg\"));\n        assert!(check_if_can_display_image(\"test.png\"));\n        assert!(check_if_can_display_image(\"test.webp\"));\n        assert!(check_if_can_display_image(\"test.jxl\"));\n        assert!(check_if_can_display_image(\"test.cr2\"));\n        assert!(check_if_can_display_image(\"test.JPG\"));\n\n        assert!(!check_if_can_display_image(\"test.txt\"));\n        assert!(!check_if_can_display_image(\"test.mp4\"));\n        assert!(!check_if_can_display_image(\"test\"));\n    }\n\n    #[test]\n    fn test_error_handling() {\n        get_dynamic_image_from_path(\"nonexistent.jpg\", None).unwrap_err();\n        decode_normal_image(\"nonexistent.jpg\").unwrap_err();\n        get_rotation_from_exif(\"nonexistent.jpg\").unwrap_err();\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/items.rs",
    "content": "use std::path::Path;\n\nuse crate::common::regex_check;\nuse crate::helpers::messages::Messages;\n\n#[cfg(target_family = \"unix\")]\npub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[\"/proc\", \"/dev\", \"/sys\", \"/snap\"];\n#[cfg(not(target_family = \"unix\"))]\npub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[\"C:\\\\Windows\"];\n\n#[cfg(all(target_family = \"unix\", target_os = \"macos\"))]\npub const DEFAULT_EXCLUDED_ITEMS: &str = \"*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,/Users/*/Library/Caches/*\";\n\n#[cfg(all(target_family = \"unix\", not(target_os = \"macos\")))]\npub const DEFAULT_EXCLUDED_ITEMS: &str = \"*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,*/snap/*,/home/*/.cache/*,/home/*/.var/app/,/home/*/.*\";\n\n#[cfg(not(target_family = \"unix\"))]\npub const DEFAULT_EXCLUDED_ITEMS: &str = \"*\\\\.git\\\\*,*\\\\node_modules\\\\*,*\\\\lost+found\\\\*,*:\\\\windows\\\\*,*:\\\\$RECYCLE.BIN\\\\*,*:\\\\$SysReset\\\\*,*:\\\\System Volume Information\\\\*,*:\\\\OneDriveTemp\\\\*,*:\\\\hiberfil.sys,*:\\\\pagefile.sys,*:\\\\swapfile.sys,*:\\\\Users\\\\*\\\\AppData\";\n\n#[derive(Debug, Clone, Default)]\npub struct ExcludedItems {\n    expressions: Vec<String>,\n    connected_expressions: Vec<SingleExcludedItem>,\n}\n\n#[derive(Debug, Clone, Default)]\npub struct SingleExcludedItem {\n    pub expression: String,\n    pub expression_splits: Vec<String>,\n    pub unique_extensions_splits: Vec<String>,\n}\n\nimpl ExcludedItems {\n    pub fn new() -> Self {\n        Default::default()\n    }\n\n    pub fn new_from(excluded_items: Vec<String>) -> Self {\n        let mut s = Self::new();\n        s.set_excluded_items(excluded_items);\n        s\n    }\n\n    pub(crate) fn set_excluded_items(&mut self, excluded_items: Vec<String>) -> Messages {\n        let mut warnings: Vec<String> = Vec::new();\n        if excluded_items.is_empty() {\n            return Messages::new();\n        }\n\n        let expressions: Vec<String> = excluded_items;\n        let mut checked_expressions: Vec<String> = Vec::new();\n\n        for expression in expressions {\n            let expression: String = expression.trim().to_string();\n\n            if expression.is_empty() {\n                continue;\n            }\n\n            #[cfg(target_family = \"windows\")]\n            let expression = expression.replace(\"/\", \"\\\\\");\n\n            if expression == \"DEFAULT\" {\n                checked_expressions.push(DEFAULT_EXCLUDED_ITEMS.to_string());\n                continue;\n            }\n            if !expression.contains('*') {\n                warnings.push(\"Excluded Items Warning: Wildcard * is required in expression, ignoring \".to_string() + expression.as_str());\n                continue;\n            }\n\n            checked_expressions.push(expression);\n        }\n\n        for checked_expression in &checked_expressions {\n            let item = new_excluded_item(checked_expression);\n            self.expressions.push(item.expression.clone());\n            self.connected_expressions.push(item);\n        }\n        Messages {\n            critical: None,\n            messages: Vec::new(),\n            warnings,\n            errors: Vec::new(),\n        }\n    }\n\n    pub(crate) fn get_excluded_items(&self) -> &Vec<String> {\n        &self.expressions\n    }\n    pub(crate) fn is_excluded(&self, path: &Path) -> bool {\n        if self.connected_expressions.is_empty() {\n            return false;\n        }\n        #[cfg(target_family = \"windows\")]\n        let path = crate::common::normalize_windows_path(path);\n\n        let path_str = path.to_string_lossy();\n\n        for expression in &self.connected_expressions {\n            if regex_check(expression, &path_str) {\n                return true;\n            }\n        }\n        false\n    }\n}\n\npub fn new_excluded_item(expression: &str) -> SingleExcludedItem {\n    let expression = expression.trim().to_string();\n    let expression_splits: Vec<String> = expression.split('*').filter_map(|e| if e.is_empty() { None } else { Some(e.to_string()) }).collect();\n    let mut unique_extensions_splits = expression_splits.clone();\n    unique_extensions_splits.sort();\n    unique_extensions_splits.dedup();\n    unique_extensions_splits.sort_by_key(|b| std::cmp::Reverse(b.len()));\n    SingleExcludedItem {\n        expression,\n        expression_splits,\n        unique_extensions_splits,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_excluded_items_new_and_basic_operations() {\n        let items = ExcludedItems::new();\n        assert!(items.expressions.is_empty());\n        assert!(items.connected_expressions.is_empty());\n\n        let items = ExcludedItems::new_from(vec![\"*/.git/*\".to_string(), \"*/node_modules/*\".to_string()]);\n        assert_eq!(items.expressions.len(), 2);\n        assert_eq!(items.get_excluded_items().len(), 2);\n    }\n\n    #[test]\n    fn test_set_excluded_items_with_default() {\n        let mut items = ExcludedItems::new();\n        let msgs = items.set_excluded_items(vec![\"DEFAULT\".to_string()]);\n        assert!(msgs.warnings.is_empty());\n        assert_eq!(items.expressions.len(), 1);\n        assert!(items.expressions[0].contains(\".git\") || items.expressions[0].contains(\"node_modules\"));\n    }\n\n    #[test]\n    fn test_set_excluded_items_warnings() {\n        let mut items = ExcludedItems::new();\n        let msgs = items.set_excluded_items(vec![\"no_wildcard\".to_string(), \"  \".to_string()]);\n        assert_eq!(msgs.warnings.len(), 1);\n        assert!(msgs.warnings[0].contains(\"Wildcard * is required\"));\n        assert!(items.expressions.is_empty());\n    }\n\n    #[test]\n    fn test_is_excluded() {\n        let mut items = ExcludedItems::new();\n        items.set_excluded_items(vec![\"*/.git/*\".to_string(), \"*/node_modules/*\".to_string(), \"/home/*/.*\".to_string()]);\n\n        assert!(items.is_excluded(Path::new(\"/home/user/.git/config\")));\n        assert!(items.is_excluded(Path::new(\"/home/user/.abscd/config\")));\n        assert!(items.is_excluded(Path::new(\"/project/node_modules/package.json\")));\n        assert!(!items.is_excluded(Path::new(\"/home/user/file.txt\")));\n\n        // Empty items - nothing excluded\n        let items_empty = ExcludedItems::new();\n        assert!(!items_empty.is_excluded(Path::new(\"/any/path\")));\n    }\n\n    #[test]\n    fn test_new_excluded_item() {\n        let item = new_excluded_item(\"  */test/*.txt  \");\n        assert_eq!(item.expression, \"*/test/*.txt\");\n        assert_eq!(item.expression_splits, vec![\"/test/\", \".txt\"]);\n        assert_eq!(item.unique_extensions_splits.len(), 2);\n\n        let item2 = new_excluded_item(\"*abc*def*abc*\");\n        assert_eq!(item2.expression_splits, vec![\"abc\", \"def\", \"abc\"]);\n        // unique_extensions_splits should be deduplicated and sorted by length\n        assert_eq!(item2.unique_extensions_splits, vec![\"abc\", \"def\"]);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/logger.rs",
    "content": "use std::env;\n\nuse file_rotate::compression::Compression;\nuse file_rotate::suffix::{AppendTimestamp, FileLimit};\nuse file_rotate::{ContentLimit, FileRotate};\nuse handsome_logger::{ColorChoice, CombinedLogger, ConfigBuilder, FormatText, SharedLogger, TermLogger, TerminalMode, TimeFormat, WriteLogger};\nuse log::{LevelFilter, Record, info, warn};\n\nuse crate::CZKAWKA_VERSION;\nuse crate::common::config_cache_path::get_config_cache_path;\nuse crate::common::get_all_available_threads;\n\npub fn setup_logger(disabled_terminal_printing: bool, app_name: &str, filtering_messages_func: fn(&Record) -> bool) {\n    log_panics::init();\n\n    let terminal_log_level = if disabled_terminal_printing && ![Ok(\"1\"), Ok(\"true\")].contains(&env::var(\"ENABLE_TERMINAL_LOGS_IN_CLI\").as_deref()) {\n        LevelFilter::Off\n    } else {\n        LevelFilter::Info\n    };\n    let file_log_level = LevelFilter::Debug;\n\n    let term_config = ConfigBuilder::default()\n        .set_level(terminal_log_level)\n        .set_message_filtering(Some(filtering_messages_func))\n        .build();\n    let file_config = ConfigBuilder::default()\n        .set_level(file_log_level)\n        .set_write_once(true)\n        .set_message_filtering(Some(filtering_messages_func))\n        .set_time_format(TimeFormat::DateTimeWithMicro, None)\n        .set_format_text(FormatText::DefaultWithThreadFile.get(), None)\n        .build();\n\n    let combined_logger = (|| {\n        let Some(config_cache_path) = get_config_cache_path() else {\n            // println!(\"No config cache path configured, using default config folder\");\n            return None;\n        };\n\n        let cache_logs_path = config_cache_path.cache_folder.join(format!(\"{app_name}.log\"));\n\n        let write_rotater = FileRotate::new(\n            &cache_logs_path,\n            AppendTimestamp::default(FileLimit::MaxFiles(3)),\n            ContentLimit::BytesSurpassed(100 * 1024 * 1024),\n            Compression::None,\n            None,\n        );\n\n        let combined_logs: Vec<Box<dyn SharedLogger>> = if [Ok(\"1\"), Ok(\"true\")].contains(&env::var(\"DISABLE_FILE_LOGGING\").as_deref()) {\n            vec![TermLogger::new_from_config(term_config.clone())]\n        } else {\n            vec![TermLogger::new_from_config(term_config.clone()), WriteLogger::new(file_config, write_rotater)]\n        };\n\n        CombinedLogger::init(combined_logs).ok().inspect(|()| {\n            info!(\"Logging to file \\\"{}\\\" and terminal\", cache_logs_path.to_string_lossy());\n        })\n    })();\n\n    if combined_logger.is_none() {\n        let _ = TermLogger::init(term_config, TerminalMode::Mixed, ColorChoice::Always);\n        info!(\"Logging to terminal only, file logging is disabled\");\n    }\n}\n\npub fn filtering_messages(record: &Record) -> bool {\n    if let Some(module_path) = record.module_path() {\n        // Printing not supported modules\n        // if ![\"krokiet\", \"czkawka\", \"log_panics\", \"smithay_client_toolkit\", \"sctk_adwaita\"]\n        //     .iter()\n        //     .any(|t| module_path.starts_with(t))\n        // {\n        //     println!(\"{:?}\", module_path);\n        //     return true;\n        // } else {\n        //     return false;\n        // }\n\n        [\"krokiet\", \"czkawka\", \"log_panics\", \"cedinia\"].iter().any(|t| module_path.starts_with(t))\n    } else {\n        true\n    }\n}\n\n#[allow(clippy::allow_attributes)]\n#[allow(unfulfilled_lint_expectations)] // Happens only on release build\n#[expect(clippy::vec_init_then_push)]\n#[expect(unused_mut)]\npub fn print_version_mode(app: &str) {\n    let rust_version = env!(\"RUST_VERSION_INTERNAL\");\n    let debug_release = if cfg!(debug_assertions) { \"debug\" } else { \"release\" };\n\n    let processors = get_all_available_threads();\n\n    let info = os_info::get();\n\n    let mut features: Vec<&str> = Vec::new();\n    #[cfg(feature = \"heif\")]\n    features.push(\"heif\");\n    #[cfg(feature = \"libavif\")]\n    features.push(\"libavif\");\n    #[cfg(feature = \"libraw\")]\n    features.push(\"libraw\");\n\n    let mut app_cpu_version = \"Baseline\";\n    let mut os_cpu_version = \"Baseline\";\n    if cfg!(target_feature = \"sse2\") {\n        app_cpu_version = \"x86-64-v1 (SSE2)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"sse2\") {\n        os_cpu_version = \"x86-64-v1 (SSE2)\";\n    }\n\n    if cfg!(target_feature = \"popcnt\") {\n        app_cpu_version = \"x86-64-v2 (SSE4.2 + POPCNT)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"popcnt\") {\n        os_cpu_version = \"x86-64-v2 (SSE4.2 + POPCNT)\";\n    }\n\n    if cfg!(target_feature = \"avx2\") {\n        app_cpu_version = \"x86-64-v3 (AVX2)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"avx2\") {\n        os_cpu_version = \"x86-64-v3 (AVX2)\";\n    }\n\n    if cfg!(target_feature = \"avx512f\") {\n        app_cpu_version = \"x86-64-v4 (AVX-512)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"avx512f\") {\n        os_cpu_version = \"x86-64-v4 (AVX-512)\";\n    }\n\n    let musl_or_glibc = if cfg!(target_os = \"linux\") {\n        let libc_versions_str = match glibc_musl_version::get_os_libc_versions() {\n            Ok(libc_versions) => {\n                let libc_versions_str = libc_versions.to_string();\n\n                match option_env!(\"CZKAWKA_LIBC_VERSIONS\") {\n                    Some(env) if env == libc_versions_str => {\n                        format!(\" [build + runtime ({libc_versions_str})]\")\n                    }\n                    Some(env) => {\n                        format!(\" [build ({env}), runtime ({libc_versions_str})]\")\n                    }\n                    None => {\n                        format!(\" [build (unknown), runtime ({libc_versions_str})]\")\n                    }\n                }\n            }\n            Err(e) => {\n                warn!(\"Cannot get libc version: {e}\");\n                \"\".to_string()\n            }\n        };\n        format!(\", libc {}{libc_versions_str}\", option_env!(\"CZKAWKA_LIBC\").unwrap_or(\"unknown(cross-compilation?)\"))\n    } else {\n        \"\".to_string()\n    };\n\n    let git_commit = env!(\"CZKAWKA_GIT_COMMIT_SHORT\");\n    let official_build = if env!(\"CZKAWKA_OFFICIAL_BUILD\") == \"1\" {\n        \"O\" // Official build\n    } else {\n        \"U\" // Unofficial build\n    };\n    let git_date = env!(\"CZKAWKA_GIT_COMMIT_DATE\");\n\n    info!(\n        \"{app} version: {CZKAWKA_VERSION}({git_commit} {official_build} {git_date}), {debug_release} mode, rust {rust_version}, os {} {} ({} {}), {processors} cpu/threads, features({}): [{}], app cpu version: {app_cpu_version}, os cpu version: {os_cpu_version}{musl_or_glibc}\",\n        info.os_type(),\n        info.version(),\n        env::consts::ARCH,\n        info.bitness(),\n        features.len(),\n        features.join(\", \"),\n    );\n    if cfg!(debug_assertions) {\n        warn!(\"You are running debug version of app which is a lot of slower than release version.\");\n    }\n\n    if option_env!(\"USING_CRANELIFT\").is_some() {\n        warn!(\"You are running app with cranelift which is intended only for fast compilation, not runtime performance.\");\n    }\n\n    if cfg!(panic = \"abort\") {\n        warn!(\"You are running app compiled with panic='abort', which may cause panics when processing untrusted data.\");\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/mod.rs",
    "content": "pub mod basic_gui_cli;\npub mod cache;\npub mod config_cache_path;\npub mod consts;\npub mod dir_traversal;\npub mod directories;\npub mod extensions;\npub mod ffmpeg_utils;\npub mod image;\npub mod items;\npub mod logger;\npub mod model;\npub mod process_utils;\npub mod progress_data;\npub mod progress_stop_handler;\npub mod tool_data;\npub mod traits;\npub mod video_utils;\n\nuse std::cmp::Ordering;\nuse std::ffi::OsString;\nuse std::io::Error;\nuse std::path::{Path, PathBuf};\nuse std::sync::Mutex;\nuse std::time::Duration;\nuse std::{fs, io, thread};\n\nuse items::SingleExcludedItem;\nuse log::debug;\n\nuse crate::common::consts::DEFAULT_WORKER_THREAD_SIZE;\nuse crate::flc;\n\nstatic NUMBER_OF_THREADS: std::sync::LazyLock<Mutex<Option<usize>>> = std::sync::LazyLock::new(|| Mutex::new(None));\nstatic ALL_AVAILABLE_THREADS: std::sync::LazyLock<Mutex<Option<usize>>> = std::sync::LazyLock::new(|| Mutex::new(None));\n\nconst MAX_SYMLINK_HARDLINK_ATTEMPTS: u8 = 5;\n\n#[cfg(all(feature = \"xdg_portal_trash\", target_os = \"linux\"))]\nthread_local! {\n    static TOKIO_RT: std::cell::RefCell<Option<Result<tokio::runtime::Runtime, String>>> = const { std::cell::RefCell::new(None) };\n}\n\n#[cfg(all(feature = \"xdg_portal_trash\", target_os = \"linux\"))]\nfn with_runtime<F, R>(f: F) -> Result<R, String>\nwhere\n    F: FnOnce(&tokio::runtime::Runtime) -> Result<R, String>,\n{\n    TOKIO_RT.with(|cell| {\n        let mut opt = cell.borrow_mut();\n\n        if opt.is_none() {\n            let rt = tokio::runtime::Builder::new_current_thread()\n                .enable_all()\n                .build()\n                .map_err(|e| format!(\"Failed to build Tokio runtime: {e}\"));\n\n            *opt = Some(rt);\n        }\n\n        match opt.as_ref().expect(\"Tokio runtime is initialized before\") {\n            Ok(rt) => f(rt),\n            Err(e) => Err(e.clone()),\n        }\n    })\n}\n\npub fn get_number_of_threads() -> usize {\n    let data = NUMBER_OF_THREADS.lock().expect(\"Cannot fail\").expect(\"Should be set before get\");\n    if data >= 1 { data } else { get_all_available_threads() }\n}\n\npub fn get_all_available_threads() -> usize {\n    let mut available_threads = ALL_AVAILABLE_THREADS.lock().expect(\"Cannot fail\");\n\n    if let Some(available_threads) = *available_threads {\n        available_threads\n    } else {\n        let threads = thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1);\n        *available_threads = Some(threads);\n        threads\n    }\n}\n\npub fn set_number_of_threads(thread_number: usize) {\n    *NUMBER_OF_THREADS.lock().expect(\"Cannot fail\") = Some(thread_number);\n\n    let additional_message = if thread_number == 0 {\n        format!(\n            \" (0 - means that all available threads will be used({}))\",\n            thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1)\n        )\n    } else {\n        \"\".to_string()\n    };\n    debug!(\"Number of threads set to {thread_number}{additional_message}\");\n\n    rayon::ThreadPoolBuilder::new()\n        .num_threads(get_number_of_threads())\n        .stack_size(DEFAULT_WORKER_THREAD_SIZE)\n        .build_global()\n        .expect(\"Cannot set number of threads\");\n}\n\npub fn check_if_folder_contains_only_empty_folders<P: AsRef<Path>>(path: P) -> Result<(), String> {\n    let path = path.as_ref();\n    if !path.is_dir() {\n        return Err(flc!(\"core_not_directory_remove\", path = path.to_string_lossy()));\n    }\n\n    let mut entries_to_check = Vec::new();\n    let Ok(initial_entry) = path.read_dir() else {\n        return Err(flc!(\"core_cannot_read_directory\", path = path.to_string_lossy()));\n    };\n    for entry in initial_entry {\n        if let Ok(entry) = entry {\n            entries_to_check.push(entry);\n        } else {\n            return Err(flc!(\"core_cannot_read_entry_from_directory\", path = path.to_string_lossy()));\n        }\n    }\n    loop {\n        let Some(entry) = entries_to_check.pop() else {\n            break;\n        };\n        let Some(file_type) = entry.file_type().ok() else {\n            return Err(flc!(\n                \"core_unknown_directory_entry\",\n                entry = entry.path().to_string_lossy().to_string(),\n                path = path.to_string_lossy()\n            ));\n        };\n\n        if !file_type.is_dir() {\n            return Err(flc!(\n                \"core_folder_contains_file_inside\",\n                entry = entry.path().to_string_lossy().to_string(),\n                folder = path.to_string_lossy()\n            ));\n        }\n        let Ok(internal_read_dir) = entry.path().read_dir() else {\n            return Err(flc!(\"core_cannot_read_directory\", path = path.to_string_lossy().to_string()));\n        };\n        for internal_elements in internal_read_dir {\n            if let Ok(internal_element) = internal_elements {\n                entries_to_check.push(internal_element);\n            } else {\n                return Err(flc!(\"core_cannot_read_entry_from_directory\", path = path.to_string_lossy().to_string()));\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// A wrapper around `trash::delete`. Note that for platforms that do not have native trash support\n/// (Android, iOS), this function will always return an [`Error`]. When the `xdg_portal_trash` feature is\n/// enabled, the portal-based implementation will only be used on Linux; on other desktop OSes the\n/// regular `trash::delete` fallback will be used instead.\nfn trash_delete<P: AsRef<Path>>(path: P) -> Result<(), String> {\n    let path = path.as_ref();\n\n    #[cfg(not(any(target_os = \"android\", target_os = \"ios\", all(feature = \"xdg_portal_trash\", target_os = \"linux\"))))]\n    {\n        trash::delete(path).map_err(|err| err.to_string())\n    }\n\n    #[cfg(all(feature = \"xdg_portal_trash\", target_os = \"linux\"))]\n    {\n        use std::os::fd::AsFd;\n        let file = std::fs::OpenOptions::new().write(true).read(true).open(path).map_err(|err| err.to_string())?;\n\n        with_runtime(|rt| rt.block_on(async move { ashpd::desktop::trash::trash_file(&file.as_fd()).await.map_err(|e| e.to_string()) }))?;\n\n        Ok(())\n    }\n\n    #[cfg(any(target_os = \"android\", target_os = \"ios\"))]\n    {\n        let _path = path;\n        Err(\"trash is not supported on this platform\".to_string())\n    }\n}\n\n/// Remove the folder if it only contains empty folders/is empty. If `remove_to_trash` is set, the folder\n/// will instead be sent to the system's recycle bin/trash equivalent rather than being deleted.\n///\n/// Note: if used on Android or iOS platforms, ensure `remove_to_trash` is false, as trash is not supported\n/// and will always return an [`Error`].\npub fn remove_folder_if_contains_only_empty_folders<P: AsRef<Path>>(path: P, remove_to_trash: bool) -> Result<(), String> {\n    check_if_folder_contains_only_empty_folders(&path)?;\n\n    let path = path.as_ref();\n\n    if remove_to_trash {\n        trash_delete(path).map_err(|e| format!(\"Cannot move folder \\\"{}\\\" to trash, reason {e}\", path.to_string_lossy()))\n    } else {\n        fs::remove_dir_all(path).map_err(|e| format!(\"Cannot remove directory \\\"{}\\\", reason {e}\", path.to_string_lossy()))\n    }\n}\n\n/// Remove a single file. If `remove_to_trash` is set, the folder will instead be sent to the system's\n/// recycle bin/trash equivalent rather than being deleted.\n///\n/// Note: if used on Android or iOS platforms, ensure `remove_to_trash` is false, as trash is not supported\n/// and will always return an [`Error`].\npub fn remove_single_file<P: AsRef<Path>>(full_path: P, remove_to_trash: bool) -> Result<(), String> {\n    if remove_to_trash {\n        if let Err(e) = trash_delete(&full_path) {\n            return Err(flc!(\"core_error_moving_to_trash\", file = full_path.as_ref().to_string_lossy().to_string(), error = e));\n        }\n    } else {\n        if let Err(e) = fs::remove_file(&full_path) {\n            return Err(flc!(\"core_error_removing\", file = full_path.as_ref().to_string_lossy().to_string(), error = e.to_string()));\n        }\n    }\n    Ok(())\n}\n\n/// Remove a single folder recursively. If `remove_to_trash` is set, the folder will instead be sent to the system's\n/// recycle bin/trash equivalent rather than being deleted.\n///\n/// Note: if used on Android or iOS platforms, ensure `remove_to_trash` is false, as trash is not supported\n/// and will always return an [`Error`].\npub fn remove_single_folder(full_path: &str, remove_to_trash: bool) -> Result<(), String> {\n    if remove_to_trash {\n        if let Err(e) = trash_delete(full_path) {\n            return Err(flc!(\"core_error_moving_to_trash\", file = full_path, error = e));\n        }\n    } else {\n        if let Err(e) = fs::remove_dir_all(full_path) {\n            return Err(flc!(\"core_error_removing\", file = full_path, error = e.to_string()));\n        }\n    }\n    Ok(())\n}\n\npub fn split_path(path: &Path) -> (String, String) {\n    match (path.parent(), path.file_name()) {\n        (Some(dir), Some(file)) => (dir.to_string_lossy().to_string(), file.to_string_lossy().into_owned()),\n        (Some(dir), None) => (dir.to_string_lossy().to_string(), String::new()),\n        (None, _) => (String::new(), String::new()),\n    }\n}\n\npub fn split_path_compare(path_a: &Path, path_b: &Path) -> Ordering {\n    match path_a.parent().cmp(&path_b.parent()) {\n        Ordering::Equal => path_a.file_name().cmp(&path_b.file_name()),\n        other => other,\n    }\n}\n\npub fn format_time(duration: Duration) -> String {\n    let hours = duration.as_secs() / 3600;\n    let minutes = duration.as_secs() % 3600 / 60;\n    let secs = duration.as_secs() % 60;\n    let millis = duration.subsec_millis();\n    if hours == 0 && minutes == 0 && secs == 0 {\n        format!(\"{millis}ms\")\n    } else if hours == 0 && minutes == 0 {\n        if millis / 10 == 0 { format!(\"{secs}s\") } else { format!(\"{secs}.{:02}s\", millis / 10) }\n    } else if hours == 0 {\n        if secs == 0 { format!(\"{minutes}m\") } else { format!(\"{minutes}m {secs}s\") }\n    } else {\n        if secs == 0 && minutes == 0 {\n            format!(\"{hours}h\")\n        } else if secs == 0 {\n            format!(\"{hours}h {minutes}m\")\n        } else {\n            format!(\"{hours}h {minutes}m {secs}s\")\n        }\n    }\n}\n\npub(crate) fn create_crash_message(library_name: &str, file_path: &str, home_library_url: &str) -> String {\n    format!(\n        \"{library_name} library crashed when opening \\\"{file_path}\\\", please check if this is fixed with the latest version of {library_name} and if it is not fixed, please report bug here - {home_library_url}\"\n    )\n}\n\n#[expect(clippy::string_slice)]\n#[expect(clippy::indexing_slicing)]\npub fn regex_check(expression_item: &SingleExcludedItem, directory_name: &str) -> bool {\n    if expression_item.expression_splits.is_empty() {\n        return true;\n    }\n\n    // Early checking if directory contains all parts needed by expression\n    for split in &expression_item.unique_extensions_splits {\n        if !directory_name.contains(split) {\n            return false;\n        }\n    }\n\n    // `git*` shouldn't be true for `/gitsfafasfs`\n    if !expression_item.expression.starts_with('*')\n        && directory_name\n            .find(&expression_item.expression_splits[0])\n            .expect(\"Cannot fail, because split must exists in directory_name\")\n            > 0\n    {\n        return false;\n    }\n    // `*home` shouldn't be true for `/homeowner`\n    if !expression_item.expression.ends_with('*')\n        && !directory_name.ends_with(expression_item.expression_splits.last().expect(\"Cannot fail, because at least one item is available\"))\n    {\n        return false;\n    }\n\n    // At the end we check if parts between * are correctly positioned\n    let mut last_split_point = directory_name.find(&expression_item.expression_splits[0]).expect(\"Cannot fail, because is checked earlier\");\n    let mut current_index: usize = 0;\n    let mut found_index: usize;\n    for spl in &expression_item.expression_splits[1..] {\n        found_index = match directory_name[current_index..].find(spl) {\n            Some(t) => t,\n            None => return false,\n        };\n        current_index = last_split_point + spl.len();\n        last_split_point = found_index + current_index;\n    }\n    true\n}\n\n#[expect(clippy::string_slice)] // Is in char boundary\npub fn normalize_windows_path<P: AsRef<Path>>(path_to_change: P) -> PathBuf {\n    let path = path_to_change.as_ref();\n\n    // Don't do anything, because network path may be case intensive\n    if path.to_string_lossy().starts_with('\\\\') {\n        return path.to_path_buf();\n    }\n\n    match path.to_str() {\n        Some(path) if path.is_char_boundary(1) => {\n            let replaced = path.replace('/', \"\\\\\");\n            let mut new_path = OsString::new();\n            if replaced[1..].starts_with(':') {\n                new_path.push(replaced[..1].to_ascii_uppercase());\n                new_path.push(replaced[1..].to_ascii_lowercase());\n            } else {\n                new_path.push(replaced.to_ascii_lowercase());\n            }\n            PathBuf::from(new_path)\n        }\n        _ => path.to_path_buf(),\n    }\n}\n\n// Function to create hardlink, when destination exists\n// This is always true in this app, because creating hardlink, to newly created file is pointless\npub fn make_hard_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {\n    let src = src.as_ref();\n    let dst = dst.as_ref();\n    let dst_dir = dst.parent().ok_or_else(|| Error::other(\"No parent\"))?;\n    let mut temp;\n    let mut attempts = MAX_SYMLINK_HARDLINK_ATTEMPTS;\n    loop {\n        temp = dst_dir.join(format!(\"{}.czkawka_tmp\", rand::random::<u128>()));\n        if !temp.exists() {\n            break;\n        }\n        attempts -= 1;\n        if attempts == 0 {\n            return Err(Error::other(\"Cannot choose temporary file for hardlink creation\"));\n        }\n    }\n    fs::rename(dst, temp.as_path())?;\n    match fs::hard_link(src, dst) {\n        Ok(()) => {\n            fs::remove_file(&temp)?;\n            Ok(())\n        }\n        Err(e) => {\n            let _ = fs::rename(&temp, dst);\n            Err(e)\n        }\n    }\n}\n\n#[cfg(any(target_family = \"unix\", target_family = \"windows\"))]\npub fn make_file_symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {\n    let src = src.as_ref();\n    let dst = dst.as_ref();\n    let dst_dir = dst.parent().ok_or_else(|| Error::other(\"No parent\"))?;\n    let mut temp;\n    let mut attempts = MAX_SYMLINK_HARDLINK_ATTEMPTS;\n    loop {\n        temp = dst_dir.join(format!(\"{}.czkawka_tmp\", rand::random::<u128>()));\n        if !temp.exists() {\n            break;\n        }\n        attempts -= 1;\n        if attempts == 0 {\n            return Err(Error::other(\"Cannot choose temporary file for symlink creation\"));\n        }\n    }\n    fs::rename(dst, temp.as_path())?;\n    let result: Result<_, _>;\n    #[cfg(target_family = \"unix\")]\n    {\n        result = std::os::unix::fs::symlink(src, dst);\n    }\n    #[cfg(target_family = \"windows\")]\n    {\n        result = std::os::windows::fs::symlink_file(src, dst);\n    }\n    match result {\n        Ok(()) => {\n            fs::remove_file(&temp)?;\n            Ok(())\n        }\n        Err(e) => {\n            let _ = fs::rename(&temp, dst);\n            Err(e)\n        }\n    }\n}\n\n#[cfg(not(any(target_family = \"unix\", target_family = \"windows\")))]\npub fn make_file_symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> io::Result<()> {\n    Err(Error::new(io::ErrorKind::Other, \"Soft links are not supported on this platform\"))\n}\n\npub fn debug_save_file(path: &str, data: &str) {\n    use std::io::Write;\n    if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(path) {\n        let _ = writeln!(f, \"{data}\");\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::fs::{File, Metadata, read_dir};\n    use std::io::Write;\n    #[cfg(target_family = \"unix\")]\n    use std::os::unix::fs::MetadataExt;\n\n    use tempfile::tempdir;\n\n    use super::*;\n    use crate::common::items::new_excluded_item;\n\n    #[cfg(target_family = \"unix\")]\n    fn assert_inode(before: &Metadata, after: &Metadata) {\n        assert_eq!(before.ino(), after.ino());\n    }\n\n    #[cfg(target_family = \"windows\")]\n    fn assert_inode(_: &Metadata, _: &Metadata) {}\n\n    #[cfg(target_family = \"unix\")]\n    fn assert_different_inode(before: &Metadata, after: &Metadata) {\n        assert_ne!(before.ino(), after.ino());\n    }\n\n    #[cfg(target_family = \"windows\")]\n    fn assert_different_inode(_before: &Metadata, _after: &Metadata) {}\n\n    #[test]\n    fn test_make_hard_link() -> io::Result<()> {\n        // Test 1: Basic hardlink creation\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"a\"), dir.path().join(\"b\"));\n            File::create(&src)?;\n            let metadata = fs::metadata(&src)?;\n            File::create(&dst)?;\n            let dst_metadata_before = fs::metadata(&dst)?;\n\n            assert_different_inode(&metadata, &dst_metadata_before);\n\n            make_hard_link(&src, &dst)?;\n\n            make_hard_link(&src, &dst)?;\n\n            assert_inode(&metadata, &fs::metadata(&dst)?);\n            assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions());\n            assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?);\n            assert_inode(&metadata, &fs::metadata(&src)?);\n            assert_eq!(metadata.permissions(), fs::metadata(&src)?.permissions());\n            assert_eq!(metadata.modified()?, fs::metadata(&src)?.modified()?);\n\n            let mut actual = read_dir(&dir)?.flatten().map(|e| e.path()).collect::<Vec<PathBuf>>();\n            actual.sort_unstable();\n            assert_eq!(vec![src, dst], actual);\n        }\n\n        // Test 2: Hardlink creation fails when source doesn't exist\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"a\"), dir.path().join(\"b\"));\n            File::create(&dst)?;\n            let metadata = fs::metadata(&dst)?;\n\n            assert!(make_hard_link(&src, &dst).is_err());\n\n            assert_inode(&metadata, &fs::metadata(&dst)?);\n            assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions());\n            assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?);\n\n            assert_eq!(vec![dst], read_dir(&dir)?.flatten().map(|e| e.path()).collect::<Vec<PathBuf>>());\n        }\n\n        // Test 3: Hardlink with content preservation\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"src_file\"), dir.path().join(\"dst_file\"));\n            let content = \"test content for hardlink\";\n            {\n                let mut f = File::create(&src)?;\n                writeln!(f, \"{content}\")?;\n            }\n            {\n                let mut f = File::create(&dst)?;\n                writeln!(f, \"old content\")?;\n            }\n\n            let src_metadata = fs::metadata(&src)?;\n            let dst_metadata_before = fs::metadata(&dst)?;\n\n            assert_different_inode(&src_metadata, &dst_metadata_before);\n\n            make_hard_link(&src, &dst)?;\n\n            let src_content = fs::read_to_string(&src)?;\n            let dst_content = fs::read_to_string(&dst)?;\n            assert_eq!(src_content, dst_content);\n            assert_eq!(src_content, format!(\"{content}\\n\"));\n            assert_inode(&src_metadata, &fs::metadata(&dst)?);\n        }\n\n        // Test 4: Hardlink on readonly file\n        #[cfg(target_family = \"unix\")]\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"readonly_src\"), dir.path().join(\"readonly_dst\"));\n\n            {\n                let mut f = File::create(&src)?;\n                writeln!(f, \"readonly content\")?;\n            }\n\n            let mut perms = fs::metadata(&src)?.permissions();\n            perms.set_readonly(true);\n            fs::set_permissions(&src, perms)?;\n\n            assert!(fs::metadata(&src)?.permissions().readonly());\n\n            {\n                let mut f = File::create(&dst)?;\n                writeln!(f, \"dst content\")?;\n            }\n\n            let src_metadata_before = fs::metadata(&src)?;\n            let dst_metadata_before = fs::metadata(&dst)?;\n\n            assert_different_inode(&src_metadata_before, &dst_metadata_before);\n\n            make_hard_link(&src, &dst).unwrap();\n\n            assert_inode(&src_metadata_before, &fs::metadata(&dst)?);\n            assert_eq!(fs::read_to_string(&src)?, fs::read_to_string(&dst)?);\n\n            assert!(fs::metadata(&src)?.permissions().readonly());\n            assert!(fs::metadata(&dst)?.permissions().readonly());\n        }\n\n        // Test 5: Hardlink on readonly destination file\n        #[cfg(target_family = \"unix\")]\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"src_normal\"), dir.path().join(\"dst_readonly\"));\n\n            {\n                let mut f = File::create(&src)?;\n                writeln!(f, \"source content\")?;\n            }\n\n            {\n                let mut f = File::create(&dst)?;\n                writeln!(f, \"destination content\")?;\n            }\n            let mut perms = fs::metadata(&dst)?.permissions();\n            perms.set_readonly(true);\n            fs::set_permissions(&dst, perms)?;\n\n            assert!(fs::metadata(&dst)?.permissions().readonly());\n\n            let src_metadata = fs::metadata(&src)?;\n            let dst_metadata_before = fs::metadata(&dst)?;\n\n            assert_different_inode(&src_metadata, &dst_metadata_before);\n\n            make_hard_link(&src, &dst).unwrap();\n\n            assert_inode(&src_metadata, &fs::metadata(&dst)?);\n            assert_eq!(fs::read_to_string(&src)?, fs::read_to_string(&dst)?);\n        }\n\n        // Test 6: Hardlink when destination doesn't exist - should fail\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"src\"), dir.path().join(\"nonexistent\"));\n            File::create(&src)?;\n\n            let result = make_hard_link(&src, &dst);\n            assert!(result.is_err(), \"Should fail when destination doesn't exist\");\n        }\n\n        // Test 7: Hardlink preserves file size\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let (src, dst) = (dir.path().join(\"large_src\"), dir.path().join(\"large_dst\"));\n\n            let large_content = \"x\".repeat(10000);\n            {\n                let mut f = File::create(&src)?;\n                write!(f, \"{large_content}\")?;\n            }\n            File::create(&dst)?;\n\n            let src_size = fs::metadata(&src)?.len();\n            let src_metadata = fs::metadata(&src)?;\n            let dst_metadata_before = fs::metadata(&dst)?;\n\n            assert_different_inode(&src_metadata, &dst_metadata_before);\n\n            make_hard_link(&src, &dst)?;\n\n            assert_eq!(src_size, fs::metadata(&dst)?.len());\n            assert_eq!(large_content, fs::read_to_string(&dst)?);\n        }\n\n        // Test 8: Multiple hardlinks to same file\n        {\n            let dir = tempfile::Builder::new().tempdir()?;\n            let src = dir.path().join(\"original\");\n            let dst1 = dir.path().join(\"link1\");\n            let dst2 = dir.path().join(\"link2\");\n\n            {\n                let mut f = File::create(&src)?;\n                writeln!(f, \"original\")?;\n            }\n            File::create(&dst1)?;\n            File::create(&dst2)?;\n\n            let src_metadata = fs::metadata(&src)?;\n            let dst1_metadata_before = fs::metadata(&dst1)?;\n            let dst2_metadata_before = fs::metadata(&dst2)?;\n\n            // Before hardlinks - all files should have different inodes\n            assert_different_inode(&src_metadata, &dst1_metadata_before);\n            assert_different_inode(&src_metadata, &dst2_metadata_before);\n            assert_different_inode(&dst1_metadata_before, &dst2_metadata_before);\n\n            make_hard_link(&src, &dst1)?;\n            make_hard_link(&src, &dst2)?;\n\n            assert_inode(&src_metadata, &fs::metadata(&dst1)?);\n            assert_inode(&src_metadata, &fs::metadata(&dst2)?);\n        }\n\n        Ok(())\n    }\n\n    // Windows needs super user permissions\n    #[cfg(target_family = \"unix\")]\n    #[test]\n    fn test_make_file_symlink() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let (src, dst) = (dir.path().join(\"a\"), dir.path().join(\"b\"));\n        let content = \"hello softlink\";\n        {\n            let mut f = File::create(&src)?;\n            writeln!(f, \"{content}\")?;\n        }\n        File::create(&dst)?;\n\n        make_file_symlink(&src, &dst)?;\n\n        let symlink_meta = fs::symlink_metadata(&dst)?;\n        assert!(symlink_meta.file_type().is_symlink());\n\n        let src_content = fs::read_to_string(&src)?;\n        let dst_content = fs::read_to_string(&dst)?;\n        assert_eq!(src_content, dst_content);\n\n        let mut actual = read_dir(&dir)?.flatten().map(|e| e.path()).collect::<Vec<PathBuf>>();\n        actual.sort_unstable();\n        assert_eq!(vec![src, dst], actual);\n        Ok(())\n    }\n\n    #[cfg(target_family = \"unix\")]\n    #[test]\n    fn test_make_file_symlink_fails() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let (src, dst) = (dir.path().join(\"a\"), dir.path().join(\"b\"));\n        {\n            let mut f = File::create(&dst)?;\n            writeln!(f, \"original\")?;\n        }\n        let metadata = fs::metadata(&dst)?;\n\n        match make_file_symlink(&src, &dst) {\n            Err(_) => {\n                assert_eq!(fs::read_to_string(&dst)?, \"original\\n\");\n                assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions());\n            }\n            Ok(()) => {\n                let symlink_meta = fs::symlink_metadata(&dst)?;\n                assert!(symlink_meta.file_type().is_symlink());\n                fs::read_to_string(&dst).unwrap_err();\n            }\n        }\n        Ok(())\n    }\n\n    #[test]\n    fn test_remove_folder_if_contains_only_empty_folders() {\n        let dir = tempdir().expect(\"Cannot create temporary directory\");\n        let sub_dir = dir.path().join(\"sub_dir\");\n        fs::create_dir(&sub_dir).expect(\"Cannot create directory\");\n\n        // Test with empty directory\n        remove_folder_if_contains_only_empty_folders(&sub_dir, false).unwrap();\n        assert!(!Path::new(&sub_dir).exists());\n\n        // Test with directory containing an empty directory\n        fs::create_dir(&sub_dir).expect(\"Cannot create directory\");\n        fs::create_dir(sub_dir.join(\"empty_sub_dir\")).expect(\"Cannot create directory\");\n        remove_folder_if_contains_only_empty_folders(&sub_dir, false).unwrap();\n        assert!(!Path::new(&sub_dir).exists());\n\n        // Test with directory containing a file\n        fs::create_dir(&sub_dir).expect(\"Cannot create directory\");\n        let mut file = File::create(sub_dir.join(\"file.txt\")).expect(\"Cannot create file\");\n        writeln!(file, \"Hello, world!\").expect(\"Cannot write to file\");\n        assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_err());\n        assert!(Path::new(&sub_dir).exists());\n    }\n\n    #[test]\n    fn test_regex() {\n        assert!(regex_check(&new_excluded_item(\"*\"), \"/home/rafal\"));\n        assert!(regex_check(&new_excluded_item(\"*home*\"), \"/home/rafal\"));\n        assert!(regex_check(&new_excluded_item(\"*home\"), \"/home\"));\n        assert!(regex_check(&new_excluded_item(\"*home/\"), \"/home/\"));\n        assert!(regex_check(&new_excluded_item(\"*home/*\"), \"/home/\"));\n        assert!(regex_check(&new_excluded_item(\"*.git*\"), \"/home/.git\"));\n        assert!(regex_check(&new_excluded_item(\"/home/*/.*\"), \"/home/user/.random\"));\n        assert!(regex_check(&new_excluded_item(\"*/home/rafal*rafal*rafal*rafal*\"), \"/home/rafal/rafalrafalrafal\"));\n        assert!(regex_check(&new_excluded_item(\"AAA\"), \"AAA\"));\n        assert!(regex_check(&new_excluded_item(\"AAA*\"), \"AAABDGG/QQPW*\"));\n        assert!(!regex_check(&new_excluded_item(\"*home\"), \"/home/\"));\n        assert!(!regex_check(&new_excluded_item(\"*home\"), \"/homefasfasfasfasf/\"));\n        assert!(!regex_check(&new_excluded_item(\"*home\"), \"/homefasfasfasfasf\"));\n        assert!(!regex_check(&new_excluded_item(\"rafal*afal*fal\"), \"rafal\"));\n        assert!(!regex_check(&new_excluded_item(\"rafal*a\"), \"rafal\"));\n        assert!(!regex_check(&new_excluded_item(\"AAAAAAAA****\"), \"/AAAAAAAAAAAAAAAAA\"));\n        assert!(!regex_check(&new_excluded_item(\"*.git/*\"), \"/home/.git\"));\n        assert!(!regex_check(&new_excluded_item(\"*home/*koc\"), \"/koc/home/\"));\n        assert!(!regex_check(&new_excluded_item(\"*home/\"), \"/home\"));\n        assert!(!regex_check(&new_excluded_item(\"*TTT\"), \"/GGG\"));\n        assert!(regex_check(\n            &new_excluded_item(\"*/home/*/.local/share/containers\"),\n            \"/var/home/roman/.local/share/containers\"\n        ));\n\n        if cfg!(target_family = \"windows\") {\n            assert!(regex_check(&new_excluded_item(\"*\\\\home\"), \"C:\\\\home\"));\n        }\n    }\n\n    #[test]\n    fn test_windows_path() {\n        assert_eq!(PathBuf::from(\"C:\\\\path.txt\"), normalize_windows_path(\"c:/PATH.tXt\"));\n        assert_eq!(PathBuf::from(\"H:\\\\reka\\\\weza\\\\roman.txt\"), normalize_windows_path(\"h:/RekA/Weza\\\\roMan.Txt\"));\n        assert_eq!(PathBuf::from(\"T:\\\\a\"), normalize_windows_path(\"T:\\\\A\"));\n        assert_eq!(PathBuf::from(\"\\\\\\\\aBBa\"), normalize_windows_path(\"\\\\\\\\aBBa\"));\n        assert_eq!(PathBuf::from(\"a\"), normalize_windows_path(\"a\"));\n        assert_eq!(PathBuf::from(\"\"), normalize_windows_path(\"\"));\n    }\n\n    #[test]\n    fn test_format_time() {\n        assert_eq!(format_time(Duration::from_millis(0)), \"0ms\");\n        assert_eq!(format_time(Duration::from_millis(1)), \"1ms\");\n        assert_eq!(format_time(Duration::from_millis(999)), \"999ms\");\n\n        assert_eq!(format_time(Duration::from_millis(1000)), \"1s\");\n        assert_eq!(format_time(Duration::from_millis(1234)), \"1.23s\");\n        assert_eq!(format_time(Duration::from_millis(5678)), \"5.67s\");\n        assert_eq!(format_time(Duration::from_secs(59)), \"59s\");\n\n        assert_eq!(format_time(Duration::from_secs(60)), \"1m\");\n        assert_eq!(format_time(Duration::from_secs(61)), \"1m 1s\");\n        assert_eq!(format_time(Duration::from_millis(61234)), \"1m 1s\");\n        assert_eq!(format_time(Duration::from_secs(125)), \"2m 5s\");\n        assert_eq!(format_time(Duration::from_secs(3599)), \"59m 59s\");\n\n        assert_eq!(format_time(Duration::from_secs(3600)), \"1h\");\n        assert_eq!(format_time(Duration::from_secs(3661)), \"1h 1m 1s\");\n        assert_eq!(format_time(Duration::from_secs(7384)), \"2h 3m 4s\");\n        assert_eq!(format_time(Duration::from_secs(86400)), \"24h\");\n\n        assert_eq!(format_time(Duration::from_millis(999)), \"999ms\");\n        assert_eq!(format_time(Duration::from_millis(1001)), \"1s\");\n        assert_eq!(format_time(Duration::from_millis(59999)), \"59.99s\");\n        assert_eq!(format_time(Duration::from_millis(60000)), \"1m\");\n        assert_eq!(format_time(Duration::from_millis(60100)), \"1m\");\n        assert_eq!(format_time(Duration::from_millis(120000)), \"2m\");\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/model.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse serde::{Deserialize, Serialize};\nuse xxhash_rust::xxh3::Xxh3;\n\nuse crate::common::traits::ResultEntry;\nuse crate::tools::duplicate::MyHasher;\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]\npub enum ToolType {\n    Duplicate,\n    EmptyFolders,\n    EmptyFiles,\n    InvalidSymlinks,\n    BrokenFiles,\n    BadExtensions,\n    BadNames,\n    BigFile,\n    SameMusic,\n    SimilarImages,\n    SimilarVideos,\n    TemporaryFiles,\n    ExifRemover,\n    VideoOptimizer,\n    #[default]\n    None,\n}\n\nimpl ToolType {\n    pub fn may_use_reference_paths(self) -> bool {\n        matches!(self, Self::Duplicate | Self::SameMusic | Self::SimilarImages | Self::SimilarVideos)\n    }\n}\n\n#[derive(PartialEq, Eq, Clone, Debug, Copy, Default, Deserialize, Serialize)]\npub enum CheckingMethod {\n    #[default]\n    None,\n    Name,\n    SizeName,\n    Size,\n    Hash,\n    AudioTags,\n    AudioContent,\n}\n\n#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct FileEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub modified_date: u64,\n}\n\nimpl ResultEntry for FileEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\n#[derive(PartialEq, Eq, Clone, Debug, Copy, Default)]\npub enum HashType {\n    #[default]\n    Blake3,\n    Crc32,\n    Xxh3,\n}\n\nimpl HashType {\n    pub(crate) fn hasher(self) -> Box<dyn MyHasher> {\n        match self {\n            Self::Blake3 => Box::new(blake3::Hasher::new()),\n            Self::Crc32 => Box::new(crc32fast::Hasher::new()),\n            Self::Xxh3 => Box::new(Xxh3::new()),\n        }\n    }\n}\n\n#[derive(Debug, PartialEq)]\npub enum WorkContinueStatus {\n    Continue,\n    Stop,\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_file_entry_basic_operations() {\n        let entry = FileEntry {\n            path: PathBuf::from(\"/test/file.txt\"),\n            size: 1024,\n            modified_date: 123456,\n        };\n\n        assert_eq!(entry.get_path(), Path::new(\"/test/file.txt\"));\n        assert_eq!(entry.get_size(), 1024);\n        assert_eq!(entry.get_modified_date(), 123456);\n\n        let entry2 = entry.clone();\n        assert_eq!(entry, entry2);\n    }\n\n    #[test]\n    fn test_hash_type_creates_hashers() {\n        let blake3_hasher = HashType::Blake3.hasher();\n        let crc32_hasher = HashType::Crc32.hasher();\n        let xxh3_hasher = HashType::Xxh3.hasher();\n\n        // Just verify they can be created\n        assert!(std::mem::size_of_val(&blake3_hasher) > 0);\n        assert!(std::mem::size_of_val(&crc32_hasher) > 0);\n        assert!(std::mem::size_of_val(&xxh3_hasher) > 0);\n    }\n\n    #[test]\n    fn test_checking_method_default() {\n        assert_eq!(CheckingMethod::default(), CheckingMethod::None);\n    }\n\n    #[test]\n    fn test_tool_type_default() {\n        assert_eq!(ToolType::default(), ToolType::None);\n    }\n\n    #[test]\n    fn test_delete_method_default() {\n        use crate::common::tool_data::DeleteMethod;\n        assert_eq!(DeleteMethod::default(), DeleteMethod::None);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/process_utils.rs",
    "content": "use std::process::{Command, Stdio};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\nuse log::{error, warn};\n\nuse crate::flc;\n\n#[expect(clippy::needless_pass_by_ref_mut)]\npub fn disable_windows_console_window(command: &mut Command) {\n    #[cfg(target_os = \"windows\")]\n    {\n        use std::os::windows::process::CommandExt;\n        const CREATE_NO_WINDOW: u32 = 0x08000000;\n        command.creation_flags(CREATE_NO_WINDOW);\n    }\n    #[cfg(not(target_os = \"windows\"))]\n    {\n        let _ = command;\n    }\n}\n\npub struct CommandOutput {\n    pub status: std::process::ExitStatus,\n    pub stdout: String,\n    pub stderr: String,\n}\n\n// Remember - Ok returned by this function does not necessarily mean that the command executed successfully\n// it only means that the command was executed and its output was captured.\n// The actual success of the command should be determined by checking the `status` field of the returned `CommandOutput`.\npub fn run_command_interruptible(mut command: Command, stop_flag: &Arc<AtomicBool>) -> Option<Result<CommandOutput, String>> {\n    if stop_flag.load(Ordering::Relaxed) {\n        return None;\n    }\n\n    disable_windows_console_window(&mut command);\n\n    command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());\n\n    let mut child = match command.spawn() {\n        Ok(c) => c,\n        Err(e) => return Some(Err(flc!(\"core_failed_to_spawn_command\", reason = e.to_string()))),\n    };\n\n    let Some(mut stdout) = child.stdout.take() else {\n        error!(\"Failed to take stdout from child process\");\n        return Some(Err(\"Failed to take stdout from child process\".to_string()));\n    };\n    let Some(mut stderr) = child.stderr.take() else {\n        error!(\"Failed to take stderr from child process\");\n        return Some(Err(\"Failed to take stderr from child process\".to_string()));\n    };\n\n    let stdout_buf = Arc::new(Mutex::new(Vec::new()));\n    let stderr_buf = Arc::new(Mutex::new(Vec::new()));\n\n    let out_buf = stdout_buf.clone();\n    let err_buf = stderr_buf.clone();\n\n    let out_handle = thread::spawn(move || {\n        let mut buf = Vec::new();\n        let _ = std::io::copy(&mut stdout, &mut buf);\n        match out_buf.lock() {\n            Ok(mut lock) => *lock = buf,\n            Err(e) => error!(\"Failed to lock stdout buffer: {e}\"),\n        }\n    });\n\n    let err_handle = thread::spawn(move || {\n        let mut buf = Vec::new();\n        let _ = std::io::copy(&mut stderr, &mut buf);\n        match err_buf.lock() {\n            Ok(mut lock) => *lock = buf,\n            Err(e) => error!(\"Failed to lock stderr buffer: {e}\"),\n        }\n    });\n\n    let start_time = Instant::now();\n    let warning_steps = [50, 250, 1250, 6000];\n    let mut next_warning_idx = 0;\n\n    loop {\n        if stop_flag.load(Ordering::Relaxed) {\n            let _ = child.kill();\n            let _ = child.wait();\n            break;\n        }\n\n        let elapsed_secs = start_time.elapsed().as_secs();\n        if let Some(warning_time) = warning_steps.get(next_warning_idx)\n            && elapsed_secs >= *warning_time\n        {\n            warn!(\"Command is still running after {warning_time} seconds, for command: {command:?}\");\n            next_warning_idx += 1;\n        }\n\n        match child.try_wait() {\n            Ok(Some(_)) => break,\n            Ok(None) => thread::sleep(Duration::from_millis(100)),\n            Err(e) => return Some(Err(flc!(\"core_failed_to_check_process_status\", reason = e.to_string()))),\n        }\n    }\n\n    let status = match child.wait() {\n        Ok(s) => s,\n        Err(e) => return Some(Err(flc!(\"core_failed_to_wait_for_process\", reason = e.to_string()))),\n    };\n\n    let _ = out_handle.join();\n    let _ = err_handle.join();\n\n    if stop_flag.load(Ordering::Relaxed) {\n        return None;\n    }\n\n    let stdout = match Arc::try_unwrap(stdout_buf) {\n        Ok(mutex) => match mutex.into_inner() {\n            Ok(buf) => buf,\n            Err(e) => {\n                error!(\"Failed to get stdout inner buffer: {e}\");\n                return Some(Err(\"Failed to get stdout inner buffer\".to_string()));\n            }\n        },\n        Err(_) => {\n            error!(\"Failed to unwrap stdout Arc - multiple references still exist\");\n            return Some(Err(\"Failed to unwrap stdout Arc\".to_string()));\n        }\n    };\n\n    let stderr = match Arc::try_unwrap(stderr_buf) {\n        Ok(mutex) => match mutex.into_inner() {\n            Ok(buf) => buf,\n            Err(e) => {\n                error!(\"Failed to get stderr inner buffer: {e}\");\n                return Some(Err(\"Failed to get stderr inner buffer\".to_string()));\n            }\n        },\n        Err(_) => {\n            error!(\"Failed to unwrap stderr Arc - multiple references still exist\");\n            return Some(Err(\"Failed to unwrap stderr Arc\".to_string()));\n        }\n    };\n\n    Some(Ok(CommandOutput {\n        status,\n        stdout: String::from_utf8_lossy(&stdout).to_string(),\n        stderr: String::from_utf8_lossy(&stderr).to_string(),\n    }))\n}\n"
  },
  {
    "path": "czkawka_core/src/common/progress_data.rs",
    "content": "use log::error;\n\nuse crate::common::model::{CheckingMethod, ToolType};\n// Empty files\n// 0 - Collecting files\n\n// Empty folders\n// 0 - Collecting folders\n\n// Big files\n// 0 - Collecting files\n\n// Same music\n// 0 - Collecting files\n// 1 - Loading cache\n// 2 - Checking tags\n// 3 - Saving cache\n// 4 - TAGS - Comparing tags\n// 4 - CONTENT - Loading cache\n// 5 - CONTENT - Calculating fingerprints\n// 6 - CONTENT - Saving cache\n// 7 - CONTENT - Comparing fingerprints\n\n// Similar images\n// 0 - Collecting files\n// 1 - Scanning images\n// 2 - Comparing hashes\n\n// Similar videos\n// 0 - Collecting files\n// 1 - Scanning videos\n// 2 - Creating thumbnails\n\n// Temporary files\n// 0 - Collecting files\n\n// Invalid symlinks\n// 0 - Collecting files\n\n// Broken files\n// 0 - Collecting files\n// 1 - Scanning files\n\n// Bad extensions\n// 0 - Collecting files\n// 1 - Scanning files\n\n// Exif Remover\n// 0 - Collecting files\n// 1 - Loading cache\n// 2 - Extracting tags\n// 3 - Saving cache\n\n// Duplicates - Hash\n// 0 - Collecting files\n// 1 - Loading cache\n// 2 - Hash - first 1KB file\n// 3 - Saving cache\n// 4 - Loading cache\n// 5 - Hash - normal hash\n// 6 - Saving cache\n\n// Duplicates - Name or SizeName or Size\n// 0 - Collecting files\n\n// Deleting files\n// Renaming files\n\n#[derive(Debug, Clone, Copy)]\npub struct ProgressData {\n    pub sstage: CurrentStage,\n    pub checking_method: CheckingMethod,\n    pub current_stage_idx: u8,\n    pub max_stage_idx: u8,\n    pub entries_checked: usize,\n    pub entries_to_check: usize,\n    pub bytes_checked: u64,\n    pub bytes_to_check: u64,\n    pub tool_type: ToolType,\n}\n\nimpl ProgressData {\n    pub fn get_empty_state(current_stage: CurrentStage) -> Self {\n        Self {\n            sstage: current_stage,\n            checking_method: CheckingMethod::None,\n            current_stage_idx: 0,\n            max_stage_idx: 0,\n            entries_checked: 0,\n            entries_to_check: 0,\n            bytes_checked: 0,\n            bytes_to_check: 0,\n            tool_type: ToolType::None,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum CurrentStage {\n    DeletingFiles,\n    RenamingFiles,\n    MovingFiles,\n    HardlinkingFiles,\n    SymlinkingFiles,\n    OptimizingVideos,\n    CleaningExif,\n\n    CollectingFiles,\n    DuplicateCacheSaving,\n    DuplicateCacheLoading,\n    DuplicatePreHashCacheSaving,\n    DuplicatePreHashCacheLoading,\n    DuplicateScanningName,\n    DuplicateScanningSizeName,\n    DuplicateScanningSize,\n    DuplicatePreHashing,\n    DuplicateFullHashing,\n\n    SameMusicCacheSavingTags,\n    SameMusicCacheLoadingTags,\n    SameMusicCacheSavingFingerprints,\n    SameMusicCacheLoadingFingerprints,\n    SameMusicReadingTags,\n    SameMusicCalculatingFingerprints,\n    SameMusicComparingTags,\n    SameMusicComparingFingerprints,\n\n    SimilarImagesCalculatingHashes,\n    SimilarImagesComparingHashes,\n    SimilarVideosCalculatingHashes,\n    SimilarVideosCreatingThumbnails,\n    BrokenFilesChecking,\n    BadExtensionsChecking,\n    BadNamesChecking,\n    ExifRemoverCacheLoading,\n    ExifRemoverExtractingTags,\n    ExifRemoverCacheSaving,\n    VideoOptimizerCreatingThumbnails,\n    VideoOptimizerProcessingVideos,\n}\n\nimpl ProgressData {\n    pub(crate) fn validate(&self) {\n        assert!(\n            self.current_stage_idx <= self.max_stage_idx,\n            \"Current stage index: {}, max stage index: {}, stage {:?}\",\n            self.current_stage_idx,\n            self.max_stage_idx,\n            self.sstage\n        );\n        assert_eq!(\n            self.max_stage_idx,\n            self.tool_type.get_max_stage(self.checking_method),\n            \"Max stage index: {}, tool type: {:?}, checking method: {:?}\",\n            self.max_stage_idx,\n            self.tool_type,\n            self.checking_method\n        );\n\n        if self.sstage != CurrentStage::CollectingFiles {\n            assert!(\n                self.entries_checked <= self.entries_to_check,\n                \"Entries checked: {}, entries to check: {}, stage {:?}\",\n                self.entries_checked,\n                self.entries_to_check,\n                self.sstage\n            );\n        }\n\n        // This could be an assert, but it is possible that in duplicate finder, file that will\n        // be checked, will increase the size of the file between collecting file to scan and\n        // scanning it. So it is better to just log it\n        if self.bytes_checked > self.bytes_to_check {\n            error!(\"Bytes checked: {}, bytes to check: {}, stage {:?}\", self.bytes_checked, self.bytes_to_check, self.sstage);\n        }\n\n        let tool_type_checking_method: Option<ToolType> = match self.checking_method {\n            CheckingMethod::AudioTags | CheckingMethod::AudioContent => Some(ToolType::SameMusic),\n            CheckingMethod::Name | CheckingMethod::SizeName | CheckingMethod::Size | CheckingMethod::Hash => Some(ToolType::Duplicate),\n            CheckingMethod::None => None,\n        };\n        if let Some(tool_type) = tool_type_checking_method {\n            assert_eq!(self.tool_type, tool_type, \"Tool type: {:?}, checking method: {:?}\", self.tool_type, self.checking_method);\n        }\n        let tool_type_current_stage: Option<ToolType> = match self.sstage {\n            CurrentStage::CollectingFiles\n            | CurrentStage::DeletingFiles\n            | CurrentStage::RenamingFiles\n            | CurrentStage::MovingFiles\n            | CurrentStage::HardlinkingFiles\n            | CurrentStage::SymlinkingFiles\n            | CurrentStage::OptimizingVideos\n            | CurrentStage::CleaningExif => None,\n            CurrentStage::DuplicateCacheSaving | CurrentStage::DuplicateCacheLoading | CurrentStage::DuplicatePreHashCacheSaving | CurrentStage::DuplicatePreHashCacheLoading => {\n                Some(ToolType::Duplicate)\n            }\n            CurrentStage::DuplicateScanningName\n            | CurrentStage::DuplicateScanningSizeName\n            | CurrentStage::DuplicateScanningSize\n            | CurrentStage::DuplicatePreHashing\n            | CurrentStage::DuplicateFullHashing => Some(ToolType::Duplicate),\n            CurrentStage::SameMusicCacheLoadingTags\n            | CurrentStage::SameMusicCacheSavingTags\n            | CurrentStage::SameMusicCacheLoadingFingerprints\n            | CurrentStage::SameMusicCacheSavingFingerprints\n            | CurrentStage::SameMusicComparingTags\n            | CurrentStage::SameMusicReadingTags\n            | CurrentStage::SameMusicComparingFingerprints\n            | CurrentStage::SameMusicCalculatingFingerprints => Some(ToolType::SameMusic),\n            CurrentStage::SimilarImagesCalculatingHashes | CurrentStage::SimilarImagesComparingHashes => Some(ToolType::SimilarImages),\n            CurrentStage::SimilarVideosCalculatingHashes | CurrentStage::SimilarVideosCreatingThumbnails => Some(ToolType::SimilarVideos),\n            CurrentStage::BrokenFilesChecking => Some(ToolType::BrokenFiles),\n            CurrentStage::BadExtensionsChecking => Some(ToolType::BadExtensions),\n            CurrentStage::BadNamesChecking => Some(ToolType::BadNames),\n            CurrentStage::ExifRemoverCacheLoading | CurrentStage::ExifRemoverExtractingTags | CurrentStage::ExifRemoverCacheSaving => Some(ToolType::ExifRemover),\n            CurrentStage::VideoOptimizerCreatingThumbnails | CurrentStage::VideoOptimizerProcessingVideos => Some(ToolType::VideoOptimizer),\n        };\n        if let Some(tool_type) = tool_type_current_stage {\n            assert_eq!(self.tool_type, tool_type, \"Tool type: {:?}, stage {:?}\", self.tool_type, self.sstage);\n        }\n    }\n}\n\nimpl ToolType {\n    pub(crate) fn get_max_stage(self, checking_method: CheckingMethod) -> u8 {\n        match self {\n            Self::Duplicate => 6,\n            Self::EmptyFolders | Self::EmptyFiles | Self::InvalidSymlinks | Self::BigFile | Self::TemporaryFiles => 0,\n            Self::BrokenFiles | Self::BadExtensions | Self::BadNames => 1,\n            Self::SimilarImages | Self::SimilarVideos | Self::VideoOptimizer => 2,\n            Self::ExifRemover => 3,\n            Self::None => unreachable!(\"ToolType::None is not allowed\"),\n            Self::SameMusic => match checking_method {\n                CheckingMethod::AudioTags => 4,\n                CheckingMethod::AudioContent => 7,\n                _ => unreachable!(\"CheckingMethod {checking_method:?} in same music mode is not allowed\"),\n            },\n        }\n    }\n}\n\nimpl CurrentStage {\n    pub fn is_special_non_tool_stage(self) -> bool {\n        matches!(\n            self,\n            Self::DeletingFiles | Self::RenamingFiles | Self::MovingFiles | Self::HardlinkingFiles | Self::SymlinkingFiles | Self::OptimizingVideos | Self::CleaningExif\n        )\n    }\n\n    pub fn get_current_stage(self) -> u8 {\n        #[expect(clippy::match_same_arms)] // Now it is easier to read\n        match self {\n            Self::DeletingFiles => 0,\n            Self::RenamingFiles => 0,\n            Self::MovingFiles => 0,\n            Self::HardlinkingFiles => 0,\n            Self::SymlinkingFiles => 0,\n            Self::OptimizingVideos => 0,\n            Self::CleaningExif => 0,\n            Self::CollectingFiles => 0,\n            Self::DuplicateScanningName => 0,\n            Self::DuplicateScanningSizeName => 0,\n            Self::DuplicateScanningSize => 0,\n            Self::DuplicatePreHashCacheLoading => 1,\n            Self::DuplicatePreHashing => 2,\n            Self::DuplicatePreHashCacheSaving => 3,\n            Self::DuplicateCacheLoading => 4,\n            Self::DuplicateFullHashing => 5,\n            Self::DuplicateCacheSaving => 6,\n            Self::SimilarImagesCalculatingHashes => 1,\n            Self::SimilarImagesComparingHashes => 2,\n            Self::SimilarVideosCalculatingHashes => 1,\n            Self::SimilarVideosCreatingThumbnails => 2,\n            Self::BrokenFilesChecking => 1,\n            Self::BadExtensionsChecking => 1,\n            Self::BadNamesChecking => 1,\n            Self::VideoOptimizerCreatingThumbnails => 2,\n            Self::VideoOptimizerProcessingVideos => 1,\n            Self::SameMusicCacheLoadingTags => 1,\n            Self::SameMusicReadingTags => 2,\n            Self::SameMusicCacheSavingTags => 3,\n            Self::SameMusicComparingTags => 4,\n            Self::SameMusicCacheLoadingFingerprints => 4,\n            Self::SameMusicCalculatingFingerprints => 5,\n            Self::SameMusicCacheSavingFingerprints => 6,\n            Self::SameMusicComparingFingerprints => 7,\n            Self::ExifRemoverCacheLoading => 1,\n            Self::ExifRemoverExtractingTags => 2,\n            Self::ExifRemoverCacheSaving => 3,\n        }\n    }\n    pub fn check_if_loading_saving_cache(self) -> bool {\n        self.check_if_saving_cache() || self.check_if_loading_cache()\n    }\n    pub fn check_if_loading_cache(self) -> bool {\n        matches!(\n            self,\n            Self::SameMusicCacheLoadingFingerprints\n                | Self::SameMusicCacheLoadingTags\n                | Self::DuplicateCacheLoading\n                | Self::DuplicatePreHashCacheLoading\n                | Self::ExifRemoverCacheLoading\n        )\n    }\n    pub fn check_if_saving_cache(self) -> bool {\n        matches!(\n            self,\n            Self::SameMusicCacheSavingFingerprints | Self::SameMusicCacheSavingTags | Self::DuplicateCacheSaving | Self::DuplicatePreHashCacheSaving | Self::ExifRemoverCacheSaving\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_tool_type_and_current_stage_integration() {\n        assert_eq!(ToolType::Duplicate.get_max_stage(CheckingMethod::Hash), 6);\n        assert_eq!(ToolType::SameMusic.get_max_stage(CheckingMethod::AudioTags), 4);\n        assert_eq!(ToolType::SameMusic.get_max_stage(CheckingMethod::AudioContent), 7);\n        assert_eq!(ToolType::SimilarImages.get_max_stage(CheckingMethod::None), 2);\n        assert_eq!(ToolType::BrokenFiles.get_max_stage(CheckingMethod::None), 1);\n\n        assert_eq!(CurrentStage::DuplicateFullHashing.get_current_stage(), 5);\n        assert_eq!(CurrentStage::SameMusicComparingFingerprints.get_current_stage(), 7);\n        assert!(CurrentStage::DeletingFiles.is_special_non_tool_stage());\n        assert!(!CurrentStage::CollectingFiles.is_special_non_tool_stage());\n    }\n\n    #[test]\n    fn test_cache_operations_detection() {\n        assert!(CurrentStage::DuplicateCacheLoading.check_if_loading_cache());\n        assert!(CurrentStage::DuplicateCacheSaving.check_if_saving_cache());\n        assert!(CurrentStage::SameMusicCacheLoadingTags.check_if_loading_saving_cache());\n        assert!(!CurrentStage::DuplicateFullHashing.check_if_loading_saving_cache());\n    }\n\n    #[test]\n    fn test_progress_data_validation_and_empty_state() {\n        let empty = ProgressData::get_empty_state(CurrentStage::CollectingFiles);\n        assert_eq!(empty.entries_checked, 0);\n        assert_eq!(empty.tool_type, ToolType::None);\n\n        let valid = ProgressData {\n            sstage: CurrentStage::DuplicateFullHashing,\n            checking_method: CheckingMethod::Hash,\n            current_stage_idx: 5,\n            max_stage_idx: 6,\n            entries_checked: 50,\n            entries_to_check: 100,\n            bytes_checked: 1000,\n            bytes_to_check: 2000,\n            tool_type: ToolType::Duplicate,\n        };\n        valid.validate();\n    }\n\n    #[test]\n    #[should_panic(expected = \"Current stage index\")]\n    fn test_validation_invalid_stage_idx() {\n        ProgressData {\n            sstage: CurrentStage::DuplicateFullHashing,\n            checking_method: CheckingMethod::Hash,\n            current_stage_idx: 7,\n            max_stage_idx: 6,\n            entries_checked: 0,\n            entries_to_check: 100,\n            bytes_checked: 0,\n            bytes_to_check: 1000,\n            tool_type: ToolType::Duplicate,\n        }\n        .validate();\n    }\n\n    #[test]\n    #[should_panic(expected = \"Entries checked\")]\n    fn test_validation_too_many_entries() {\n        ProgressData {\n            sstage: CurrentStage::DuplicateFullHashing,\n            checking_method: CheckingMethod::Hash,\n            current_stage_idx: 5,\n            max_stage_idx: 6,\n            entries_checked: 150,\n            entries_to_check: 100,\n            bytes_checked: 0,\n            bytes_to_check: 1000,\n            tool_type: ToolType::Duplicate,\n        }\n        .validate();\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/progress_stop_handler.rs",
    "content": "use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize};\nuse std::sync::{Arc, atomic};\nuse std::thread;\nuse std::thread::{JoinHandle, sleep};\nuse std::time::{Duration, Instant};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::model::{CheckingMethod, ToolType};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\npub const LOOP_DURATION: u32 = 20;\npub const SEND_PROGRESS_DATA_TIME_BETWEEN: u32 = 200;\n\npub(crate) struct ProgressThreadHandler {\n    progress_thread_handle: JoinHandle<()>,\n    progress_thread_running: Arc<AtomicBool>,\n    progress_status: ProgressStatus,\n}\nimpl ProgressThreadHandler {\n    pub fn new(progress_thread_handle: JoinHandle<()>, progress_thread_running: Arc<AtomicBool>, progress_status: ProgressStatus) -> Self {\n        Self {\n            progress_thread_handle,\n            progress_thread_running,\n            progress_status,\n        }\n    }\n    pub fn join_thread(self) {\n        self.progress_thread_running.store(false, atomic::Ordering::Relaxed);\n        self.progress_thread_handle\n            .join()\n            .expect(\"Cannot join progress thread - quite fatal error, but I hope, that it will never happen :)\");\n    }\n    pub fn increase_items(&self, count: usize) {\n        self.progress_status.items_counter.fetch_add(count, atomic::Ordering::Relaxed);\n    }\n    pub fn increase_size(&self, size: u64) {\n        self.progress_status.size_counter.fetch_add(size, atomic::Ordering::Relaxed);\n    }\n    pub fn items_counter(&self) -> &Arc<AtomicUsize> {\n        &self.progress_status.items_counter\n    }\n    pub fn size_counter(&self) -> &Arc<AtomicU64> {\n        &self.progress_status.size_counter\n    }\n}\n\n#[derive(Clone)]\npub(crate) struct ProgressStatus {\n    items_counter: Arc<AtomicUsize>,\n    size_counter: Arc<AtomicU64>,\n}\nimpl ProgressStatus {\n    pub fn new() -> Self {\n        Self {\n            items_counter: Arc::new(AtomicUsize::new(0)),\n            size_counter: Arc::new(AtomicU64::new(0)),\n        }\n    }\n}\n\npub(crate) fn prepare_thread_handler_common(\n    progress_sender: Option<&Sender<ProgressData>>,\n    sstage: CurrentStage,\n    max_items: usize,\n    test_type: (ToolType, CheckingMethod),\n    max_size: u64,\n) -> ProgressThreadHandler {\n    let (tool_type, checking_method) = test_type;\n    assert_ne!(tool_type, ToolType::None, \"Cannot send progress data for ToolType::None\");\n    let progress_status = ProgressStatus::new();\n    let progress_thread_running = Arc::new(AtomicBool::new(true));\n\n    let progress_thread_sender = if let Some(progress_sender) = progress_sender.cloned() {\n        let progress_status = progress_status.clone();\n        let progress_thread_running = progress_thread_running.clone();\n        thread::spawn(move || {\n            // Use earlier time, to send immediately first message\n            let mut time_since_last_send = Instant::now().checked_sub(Duration::from_secs(10u64)).unwrap_or_else(Instant::now);\n\n            loop {\n                if time_since_last_send.elapsed().as_millis() > SEND_PROGRESS_DATA_TIME_BETWEEN as u128 {\n                    let progress_data = ProgressData {\n                        sstage,\n                        checking_method,\n                        current_stage_idx: sstage.get_current_stage(),\n                        max_stage_idx: tool_type.get_max_stage(checking_method),\n                        entries_checked: progress_status.items_counter.load(atomic::Ordering::Relaxed),\n                        entries_to_check: max_items,\n                        bytes_checked: progress_status.size_counter.load(atomic::Ordering::Relaxed),\n                        bytes_to_check: max_size,\n                        tool_type,\n                    };\n\n                    progress_data.validate();\n\n                    progress_sender.send(progress_data).expect(\"Cannot send progress data\");\n                    time_since_last_send = Instant::now();\n                }\n                if !progress_thread_running.load(atomic::Ordering::Relaxed) {\n                    break;\n                }\n                sleep(Duration::from_millis(LOOP_DURATION as u64));\n            }\n        })\n    } else {\n        thread::spawn(|| {})\n    };\n    ProgressThreadHandler::new(progress_thread_sender, progress_thread_running, progress_status)\n}\n\n#[inline]\npub(crate) fn check_if_stop_received(stop_flag: &Arc<AtomicBool>) -> bool {\n    stop_flag.load(atomic::Ordering::Relaxed)\n}\n\n#[fun_time(message = \"send_info_and_wait_for_ending_all_threads\", level = \"debug\")]\npub(crate) fn send_info_and_wait_for_ending_all_threads(progress_thread_run: &Arc<AtomicBool>, progress_thread_handle: JoinHandle<()>) {\n    progress_thread_run.store(false, atomic::Ordering::Relaxed);\n    progress_thread_handle.join().expect(\"Cannot join progress thread - quite fatal error, but happens rarely\");\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_progress_status_and_stop_flag() {\n        let status = ProgressStatus::new();\n        assert_eq!(status.items_counter.load(atomic::Ordering::Relaxed), 0);\n        assert_eq!(status.size_counter.load(atomic::Ordering::Relaxed), 0);\n\n        status.items_counter.fetch_add(10, atomic::Ordering::Relaxed);\n        status.size_counter.fetch_add(1024, atomic::Ordering::Relaxed);\n\n        assert_eq!(status.items_counter.load(atomic::Ordering::Relaxed), 10);\n        assert_eq!(status.size_counter.load(atomic::Ordering::Relaxed), 1024);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        assert!(!check_if_stop_received(&stop_flag));\n        stop_flag.store(true, atomic::Ordering::Relaxed);\n        assert!(check_if_stop_received(&stop_flag));\n    }\n\n    #[test]\n    fn test_progress_thread_handler_with_sender() {\n        let (sender, _receiver) = crossbeam_channel::unbounded();\n        let handler = prepare_thread_handler_common(Some(&sender), CurrentStage::DuplicateFullHashing, 100, (ToolType::Duplicate, CheckingMethod::Hash), 10000);\n\n        assert_eq!(handler.items_counter().load(atomic::Ordering::Relaxed), 0);\n        assert_eq!(handler.size_counter().load(atomic::Ordering::Relaxed), 0);\n\n        handler.increase_items(5);\n        handler.increase_size(512);\n        handler.increase_items(3);\n        handler.increase_size(256);\n\n        assert_eq!(handler.items_counter().load(atomic::Ordering::Relaxed), 8);\n        assert_eq!(handler.size_counter().load(atomic::Ordering::Relaxed), 768);\n\n        handler.join_thread();\n    }\n\n    #[test]\n    fn test_progress_thread_handler_without_sender() {\n        let handler = prepare_thread_handler_common(None, CurrentStage::CollectingFiles, 50, (ToolType::EmptyFiles, CheckingMethod::None), 5000);\n\n        handler.increase_items(10);\n        handler.increase_size(1000);\n\n        assert_eq!(handler.items_counter().load(atomic::Ordering::Relaxed), 10);\n        assert_eq!(handler.size_counter().load(atomic::Ordering::Relaxed), 1000);\n\n        handler.join_thread();\n    }\n\n    #[test]\n    #[should_panic(expected = \"Cannot send progress data for ToolType::None\")]\n    fn test_panics_on_none_tool_type() {\n        prepare_thread_handler_common(None, CurrentStage::CollectingFiles, 50, (ToolType::None, CheckingMethod::None), 5000);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/tool_data.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Duration;\n\nuse crossbeam_channel::Sender;\nuse humansize::{BINARY, format_size};\nuse log::info;\nuse rayon::prelude::*;\n\nuse crate::common::directories::Directories;\nuse crate::common::extensions::Extensions;\nuse crate::common::items::ExcludedItems;\nuse crate::common::model::{CheckingMethod, ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::check_if_stop_received;\nuse crate::common::traits::ResultEntry;\nuse crate::common::{make_hard_link, remove_folder_if_contains_only_empty_folders, remove_single_file};\nuse crate::helpers::delayed_sender::DelayedSender;\nuse crate::helpers::messages::Messages;\n\n#[derive(Debug, Clone, Default)]\npub struct CommonToolData {\n    pub(crate) tool_type: ToolType,\n    pub(crate) text_messages: Messages,\n    pub(crate) directories: Directories,\n    pub(crate) extensions: Extensions,\n    pub(crate) excluded_items: ExcludedItems,\n    pub(crate) recursive_search: bool,\n    pub(crate) delete_method: DeleteMethod,\n    pub(crate) maximal_file_size: u64,\n    pub(crate) minimal_file_size: u64,\n    pub(crate) stopped_search: bool,\n    pub(crate) use_cache: bool,\n    pub(crate) delete_outdated_cache: bool,\n    pub(crate) save_also_as_json: bool,\n    pub(crate) use_reference_folders: bool,\n    pub(crate) dry_run: bool,\n    pub(crate) move_to_trash: bool,\n    pub(crate) hide_hard_links: bool,\n}\n\n#[derive(Debug, Clone, Default)]\npub struct DeleteResult {\n    deleted_files: usize,\n    gained_bytes: u64,\n    failed_to_delete_files: usize,\n    errors: Vec<String>,\n    infos: Vec<String>,\n}\n\nimpl DeleteResult {\n    pub(crate) fn add_to_messages(&self, messages: &mut Messages) {\n        messages.errors.extend(self.errors.clone());\n        messages.messages.extend(self.infos.clone());\n    }\n}\n\n#[derive(Debug, Clone, Eq, PartialEq)]\npub enum DeleteItemType<T: ResultEntry + Sized + Send + Sync> {\n    DeletingFiles(Vec<T>),\n    DeletingFolders(Vec<T>),\n    HardlinkingFiles(Vec<(T, Vec<T>)>),\n}\n\nimpl<T: ResultEntry + Sized + Send + Sync> DeleteItemType<T> {\n    fn calculate_size_to_delete(&self) -> u64 {\n        match &self {\n            Self::DeletingFiles(items) | Self::DeletingFolders(items) => items.iter().map(|item| item.get_size()).sum(),\n            Self::HardlinkingFiles(items) => items.iter().map(|(item, _)| item.get_size()).sum(),\n        }\n    }\n\n    fn calculate_entries_to_delete(&self) -> usize {\n        match &self {\n            Self::DeletingFiles(items) | Self::DeletingFolders(items) => items.len(),\n            Self::HardlinkingFiles(items) => items.iter().map(|(_original, files)| files.len()).sum(),\n        }\n    }\n}\n\n#[derive(Eq, PartialEq, Clone, Debug, Copy, Default)]\npub enum DeleteMethod {\n    #[default]\n    None,\n    Delete, // Just delete items\n    AllExceptNewest,\n    AllExceptOldest,\n    OneOldest,\n    OneNewest,\n    HardLink,\n    AllExceptBiggest,\n    AllExceptSmallest,\n    OneBiggest,\n    OneSmallest,\n}\n\nimpl CommonToolData {\n    pub fn new(tool_type: ToolType) -> Self {\n        Self {\n            tool_type,\n            text_messages: Messages::new(),\n            directories: Directories::new(),\n            extensions: Extensions::new(),\n            excluded_items: ExcludedItems::new(),\n            recursive_search: true,\n            delete_method: DeleteMethod::None,\n            maximal_file_size: u64::MAX,\n            minimal_file_size: 0,\n            stopped_search: false,\n            use_cache: true,\n            delete_outdated_cache: true,\n            save_also_as_json: false,\n            use_reference_folders: false,\n            dry_run: false,\n            move_to_trash: false,\n            hide_hard_links: false,\n        }\n    }\n}\n\npub trait CommonData {\n    type Info;\n    type Parameters;\n\n    fn get_information(&self) -> Self::Info;\n    fn get_params(&self) -> Self::Parameters;\n\n    fn get_cd(&self) -> &CommonToolData;\n    fn get_cd_mut(&mut self) -> &mut CommonToolData;\n    fn get_check_method(&self) -> CheckingMethod {\n        CheckingMethod::None\n    }\n    fn get_test_type(&self) -> (ToolType, CheckingMethod) {\n        (self.get_cd().tool_type, self.get_check_method())\n    }\n    fn found_any_items(&self) -> bool;\n\n    fn get_tool_type(&self) -> ToolType {\n        self.get_cd().tool_type\n    }\n\n    fn set_hide_hard_links(&mut self, hide_hard_links: bool) {\n        self.get_cd_mut().hide_hard_links = hide_hard_links;\n    }\n    fn get_hide_hard_links(&self) -> bool {\n        self.get_cd().hide_hard_links\n    }\n\n    fn set_dry_run(&mut self, dry_run: bool) {\n        self.get_cd_mut().dry_run = dry_run;\n    }\n    fn get_dry_run(&self) -> bool {\n        self.get_cd().dry_run\n    }\n\n    fn set_use_cache(&mut self, use_cache: bool) {\n        self.get_cd_mut().use_cache = use_cache;\n    }\n    fn get_use_cache(&self) -> bool {\n        self.get_cd().use_cache\n    }\n\n    fn set_delete_outdated_cache(&mut self, delete_outdated_cache: bool) {\n        self.get_cd_mut().delete_outdated_cache = delete_outdated_cache;\n    }\n    fn get_delete_outdated_cache(&self) -> bool {\n        self.get_cd().delete_outdated_cache\n    }\n\n    fn get_stopped_search(&self) -> bool {\n        self.get_cd().stopped_search\n    }\n    fn set_stopped_search(&mut self, stopped_search: bool) {\n        self.get_cd_mut().stopped_search = stopped_search;\n    }\n\n    fn set_maximal_file_size(&mut self, maximal_file_size: u64) {\n        self.get_cd_mut().maximal_file_size = match maximal_file_size {\n            0 => 1,\n            t => t,\n        };\n    }\n    fn get_maximal_file_size(&self) -> u64 {\n        self.get_cd().maximal_file_size\n    }\n\n    fn set_minimal_file_size(&mut self, minimal_file_size: u64) {\n        self.get_cd_mut().minimal_file_size = match minimal_file_size {\n            0 => 1,\n            t => t,\n        };\n    }\n    fn get_minimal_file_size(&self) -> u64 {\n        self.get_cd().minimal_file_size\n    }\n\n    #[cfg(target_family = \"unix\")]\n    fn set_exclude_other_filesystems(&mut self, exclude_other_filesystems: bool) {\n        self.get_cd_mut().directories.set_exclude_other_filesystems(exclude_other_filesystems);\n    }\n    #[cfg(not(target_family = \"unix\"))]\n    fn set_exclude_other_filesystems(&mut self, _exclude_other_filesystems: bool) {}\n\n    fn get_text_messages(&self) -> &Messages {\n        &self.get_cd().text_messages\n    }\n    fn get_text_messages_mut(&mut self) -> &mut Messages {\n        &mut self.get_cd_mut().text_messages\n    }\n\n    fn set_save_also_as_json(&mut self, save_also_as_json: bool) {\n        self.get_cd_mut().save_also_as_json = save_also_as_json;\n    }\n    fn get_save_also_as_json(&self) -> bool {\n        self.get_cd().save_also_as_json\n    }\n\n    fn set_recursive_search(&mut self, recursive_search: bool) {\n        self.get_cd_mut().recursive_search = recursive_search;\n    }\n    fn get_recursive_search(&self) -> bool {\n        self.get_cd().recursive_search\n    }\n\n    fn set_use_reference_folders(&mut self, use_reference_folders: bool) {\n        self.get_cd_mut().use_reference_folders = use_reference_folders;\n    }\n    fn get_use_reference_folders(&self) -> bool {\n        self.get_cd().use_reference_folders\n    }\n\n    fn set_delete_method(&mut self, delete_method: DeleteMethod) {\n        self.get_cd_mut().delete_method = delete_method;\n    }\n    fn get_delete_method(&self) -> DeleteMethod {\n        self.get_cd().delete_method\n    }\n\n    // Only used for internal deleting - probably only useful in CLI, but not in GUI which probably uses its own delete method selection\n    fn set_move_to_trash(&mut self, move_to_trash: bool) {\n        self.get_cd_mut().move_to_trash = move_to_trash;\n    }\n    fn get_move_to_trash(&self) -> bool {\n        self.get_cd().move_to_trash\n    }\n\n    fn set_included_paths(&mut self, included_paths: Vec<PathBuf>) {\n        let messages = self.get_cd_mut().directories.set_included_paths(included_paths);\n        self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n    }\n\n    fn set_excluded_paths(&mut self, excluded_paths: Vec<PathBuf>) {\n        let messages = self.get_cd_mut().directories.set_excluded_paths(excluded_paths);\n        self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n    }\n\n    fn set_reference_paths(&mut self, reference_paths: Vec<PathBuf>) {\n        let messages = self.get_cd_mut().directories.set_reference_paths(reference_paths);\n        self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n    }\n\n    fn set_allowed_extensions(&mut self, allowed_extensions: Vec<String>) {\n        let messages = self.get_cd_mut().extensions.set_allowed_extensions(allowed_extensions);\n        self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n    }\n\n    fn set_excluded_extensions(&mut self, excluded_extensions: Vec<String>) {\n        let messages = self.get_cd_mut().extensions.set_excluded_extensions(excluded_extensions);\n        self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n    }\n\n    fn set_excluded_items(&mut self, excluded_items: Vec<String>) {\n        let messages = self.get_cd_mut().excluded_items.set_excluded_items(excluded_items);\n        self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n    }\n\n    fn get_extensions_mut(&mut self) -> &mut Extensions {\n        &mut self.get_cd_mut().extensions\n    }\n\n    #[expect(clippy::result_unit_err)]\n    fn prepare_items(&mut self, tool_extensions: Option<&[&str]>) -> Result<(), ()> {\n        let recursive_search = self.get_cd().recursive_search;\n        // Optimizes directories and removes recursive calls\n        match self.get_cd_mut().directories.optimize_directories(recursive_search, false) {\n            Ok(messages) => {\n                self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n            }\n            Err(messages) => {\n                self.get_cd_mut().text_messages.extend_with_another_messages(messages);\n                return Err(());\n            }\n        }\n\n        if let Err(e) = self.get_extensions_mut().set_and_validate_extensions(tool_extensions) {\n            self.get_cd_mut().text_messages.critical = Some(e);\n            return Err(());\n        }\n\n        Ok(())\n    }\n\n    fn delete_simple_elements_and_add_to_messages<T: ResultEntry + Sized + Send + Sync>(\n        &mut self,\n        stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n        delete_item_type: DeleteItemType<T>,\n    ) -> WorkContinueStatus {\n        let delete_results = self.delete_elements(stop_flag, progress_sender, delete_item_type);\n\n        if check_if_stop_received(stop_flag) {\n            WorkContinueStatus::Stop\n        } else {\n            delete_results.add_to_messages(self.get_text_messages_mut());\n            WorkContinueStatus::Continue\n        }\n    }\n\n    #[expect(clippy::indexing_slicing)] // Safe, because input is always checked to have at least 1 element\n    fn delete_advanced_elements_and_add_to_messages<T: ResultEntry + Sized + Send + Sync + Clone>(\n        &mut self,\n        stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n        files_to_process: Vec<Vec<T>>,\n    ) -> WorkContinueStatus {\n        let delete_method = self.get_cd().delete_method;\n        let sorting_by_size = matches!(\n            delete_method,\n            DeleteMethod::AllExceptBiggest | DeleteMethod::AllExceptSmallest | DeleteMethod::OneBiggest | DeleteMethod::OneSmallest\n        );\n        let sort_items = |mut input: Vec<T>| -> Vec<T> {\n            input.sort_unstable_by_key(if sorting_by_size { ResultEntry::get_size } else { ResultEntry::get_modified_date });\n            input\n        };\n\n        let delete_results = if delete_method == DeleteMethod::HardLink {\n            let res = files_to_process\n                .into_iter()\n                .map(|values| {\n                    let mut all_values = sort_items(values);\n                    let original = all_values.remove(0);\n                    (original, all_values)\n                })\n                .collect::<Vec<_>>();\n            self.delete_elements(stop_flag, progress_sender, DeleteItemType::HardlinkingFiles(res))\n        } else {\n            let res = files_to_process\n                .into_iter()\n                .flat_map(|values| {\n                    // TODO - probably a little too much cloning, so later could be this optimized\n                    let len = values.len();\n                    let all_values = sort_items(values);\n                    match delete_method {\n                        DeleteMethod::Delete => &all_values,\n                        DeleteMethod::AllExceptNewest | DeleteMethod::AllExceptBiggest => &all_values[..(len - 1)],\n                        DeleteMethod::AllExceptOldest | DeleteMethod::AllExceptSmallest => &all_values[1..],\n                        DeleteMethod::OneOldest | DeleteMethod::OneSmallest => &all_values[..1],\n                        DeleteMethod::OneNewest | DeleteMethod::OneBiggest => &all_values[(len - 1)..],\n                        DeleteMethod::HardLink | DeleteMethod::None => unreachable!(\"HardLink and None should be handled before\"),\n                    }\n                    .to_vec()\n                })\n                .collect::<Vec<_>>();\n            self.delete_elements(stop_flag, progress_sender, DeleteItemType::DeletingFiles(res))\n        };\n\n        if check_if_stop_received(stop_flag) {\n            WorkContinueStatus::Stop\n        } else {\n            delete_results.add_to_messages(self.get_text_messages_mut());\n            WorkContinueStatus::Continue\n        }\n    }\n\n    fn delete_elements<T: ResultEntry + Sized + Send + Sync>(\n        &self,\n        stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n        delete_item_type: DeleteItemType<T>,\n    ) -> DeleteResult {\n        let dry_run = self.get_cd().dry_run;\n        let move_to_trash = self.get_cd().move_to_trash;\n        let mut progress = ProgressData::get_empty_state(CurrentStage::DeletingFiles);\n        progress.bytes_to_check = delete_item_type.calculate_size_to_delete();\n        progress.entries_to_check = delete_item_type.calculate_entries_to_delete();\n\n        let is_hardlinking = matches!(delete_item_type, DeleteItemType::HardlinkingFiles(_));\n\n        let msg_common = format!(\n            \"{} items, total size: {} bytes, dry_run: {dry_run}\",\n            progress.entries_to_check,\n            format_size(progress.bytes_to_check, BINARY)\n        );\n        if is_hardlinking {\n            info!(\"Hardlinking {msg_common}\");\n        } else {\n            info!(\"Deleting {msg_common}\");\n        }\n\n        let delayed_sender = progress_sender.map(|e| DelayedSender::new(e.clone(), Duration::from_millis(200)));\n\n        let bytes_processed = Arc::new(std::sync::atomic::AtomicU64::new(0));\n        let files_processed = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n\n        let res = match delete_item_type {\n            DeleteItemType::DeletingFiles(ref items) | DeleteItemType::DeletingFolders(ref items) => items\n                .into_par_iter()\n                .map(|e| {\n                    if check_if_stop_received(stop_flag) {\n                        return None;\n                    }\n\n                    let mut progress_tmp = progress;\n                    progress_tmp.bytes_checked = bytes_processed.fetch_add(e.get_size(), std::sync::atomic::Ordering::Relaxed);\n                    progress_tmp.entries_checked = files_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n\n                    if let Some(e) = delayed_sender.as_ref() {\n                        e.send(progress_tmp);\n                    }\n\n                    if dry_run {\n                        return Some(vec![(e, None)]);\n                    }\n\n                    let delete_res = if matches!(delete_item_type, DeleteItemType::DeletingFiles(_)) {\n                        remove_single_file(e.get_path(), move_to_trash)\n                    } else {\n                        remove_folder_if_contains_only_empty_folders(e.get_path(), move_to_trash)\n                    };\n\n                    match delete_res {\n                        Ok(()) => Some(vec![(e, None)]),\n                        Err(err) => Some(vec![(e, Some(err))]),\n                    }\n                })\n                .while_some()\n                .flatten()\n                .collect::<Vec<_>>(),\n            DeleteItemType::HardlinkingFiles(ref items) => items\n                .into_par_iter()\n                .map(|(original, files)| {\n                    if check_if_stop_received(stop_flag) {\n                        return None;\n                    }\n\n                    let mut progress_tmp = progress;\n                    progress_tmp.bytes_checked = bytes_processed.fetch_add(files.iter().map(|e| e.get_size()).sum(), std::sync::atomic::Ordering::Relaxed);\n                    progress_tmp.entries_checked = files_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n\n                    if let Some(e) = delayed_sender.as_ref() {\n                        e.send(progress_tmp);\n                    }\n\n                    if dry_run {\n                        return Some(files.iter().map(|e| (e, None)).collect::<Vec<_>>());\n                    }\n\n                    let res = files\n                        .iter()\n                        .map(|file| {\n                            let err = match make_hard_link(original.get_path(), file.get_path()) {\n                                Ok(()) => None,\n                                Err(err) => Some(format!(\n                                    \"Failed to hardlink \\\"{}\\\" to \\\"{}\\\": {err}\",\n                                    original.get_path().to_string_lossy(),\n                                    file.get_path().to_string_lossy()\n                                )),\n                            };\n                            (file, err)\n                        })\n                        .collect::<Vec<_>>();\n\n                    Some(res)\n                })\n                .while_some()\n                .flatten()\n                .collect::<Vec<_>>(),\n        };\n\n        let mut delete_result = DeleteResult::default();\n\n        for (file_entry, delete_err) in res {\n            if let Some(err) = delete_err {\n                delete_result.errors.push(err);\n                delete_result.failed_to_delete_files += 1;\n            } else {\n                if dry_run {\n                    if is_hardlinking {\n                        delete_result.infos.push(format!(\n                            \"Would hardlink: \\\"{}\\\" to \\\"{}\\\"\",\n                            file_entry.get_path().to_string_lossy(),\n                            file_entry.get_path().to_string_lossy()\n                        ));\n                    } else {\n                        delete_result.infos.push(format!(\"Would delete: \\\"{}\\\"\", file_entry.get_path().to_string_lossy()));\n                    }\n                }\n                delete_result.deleted_files += 1;\n                delete_result.gained_bytes += file_entry.get_size();\n            }\n        }\n\n        if !dry_run {\n            let action = if is_hardlinking { \"hardlink\" } else { \"delete\" };\n            let action2 = if is_hardlinking { \"hardlinked\" } else { \"deleted\" };\n            info!(\n                \"{} items {action2}, {} gained, {} failed to {action}\",\n                delete_result.deleted_files,\n                format_size(delete_result.gained_bytes, BINARY),\n                delete_result.failed_to_delete_files\n            );\n        }\n\n        delete_result\n    }\n\n    #[expect(clippy::print_stdout)]\n    fn debug_print_common(&self) {\n        println!(\"---------------DEBUG PRINT COMMON---------------\");\n        println!(\"Included paths(before optimization) - {:?}\", self.get_cd().directories.original_included_paths);\n        println!(\"Excluded paths(before optimization) - {:?}\", self.get_cd().directories.original_excluded_paths);\n        println!(\"Reference paths(before optimization) - {:?}\", self.get_cd().directories.original_reference_paths);\n        println!(\"Included directories(optimized) - {:?}\", self.get_cd().directories.included_directories);\n        println!(\"Included files(optimized) - {:?}\", self.get_cd().directories.included_files);\n        println!(\"Excluded directories(optimized) - {:?}\", self.get_cd().directories.excluded_directories);\n        println!(\"Excluded files(optimized) - {:?}\", self.get_cd().directories.excluded_files);\n        println!(\"Reference directories(optimized) - {:?}\", self.get_cd().directories.reference_directories);\n        println!(\"Reference files(optimized) - {:?}\", self.get_cd().directories.reference_files);\n        println!(\"Tool type: {:?}\", self.get_cd().tool_type);\n        println!(\"Directories: {:?}\", self.get_cd().directories);\n        println!(\"Extensions: {:?}\", self.get_cd().extensions);\n        println!(\"Excluded items: {:?}\", self.get_cd().excluded_items);\n        println!(\"Recursive search: {}\", self.get_cd().recursive_search);\n        println!(\"Maximal file size: {}\", self.get_cd().maximal_file_size);\n        println!(\"Minimal file size: {}\", self.get_cd().minimal_file_size);\n        println!(\"Stopped search: {}\", self.get_cd().stopped_search);\n        println!(\"Use cache: {}\", self.get_cd().use_cache);\n        println!(\"Delete outdated cache: {}\", self.get_cd().delete_outdated_cache);\n        println!(\"Save also as json: {}\", self.get_cd().save_also_as_json);\n        println!(\"Delete method: {:?}\", self.get_cd().delete_method);\n        println!(\"Use reference folders: {}\", self.get_cd().use_reference_folders);\n        println!(\"Dry run: {}\", self.get_cd().dry_run);\n        println!(\"Hide hard links: {}\", self.get_cd().hide_hard_links);\n\n        println!(\"---------------DEBUG PRINT MESSAGES---------------\");\n        println!(\"Errors size - {}\", self.get_cd().text_messages.errors.len());\n        println!(\"Warnings size - {}\", self.get_cd().text_messages.warnings.len());\n        println!(\"Messages size - {}\", self.get_cd().text_messages.messages.len());\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::fs;\n\n    use tempfile::TempDir;\n\n    use super::*;\n    use crate::common::model::FileEntry;\n\n    // Mock implementation for testing\n    struct MockTool {\n        common_data: CommonToolData,\n    }\n\n    impl CommonData for MockTool {\n        type Info = ();\n        type Parameters = ();\n\n        fn get_information(&self) -> Self::Info {}\n        fn get_params(&self) -> Self::Parameters {}\n        fn get_cd(&self) -> &CommonToolData {\n            &self.common_data\n        }\n        fn get_cd_mut(&mut self) -> &mut CommonToolData {\n            &mut self.common_data\n        }\n        fn found_any_items(&self) -> bool {\n            false\n        }\n    }\n\n    impl MockTool {\n        fn new() -> Self {\n            Self {\n                common_data: CommonToolData::new(ToolType::Duplicate),\n            }\n        }\n    }\n\n    #[test]\n    fn test_delete_result_add_to_messages() {\n        let delete_result = DeleteResult {\n            deleted_files: 5,\n            gained_bytes: 1024,\n            failed_to_delete_files: 2,\n            errors: vec![\"Error 1\".to_string(), \"Error 2\".to_string()],\n            infos: vec![\"Info 1\".to_string()],\n        };\n\n        let mut messages = Messages::new();\n        delete_result.add_to_messages(&mut messages);\n\n        assert_eq!(messages.errors.len(), 2);\n        assert_eq!(messages.messages.len(), 1);\n        assert!(messages.errors.contains(&\"Error 1\".to_string()));\n        assert!(messages.messages.contains(&\"Info 1\".to_string()));\n    }\n\n    #[test]\n    fn test_delete_item_type_calculate_size_and_entries() {\n        let files = vec![\n            FileEntry {\n                path: PathBuf::from(\"/a\"),\n                size: 100,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: PathBuf::from(\"/b\"),\n                size: 200,\n                modified_date: 2,\n            },\n            FileEntry {\n                path: PathBuf::from(\"/c\"),\n                size: 300,\n                modified_date: 3,\n            },\n        ];\n\n        let delete_files = DeleteItemType::DeletingFiles(files.clone());\n        assert_eq!(delete_files.calculate_size_to_delete(), 600);\n        assert_eq!(delete_files.calculate_entries_to_delete(), 3);\n\n        let delete_folders = DeleteItemType::DeletingFolders(files.clone());\n        assert_eq!(delete_folders.calculate_size_to_delete(), 600);\n        assert_eq!(delete_folders.calculate_entries_to_delete(), 3);\n\n        let hardlink_files = DeleteItemType::HardlinkingFiles(vec![\n            (files[0].clone(), vec![files[1].clone()]),\n            (files[2].clone(), vec![files[0].clone(), files[1].clone()]),\n        ]);\n        assert_eq!(hardlink_files.calculate_size_to_delete(), 400);\n        assert_eq!(hardlink_files.calculate_entries_to_delete(), 3);\n    }\n\n    #[test]\n    fn test_common_tool_data_new() {\n        let tool_data = CommonToolData::new(ToolType::Duplicate);\n        assert_eq!(tool_data.tool_type, ToolType::Duplicate);\n        assert_eq!(tool_data.delete_method, DeleteMethod::None);\n        assert_eq!(tool_data.maximal_file_size, u64::MAX);\n        assert_eq!(tool_data.minimal_file_size, 0);\n        assert!(tool_data.recursive_search);\n        assert!(!tool_data.stopped_search);\n        assert!(tool_data.use_cache);\n        assert!(tool_data.delete_outdated_cache);\n        assert!(!tool_data.save_also_as_json);\n        assert!(!tool_data.use_reference_folders);\n        assert!(!tool_data.dry_run);\n    }\n\n    #[test]\n    fn test_delete_elements_dry_run() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        fs::write(&file1, \"test content 1\").unwrap();\n        fs::write(&file2, \"test content 2\").unwrap();\n\n        let files = vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 14,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 14,\n                modified_date: 2,\n            },\n        ];\n\n        let mut tool = MockTool::new();\n        tool.common_data.dry_run = true;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files));\n\n        assert_eq!(delete_result.deleted_files, 2, \"Should mark 2 files as deleted\");\n        assert_eq!(delete_result.failed_to_delete_files, 0, \"Should have no failed deletions\");\n        assert_eq!(delete_result.gained_bytes, 28, \"Should calculate gained bytes\");\n        assert_eq!(delete_result.infos.len(), 2, \"Should have 2 info messages in dry run\");\n        assert!(file1.exists(), \"File should still exist in dry run\");\n        assert!(file2.exists(), \"File should still exist in dry run\");\n    }\n\n    #[test]\n    fn test_delete_elements_actual_deletion() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        fs::write(&file1, \"test content 1\").unwrap();\n        fs::write(&file2, \"test content 2\").unwrap();\n\n        let files = vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 14,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 14,\n                modified_date: 2,\n            },\n        ];\n\n        let tool = MockTool::new();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files));\n\n        assert_eq!(delete_result.deleted_files, 2, \"Should delete 2 files\");\n        assert_eq!(delete_result.failed_to_delete_files, 0, \"Should have no failed deletions\");\n        assert_eq!(delete_result.gained_bytes, 28, \"Should gain 28 bytes\");\n        assert!(!file1.exists(), \"File 1 should be deleted\");\n        assert!(!file2.exists(), \"File 2 should be deleted\");\n    }\n\n    #[test]\n    fn test_delete_elements_with_stop_flag() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        fs::write(&file1, \"test content\").unwrap();\n\n        let files = vec![FileEntry {\n            path: file1.clone(),\n            size: 12,\n            modified_date: 1,\n        }];\n\n        let tool = MockTool::new();\n        let stop_flag = Arc::new(AtomicBool::new(true)); // Stop flag set to true\n        let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files));\n\n        assert_eq!(delete_result.deleted_files, 0, \"Should not delete any files when stopped\");\n        assert!(file1.exists(), \"File should still exist\");\n    }\n\n    #[test]\n    fn test_delete_elements_nonexistent_file() {\n        let temp_dir = TempDir::new().unwrap();\n        let nonexistent_file = temp_dir.path().join(\"nonexistent.txt\");\n\n        let files = vec![FileEntry {\n            path: nonexistent_file,\n            size: 100,\n            modified_date: 1,\n        }];\n\n        let tool = MockTool::new();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files));\n\n        assert_eq!(delete_result.deleted_files, 0, \"Should not delete nonexistent file\");\n        assert_eq!(delete_result.failed_to_delete_files, 1, \"Should report 1 failed deletion\");\n        assert_eq!(delete_result.errors.len(), 1, \"Should have 1 error message\");\n    }\n\n    #[test]\n    fn test_delete_simple_elements_and_add_to_messages() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        fs::write(&file1, \"content1\").unwrap();\n        fs::write(&file2, \"content2\").unwrap();\n\n        let files = vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 8,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 8,\n                modified_date: 2,\n            },\n        ];\n\n        let mut tool = MockTool::new();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_simple_elements_and_add_to_messages(&stop_flag, None, DeleteItemType::DeletingFiles(files));\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(!file1.exists(), \"File 1 should be deleted\");\n        assert!(!file2.exists(), \"File 2 should be deleted\");\n        assert_eq!(tool.common_data.text_messages.errors.len(), 0, \"Should have no errors\");\n    }\n\n    #[test]\n    fn test_delete_simple_elements_with_stop_flag() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        fs::write(&file1, \"content\").unwrap();\n\n        let files = vec![FileEntry {\n            path: file1.clone(),\n            size: 7,\n            modified_date: 1,\n        }];\n\n        let mut tool = MockTool::new();\n        let stop_flag = Arc::new(AtomicBool::new(true));\n        let status = tool.delete_simple_elements_and_add_to_messages(&stop_flag, None, DeleteItemType::DeletingFiles(files));\n\n        assert_eq!(status, WorkContinueStatus::Stop, \"Should stop\");\n        assert!(file1.exists(), \"File should still exist\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_all_except_newest() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n        fs::write(&file3, \"c\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 1,\n                modified_date: 2,\n            },\n            FileEntry {\n                path: file3.clone(),\n                size: 1,\n                modified_date: 3,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::AllExceptNewest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(!file1.exists(), \"Oldest file should be deleted\");\n        assert!(!file2.exists(), \"Middle file should be deleted\");\n        assert!(file3.exists(), \"Newest file should be kept\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_all_except_oldest() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n        fs::write(&file3, \"c\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 1,\n                modified_date: 2,\n            },\n            FileEntry {\n                path: file3.clone(),\n                size: 1,\n                modified_date: 3,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::AllExceptOldest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(file1.exists(), \"Oldest file should be kept\");\n        assert!(!file2.exists(), \"Middle file should be deleted\");\n        assert!(!file3.exists(), \"Newest file should be deleted\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_one_oldest() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n        fs::write(&file3, \"c\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 1,\n                modified_date: 2,\n            },\n            FileEntry {\n                path: file3.clone(),\n                size: 1,\n                modified_date: 3,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::OneOldest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(!file1.exists(), \"Oldest file should be deleted\");\n        assert!(file2.exists(), \"Middle file should be kept\");\n        assert!(file3.exists(), \"Newest file should be kept\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_one_newest() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n        fs::write(&file3, \"c\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 1,\n                modified_date: 2,\n            },\n            FileEntry {\n                path: file3.clone(),\n                size: 1,\n                modified_date: 3,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::OneNewest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(file1.exists(), \"Oldest file should be kept\");\n        assert!(file2.exists(), \"Middle file should be kept\");\n        assert!(!file3.exists(), \"Newest file should be deleted\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_all_except_biggest() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"bb\").unwrap();\n        fs::write(&file3, \"ccc\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 2,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file3.clone(),\n                size: 3,\n                modified_date: 1,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::AllExceptBiggest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(!file1.exists(), \"Smallest file should be deleted\");\n        assert!(!file2.exists(), \"Middle file should be deleted\");\n        assert!(file3.exists(), \"Biggest file should be kept\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_all_except_smallest() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"bb\").unwrap();\n        fs::write(&file3, \"ccc\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 2,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file3.clone(),\n                size: 3,\n                modified_date: 1,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::AllExceptSmallest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(file1.exists(), \"Smallest file should be kept\");\n        assert!(!file2.exists(), \"Middle file should be deleted\");\n        assert!(!file3.exists(), \"Biggest file should be deleted\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_delete_all() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1.clone(),\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2.clone(),\n                size: 1,\n                modified_date: 2,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::Delete;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(!file1.exists(), \"All files should be deleted\");\n        assert!(!file2.exists(), \"All files should be deleted\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_multiple_groups() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        let file3 = temp_dir.path().join(\"file3.txt\");\n        let file4 = temp_dir.path().join(\"file4.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n        fs::write(&file3, \"c\").unwrap();\n        fs::write(&file4, \"d\").unwrap();\n\n        let files_group = vec![\n            vec![\n                FileEntry {\n                    path: file1.clone(),\n                    size: 1,\n                    modified_date: 1,\n                },\n                FileEntry {\n                    path: file2.clone(),\n                    size: 1,\n                    modified_date: 2,\n                },\n            ],\n            vec![\n                FileEntry {\n                    path: file3.clone(),\n                    size: 1,\n                    modified_date: 1,\n                },\n                FileEntry {\n                    path: file4.clone(),\n                    size: 1,\n                    modified_date: 2,\n                },\n            ],\n        ];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::AllExceptNewest;\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Continue, \"Should continue\");\n        assert!(!file1.exists(), \"Oldest from group 1 should be deleted\");\n        assert!(file2.exists(), \"Newest from group 1 should be kept\");\n        assert!(!file3.exists(), \"Oldest from group 2 should be deleted\");\n        assert!(file4.exists(), \"Newest from group 2 should be kept\");\n    }\n\n    #[test]\n    fn test_delete_advanced_elements_with_stop_flag() {\n        let temp_dir = TempDir::new().unwrap();\n        let file1 = temp_dir.path().join(\"file1.txt\");\n        let file2 = temp_dir.path().join(\"file2.txt\");\n        fs::write(&file1, \"a\").unwrap();\n        fs::write(&file2, \"b\").unwrap();\n\n        let files_group = vec![vec![\n            FileEntry {\n                path: file1,\n                size: 1,\n                modified_date: 1,\n            },\n            FileEntry {\n                path: file2,\n                size: 1,\n                modified_date: 2,\n            },\n        ]];\n\n        let mut tool = MockTool::new();\n        tool.common_data.delete_method = DeleteMethod::AllExceptNewest;\n\n        let stop_flag = Arc::new(AtomicBool::new(true));\n        let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group);\n\n        assert_eq!(status, WorkContinueStatus::Stop, \"Should stop\");\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/common/traits.rs",
    "content": "use std::fs::File;\nuse std::io::{BufWriter, Write};\nuse std::path::Path;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse serde::Serialize;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::CommonData;\n\npub trait DebugPrint {\n    fn debug_print(&self);\n}\n\npub trait PrintResults: CommonData {\n    fn write_base_search_paths<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        let dirs = &self.get_cd().directories;\n        let included_paths = dirs.included_files.iter().chain(dirs.included_directories.iter()).collect::<Vec<_>>();\n        let excluded_paths = dirs.excluded_files.iter().chain(dirs.excluded_directories.iter()).collect::<Vec<_>>();\n        let reference_paths = dirs.reference_files.iter().chain(dirs.reference_directories.iter()).collect::<Vec<_>>();\n        let excluded_items = self.get_cd().excluded_items.get_excluded_items();\n        if self.get_cd().tool_type.may_use_reference_paths() {\n            writeln!(\n                writer,\n                \"Results of searching {included_paths:?} with reference paths {reference_paths:?}, excluded paths {excluded_paths:?} and excluded items {excluded_items:?}\"\n            )?;\n            writeln!(\n                writer,\n                \"(Before optimizations - included paths: {:?}, excluded paths: {:?}, reference paths: {:?})\",\n                dirs.original_included_paths, dirs.original_excluded_paths, dirs.original_reference_paths\n            )?;\n        } else {\n            writeln!(\n                writer,\n                \"Results of searching {included_paths:?} with excluded paths {excluded_paths:?} and excluded items {excluded_items:?}\"\n            )?;\n            writeln!(\n                writer,\n                \"(Before optimizations - included paths: {:?}, excluded paths: {:?})\",\n                dirs.original_included_paths, dirs.original_excluded_paths\n            )?;\n        }\n\n        Ok(())\n    }\n\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()>;\n\n    #[fun_time(message = \"print_results_to_output\", level = \"debug\")]\n    fn print_results_to_output(&self) {\n        let stdout = std::io::stdout();\n        let mut handle = stdout.lock();\n        // Panics here are allowed, because it is used only in CLI\n        self.write_results(&mut handle).expect(\"Error while writing to stdout\");\n        handle.flush().expect(\"Error while flushing stdout\");\n    }\n\n    #[fun_time(message = \"print_results_to_file\", level = \"debug\")]\n    fn print_results_to_file(&self, file_name: &str) -> std::io::Result<()> {\n        let file_name: String = match file_name {\n            \"\" => \"results.txt\".to_string(),\n            k => k.to_string(),\n        };\n\n        let file_handler = File::create(file_name)?;\n        let mut writer = BufWriter::new(file_handler);\n        self.write_results(&mut writer)?;\n        writer.flush()?;\n        Ok(())\n    }\n\n    #[fun_time(message = \"print_results_to_writer\", level = \"debug\")]\n    fn print_results_to_writer<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_results(writer)\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()>;\n\n    fn save_results_to_file_as_json_internal<T: Serialize + std::fmt::Debug>(&self, file_name: &str, item_to_serialize: &T, pretty_print: bool) -> std::io::Result<()> {\n        if pretty_print {\n            self.save_results_to_file_as_json_pretty(file_name, item_to_serialize)\n        } else {\n            self.save_results_to_file_as_json_compact(file_name, item_to_serialize)\n        }\n    }\n\n    #[fun_time(message = \"save_results_to_file_as_json_pretty\", level = \"debug\")]\n    fn save_results_to_file_as_json_pretty<T: Serialize + std::fmt::Debug>(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> {\n        let file_handler = File::create(file_name)?;\n        let mut writer = BufWriter::new(file_handler);\n        serde_json::to_writer_pretty(&mut writer, item_to_serialize)?;\n        Ok(())\n    }\n\n    #[fun_time(message = \"save_results_to_file_as_json_compact\", level = \"debug\")]\n    fn save_results_to_file_as_json_compact<T: Serialize + std::fmt::Debug>(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> {\n        let file_handler = File::create(file_name)?;\n        let mut writer = BufWriter::new(file_handler);\n        serde_json::to_writer(&mut writer, item_to_serialize)?;\n        Ok(())\n    }\n\n    fn save_all_in_one(&self, folder: &str, base_file_name: &str) -> std::io::Result<()> {\n        let pretty_name = format!(\"{folder}/{base_file_name}_pretty.json\");\n        self.save_results_to_file_as_json(&pretty_name, true)?;\n        let compact_name = format!(\"{folder}/{base_file_name}_compact.json\");\n        self.save_results_to_file_as_json(&compact_name, false)?;\n        let txt_name = format!(\"{folder}/{base_file_name}.txt\");\n        self.print_results_to_file(&txt_name)?;\n        Ok(())\n    }\n}\n\npub trait DeletingItems {\n    #[must_use]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus;\n}\n\npub trait FixingItems {\n    type FixParams;\n\n    fn fix_items(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>, fix_params: Self::FixParams);\n}\n\npub trait ResultEntry {\n    fn get_path(&self) -> &Path;\n    fn get_modified_date(&self) -> u64;\n    fn get_size(&self) -> u64;\n}\n\npub trait Search {\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>);\n}\n\npub trait AllTraits: DebugPrint + PrintResults + DeletingItems + CommonData + Search {}\n"
  },
  {
    "path": "czkawka_core/src/common/video_utils.rs",
    "content": "use std::fs;\nuse std::path::{Path, PathBuf};\nuse std::process::{Command, Stdio};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse blake3::Hasher;\nuse image::{GenericImage, RgbImage};\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::consts::VIDEO_RESOLUTION_LIMIT;\nuse crate::common::process_utils::disable_windows_console_window;\nuse crate::common::progress_stop_handler::check_if_stop_received;\nuse crate::flc;\nuse crate::helpers::ffprobe::ffprobe;\n\npub const VIDEO_THUMBNAILS_SUBFOLDER: &str = \"video_thumbnails\";\n\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct VideoMetadata {\n    pub fps: Option<f64>,\n    pub codec: Option<String>,\n    pub bitrate: Option<u64>,\n    pub width: Option<u32>,\n    pub height: Option<u32>,\n    pub duration: Option<f64>,\n}\n\nimpl VideoMetadata {\n    pub fn from_path(path: &Path) -> Result<Self, String> {\n        let info = ffprobe(path).map_err(|e| flc!(\"core_failed_to_read_video_properties\", reason = e.to_string()))?;\n\n        let mut metadata = Self::default();\n\n        if let Some(duration_str) = &info.format.duration\n            && let Ok(d) = duration_str.parse::<f64>()\n        {\n            metadata.duration = Some(d);\n        }\n\n        if let Some(stream) = info.streams.into_iter().find(|s| s.codec_type.as_deref() == Some(\"video\")) {\n            metadata.codec = stream.codec_name;\n\n            if let Some(bit_rate_str) = stream.bit_rate.or(info.format.bit_rate)\n                && let Ok(b) = bit_rate_str.parse::<u64>()\n            {\n                metadata.bitrate = Some(b);\n            }\n\n            if let Some(w) = stream.width\n                && w >= 0\n            {\n                if w > VIDEO_RESOLUTION_LIMIT as i64 {\n                    return Err(flc!(\"core_video_width_exceeds_limit\", width = w, limit = VIDEO_RESOLUTION_LIMIT));\n                }\n                metadata.width = Some(w as u32);\n            }\n            if let Some(h) = stream.height\n                && h >= 0\n            {\n                if h > VIDEO_RESOLUTION_LIMIT as i64 {\n                    return Err(flc!(\"core_video_height_exceeds_limit\", height = h, limit = VIDEO_RESOLUTION_LIMIT));\n                }\n                metadata.height = Some(h as u32);\n            }\n\n            let fps_opt = if !stream.avg_frame_rate.is_empty() && stream.avg_frame_rate != \"0/0\" {\n                Some(stream.avg_frame_rate)\n            } else if !stream.r_frame_rate.is_empty() && stream.r_frame_rate != \"0/0\" {\n                Some(stream.r_frame_rate)\n            } else {\n                None\n            };\n\n            if let Some(fps_str) = fps_opt {\n                let fps_val = if fps_str.contains('/') {\n                    let mut parts = fps_str.splitn(2, '/');\n                    if let (Some(n), Some(d)) = (parts.next(), parts.next()) {\n                        if let (Ok(nv), Ok(dv)) = (n.parse::<f64>(), d.parse::<f64>()) {\n                            if dv != 0.0 { Some(nv / dv) } else { None }\n                        } else {\n                            None\n                        }\n                    } else {\n                        None\n                    }\n                } else {\n                    fps_str.parse::<f64>().ok()\n                };\n\n                if let Some(fps_v) = fps_val {\n                    metadata.fps = Some(fps_v);\n                }\n            }\n        }\n\n        Ok(metadata)\n    }\n}\n\npub(crate) fn extract_frame_ffmpeg(video_path: &Path, timestamp: f32, max_values: Option<(u32, u32)>) -> Result<RgbImage, String> {\n    // This function returns strange status 234, when path contains non default UTF-8 characters, not sure why\n    if !video_path.exists() {\n        return Err(flc!(\"core_video_file_does_not_exist\", path = video_path.to_string_lossy()));\n    }\n\n    let mut command = Command::new(\"ffmpeg\");\n    let command_mut = &mut command;\n\n    disable_windows_console_window(command_mut);\n\n    command_mut.arg(\"-threads\").arg(\"1\").arg(\"-ss\").arg(timestamp.to_string()).arg(\"-i\").arg(video_path);\n\n    if let Some((max_width, max_height)) = max_values {\n        let vf_filter = format!(\"scale='min({max_width},iw)':'min({max_height},ih)':force_original_aspect_ratio=decrease\");\n        command_mut.arg(\"-vf\").arg(&vf_filter);\n    }\n\n    let output = command_mut\n        .arg(\"-vframes\")\n        .arg(\"1\")\n        .arg(\"-f\")\n        .arg(\"image2pipe\")\n        .arg(\"-vcodec\")\n        .arg(\"png\")\n        .arg(\"pipe:1\")\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::null())\n        .output()\n        .map_err(|e| flc!(\"core_failed_to_execute_ffmpeg\", reason = e.to_string()))?;\n\n    if !output.status.success() {\n        let stderr = String::from_utf8_lossy(&output.stderr).replace(\"\\r\\n\", \"\\n\").replace(\"\\n\", \" \");\n        return Err(flc!(\n            \"core_ffmpeg_failed_with_status\",\n            status = output.status.to_string(),\n            stderr = stderr,\n            command = format!(\"{:?}\", command)\n        ));\n    }\n\n    let img = image::load_from_memory(&output.stdout).map_err(|e| flc!(\"core_failed_to_load_image_frame\", reason = e.to_string()))?;\n\n    Ok(img.into_rgb8())\n}\n\npub fn generate_thumbnail(\n    stop_flag: &Arc<AtomicBool>,\n    video_path: &Path,\n    size: u64,\n    modified_date: u64,\n    duration: Option<f64>,\n    thumbnails_dir: &Path,\n    thumbnail_video_percentage_from_start: u8,\n    generate_grid_instead_of_single: bool,\n    thumbnail_grid_tiles_per_side: u8,\n    generate_thumbnails: bool,\n) -> Result<Option<PathBuf>, String> {\n    let mut hasher = Hasher::new();\n\n    if generate_grid_instead_of_single {\n        hasher.update(format!(\"{size}___{modified_date}___{}___GRID_{thumbnail_grid_tiles_per_side}\", video_path.to_string_lossy()).as_bytes());\n    } else {\n        hasher.update(\n            format!(\n                \"{thumbnail_video_percentage_from_start}___{size}___{modified_date}___{}___SINGLE\",\n                video_path.to_string_lossy()\n            )\n            .as_bytes(),\n        );\n    }\n    let hash = hasher.finalize();\n    let thumbnail_filename = format!(\"{}.jpg\", hash.to_hex());\n    let thumbnail_path = thumbnails_dir.join(thumbnail_filename);\n\n    if thumbnail_path.exists() {\n        let _ = filetime::set_file_mtime(&thumbnail_path, filetime::FileTime::now());\n        return Ok(Some(thumbnail_path));\n    }\n\n    if !generate_thumbnails {\n        return Ok(None);\n    }\n\n    let seek_time = duration.map_or(5.0, |d| d * (thumbnail_video_percentage_from_start as f64) / 100.0);\n    let duration_per_tile_items = duration.map_or(0.5, |d| d / (thumbnail_grid_tiles_per_side * thumbnail_grid_tiles_per_side + 2) as f64);\n\n    let max_height = 1080 / thumbnail_grid_tiles_per_side as u32;\n    let max_width = 1920 / thumbnail_grid_tiles_per_side as u32;\n\n    if generate_grid_instead_of_single {\n        let frame_times = (0..(thumbnail_grid_tiles_per_side * thumbnail_grid_tiles_per_side))\n            .map(|i| duration_per_tile_items as f32 * (i + 1) as f32)\n            .collect::<Vec<f32>>();\n        let mut imgs = Vec::new();\n        for ft in frame_times {\n            if check_if_stop_received(stop_flag) {\n                return Err(flc!(\"core_thumbnail_generation_stopped_by_user\"));\n            }\n\n            match extract_frame_ffmpeg(video_path, ft, Some((max_width, max_height))) {\n                Ok(img) => imgs.push(img),\n                Err(e) => {\n                    let _ = fs::write(&thumbnail_path, b\"\");\n                    return Err(flc!(\"core_failed_to_extract_frame\", time = ft, file = video_path.to_string_lossy(), reason = e));\n                }\n            }\n        }\n        assert_eq!(imgs.len(), (thumbnail_grid_tiles_per_side * thumbnail_grid_tiles_per_side) as usize);\n\n        let first_img = &imgs.first().expect(\"Cannot be empty here, because at least tiles_size^2 images are extracted\");\n\n        if imgs.iter().any(|img| img.height() != first_img.height() || img.width() != first_img.width()) {\n            let _ = fs::write(&thumbnail_path, b\"\");\n            return Err(flc!(\"core_failed_to_generate_thumbnail_frames_different_dimensions\", file = video_path.to_string_lossy()));\n        }\n        let mut new_thumbnail = RgbImage::new(\n            first_img.width() * thumbnail_grid_tiles_per_side as u32,\n            first_img.height() * thumbnail_grid_tiles_per_side as u32,\n        );\n        for (idx, img) in imgs.iter().enumerate() {\n            let x = (idx % thumbnail_grid_tiles_per_side as usize) as u32 * img.width();\n            let y = (idx / thumbnail_grid_tiles_per_side as usize) as u32 * img.height();\n            new_thumbnail\n                .copy_from(img, x, y)\n                .map_err(|e| flc!(\"core_failed_to_generate_thumbnail\", file = video_path.to_string_lossy(), reason = e.to_string()))?;\n        }\n\n        if let Err(e) = new_thumbnail.save(&thumbnail_path) {\n            let _ = fs::write(&thumbnail_path, b\"\");\n            return Err(flc!(\"core_failed_to_save_thumbnail\", file = video_path.to_string_lossy(), reason = e.to_string()));\n        }\n    } else {\n        match extract_frame_ffmpeg(video_path, seek_time as f32, Some((max_width, max_height))) {\n            Ok(img) => {\n                if let Err(e) = img.save(&thumbnail_path) {\n                    let _ = fs::write(&thumbnail_path, b\"\");\n                    return Err(flc!(\"core_failed_to_save_thumbnail\", file = video_path.to_string_lossy(), reason = e.to_string()));\n                }\n            }\n            Err(e) => {\n                let _ = fs::write(&thumbnail_path, b\"\");\n                return Err(flc!(\n                    \"core_failed_to_extract_frame_at_seek_time\",\n                    time = seek_time,\n                    file = video_path.to_string_lossy(),\n                    reason = e\n                ));\n            }\n        }\n    }\n    Ok(Some(thumbnail_path))\n}\n"
  },
  {
    "path": "czkawka_core/src/helpers/audio_checker.rs",
    "content": "use std::fs::File;\nuse std::io;\n\nuse symphonia::core::codecs::CODEC_TYPE_NULL;\nuse symphonia::core::errors::Error;\nuse symphonia::core::errors::Error::IoError;\nuse symphonia::core::io::MediaSourceStream;\n\npub fn parse_audio_file(file_handler: File) -> Result<(), Error> {\n    let mss = MediaSourceStream::new(Box::new(file_handler), Default::default());\n\n    let Ok(probed) = symphonia::default::get_probe().format(&Default::default(), mss, &Default::default(), &Default::default()) else {\n        return Err(Error::Unsupported(\"probe info not available/file not recognized\"));\n    };\n\n    let mut format = probed.format;\n\n    let Some(track) = format.tracks().iter().find(|t| t.codec_params.codec != CODEC_TYPE_NULL) else {\n        return Err(Error::Unsupported(\"not supported audio track\"));\n    };\n\n    let Ok(mut decoder) = symphonia::default::get_codecs().make(&track.codec_params, &Default::default()) else {\n        return Err(Error::Unsupported(\"not supported codec\"));\n    };\n\n    loop {\n        let packet = match format.next_packet() {\n            Ok(packet) => packet,\n            Err(Error::ResetRequired) => {\n                return Err(Error::ResetRequired);\n            }\n            Err(err) => {\n                if let IoError(ref er) = err {\n                    // Catch eof, not sure how to do it properly\n                    if er.kind() == io::ErrorKind::UnexpectedEof {\n                        return Ok(());\n                    }\n                }\n                return Err(err);\n            }\n        };\n\n        decoder.decode(&packet)?;\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/helpers/debug_timer.rs",
    "content": "use std::time::{Duration, Instant};\n\n/// Timer for measuring elapsed time between checkpoints.\n///\n/// # How to use - examples\n///\n/// Basic usage:\n/// ```\n/// use czkawka_core::helpers::debug_timer::Timer;\n/// use std::thread::sleep;\n/// use std::time::Duration;\n///\n/// let mut timer = Timer::new(\"MyTimer\");\n/// sleep(Duration::from_millis(50));\n/// timer.checkpoint(\"step1\");\n/// sleep(Duration::from_millis(30));\n/// timer.checkpoint(\"step2\");\n/// let report = timer.report(\"all_steps\", false);\n/// println!(\"{}\", report);\n/// ```\n///\n/// Output example:\n/// ```text\n/// MyTimer - step1: 50.0ms,\n/// MyTimer - step2: 30.0ms,\n/// MyTimer - all_steps: 80.0ms\n/// ```\n///\n/// One-line output:\n/// ```\n/// use czkawka_core::helpers::debug_timer::Timer;\n/// use std::thread::sleep;\n/// use std::time::Duration;\n///\n/// let mut timer = Timer::new(\"MyTimer\");\n/// sleep(Duration::from_millis(10));\n/// timer.checkpoint(\"a\");\n/// sleep(Duration::from_millis(20));\n/// timer.checkpoint(\"b\");\n/// let report = timer.report(\"total\", true);\n/// println!(\"{}\", report);\n/// ```\n///\n/// Output example:\n/// ```text\n/// MyTimer - a: 10.0ms, b: 20.0ms, total: 30.0ms\n/// ```\npub struct Timer {\n    /// Name or label for the timer.\n    base: String,\n    /// Time when the timer was started.\n    start_time: Instant,\n    /// Time of the last checkpoint.\n    last_time: Instant,\n    /// List of (checkpoint name, duration since last checkpoint).\n    times: Vec<(String, Duration)>,\n}\n\nimpl Timer {\n    /// Creates a new timer with a given label.\n    pub fn new(base: &str) -> Self {\n        Self {\n            base: base.to_string(),\n            start_time: Instant::now(),\n            last_time: Instant::now(),\n            times: Vec::new(),\n        }\n    }\n\n    /// Records a checkpoint with the given name.\n    pub fn checkpoint(&mut self, name: &str) {\n        let elapsed = self.last_time.elapsed();\n        self.times.push((name.to_string(), elapsed));\n        self.last_time = Instant::now();\n    }\n\n    /// Returns a formatted report of all checkpoints and total time.\n    ///\n    /// If `in_one_line` is true, outputs all checkpoints in a single line.\n    /// Otherwise, outputs each checkpoint on a separate line.\n    pub fn report(&mut self, all_steps_name: &str, in_one_line: bool) -> String {\n        let all_elapsed = self.start_time.elapsed();\n        self.times.push((all_steps_name.to_string(), all_elapsed));\n\n        if in_one_line {\n            let times = self.times.iter().map(|(name, time)| format!(\"{name}: {time:?}\")).collect::<Vec<_>>().join(\", \");\n            format!(\"{} - {}\", self.base, times)\n        } else {\n            self.times\n                .iter()\n                .map(|(name, time)| format!(\"{} - {name}: {time:?}\", self.base))\n                .collect::<Vec<_>>()\n                .join(\", \\n\")\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::thread::sleep;\n\n    use super::*;\n\n    #[test]\n    fn test_timer_basic_functionality() {\n        let mut timer = Timer::new(\"TestTimer\");\n        assert_eq!(timer.base, \"TestTimer\");\n        assert_eq!(timer.times.len(), 0);\n\n        sleep(Duration::from_millis(10));\n        timer.checkpoint(\"step1\");\n        assert_eq!(timer.times.len(), 1);\n        assert_eq!(timer.times[0].0, \"step1\");\n\n        sleep(Duration::from_millis(10));\n        timer.checkpoint(\"step2\");\n        assert_eq!(timer.times.len(), 2);\n        assert_eq!(timer.times[1].0, \"step2\");\n    }\n\n    #[test]\n    fn test_timer_report_multiline() {\n        let mut timer = Timer::new(\"MultilineTimer\");\n        sleep(Duration::from_millis(5));\n        timer.checkpoint(\"checkpoint1\");\n        sleep(Duration::from_millis(5));\n        timer.checkpoint(\"checkpoint2\");\n\n        let report = timer.report(\"total\", false);\n        assert!(report.contains(\"MultilineTimer - checkpoint1:\"));\n        assert!(report.contains(\"MultilineTimer - checkpoint2:\"));\n        assert!(report.contains(\"MultilineTimer - total:\"));\n        assert!(report.contains(\", \\n\"));\n    }\n\n    #[test]\n    fn test_timer_report_oneline() {\n        let mut timer = Timer::new(\"OnelineTimer\");\n        sleep(Duration::from_millis(5));\n        timer.checkpoint(\"a\");\n        sleep(Duration::from_millis(5));\n        timer.checkpoint(\"b\");\n\n        let report = timer.report(\"final\", true);\n        assert!(report.starts_with(\"OnelineTimer - \"));\n        assert!(report.contains(\"a:\"));\n        assert!(report.contains(\"b:\"));\n        assert!(report.contains(\"final:\"));\n        assert!(report.contains(\", \"));\n        assert!(!report.contains(\"\\n\"));\n    }\n\n    #[test]\n    fn test_timer_no_checkpoints() {\n        let mut timer = Timer::new(\"EmptyTimer\");\n        let report = timer.report(\"done\", false);\n        assert!(report.contains(\"EmptyTimer - done:\"));\n        assert_eq!(report.matches('\\n').count(), 0);\n    }\n\n    #[test]\n    fn test_timer_elapsed_time_accumulates() {\n        let mut timer = Timer::new(\"AccumulateTimer\");\n        sleep(Duration::from_millis(20));\n        timer.checkpoint(\"step1\");\n\n        assert!(timer.times[0].1.as_millis() >= 15);\n\n        sleep(Duration::from_millis(20));\n        timer.checkpoint(\"step2\");\n\n        assert!(timer.times[1].1.as_millis() >= 15);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/helpers/delayed_sender.rs",
    "content": "//! DelayedSender: A utility for batching or throttling messages sent between threads.\n\nuse std::sync::atomic::AtomicBool;\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::{Duration, Instant};\n\n/// A sender that delays sending values until a specified wait time has passed since the last sent value.\n///\n/// This is useful for batching updates or reducing the frequency of sending messages in a multi-threaded environment.\n/// Note: Using mutexes in the send function from multiple threads can lead to performance issues (waiting for mutex release),\n/// but for now, the performance impact is minimal. In the future, a more efficient channel could be used.\npub struct DelayedSender<T: Send + 'static> {\n    slot: Arc<Mutex<Option<T>>>,\n    stop_flag: Arc<AtomicBool>,\n}\n\nimpl<T: Send + 'static> DelayedSender<T> {\n    /// Creates a new DelayedSender.\n    ///\n    /// # Arguments\n    /// * `sender` - The channel sender to forward values to.\n    /// * `wait_time` - The minimum duration to wait between sends.\n    pub fn new(sender: crossbeam_channel::Sender<T>, wait_time: Duration) -> Self {\n        let slot = Arc::new(Mutex::new(None));\n        let slot_clone = Arc::clone(&slot);\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let stop_flag_clone = Arc::clone(&stop_flag);\n        let _join = thread::spawn(move || {\n            let mut last_send_time: Option<Instant> = None;\n            let duration_between_checks = Duration::from_secs_f64(wait_time.as_secs_f64() / 5.0);\n\n            loop {\n                if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) {\n                    break;\n                }\n                if let Some(last_send_time) = last_send_time\n                    && last_send_time.elapsed() < wait_time\n                {\n                    thread::sleep(duration_between_checks);\n                    continue;\n                }\n\n                let Some(value) = slot_clone.lock().expect(\"Failed to lock slot in DelayedSender\").take() else {\n                    thread::sleep(duration_between_checks);\n                    continue;\n                };\n\n                if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) {\n                    break;\n                }\n                if let Err(e) = sender.send(value) {\n                    log::error!(\"Failed to send value: {e:?}\");\n                }\n                last_send_time = Some(Instant::now());\n            }\n        });\n\n        Self { slot, stop_flag }\n    }\n\n    /// Sends a value, replacing any previous value that has not yet been sent.\n    pub fn send(&self, value: T) {\n        let mut slot = self.slot.lock().expect(\"Failed to lock slot in DelayedSender\");\n        *slot = Some(value);\n    }\n}\n\nimpl<T: Send + 'static> Drop for DelayedSender<T> {\n    fn drop(&mut self) {\n        // After dropping DelayedSender, no more values will be sent.\n        // Previously, some values were cached and sent after later operations.\n        self.stop_flag.store(true, std::sync::atomic::Ordering::Relaxed);\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_delayed_sender_basic_send() {\n        let (sender, receiver) = crossbeam_channel::unbounded();\n        let delayed_sender = DelayedSender::new(sender, Duration::from_millis(50));\n\n        delayed_sender.send(42);\n        thread::sleep(Duration::from_millis(100));\n\n        let result = receiver.try_recv();\n        result.unwrap();\n        assert_eq!(result.unwrap(), 42);\n    }\n\n    #[test]\n    fn test_delayed_sender_batching() {\n        let (sender, receiver) = crossbeam_channel::unbounded();\n        let delayed_sender = DelayedSender::new(sender, Duration::from_millis(100));\n\n        delayed_sender.send(1);\n        thread::sleep(Duration::from_millis(50));\n\n        let first = receiver.try_recv();\n        first.unwrap();\n        assert_eq!(first.unwrap(), 1);\n\n        delayed_sender.send(2);\n        thread::sleep(Duration::from_millis(10));\n        delayed_sender.send(3);\n        thread::sleep(Duration::from_millis(10));\n        delayed_sender.send(4);\n\n        thread::sleep(Duration::from_millis(150));\n\n        let result = receiver.try_recv();\n        result.unwrap();\n        assert_eq!(result.unwrap(), 4);\n\n        let result2 = receiver.try_recv();\n        result2.unwrap_err();\n    }\n\n    #[test]\n    fn test_delayed_sender_multiple_sends() {\n        let (sender, receiver) = crossbeam_channel::unbounded();\n        let delayed_sender = DelayedSender::new(sender, Duration::from_millis(50));\n\n        delayed_sender.send(10);\n        thread::sleep(Duration::from_millis(100));\n\n        delayed_sender.send(20);\n        thread::sleep(Duration::from_millis(100));\n\n        let first = receiver.try_recv();\n        first.unwrap();\n        assert_eq!(first.unwrap(), 10);\n\n        let second = receiver.try_recv();\n        second.unwrap();\n        assert_eq!(second.unwrap(), 20);\n    }\n\n    #[test]\n    fn test_delayed_sender_drop_stops_thread() {\n        let (sender, receiver) = crossbeam_channel::unbounded();\n        {\n            let delayed_sender = DelayedSender::new(sender, Duration::from_millis(50));\n            delayed_sender.send(100);\n        }\n\n        thread::sleep(Duration::from_millis(150));\n\n        let count = receiver.try_iter().count();\n        assert!(count <= 1);\n    }\n\n    #[test]\n    fn test_delayed_sender_no_send_without_wait() {\n        let (sender, receiver) = crossbeam_channel::unbounded();\n        let delayed_sender = DelayedSender::new(sender, Duration::from_millis(100));\n\n        delayed_sender.send(5);\n        thread::sleep(Duration::from_millis(20));\n\n        let first = receiver.try_recv();\n        first.unwrap();\n        assert_eq!(first.unwrap(), 5);\n\n        delayed_sender.send(10);\n        thread::sleep(Duration::from_millis(20));\n\n        let result = receiver.try_recv();\n        result.unwrap_err();\n\n        // But should be sent after full wait_time\n        thread::sleep(Duration::from_millis(100));\n        let result = receiver.try_recv();\n        result.unwrap();\n        assert_eq!(result.unwrap(), 10);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/helpers/ffprobe.rs",
    "content": "//! Simple wrapper for the [ffprobe](https://ffmpeg.org/ffprobe.html) CLI utility,\n//! which is part of the ffmpeg tool suite.\n//!\n//! This crate allows retrieving typed information about media files (images and videos)\n//! by invoking `ffprobe` with JSON output options and deserializing the data\n//! into convenient Rust types.\n//!\n//!\n//!\n//! ```rust, no_run\n//! use czkawka_core::helpers::ffprobe::ffprobe;\n//! match ffprobe(\"path/to/video.mp4\") {\n//!    Ok(info) => {\n//!        dbg!(info);\n//!    },\n//!    Err(err) => {\n//!        eprintln!(\"Could not analyze file with ffprobe: {:?}\", err);\n//!     },\n//! }\n//! ```\n//!\n//! CODE IS COPIED FROM https://github.com/theduke/ffprobe-rs\n//! I WILL BE ABLE TO AGAIN USE IT AFTER A NEW VERSION IS RELEASED\n//! https://github.com/theduke/ffprobe-rs/issues/33\n//! LICENSE: MIT\n\n#[cfg(target_os = \"windows\")]\nuse std::os::windows::process::CommandExt;\n\n/// Execute ffprobe with default settings and return the extracted data.\n///\n/// See [`ffprobe_config`] if you need to customize settings.\npub fn ffprobe(path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {\n    ffprobe_config(\n        Config {\n            count_frames: false,\n            ffprobe_bin: \"ffprobe\".into(),\n        },\n        path,\n    )\n}\n\n/// Run ffprobe with a custom config.\n/// See [`ConfigBuilder`] for more details.\npub fn ffprobe_config(config: Config, path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {\n    let path = path.as_ref();\n\n    let mut cmd = std::process::Command::new(config.ffprobe_bin);\n\n    // Default args.\n    cmd.args([\"-v\", \"error\", \"-show_format\", \"-show_streams\", \"-print_format\", \"json\"]);\n\n    if config.count_frames {\n        cmd.arg(\"-count_frames\");\n    }\n\n    cmd.arg(path);\n\n    // Prevent CMD popup on Windows.\n    #[cfg(target_os = \"windows\")]\n    cmd.creation_flags(0x08000000);\n\n    let out = cmd.output().map_err(FfProbeError::Io)?;\n\n    if !out.status.success() {\n        return Err(FfProbeError::Status(out));\n    }\n\n    serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)\n}\n\n/// ffprobe configuration.\n///\n/// Use [`Config::builder`] for constructing a new config.\n#[derive(Clone, Debug)]\npub struct Config {\n    count_frames: bool,\n    ffprobe_bin: std::path::PathBuf,\n}\n\nimpl Config {\n    /// Construct a new ConfigBuilder.\n    pub fn builder() -> ConfigBuilder {\n        ConfigBuilder::new()\n    }\n}\n\n/// Build the ffprobe configuration.\npub struct ConfigBuilder {\n    config: Config,\n}\n\nimpl ConfigBuilder {\n    pub fn new() -> Self {\n        Self {\n            config: Config {\n                count_frames: false,\n                ffprobe_bin: \"ffprobe\".into(),\n            },\n        }\n    }\n\n    /// Enable the -count_frames setting.\n    /// Will fully decode the file and count the frames.\n    /// Frame count will be available in [`Stream::nb_read_frames`].\n    pub fn count_frames(mut self, count_frames: bool) -> Self {\n        self.config.count_frames = count_frames;\n        self\n    }\n\n    /// Specify which binary name (e.g. `\"ffprobe-6\"`) or path (e.g. `\"/opt/bin/ffprobe\"`) to use\n    /// for executing `ffprobe`.\n    pub fn ffprobe_bin(mut self, ffprobe_bin: impl AsRef<std::path::Path>) -> Self {\n        self.config.ffprobe_bin = ffprobe_bin.as_ref().to_path_buf();\n        self\n    }\n\n    /// Finalize the builder into a [`Config`].\n    pub fn build(self) -> Config {\n        self.config\n    }\n\n    /// Run ffprobe with the config produced by this builder.\n    pub fn run(self, path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {\n        ffprobe_config(self.config, path)\n    }\n}\n\nimpl Default for ConfigBuilder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[derive(Debug)]\n#[non_exhaustive]\npub enum FfProbeError {\n    Io(std::io::Error),\n    Status(std::process::Output),\n    Deserialize(serde_json::Error),\n}\n\nimpl std::fmt::Display for FfProbeError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Io(e) => e.fmt(f),\n            Self::Status(o) => {\n                write!(f, \"ffprobe exited with status code {}: {}\", o.status, String::from_utf8_lossy(&o.stderr))\n            }\n            Self::Deserialize(e) => e.fmt(f),\n        }\n    }\n}\n\nimpl std::error::Error for FfProbeError {}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n\npub struct FfProbe {\n    pub streams: Vec<Stream>,\n    pub format: Format,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n\npub struct Stream {\n    pub index: i64,\n    pub codec_name: Option<String>,\n    pub sample_aspect_ratio: Option<String>,\n    pub display_aspect_ratio: Option<String>,\n    pub color_range: Option<String>,\n    pub color_space: Option<String>,\n    pub bits_per_raw_sample: Option<String>,\n    pub channel_layout: Option<String>,\n    pub max_bit_rate: Option<String>,\n    pub nb_frames: Option<String>,\n    /// Number of frames seen by the decoder.\n    /// Requires full decoding and is only available if the 'count_frames'\n    /// setting was enabled.\n    pub nb_read_frames: Option<String>,\n    pub codec_long_name: Option<String>,\n    pub codec_type: Option<String>,\n    pub codec_time_base: Option<String>,\n    pub codec_tag_string: String,\n    pub codec_tag: String,\n    pub sample_fmt: Option<String>,\n    pub sample_rate: Option<String>,\n    pub channels: Option<i64>,\n    pub bits_per_sample: Option<i64>,\n    pub r_frame_rate: String,\n    pub avg_frame_rate: String,\n    pub time_base: String,\n    pub start_pts: Option<i64>,\n    pub start_time: Option<String>,\n    pub duration_ts: Option<i64>,\n    pub duration: Option<String>,\n    pub bit_rate: Option<String>,\n    pub disposition: Disposition,\n    pub tags: Option<StreamTags>,\n    pub profile: Option<String>,\n    pub width: Option<i64>,\n    pub height: Option<i64>,\n    pub coded_width: Option<i64>,\n    pub coded_height: Option<i64>,\n    pub closed_captions: Option<i64>,\n    pub has_b_frames: Option<i64>,\n    pub pix_fmt: Option<String>,\n    pub level: Option<i64>,\n    pub chroma_location: Option<String>,\n    pub refs: Option<i64>,\n    pub is_avc: Option<String>,\n    pub nal_length: Option<String>,\n    pub nal_length_size: Option<String>,\n    pub field_order: Option<String>,\n    pub id: Option<String>,\n    #[serde(default)]\n    pub side_data_list: Vec<SideData>,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n// Allowed to prevent having to break compatibility of float fields are added.\n#[expect(clippy::derive_partial_eq_without_eq)]\npub struct SideData {\n    pub side_data_type: String,\n    pub rotation: Option<i16>,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n// Allowed to prevent having to break compatibility of float fields are added.\n#[expect(clippy::derive_partial_eq_without_eq)]\npub struct Disposition {\n    pub default: i64,\n    pub dub: i64,\n    pub original: i64,\n    pub comment: i64,\n    pub lyrics: i64,\n    pub karaoke: i64,\n    pub forced: i64,\n    pub hearing_impaired: i64,\n    pub visual_impaired: i64,\n    pub clean_effects: i64,\n    pub attached_pic: i64,\n    pub timed_thumbnails: i64,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n// Allowed to prevent having to break compatibility of float fields are added.\n#[expect(clippy::derive_partial_eq_without_eq)]\npub struct StreamTags {\n    pub language: Option<String>,\n    pub creation_time: Option<String>,\n    pub handler_name: Option<String>,\n    pub encoder: Option<String>,\n    pub timecode: Option<String>,\n    pub reel_name: Option<String>,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n\npub struct Format {\n    pub filename: String,\n    pub nb_streams: i64,\n    pub nb_programs: i64,\n    pub format_name: String,\n    pub format_long_name: Option<String>,\n    pub start_time: Option<String>,\n    pub duration: Option<String>,\n    pub size: Option<String>,\n    pub bit_rate: Option<String>,\n    pub probe_score: i64,\n    pub tags: Option<FormatTags>,\n}\n\nimpl Format {\n    /// Get the duration parsed into a [`std::time::Duration`].\n    pub fn try_get_duration(&self) -> Option<Result<std::time::Duration, std::num::ParseFloatError>> {\n        self.duration.as_ref().map(|duration| match duration.parse::<f64>() {\n            Ok(num) => Ok(std::time::Duration::from_secs_f64(num)),\n            Err(error) => Err(error),\n        })\n    }\n\n    /// Get the duration parsed into a [`std::time::Duration`].\n    ///\n    /// Will return [`None`] if no duration is available, or if parsing fails.\n    /// See [`Self::try_get_duration`] for a method that returns an error.\n    pub fn get_duration(&self) -> Option<std::time::Duration> {\n        self.try_get_duration()?.ok()\n    }\n}\n\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct FormatTags {\n    #[serde(rename = \"WMFSDKNeeded\")]\n    pub wmfsdkneeded: Option<String>,\n    #[serde(rename = \"DeviceConformanceTemplate\")]\n    pub device_conformance_template: Option<String>,\n    #[serde(rename = \"WMFSDKVersion\")]\n    pub wmfsdkversion: Option<String>,\n    #[serde(rename = \"IsVBR\")]\n    pub is_vbr: Option<String>,\n    pub major_brand: Option<String>,\n    pub minor_version: Option<String>,\n    pub compatible_brands: Option<String>,\n    pub creation_time: Option<String>,\n    pub encoder: Option<String>,\n\n    #[serde(flatten)]\n    pub extra: std::collections::HashMap<String, serde_json::Value>,\n}\n"
  },
  {
    "path": "czkawka_core/src/helpers/messages.rs",
    "content": "//! Messages: Utility for collecting and printing messages, warnings, and errors.\n\nuse crate::flc;\n\n/// Stores messages, warnings, and errors for reporting.\n#[derive(Debug, Default, Clone)]\npub struct Messages {\n    pub critical: Option<String>,\n    /// Informational messages.\n    pub messages: Vec<String>,\n    /// Warning messages.\n    pub warnings: Vec<String>,\n    /// Error messages.\n    pub errors: Vec<String>,\n}\n\n#[derive(Debug, Clone, Copy)]\npub enum MessageLimit {\n    NoLimit,\n    Characters(usize),\n    Lines(usize),\n}\n\nimpl Messages {\n    /// Creates a new, empty `Messages` struct.\n    pub fn new() -> Self {\n        Default::default()\n    }\n\n    /// Creates a new `Messages` struct with errors.\n    pub fn new_from_errors(errors: Vec<String>) -> Self {\n        Self { errors, ..Default::default() }\n    }\n\n    /// Creates a new `Messages` struct with warnings.\n    pub fn new_from_warnings(warnings: Vec<String>) -> Self {\n        Self { warnings, ..Default::default() }\n    }\n\n    /// Creates a new `Messages` struct with messages.\n    pub fn new_from_messages(messages: Vec<String>) -> Self {\n        Self { messages, ..Default::default() }\n    }\n\n    /// Prints all messages, warnings, and errors to the provided writer.\n    pub fn print_messages_to_writer<T: std::io::Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        let text = self.create_messages_text(MessageLimit::NoLimit);\n        writer.write_all(text.as_bytes())\n    }\n\n    /// Creates a formatted string containing all messages, warnings, and errors.\n    pub fn create_messages_text(&self, limit: MessageLimit) -> String {\n        let mut text_to_return: String = String::new();\n\n        if let Some(critical) = &self.critical {\n            text_to_return += \"------------------------------CRITICAL ERROR---------------------------\\n\";\n            text_to_return += critical;\n            text_to_return += \"\\n\";\n            text_to_return += \"--------------------------END OF CRITICAL ERROR------------------------\\n\";\n        }\n\n        if !self.errors.is_empty() {\n            text_to_return += \"--------------------------------ERRORS---------------------------------\\n\";\n            for i in &self.errors {\n                text_to_return += i;\n                text_to_return += \"\\n\";\n            }\n            text_to_return += \"----------------------------END OF ERRORS------------------------------\\n\";\n        }\n\n        if !self.messages.is_empty() {\n            text_to_return += \"-------------------------------MESSAGES--------------------------------\\n\";\n            for i in &self.messages {\n                text_to_return += i;\n                text_to_return += \"\\n\";\n            }\n            text_to_return += \"---------------------------END OF MESSAGES-----------------------------\\n\";\n        }\n\n        if !self.warnings.is_empty() {\n            text_to_return += \"-------------------------------WARNINGS--------------------------------\\n\";\n            for i in &self.warnings {\n                text_to_return += i;\n                text_to_return += \"\\n\";\n            }\n            text_to_return += \"---------------------------END OF WARNINGS-----------------------------\\n\";\n        }\n\n        let mut text_to_return = text_to_return.trim().to_string();\n        match limit {\n            MessageLimit::NoLimit => {}\n            MessageLimit::Characters(max_chars) => {\n                let char_count = text_to_return.chars().count();\n                if char_count > max_chars {\n                    let truncated: String = text_to_return.chars().take(max_chars).collect();\n                    text_to_return = truncated;\n                    text_to_return += \"\\n\\n\";\n                    text_to_return += &flc!(\"core_messages_limit_reached_characters\", current = char_count, limit = max_chars);\n                    text_to_return += \"\\n\";\n                }\n            }\n            MessageLimit::Lines(max_lines) => {\n                let line_count = text_to_return.lines().count();\n                if line_count > max_lines {\n                    let lines: Vec<&str> = text_to_return.lines().take(max_lines).collect();\n                    text_to_return = lines.join(\"\\n\");\n                    text_to_return += \"\\n\\n\";\n                    text_to_return += &flc!(\"core_messages_limit_reached_lines\", current = line_count, limit = max_lines);\n                    text_to_return += \"\\n\";\n                }\n            }\n        }\n\n        text_to_return\n    }\n\n    /// Extends this `Messages` struct with another, appending all messages, warnings, and errors.\n    pub fn extend_with_another_messages(&mut self, messages: Self) {\n        let (messages, warnings, errors, critical) = (messages.messages, messages.warnings, messages.errors, messages.critical);\n        self.messages.extend(messages);\n        self.warnings.extend(warnings);\n        self.errors.extend(errors);\n        if critical.is_some() {\n            self.critical = critical;\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_messages_constructors_and_text_formatting() {\n        // Test new()\n        let msg = Messages::new();\n        assert!(msg.messages.is_empty());\n        assert!(msg.warnings.is_empty());\n        assert!(msg.errors.is_empty());\n        assert_eq!(msg.create_messages_text(MessageLimit::NoLimit), \"\");\n\n        // Test new_from_errors()\n        let errors = vec![\"Error 1\".to_string(), \"Error 2\".to_string()];\n        let msg = Messages::new_from_errors(errors.clone());\n        assert_eq!(msg.errors, errors);\n        let text = msg.create_messages_text(MessageLimit::NoLimit);\n        assert!(text.contains(\"ERRORS\"));\n        assert!(text.contains(\"Error 1\"));\n\n        // Test new_from_warnings()\n        let warnings = vec![\"Warning 1\".to_string()];\n        let msg = Messages::new_from_warnings(warnings.clone());\n        assert_eq!(msg.warnings, warnings);\n        let text = msg.create_messages_text(MessageLimit::NoLimit);\n        assert!(text.contains(\"WARNINGS\"));\n\n        // Test new_from_messages()\n        let messages = vec![\"Message 1\".to_string()];\n        let msg = Messages::new_from_messages(messages.clone());\n        assert_eq!(msg.messages, messages);\n        let text = msg.create_messages_text(MessageLimit::NoLimit);\n        assert!(text.contains(\"MESSAGES\"));\n\n        // Test all types together\n        let mut msg = Messages::new();\n        msg.messages.push(\"Info\".to_string());\n        msg.warnings.push(\"Warn\".to_string());\n        msg.errors.push(\"Err\".to_string());\n        let text = msg.create_messages_text(MessageLimit::NoLimit);\n        assert!(text.contains(\"MESSAGES\"));\n        assert!(text.contains(\"Info\"));\n        assert!(text.contains(\"WARNINGS\"));\n        assert!(text.contains(\"Warn\"));\n        assert!(text.contains(\"ERRORS\"));\n        assert!(text.contains(\"Err\"));\n    }\n\n    #[test]\n    fn test_extend_and_writer() {\n        // Test extend_with_another_messages()\n        let mut msg1 = Messages::new();\n        msg1.messages.push(\"Msg1\".to_string());\n        msg1.warnings.push(\"Warn1\".to_string());\n        msg1.errors.push(\"Err1\".to_string());\n\n        let mut msg2 = Messages::new();\n        msg2.messages.push(\"Msg2\".to_string());\n        msg2.warnings.push(\"Warn2\".to_string());\n        msg2.errors.push(\"Err2\".to_string());\n\n        msg1.extend_with_another_messages(msg2);\n\n        assert_eq!(msg1.messages.len(), 2);\n        assert_eq!(msg1.warnings.len(), 2);\n        assert_eq!(msg1.errors.len(), 2);\n        assert!(msg1.messages.contains(&\"Msg1\".to_string()));\n        assert!(msg1.messages.contains(&\"Msg2\".to_string()));\n\n        // Test print_messages_to_writer()\n        let mut buffer = Vec::new();\n        let result = msg1.print_messages_to_writer(&mut buffer);\n        result.unwrap();\n\n        let output = String::from_utf8(buffer).unwrap();\n        assert!(output.contains(\"Msg1\"));\n        assert!(output.contains(\"Warn2\"));\n        assert!(output.contains(\"Err1\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/helpers/mod.rs",
    "content": "//! Helper modules: generic utilities, traits, structs, ready to copy/paste to other projects.\n\npub mod audio_checker;\npub mod debug_timer;\npub mod delayed_sender;\npub mod ffprobe;\npub mod messages;\n"
  },
  {
    "path": "czkawka_core/src/lib.rs",
    "content": "pub mod common;\npub mod helpers;\npub mod localizer_core;\npub mod tools;\n\npub mod re_exported {\n    pub use fast_image_resize::FilterType as FirFilterType;\n    pub use image_hasher::{FilterType, HashAlg};\n    pub use vid_dup_finder_lib::Cropdetect;\n}\n\npub const CZKAWKA_VERSION: &str = env!(\"CARGO_PKG_VERSION\");\npub const TOOLS_NUMBER: usize = 14;\n"
  },
  {
    "path": "czkawka_core/src/localizer_core.rs",
    "content": "use std::collections::HashMap;\n\nuse i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};\nuse i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};\nuse rust_embed::RustEmbed;\n\n#[derive(RustEmbed)]\n#[folder = \"i18n/\"]\nstruct Localizations;\n\npub static LANGUAGE_LOADER_CORE: std::sync::LazyLock<FluentLanguageLoader> = std::sync::LazyLock::new(|| {\n    let loader: FluentLanguageLoader = fluent_language_loader!();\n\n    loader.load_fallback_language(&Localizations).expect(\"Error while loading fallback language\");\n\n    loader\n});\n\n#[macro_export]\nmacro_rules! flc {\n    ( $($tt:tt)* ) => {{\n        i18n_embed_fl::fl!($crate::localizer_core::LANGUAGE_LOADER_CORE, $($tt)*)\n    }};\n}\n\npub fn localizer_core() -> Box<dyn Localizer> {\n    Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_CORE, &Localizations))\n}\n\npub fn generate_translation_hashmap(vec: Vec<(&'static str, String)>) -> HashMap<&'static str, String> {\n    let mut hashmap: HashMap<&'static str, String> = Default::default();\n    for (key, value) in vec {\n        hashmap.insert(key, value);\n    }\n    hashmap\n}\n\npub fn fnc_get_similarity_very_high() -> String {\n    flc!(\"core_similarity_very_high\")\n}\n\npub fn fnc_get_similarity_minimal() -> String {\n    flc!(\"core_similarity_minimal\")\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_extensions/core.rs",
    "content": "use std::collections::BTreeSet;\nuse std::mem;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse indexmap::IndexMap;\nuse log::debug;\nuse mime_guess::get_mime_extensions;\nuse rayon::prelude::*;\n\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{FileEntry, ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::tools::bad_extensions::workarounds::{DISABLED_EXTENSIONS, WORKAROUNDS};\nuse crate::tools::bad_extensions::{BadExtensions, BadExtensionsParameters, BadFileEntry, Info};\n\n// Text longer than 10 characters is not considered as extension\nconst MAX_EXTENSION_LENGTH: usize = 10;\n\nimpl BadExtensions {\n    pub fn new(params: BadExtensionsParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::BadExtensions),\n            information: Info::default(),\n            files_to_check: Default::default(),\n            bad_extensions_files: Default::default(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.files_to_check = grouped_file_entries.into_values().flatten().collect();\n                self.common_data.text_messages.warnings.extend(warnings);\n\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    #[fun_time(message = \"look_for_bad_extensions_files\", level = \"debug\")]\n    pub(crate) fn look_for_bad_extensions_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.files_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::BadExtensionsChecking, self.files_to_check.len(), self.get_test_type(), 0);\n\n        let files_to_check = mem::take(&mut self.files_to_check);\n\n        let mut workarounds: IndexMap<&str, Vec<&str>> = Default::default();\n        for (proper, found) in WORKAROUNDS {\n            workarounds.entry(found).or_default().push(proper);\n        }\n\n        self.bad_extensions_files = self.verify_extensions(files_to_check, progress_handler.items_counter(), stop_flag, &workarounds);\n\n        progress_handler.join_thread();\n\n        // Break if stop was clicked\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        self.information.number_of_files_with_bad_extension = self.bad_extensions_files.len();\n\n        debug!(\"Found {} files with invalid extension.\", self.information.number_of_files_with_bad_extension);\n\n        WorkContinueStatus::Continue\n    }\n\n    fn verify_extension_of_file(&self, file_entry: FileEntry, workarounds: &IndexMap<&str, Vec<&str>>) -> Option<BadFileEntry> {\n        // Check what exactly content file contains\n        let kind = match infer::get_from_path(&file_entry.path) {\n            Ok(k) => k?,\n            Err(_) => return None,\n        };\n        let proper_extension = kind.extension();\n\n        let current_extension = Self::get_and_validate_extension(&file_entry, proper_extension)?;\n\n        // Check for all extensions that file can use(not sure if it is worth to do it)\n        let (mut all_available_extensions, valid_extensions) = Self::check_for_all_extensions_that_file_can_use(workarounds, &current_extension, proper_extension);\n\n        if all_available_extensions.is_empty() {\n            // Not found any extension\n            return None;\n        } else if current_extension.is_empty() {\n            if !self.params.include_files_without_extension {\n                return None;\n            }\n        } else if all_available_extensions.take(&current_extension).is_some() {\n            // Found proper extension\n            return None;\n        }\n\n        Some(BadFileEntry {\n            path: file_entry.path,\n            modified_date: file_entry.modified_date,\n            size: file_entry.size,\n            current_extension,\n            proper_extensions_group: valid_extensions,\n            proper_extension: proper_extension.to_string(),\n        })\n    }\n\n    #[fun_time(message = \"verify_extensions\", level = \"debug\")]\n    fn verify_extensions(\n        &self,\n        files_to_check: Vec<FileEntry>,\n        items_counter: &Arc<AtomicUsize>,\n        stop_flag: &Arc<AtomicBool>,\n        workarounds: &IndexMap<&str, Vec<&str>>,\n    ) -> Vec<BadFileEntry> {\n        files_to_check\n            .into_par_iter()\n            .map(|file_entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n                let res = self.verify_extension_of_file(file_entry, workarounds);\n                items_counter.fetch_add(1, Ordering::Relaxed);\n                Some(res)\n            })\n            .while_some()\n            .flatten()\n            .collect::<Vec<_>>()\n    }\n\n    fn get_and_validate_extension(file_entry: &FileEntry, proper_extension: &str) -> Option<String> {\n        let current_extension;\n        // Extract current extension from file\n        if let Some(extension) = file_entry.path.extension() {\n            let extension = extension.to_string_lossy().to_lowercase();\n            if DISABLED_EXTENSIONS.contains(&extension.as_str()) {\n                return None;\n            }\n            if extension.len() > MAX_EXTENSION_LENGTH {\n                current_extension = String::new();\n            } else {\n                current_extension = extension;\n            }\n        } else {\n            current_extension = String::new();\n        }\n\n        // Already have proper extension, no need to do more things\n        if current_extension == proper_extension {\n            return None;\n        }\n        Some(current_extension)\n    }\n\n    fn check_for_all_extensions_that_file_can_use(workarounds: &IndexMap<&str, Vec<&str>>, current_extension: &str, proper_extension: &str) -> (BTreeSet<String>, String) {\n        let mut all_available_extensions: BTreeSet<String> = Default::default();\n        for mim in mime_guess::from_ext(proper_extension) {\n            if let Some(all_ext) = get_mime_extensions(&mim) {\n                for ext in all_ext {\n                    all_available_extensions.insert((*ext).to_string());\n                }\n            }\n        }\n\n        // Workarounds:\n        if !current_extension.is_empty()\n            && let Some(vec_pre) = workarounds.get(current_extension)\n        {\n            for pre in vec_pre {\n                if all_available_extensions.contains(*pre) {\n                    all_available_extensions.insert(current_extension.to_string());\n                    break;\n                }\n            }\n        }\n\n        let valid_extensions = if all_available_extensions.is_empty() {\n            String::new()\n        } else {\n            let mut guessed_multiple_extensions = format!(\"({proper_extension}) - \");\n            for ext in &all_available_extensions {\n                guessed_multiple_extensions.push_str(ext);\n                guessed_multiple_extensions.push(',');\n            }\n            guessed_multiple_extensions.pop();\n            guessed_multiple_extensions\n        };\n\n        (all_available_extensions, valid_extensions)\n    }\n\n    #[fun_time(message = \"fix_bad_extensions\", level = \"debug\")]\n    pub fn fix_bad_extensions(&mut self, _fix_params: super::BadExtensionsFixParams, stop_flag: &Arc<AtomicBool>) {\n        let warnings: Vec<_> = mem::take(&mut self.bad_extensions_files)\n            .into_par_iter()\n            .map(|entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let new_path = entry.path.with_extension(&entry.proper_extension);\n\n                if new_path.exists() {\n                    return Some(Some(format!(\"Cannot rename {:?} to {:?}: target file already exists\", entry.path, new_path)));\n                }\n\n                match std::fs::rename(&entry.path, &new_path) {\n                    Ok(()) => Some(None),\n                    Err(e) => Some(Some(format!(\"Failed to rename {:?} to {:?}: {}\", entry.path, new_path, e))),\n                }\n            })\n            .while_some()\n            .flatten()\n            .collect();\n\n        self.common_data.text_messages.warnings.extend(warnings);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_extensions/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\nmod workarounds;\n\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::Serialize;\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\n#[derive(Clone, Serialize, Debug)]\npub struct BadFileEntry {\n    pub path: PathBuf,\n    pub modified_date: u64,\n    pub size: u64,\n    pub current_extension: String,\n    pub proper_extensions_group: String,\n    pub proper_extension: String,\n}\n\nimpl ResultEntry for BadFileEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_files_with_bad_extension: usize,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone, Debug, Default, Copy)]\npub struct BadExtensionsFixParams {}\n\n#[derive(Clone)]\npub struct BadExtensionsParameters {\n    pub include_files_without_extension: bool,\n}\n\nimpl BadExtensionsParameters {\n    pub fn new() -> Self {\n        Self {\n            include_files_without_extension: false,\n        }\n    }\n}\nimpl Default for BadExtensionsParameters {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\npub struct BadExtensions {\n    common_data: CommonToolData,\n    information: Info,\n    files_to_check: Vec<FileEntry>,\n    bad_extensions_files: Vec<BadFileEntry>,\n    params: BadExtensionsParameters,\n}\n\nimpl BadExtensions {\n    pub const fn get_bad_extensions_files(&self) -> &Vec<BadFileEntry> {\n        &self.bad_extensions_files\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_extensions/tests.rs",
    "content": "use std::fs;\nuse std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::bad_extensions::{BadExtensions, BadExtensionsParameters};\n\n#[test]\nfn test_find_bad_extension_png_as_jpg() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create a PNG file with .jpg extension\n    let png_data = vec![\n        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature\n        0x00, 0x00, 0x00, 0x0D, // IHDR chunk\n    ];\n    let mut file = fs::File::create(path.join(\"image.jpg\")).unwrap();\n    file.write_all(&png_data).unwrap();\n\n    let params = BadExtensionsParameters::new();\n    let mut finder = BadExtensions::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let bad_files = finder.get_bad_extensions_files();\n    assert_eq!(bad_files.len(), 1, \"Should find 1 file with bad extension\");\n    assert_eq!(bad_files[0].current_extension, \"jpg\", \"Current extension should be jpg\");\n    assert_eq!(bad_files[0].proper_extension, \"png\", \"Proper extension should be png\");\n}\n\n#[test]\nfn test_correct_extension() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create a PNG file with correct .png extension\n    let png_data = vec![\n        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature\n        0x00, 0x00, 0x00, 0x0D,\n    ];\n    let mut file = fs::File::create(path.join(\"image.png\")).unwrap();\n    file.write_all(&png_data).unwrap();\n\n    let params = BadExtensionsParameters::new();\n    let mut finder = BadExtensions::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let bad_files = finder.get_bad_extensions_files();\n    assert_eq!(bad_files.len(), 0, \"Should find no files with bad extension\");\n}\n\n#[test]\nfn test_file_without_extension_excluded() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create a PNG file without extension\n    let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D];\n    let mut file = fs::File::create(path.join(\"image_no_ext\")).unwrap();\n    file.write_all(&png_data).unwrap();\n\n    let mut params = BadExtensionsParameters::new();\n    params.include_files_without_extension = false;\n    let mut finder = BadExtensions::new(params);\n\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let bad_files = finder.get_bad_extensions_files();\n    assert_eq!(bad_files.len(), 0, \"Should not include files without extension when disabled\");\n}\n\n#[test]\nfn test_file_without_extension_included() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create a PNG file without extension\n    let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D];\n    let mut file = fs::File::create(path.join(\"image_no_ext\")).unwrap();\n    file.write_all(&png_data).unwrap();\n\n    let mut params = BadExtensionsParameters::new();\n    params.include_files_without_extension = true;\n\n    let mut finder = BadExtensions::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let bad_files = finder.get_bad_extensions_files();\n    assert_eq!(bad_files.len(), 1, \"Should include files without extension when enabled\");\n    assert_eq!(bad_files[0].current_extension, \"\", \"Current extension should be empty\");\n    assert_eq!(bad_files[0].proper_extension, \"png\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_extensions/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search};\nuse crate::tools::bad_extensions::{BadExtensions, BadExtensionsFixParams, BadExtensionsParameters, Info};\n\nimpl AllTraits for BadExtensions {}\n\nimpl Search for BadExtensions {\n    #[fun_time(message = \"find_bad_extensions_files\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.look_for_bad_extensions_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DeletingItems for BadExtensions {\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFolders(self.bad_extensions_files.clone())),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl FixingItems for BadExtensions {\n    type FixParams = BadExtensionsFixParams;\n\n    #[fun_time(message = \"fix_items\", level = \"debug\")]\n    fn fix_items(&mut self, stop_flag: &Arc<AtomicBool>, _progress_sender: Option<&Sender<ProgressData>>, fix_params: Self::FixParams) {\n        self.fix_bad_extensions(fix_params, stop_flag);\n    }\n}\n\nimpl DebugPrint for BadExtensions {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        println!(\"---------------DEBUG PRINT---------------\");\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for BadExtensions {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n        writeln!(writer, \"Found {} files with invalid extension.\\n\", self.information.number_of_files_with_bad_extension)?;\n\n        for file_entry in &self.bad_extensions_files {\n            writeln!(writer, \"\\\"{}\\\" ----- {}\", file_entry.path.to_string_lossy(), file_entry.proper_extensions_group)?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.bad_extensions_files, pretty_print)\n    }\n}\n\nimpl CommonData for BadExtensions {\n    type Info = Info;\n    type Parameters = BadExtensionsParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.get_information().number_of_files_with_bad_extension > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_extensions/workarounds.rs",
    "content": "pub(crate) const DISABLED_EXTENSIONS: &[&str] = &[\"file\", \"cache\", \"bak\", \"data\", \"tmp\"]; // Such files can have any type inside\n\n// This adds several workarounds for bugs/invalid recognizing types by external libraries\n// (\"real_content_extension\", \"current_file_extension\")\npub(crate) const WORKAROUNDS: &[(&str, &str)] = &[\n    // Wine/Windows\n    (\"der\", \"cat\"),\n    (\"exe\", \"acm\"),\n    (\"exe\", \"ax\"),\n    (\"exe\", \"bck\"),\n    (\"exe\", \"com\"),\n    (\"exe\", \"cpl\"),\n    (\"exe\", \"dll16\"),\n    (\"exe\", \"dll\"),\n    (\"exe\", \"drv16\"),\n    (\"exe\", \"drv\"),\n    (\"exe\", \"ds\"),\n    (\"exe\", \"efi\"),\n    (\"exe\", \"exe16\"),\n    (\"exe\", \"fon\"), // Type of font or something else\n    (\"exe\", \"mod16\"),\n    (\"exe\", \"msstyles\"),\n    (\"exe\", \"mui\"),\n    (\"exe\", \"mun\"),\n    (\"exe\", \"orig\"),\n    (\"exe\", \"ps1xml\"),\n    (\"exe\", \"rll\"),\n    (\"exe\", \"rs\"),\n    (\"exe\", \"scr\"),\n    (\"exe\", \"signed\"),\n    (\"exe\", \"sys\"),\n    (\"exe\", \"tlb\"),\n    (\"exe\", \"tsp\"),\n    (\"exe\", \"vdm\"),\n    (\"exe\", \"vxd\"),\n    (\"exe\", \"winmd\"),\n    (\"gz\", \"loggz\"),\n    (\"xml\", \"adml\"),\n    (\"xml\", \"admx\"),\n    (\"xml\", \"camp\"),\n    (\"xml\", \"cdmp\"),\n    (\"xml\", \"cdxml\"),\n    (\"xml\", \"dgml\"),\n    (\"xml\", \"diagpkg\"),\n    (\"xml\", \"gmmp\"),\n    (\"xml\", \"library-ms\"),\n    (\"xml\", \"man\"),\n    (\"xml\", \"manifest\"),\n    (\"xml\", \"msc\"),\n    (\"xml\", \"mum\"),\n    (\"xml\", \"resx\"),\n    (\"zip\", \"msix\"),\n    (\"zip\", \"wmz\"),\n    // Games specific extensions - cannot be used here common extensions like zip\n    (\"gz\", \"h3m\"),     // Heroes 3\n    (\"zip\", \"hashdb\"), // Gog\n    (\"c2\", \"zip\"),     // King of the Dark Age\n    (\"c2\", \"bmp\"),     // King of the Dark Age\n    (\"c2\", \"avi\"),     // King of the Dark Age\n    (\"c2\", \"exe\"),     // King of the Dark Age\n    // Raw images\n    (\"tif\", \"nef\"),\n    (\"tif\", \"dng\"),\n    (\"tif\", \"arw\"),\n    // Other\n    (\"der\", \"keystore\"),  // Godot/Android keystore\n    (\"exe\", \"pyd\"),       // Python/Mingw\n    (\"gz\", \"blend\"),      // Blender\n    (\"gz\", \"crate\"),      // Cargo\n    (\"gz\", \"svgz\"),       // Archive svg\n    (\"gz\", \"tgz\"),        // Archive\n    (\"heic\", \"heif\"),     // Image\n    (\"heif\", \"heic\"),     // Image\n    (\"html\", \"dtd\"),      // Mingw\n    (\"html\", \"ent\"),      // Mingw\n    (\"html\", \"md\"),       // Markdown\n    (\"html\", \"svelte\"),   // Svelte\n    (\"jpg\", \"jfif\"),      // Photo format\n    (\"m4v\", \"mp4\"),       // m4v and mp4 are interchangeable\n    (\"mobi\", \"azw3\"),     // Ebook format\n    (\"mpg\", \"vob\"),       // Weddings in parts have usually vob extension\n    (\"obj\", \"bin\"),       // Multiple apps, Czkawka, Nvidia, Windows\n    (\"obj\", \"o\"),         // Compilators\n    (\"odp\", \"otp\"),       // LibreOffice\n    (\"ods\", \"ots\"),       // Libreoffice\n    (\"odt\", \"ott\"),       // Libreoffice\n    (\"ogg\", \"ogv\"),       // Audio format\n    (\"pem\", \"key\"),       // curl, openssl\n    (\"png\", \"kpp\"),       // Krita presets\n    (\"pptx\", \"ppsx\"),     // Powerpoint\n    (\"sh\", \"bash\"),       // Linux\n    (\"sh\", \"guess\"),      // GNU\n    (\"sh\", \"lua\"),        // Lua\n    (\"sh\", \"js\"),         // Javascript\n    (\"sh\", \"pl\"),         // Gnome/Linux\n    (\"sh\", \"pm\"),         // Gnome/Linux\n    (\"sh\", \"py\"),         // Python\n    (\"sh\", \"pyx\"),        // Python\n    (\"sh\", \"rs\"),         // Rust\n    (\"sh\", \"sample\"),     // Git\n    (\"xml\", \"bsp\"),       // Quartus\n    (\"xml\", \"cbp\"),       // CodeBlocks config\n    (\"xml\", \"cfg\"),       // Multiple apps - Godot\n    (\"xml\", \"cmb\"),       // Cambalache\n    (\"xml\", \"conf\"),      // Multiple apps - Python\n    (\"xml\", \"config\"),    // Multiple apps - QT Creator\n    (\"xml\", \"dae\"),       // 3D models\n    (\"xml\", \"docbook\"),   //\n    (\"xml\", \"fb2\"),       //\n    (\"xml\", \"filters\"),   // Visual studio\n    (\"xml\", \"gir\"),       // GTK\n    (\"xml\", \"glade\"),     // Glade\n    (\"xml\", \"iml\"),       // Intelij Idea\n    (\"xml\", \"kdenlive\"),  // KDenLive\n    (\"xml\", \"lang\"),      // ?\n    (\"xml\", \"nuspec\"),    // Nuget\n    (\"xml\", \"policy\"),    // SystemD\n    (\"xml\", \"qsys\"),      // Quartus\n    (\"xml\", \"sopcinfo\"),  // Quartus\n    (\"xml\", \"svg\"),       // SVG\n    (\"xml\", \"ui\"),        // Cambalache, Glade\n    (\"xml\", \"user\"),      // Qtcreator\n    (\"xml\", \"vbox\"),      // VirtualBox\n    (\"xml\", \"vbox-prev\"), // VirtualBox\n    (\"xml\", \"vcproj\"),    // VisualStudio\n    (\"xml\", \"vcxproj\"),   // VisualStudio\n    (\"xml\", \"xba\"),       // Libreoffice\n    (\"xml\", \"xcd\"),       // Libreoffice files\n    (\"zip\", \"apk\"),       // Android apk\n    (\"zip\", \"cbz\"),       // Comics\n    (\"zip\", \"dat\"),       // Multiple - python, brave\n    (\"zip\", \"doc\"),       // Word\n    (\"zip\", \"docx\"),      // Word\n    (\"zip\", \"epub\"),      // Ebook format\n    (\"zip\", \"jar\"),       // Java\n    (\"zip\", \"kra\"),       // Krita\n    (\"zip\", \"kgm\"),       // Krita\n    (\"zip\", \"nupkg\"),     // Nuget packages\n    (\"zip\", \"odg\"),       // Libreoffice\n    (\"zip\", \"pptx\"),      // Powerpoint\n    (\"zip\", \"whl\"),       // Python packages\n    (\"zip\", \"xlsx\"),      // Excel\n    (\"zip\", \"xpi\"),       // Firefox extensions\n    (\"zip\", \"zcos\"),      // Scilab\n    // Probably invalid\n    (\"html\", \"svg\"),\n    (\"xml\", \"html\"),\n    // Probably bug in external library\n    (\"msi\", \"ppt\"), // Not sure why ppt is not recognized\n    (\"msi\", \"doc\"), // Not sure why doc is not recognized\n    (\"exe\", \"xls\"), // Not sure why xls is not recognized\n];\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_names/core.rs",
    "content": "use std::path::Path;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::{fs, mem};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::debug;\nuse rayon::prelude::*;\n\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::tools::bad_names::{BadNameEntry, BadNames, BadNamesParameters, Info, NameFixerParams, NameIssues};\n\nimpl BadNames {\n    pub fn new(params: BadNamesParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::BadNames),\n            information: Info::default(),\n            files_to_check: Default::default(),\n            bad_names_files: Default::default(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.files_to_check = grouped_file_entries.into_values().flatten().collect();\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"check_files - Found {} files to check.\", self.files_to_check.len());\n\n                WorkContinueStatus::Continue\n            }\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    #[fun_time(message = \"look_for_bad_names_files\", level = \"debug\")]\n    pub(crate) fn look_for_bad_names_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.files_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::BadNamesChecking,\n            self.files_to_check.len(),\n            self.get_test_type(),\n            self.files_to_check.iter().map(|item| item.size).sum::<u64>(),\n        );\n\n        let files_to_check = std::mem::take(&mut self.files_to_check);\n        let checked_issues = self.params.checked_issues.clone();\n\n        debug!(\"look_for_bad_names_files - started checking for bad names\");\n        let bad_names_files: Vec<BadNameEntry> = files_to_check\n            .into_par_iter()\n            .filter_map(|file_entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let size = file_entry.size;\n                let result = check_and_generate_new_name(&file_entry.path, &checked_issues).map(|new_name| BadNameEntry {\n                    path: file_entry.path,\n                    modified_date: file_entry.modified_date,\n                    size: file_entry.size,\n                    new_name,\n                });\n\n                progress_handler.increase_items(1);\n                progress_handler.increase_size(size);\n\n                result\n            })\n            .collect();\n\n        debug!(\"look_for_bad_names_files - ended checking for bad names\");\n        progress_handler.join_thread();\n\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        self.bad_names_files = bad_names_files;\n        self.information.number_of_files_with_bad_names = self.bad_names_files.len();\n        debug!(\"Found {} files with bad names.\", self.information.number_of_files_with_bad_names);\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"fix_bad_names\", level = \"debug\")]\n    pub fn fix_bad_names(&mut self, _fix_params: NameFixerParams, stop_flag: &Arc<AtomicBool>) {\n        let warnings: Vec<_> = mem::take(&mut self.bad_names_files)\n            .into_par_iter()\n            .map(|entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let new_path = entry.path.with_file_name(&entry.new_name);\n\n                match fs::rename(&entry.path, &new_path) {\n                    Ok(()) => Some(None),\n                    Err(e) => Some(Some(format!(\"Failed to rename {:?}: {}\", entry.path, e))),\n                }\n            })\n            .while_some()\n            .flatten()\n            .collect();\n\n        self.common_data.text_messages.warnings.extend(warnings);\n    }\n}\n\n// Check file name against NameIssues and generate a new fixed name if issues are found\npub fn check_and_generate_new_name(path: &Path, checked_issues: &NameIssues) -> Option<String> {\n    let file_name = path.file_name()?.to_string_lossy();\n    let mut stem = path.file_stem()?.to_string_lossy().to_string();\n    let mut extension = path.extension().map(|e| e.to_string_lossy().to_string());\n\n    if checked_issues.uppercase_extension\n        && let Some(ref mut ext) = extension\n        && ext.chars().any(|c| c.is_uppercase())\n    {\n        *ext = ext.to_lowercase();\n    }\n\n    if checked_issues.emoji_used {\n        stem = stem.chars().filter(|c| !is_emoji(*c)).collect();\n\n        if let Some(ref mut ext) = extension {\n            *ext = ext.chars().filter(|c| !is_emoji(*c)).collect();\n        }\n    }\n\n    if checked_issues.non_ascii_graphical {\n        stem = deunicode::deunicode(&stem);\n\n        if let Some(ref mut ext) = extension {\n            *ext = deunicode::deunicode(ext).chars().filter(|e| e.is_ascii_graphic() || *e == ' ').collect();\n        }\n    }\n\n    if let Some(allowed_chars) = &checked_issues.restricted_charset_allowed {\n        stem = deunicode::deunicode(&stem).chars().filter(|c| is_alphanumeric(*c) || allowed_chars.contains(c)).collect();\n\n        if let Some(ref mut ext) = extension {\n            *ext = deunicode::deunicode(ext).chars().filter(|c| is_alphanumeric(*c) || allowed_chars.contains(c)).collect();\n        }\n    }\n\n    if checked_issues.remove_duplicated_non_alphanumeric {\n        stem = remove_duplicated_non_alphanumeric(&stem);\n\n        if let Some(ref mut ext) = extension {\n            *ext = remove_duplicated_non_alphanumeric(ext);\n        }\n    }\n\n    if checked_issues.space_at_start_or_end {\n        stem = stem.trim().to_string();\n\n        if let Some(ref mut ext) = extension {\n            *ext = ext.trim().to_string();\n        }\n    }\n\n    let new_name = if let Some(ext) = extension {\n        if ext.is_empty() { stem } else { format!(\"{stem}.{ext}\") }\n    } else {\n        stem\n    };\n\n    if new_name != file_name.as_ref() as &str { Some(new_name) } else { None }\n}\n\nfn is_alphanumeric(c: char) -> bool {\n    c.is_ascii_alphanumeric()\n}\n\nfn remove_duplicated_non_alphanumeric(s: &str) -> String {\n    let mut result = String::with_capacity(s.len());\n    let mut chars = s.chars().peekable();\n\n    while let Some(c) = chars.next() {\n        result.push(c);\n\n        if !c.is_ascii_alphanumeric() {\n            // Skip consecutive identical non-alphanumeric characters\n            while let Some(&next_c) = chars.peek() {\n                if next_c == c {\n                    chars.next();\n                } else {\n                    break;\n                }\n            }\n        }\n    }\n\n    result\n}\n\nfn is_emoji(c: char) -> bool {\n    let code = c as u32;\n    matches!(code,\n        // Misc symbols + pictographs\n        0x231A..=0x231B |\n        0x23E9..=0x23EC |\n        0x23F0 |\n        0x23F3 |\n        0x25FD..=0x25FE |\n        0x2600..=0x2604 |\n        0x2614..=0x2615 |\n        0x2648..=0x2653 |\n        0x267F |\n        0x2693 |\n        0x26A1 |\n        0x26AA..=0x26AB |\n        0x26BD..=0x26BE |\n        0x26C4..=0x26C8 |\n        0x26CE |\n        0x26D4 |\n        0x26EA |\n        0x26F2..=0x26F3 |\n        0x26F5 |\n        0x26FA |\n        0x26FD |\n        0x2705 |\n        0x270A..=0x270B |\n        0x2728 |\n        0x274C |\n        0x274E |\n        0x2753..=0x2757 |\n        0x2763..=0x2764 |\n        0x2795..=0x2797 |\n        0x27B0 |\n        0x27BF |\n        0x2B1B..=0x2B1C |\n        0x2B50 |\n        0x2B55 |\n\n        // Enclosed characters\n        0x1F004 |\n        0x1F0CF |\n        0x1F18E |\n        0x1F191..=0x1F19A |\n        0x1F201 |\n        0x1F21A |\n        0x1F22F |\n        0x1F232..=0x1F23A |\n        0x1F250..=0x1F251 |\n\n        // Main emoji blocks\n        0x1F300..=0x1F5FF |\n        0x1F600..=0x1F64F |\n        0x1F680..=0x1F6FF |\n        0x1F900..=0x1F9FF |\n\n        // Regional indicator symbols (flags)\n        0x1F1E6..=0x1F1FF\n    )\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_names/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\n#[derive(Clone, Serialize, Deserialize, Debug)]\npub struct BadNameEntry {\n    pub path: PathBuf,\n    pub modified_date: u64,\n    pub size: u64,\n    pub new_name: String,\n}\n\nimpl ResultEntry for BadNameEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\n#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]\npub struct NameIssues {\n    pub uppercase_extension: bool,\n    pub emoji_used: bool,\n    pub space_at_start_or_end: bool,\n    pub non_ascii_graphical: bool,\n    pub restricted_charset_allowed: Option<Vec<char>>,\n    pub remove_duplicated_non_alphanumeric: bool,\n}\n\nimpl NameIssues {\n    pub fn all() -> Self {\n        Self {\n            uppercase_extension: true,\n            emoji_used: true,\n            space_at_start_or_end: true,\n            non_ascii_graphical: true,\n            restricted_charset_allowed: Some(vec!['_', '-', ' ', '.']),\n            remove_duplicated_non_alphanumeric: true,\n        }\n    }\n\n    pub fn none() -> Self {\n        Self::default()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        !self.uppercase_extension\n            && !self.emoji_used\n            && !self.space_at_start_or_end\n            && !self.non_ascii_graphical\n            && self.restricted_charset_allowed.is_none()\n            && !self.remove_duplicated_non_alphanumeric\n    }\n}\n\n#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]\npub struct NameFixerParams {\n    // Empty - fixing has no parameters\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_files_with_bad_names: usize,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone)]\npub struct BadNamesParameters {\n    pub checked_issues: NameIssues,\n}\n\nimpl BadNamesParameters {\n    pub fn new(checked_issues: NameIssues) -> Self {\n        Self { checked_issues }\n    }\n}\n\nimpl Default for BadNamesParameters {\n    fn default() -> Self {\n        Self {\n            checked_issues: NameIssues::all(),\n        }\n    }\n}\n\npub struct BadNames {\n    common_data: CommonToolData,\n    information: Info,\n    files_to_check: Vec<FileEntry>,\n    bad_names_files: Vec<BadNameEntry>,\n    params: BadNamesParameters,\n}\n\nimpl BadNames {\n    pub const fn get_bad_names_files(&self) -> &Vec<BadNameEntry> {\n        &self.bad_names_files\n    }\n\n    pub fn get_params(&self) -> &BadNamesParameters {\n        &self.params\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_names/tests.rs",
    "content": "#[cfg(test)]\nmod tests2 {\n    use std::fs;\n    use std::sync::Arc;\n    use std::sync::atomic::AtomicBool;\n\n    use crate::common::tool_data::CommonData;\n    use crate::common::traits::Search;\n    use crate::tools::bad_names::{BadNames, BadNamesParameters, NameIssues};\n\n    #[test]\n    fn test_uppercase_extension_detection() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\"test.TXT\");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: true,\n            emoji_used: false,\n            space_at_start_or_end: false,\n            non_ascii_graphical: false,\n            restricted_charset_allowed: None,\n            remove_duplicated_non_alphanumeric: false,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test.txt\");\n    }\n\n    #[test]\n    fn test_emoji_detection() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\"test😀.txt\");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: false,\n            emoji_used: true,\n            space_at_start_or_end: false,\n            non_ascii_graphical: false,\n            restricted_charset_allowed: None,\n            remove_duplicated_non_alphanumeric: false,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test.txt\");\n    }\n\n    #[test]\n    fn test_space_at_start_end_stem_detection() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\" test .txt\");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: false,\n            emoji_used: false,\n            space_at_start_or_end: true,\n            non_ascii_graphical: false,\n            restricted_charset_allowed: None,\n            remove_duplicated_non_alphanumeric: false,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test.txt\");\n    }\n\n    #[test]\n    fn test_space_at_start_end_extension_detection() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\"test. txt \");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: false,\n            emoji_used: false,\n            space_at_start_or_end: true,\n            non_ascii_graphical: false,\n            restricted_charset_allowed: None,\n            remove_duplicated_non_alphanumeric: false,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test.txt\");\n    }\n\n    #[test]\n    fn test_non_ascii_graphical_detection() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\"tëst.txt\");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: false,\n            emoji_used: false,\n            space_at_start_or_end: false,\n            non_ascii_graphical: true,\n            restricted_charset_allowed: None,\n            remove_duplicated_non_alphanumeric: false,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test.txt\");\n    }\n\n    #[test]\n    fn test_restricted_charset_detection() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\"test@file.txt\");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: false,\n            emoji_used: false,\n            space_at_start_or_end: false,\n            non_ascii_graphical: false,\n            restricted_charset_allowed: Some(vec!['_', '-', ' ']),\n            remove_duplicated_non_alphanumeric: false,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"testfile.txt\");\n    }\n\n    #[test]\n    fn test_duplicated_non_alphanumeric() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\"test__file--name.txt\");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let params = BadNamesParameters::new(NameIssues {\n            uppercase_extension: false,\n            emoji_used: false,\n            space_at_start_or_end: false,\n            non_ascii_graphical: false,\n            restricted_charset_allowed: None,\n            remove_duplicated_non_alphanumeric: true,\n        });\n        let mut bad_names = BadNames::new(params);\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test_file-name.txt\");\n    }\n\n    #[test]\n    fn test_multiple_issues() {\n        let temp_dir = tempfile::tempdir().unwrap();\n        let test_file = temp_dir.path().join(\" tëst😀 .TXT \");\n        fs::write(&test_file, \"test\").unwrap();\n\n        let mut bad_names = BadNames::new(BadNamesParameters::new(NameIssues::all()));\n        bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        bad_names.search(&stop_flag, None);\n\n        assert_eq!(bad_names.get_bad_names_files().len(), 1);\n        assert_eq!(bad_names.get_bad_names_files()[0].new_name, \"test.txt\");\n    }\n\n    use std::path::Path;\n\n    use crate::tools::bad_names::core::check_and_generate_new_name;\n\n    #[test]\n    fn test_uppercase_extension_unit() {\n        let check_params = NameIssues {\n            uppercase_extension: true,\n            ..NameIssues::default()\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\"test.TXT\", \"test.txt\"),\n            (\"file.Jpg\", \"file.jpg\"),\n            (\"document.PDF\", \"document.pdf\"),\n            (\"image.PnG\", \"image.png\"),\n            (\"video.MP4\", \"video.mp4\"),\n            (\"archive.ZIP\", \"archive.zip\"),\n            (\"data.CSV\", \"data.csv\"),\n            (\"presentation.PPTX\", \"presentation.pptx\"),\n            (\"script.Py\", \"script.py\"),\n            (\"code.Js\", \"code.js\"),\n            (\"style.Css\", \"style.css\"),\n            (\"page.Html\", \"page.html\"),\n            (\"config.Json\", \"config.json\"),\n            (\"readme.Md\", \"readme.md\"),\n            (\"Makefile.Mk\", \"Makefile.mk\"),\n            (\"abc.cde.TXT\", \"abc.cde.txt\"),\n            (\"file.backup.PDF\", \"file.backup.pdf\"),\n            (\"my.file.name.JPG\", \"my.file.name.jpg\"),\n            (\"test.1.2.3.Zip\", \"test.1.2.3.zip\"),\n            (\"document.v2.0.Doc\", \"document.v2.0.doc\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                if new_name != expected_output {\n                    errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                }\n\n                let fixed_path = Path::new(&new_name);\n                if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                    errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                }\n            } else {\n                errors.push(format!(\"Input: '{input}' was not fixed\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"Uppercase extension tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_emoji_removal_unit() {\n        let check_params = NameIssues {\n            emoji_used: true,\n            ..NameIssues::default()\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\"test😀.txt\", \"test.txt\"),\n            (\"file🎉🎊.doc\", \"file.doc\"),\n            (\"image❤.png\", \"image.png\"),\n            (\"video🔥.mp4\", \"video.mp4\"),\n            (\"doc👍.pdf\", \"doc.pdf\"),\n            (\"report😊😊😊.xlsx\", \"report.xlsx\"),\n            (\"photo🌟.jpg\", \"photo.jpg\"),\n            (\"music🎵🎶.mp3\", \"music.mp3\"),\n            (\"readme📝.md\", \"readme.md\"),\n            (\"party🎈🎉🎊🎁.txt\", \"party.txt\"),\n            (\"love💕💖💗💘.doc\", \"love.doc\"),\n            (\"fire🔥🔥🔥.log\", \"fire.log\"),\n            (\"star⭐.txt\", \"star.txt\"),\n            (\"food🍕🍔🍟.jpg\", \"food.jpg\"),\n            (\"weather☀🌧⛈.csv\", \"weather.csv\"),\n            (\"test😀.backup.txt\", \"test.backup.txt\"),\n            (\"my.file🎉.doc\", \"my.file.doc\"),\n            (\"archive.v1.2🔥.zip\", \"archive.v1.2.zip\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                if new_name != expected_output {\n                    errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                }\n\n                let fixed_path = Path::new(&new_name);\n                if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                    errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                }\n            } else {\n                errors.push(format!(\"Input: '{input}' was not fixed\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"Emoji removal tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_space_at_start_end_unit() {\n        let check_params = NameIssues {\n            space_at_start_or_end: true,\n            ..NameIssues::default()\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\" test.txt\", \"test.txt\"),\n            (\"test .txt\", \"test.txt\"),\n            (\" test .txt\", \"test.txt\"),\n            (\"  test  .txt\", \"test.txt\"),\n            (\"test. txt \", \"test.txt\"),\n            (\"   file   .doc\", \"file.doc\"),\n            (\"image .png\", \"image.png\"),\n            (\" video.mp4\", \"video.mp4\"),\n            (\"document .pdf\", \"document.pdf\"),\n            (\" report .xlsx\", \"report.xlsx\"),\n            (\"     data     .csv\", \"data.csv\"),\n            (\"photo . jpg \", \"photo.jpg\"),\n            (\" music .mp3\", \"music.mp3\"),\n            (\"readme . md \", \"readme.md\"),\n            (\"  archive  . zip \", \"archive.zip\"),\n            (\" abc.cde.txt\", \"abc.cde.txt\"),\n            (\"abc.cde .txt\", \"abc.cde.txt\"),\n            (\" my.file.name .doc\", \"my.file.name.doc\"),\n            (\"  test.1.2  . pdf \", \"test.1.2.pdf\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                if new_name != expected_output {\n                    errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                }\n\n                let fixed_path = Path::new(&new_name);\n                if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                    errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                }\n            } else {\n                errors.push(format!(\"Input: '{input}' was not fixed\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"Space at start/end tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_non_ascii_graphical_unit() {\n        let check_params = NameIssues {\n            non_ascii_graphical: true,\n            ..NameIssues::default()\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\"tëst.txt\", \"test.txt\"),\n            (\"café.pdf\", \"cafe.pdf\"),\n            (\"Kraków.doc\", \"Krakow.doc\"),\n            (\"Łódź.txt\", \"Lodz.txt\"),\n            (\"naïve.doc\", \"naive.doc\"),\n            (\"résumé.pdf\", \"resume.pdf\"),\n            (\"São Paulo.txt\", \"Sao Paulo.txt\"),\n            (\"Zürich.doc\", \"Zurich.doc\"),\n            (\"Москва.txt\", \"Moskva.txt\"),\n            (\"日本.txt\", \"Ri Ben.txt\"),\n            (\"über.pdf\", \"uber.pdf\"),\n            (\"señor.txt\", \"senor.txt\"),\n            (\"Ærø.doc\", \"AEro.doc\"),\n            (\"niño.txt\", \"nino.txt\"),\n            (\"Björk.mp3\", \"Bjork.mp3\"),\n            (\"François.doc\", \"Francois.doc\"),\n            (\"Ñoño.txt\", \"Nono.txt\"),\n            (\"Østergård.pdf\", \"Ostergard.pdf\"),\n            (\"Łukasz.txt\", \"Lukasz.txt\"),\n            (\"Müller.doc\", \"Muller.doc\"),\n            (\"pièces\", \"pieces\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                if new_name != expected_output {\n                    errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                }\n\n                let fixed_path = Path::new(&new_name);\n                if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                    errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                }\n            } else {\n                errors.push(format!(\"Input: '{input}' was not fixed\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"Non-ASCII graphical tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_restricted_charset_unit() {\n        let check_params = NameIssues {\n            restricted_charset_allowed: Some(vec!['_', '-', ' ']),\n            ..NameIssues::default()\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\"test@file.txt\", \"testfile.txt\"),\n            (\"my#doc.pdf\", \"mydoc.pdf\"),\n            (\"file$name.doc\", \"filename.doc\"),\n            (\"data%set.csv\", \"dataset.csv\"),\n            (\"script&code.js\", \"scriptcode.js\"),\n            (\"image*pic.png\", \"imagepic.png\"),\n            (\"video(1).mp4\", \"video1.mp4\"),\n            (\"photo[2].jpg\", \"photo2.jpg\"),\n            (\"doc{test}.pdf\", \"doctest.pdf\"),\n            (\"file|name.txt\", \"filename.txt\"),\n            (\"test:file.doc\", \"testfile.doc\"),\n            (\"name;value.csv\", \"namevalue.csv\"),\n            (\"file'name.txt\", \"filename.txt\"),\n            (\"test\\\"quote.doc\", \"testquote.doc\"),\n            (\"data<less.xml\", \"dataless.xml\"),\n            (\"file>more.txt\", \"filemore.txt\"),\n            (\"question?.log\", \"question.log\"),\n            (\"wild*.txt\", \"wild.txt\"),\n            (\"comma,.csv\", \"comma.csv\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                if new_name != expected_output {\n                    errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                }\n\n                let fixed_path = Path::new(&new_name);\n                if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                    errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                }\n            } else {\n                errors.push(format!(\"Input: '{input}' was not fixed\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"Restricted charset tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_duplicated_non_alphanumeric_unit() {\n        let check_params = NameIssues {\n            remove_duplicated_non_alphanumeric: true,\n            ..NameIssues::default()\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\"test__file.txt\", \"test_file.txt\"),\n            (\"my--doc.pdf\", \"my-doc.pdf\"),\n            (\"file  name.doc\", \"file name.doc\"),\n            (\"data...set.csv\", \"data.set.csv\"),\n            (\"script___code.js\", \"script_code.js\"),\n            (\"image---pic.png\", \"image-pic.png\"),\n            (\"test____file----name.txt\", \"test_file-name.txt\"),\n            (\"multiple   spaces.doc\", \"multiple spaces.doc\"),\n            (\"under______score.log\", \"under_score.log\"),\n            (\"dash-------line.txt\", \"dash-line.txt\"),\n            (\"mixed__--__test.doc\", \"mixed_-_test.doc\"),\n            (\"file,,,,name.csv\", \"file,name.csv\"),\n            (\"test;;;;code.txt\", \"test;code.txt\"),\n            (\"data::::value.xml\", \"data:value.xml\"),\n            (\"triple___---...test.txt\", \"triple_-.test.txt\"),\n            (\"many        spaces.doc\", \"many spaces.doc\"),\n            (\"dots......dots.txt\", \"dots.dots.txt\"),\n            (\"under_score.txt\", \"under_score.txt\"),\n            (\"normal-file.txt\", \"normal-file.txt\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            let result = check_and_generate_new_name(path, &check_params);\n\n            if input == expected_output {\n                // No change expected\n                if let Some(result) = result {\n                    errors.push(format!(\"Input: '{input}' should not be modified but got: '{result}'\"));\n                }\n            } else {\n                // Change expected\n                if let Some(new_name) = result {\n                    if new_name != expected_output {\n                        errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                    }\n\n                    let fixed_path = Path::new(&new_name);\n                    if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                        errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                    }\n                } else {\n                    errors.push(format!(\"Input: '{input}' was not fixed\"));\n                }\n            }\n        }\n\n        assert!(errors.is_empty(), \"Duplicated non-alphanumeric tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_combined_all_issues_unit() {\n        let check_params = NameIssues {\n            uppercase_extension: true,\n            emoji_used: true,\n            space_at_start_or_end: true,\n            non_ascii_graphical: true,\n            restricted_charset_allowed: Some(vec!['_', '-', ' ']),\n            remove_duplicated_non_alphanumeric: true,\n        };\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\" tëst😀 .TXT \", \"test.txt\"),\n            (\"  café☕  .Pdf  \", \"cafe.pdf\"),\n            (\" über@file😊 .Txt \", \"uberfile.txt\"),\n            (\"test__😀__file.JPG\", \"test_file.jpg\"),\n            (\" Kraków🎉 .Doc \", \"Krakow.doc\"),\n            (\"  résumé##  .PDF  \", \"resume.pdf\"),\n            (\"São Paulo  .TXT\", \"Sao Paulo.txt\"),\n            (\" file___name😀😀.PNG \", \"file_name.png\"),\n            (\"test  @@  emoji🎉.MP4\", \"test emoji.mp4\"),\n            (\" Łódź---file .CSV \", \"Lodz-file.csv\"),\n            (\"über__müller😊.XLSX\", \"uber_muller.xlsx\"),\n            (\" data___set🔥 . JSON \", \"data_set.json\"),\n            (\"test  ##  ëmoji😀.Doc\", \"test emoji.doc\"),\n            (\" François___Müller .PDF \", \"Francois_Muller.pdf\"),\n            (\"multi___issue___test😀😀 .TXT \", \"multi_issue_test.txt\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                if new_name != expected_output {\n                    errors.push(format!(\"Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                }\n\n                let fixed_path = Path::new(&new_name);\n                if check_and_generate_new_name(fixed_path, &check_params).is_some() {\n                    errors.push(format!(\"Double fix should return None for: '{new_name}'\"));\n                }\n            } else {\n                errors.push(format!(\"Input: '{input}' was not fixed\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"Combined all issues tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_no_issues_no_changes() {\n        let check_params = NameIssues::all();\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            \"normal_file.txt\",\n            \"test-file.doc\",\n            \"MyDocument.pdf\",\n            \"data_2024.csv\",\n            \"image-001.jpg\",\n            \"video_final.mp4\",\n            \"report-2024-01.xlsx\",\n            \"README.md\",\n            \"config.json\",\n            \"script.py\",\n        ];\n\n        for input in test_cases {\n            let path = Path::new(input);\n            if let Some(new_name) = check_and_generate_new_name(path, &check_params) {\n                errors.push(format!(\"Input: '{input}' should not be changed but got: '{new_name}'\"));\n            }\n        }\n\n        assert!(errors.is_empty(), \"No issues no changes tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n\n    #[test]\n    fn test_edge_cases_unit() {\n        let check_params = NameIssues::all();\n\n        let mut errors = Vec::new();\n        let test_cases = [\n            (\"😀.txt\", \".txt\"),\n            (\"   .TXT\", \".txt\"),\n            (\"😀😀😀.txt\", \".txt\"),\n            (\"___\", \"_\"),\n            (\"---\", \"-\"),\n            (\"...\", \".\"),\n            (\" 😀 .TXT \", \".txt\"),\n            (\"test.\", \"test\"),\n            (\".test\", \".test\"),\n        ];\n\n        for (input, expected_output) in test_cases {\n            let path = Path::new(input);\n            let result = check_and_generate_new_name(path, &check_params);\n\n            if input == expected_output {\n                if let Some(new_name) = result {\n                    errors.push(format!(\"Edge case input: '{input}' should not be modified but got: '{new_name}'\"));\n                }\n            } else {\n                if let Some(new_name) = result {\n                    if new_name != expected_output {\n                        errors.push(format!(\"Edge case input: '{input}', Expected: '{expected_output}', Got: '{new_name}'\"));\n                    }\n                } else {\n                    errors.push(format!(\"Edge case input: '{input}' was not fixed\"));\n                }\n            }\n        }\n\n        assert!(errors.is_empty(), \"Edge cases tests failed:\\n{}\", errors.join(\"\\n\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/bad_names/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search};\nuse crate::flc;\nuse crate::tools::bad_names::{BadNames, BadNamesParameters, Info, NameFixerParams};\n\nimpl AllTraits for BadNames {}\n\nimpl Search for BadNames {\n    #[fun_time(message = \"find_bad_names\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.params.checked_issues.is_empty() {\n                self.common_data.text_messages.critical = Some(flc!(\"core_needs_to_set_at_least_one_bad_name_option\"));\n                return;\n            }\n\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.look_for_bad_names_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for BadNames {\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        self.debug_print_common();\n    }\n}\n\nimpl PrintResults for BadNames {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        if !self.bad_names_files.is_empty() {\n            writeln!(writer, \"Found {} files with bad names.\", self.information.number_of_files_with_bad_names)?;\n            for file_entry in &self.bad_names_files {\n                writeln!(writer, \"\\\"{}\\\" -> \\\"{}\\\"\", file_entry.path.to_string_lossy(), file_entry.new_name)?;\n            }\n        } else {\n            write!(writer, \"Not found any files with bad names.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.bad_names_files, pretty_print)\n    }\n}\n\nimpl DeletingItems for BadNames {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.bad_names_files.clone())),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl FixingItems for BadNames {\n    type FixParams = NameFixerParams;\n    #[fun_time(message = \"fix_items\", level = \"debug\")]\n    fn fix_items(&mut self, stop_flag: &Arc<AtomicBool>, _progress_sender: Option<&Sender<ProgressData>>, fix_params: Self::FixParams) {\n        self.fix_bad_names(fix_params, stop_flag);\n    }\n}\n\nimpl CommonData for BadNames {\n    type Info = Info;\n    type Parameters = BadNamesParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_files_with_bad_names > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/big_file/core.rs",
    "content": "use std::cmp::Reverse;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::debug;\nuse rayon::prelude::*;\n\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::tools::big_file::{BigFile, BigFileParameters, Info, SearchMode};\n\nimpl BigFile {\n    pub fn new(params: BigFileParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::BigFile),\n            information: Info::default(),\n            big_files: Default::default(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"look_for_big_files\", level = \"debug\")]\n    pub(crate) fn look_for_big_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .minimal_file_size(1)\n            .maximal_file_size(u64::MAX)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                let mut all_files = grouped_file_entries.into_values().flatten().collect::<Vec<_>>();\n\n                if self.get_params().search_mode == SearchMode::BiggestFiles {\n                    all_files.par_sort_unstable_by_key(|fe| Reverse(fe.size));\n                } else {\n                    all_files.par_sort_unstable_by_key(|fe| fe.size);\n                }\n\n                all_files.truncate(self.get_params().number_of_files_to_check);\n\n                self.big_files = all_files;\n\n                self.common_data.text_messages.warnings.extend(warnings);\n                self.information.number_of_real_files = self.big_files.len();\n                debug!(\"check_files - Found {} biggest/smallest files.\", self.big_files.len());\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/big_file/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::time::Duration;\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum SearchMode {\n    BiggestFiles,\n    SmallestFiles,\n}\n\n#[derive(Debug, Default, Clone)]\npub struct Info {\n    pub number_of_real_files: usize,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone)]\npub struct BigFileParameters {\n    pub number_of_files_to_check: usize,\n    pub search_mode: SearchMode,\n}\n\nimpl BigFileParameters {\n    pub fn new(number_of_files: usize, search_mode: SearchMode) -> Self {\n        Self {\n            number_of_files_to_check: number_of_files.max(1),\n            search_mode,\n        }\n    }\n}\n\npub struct BigFile {\n    common_data: CommonToolData,\n    information: Info,\n    big_files: Vec<FileEntry>,\n    params: BigFileParameters,\n}\n\nimpl BigFile {\n    pub const fn get_big_files(&self) -> &Vec<FileEntry> {\n        &self.big_files\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/big_file/tests.rs",
    "content": "use std::fs;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::big_file::{BigFile, BigFileParameters, SearchMode};\n\n#[test]\nfn test_find_biggest_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create files with different sizes\n    fs::write(path.join(\"small.txt\"), b\"12\").unwrap(); // 2 bytes\n    fs::write(path.join(\"medium.txt\"), b\"12345\").unwrap(); // 5 bytes\n    fs::write(path.join(\"large.txt\"), vec![b'A'; 100]).unwrap(); // 100 bytes\n\n    let params = BigFileParameters::new(2, SearchMode::BiggestFiles);\n    let mut finder = BigFile::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let big_files = finder.get_big_files();\n    assert_eq!(big_files.len(), 2, \"Should find 2 biggest files\");\n    assert_eq!(big_files[0].size, 100, \"First file should be 100 bytes\");\n    assert_eq!(big_files[1].size, 5, \"Second file should be 5 bytes\");\n}\n\n#[test]\nfn test_find_smallest_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create files with different sizes\n    fs::write(path.join(\"small.txt\"), b\"12\").unwrap(); // 2 bytes\n    fs::write(path.join(\"medium.txt\"), b\"12345\").unwrap(); // 5 bytes\n    fs::write(path.join(\"large.txt\"), vec![b'A'; 100]).unwrap(); // 100 bytes\n\n    let params = BigFileParameters::new(2, SearchMode::SmallestFiles);\n    let mut finder = BigFile::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let big_files = finder.get_big_files();\n    assert_eq!(big_files.len(), 2, \"Should find 2 smallest files\");\n    assert_eq!(big_files[0].size, 2, \"First file should be 2 bytes\");\n    assert_eq!(big_files[1].size, 5, \"Second file should be 5 bytes\");\n}\n\n#[test]\nfn test_limit_number_of_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create 5 files\n    for i in 1..=5 {\n        fs::write(path.join(format!(\"file{i}.txt\")), vec![b'A'; i * 10]).unwrap();\n    }\n\n    let params = BigFileParameters::new(3, SearchMode::BiggestFiles);\n    let mut finder = BigFile::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let big_files = finder.get_big_files();\n    assert_eq!(big_files.len(), 3, \"Should limit results to 3 files\");\n}\n\n#[test]\nfn test_empty_directory() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let params = BigFileParameters::new(5, SearchMode::BiggestFiles);\n    let mut finder = BigFile::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let big_files = finder.get_big_files();\n    assert!(big_files.is_empty(), \"Should find no files in empty directory\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/big_file/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::big_file::{BigFile, BigFileParameters, Info, SearchMode};\n\nimpl AllTraits for BigFile {}\n\nimpl DeletingItems for BigFile {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.big_files.clone())),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl DebugPrint for BigFile {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n\n        println!(\"### INDIVIDUAL DEBUG PRINT ###\");\n        println!(\"Info: {:?}\", self.information);\n        println!(\"Number of files to check - {}\", self.get_params().number_of_files_to_check);\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for BigFile {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        if self.information.number_of_real_files != 0 {\n            if self.get_params().search_mode == SearchMode::BiggestFiles {\n                writeln!(writer, \"{} the biggest files.\\n\\n\", self.information.number_of_real_files)?;\n            } else {\n                writeln!(writer, \"{} the smallest files.\\n\\n\", self.information.number_of_real_files)?;\n            }\n            for file_entry in &self.big_files {\n                writeln!(\n                    writer,\n                    \"{} ({}) - \\\"{}\\\"\",\n                    format_size(file_entry.size, BINARY),\n                    file_entry.size,\n                    file_entry.path.to_string_lossy()\n                )?;\n            }\n        } else {\n            writeln!(writer, \"Not found any files.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.big_files, pretty_print)\n    }\n}\n\nimpl Search for BigFile {\n    #[fun_time(message = \"find_big_files\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            if self.look_for_big_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl CommonData for BigFile {\n    type Info = Info;\n    type Parameters = BigFileParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information.clone()\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_real_files > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/broken_files/core.rs",
    "content": "use std::collections::BTreeMap;\nuse std::fs::File;\nuse std::path::Path;\nuse std::process::Command;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::{mem, panic};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::{debug, error};\nuse lopdf::Document;\nuse rayon::prelude::*;\n\nuse crate::common::cache::{CACHE_BROKEN_FILES_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path};\nuse crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_BROKEN_FILES_EXTENSIONS, PDF_FILES_EXTENSIONS, VIDEO_FILES_EXTENSIONS, ZIP_FILES_EXTENSIONS};\nuse crate::common::create_crash_message;\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::process_utils::run_command_interruptible;\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::helpers::audio_checker;\nuse crate::tools::broken_files::{BrokenEntry, BrokenFiles, BrokenFilesParameters, Info, TypeOfFile};\n\nimpl BrokenFiles {\n    pub fn new(params: BrokenFilesParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::BrokenFiles),\n            information: Info::default(),\n            files_to_check: Default::default(),\n            broken_files: Default::default(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.files_to_check = grouped_file_entries\n                    .into_values()\n                    .flatten()\n                    .map(|fe| {\n                        let broken_entry = fe.into_broken_entry();\n                        (broken_entry.path.to_string_lossy().to_string(), broken_entry)\n                    })\n                    .collect();\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"check_files - Found {} files to check.\", self.files_to_check.len());\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    fn check_broken_image(mut file_entry: BrokenEntry) -> BrokenEntry {\n        let mut file_entry_clone = file_entry.clone();\n\n        panic::catch_unwind(|| {\n            match image::open(&file_entry.path) {\n                Ok(img) => {\n                    if img.width() == 0 || img.height() == 0 {\n                        file_entry.error_string = \"Image has zero width or height\".to_string();\n                    }\n                }\n                Err(e) => {\n                    file_entry.error_string = e.to_string().trim().to_string();\n                }\n            }\n            file_entry\n        })\n        .unwrap_or_else(|_| {\n            let message = create_crash_message(\"Image-rs\", &file_entry_clone.path.to_string_lossy(), \"https://github.com/image-rs/image\");\n            error!(\"{message}\");\n            file_entry_clone.error_string = message;\n            file_entry_clone\n        })\n    }\n    fn check_broken_zip(mut file_entry: BrokenEntry) -> Option<BrokenEntry> {\n        match File::open(&file_entry.path) {\n            Ok(file) => {\n                if let Err(e) = zip::ZipArchive::new(file) {\n                    file_entry.error_string = e.to_string().trim().to_string();\n                }\n                Some(file_entry)\n            }\n            Err(_inspected) => None,\n        }\n    }\n    fn check_broken_audio(mut file_entry: BrokenEntry) -> Option<BrokenEntry> {\n        match File::open(&file_entry.path) {\n            Ok(file) => {\n                let mut file_entry_clone = file_entry.clone();\n\n                panic::catch_unwind(|| {\n                    if let Err(e) = audio_checker::parse_audio_file(file) {\n                        let err_str = e.to_string();\n                        if !err_str.contains(\"not supported codec\") {\n                            file_entry.error_string = err_str.trim().to_string();\n                        }\n                    }\n                    Some(file_entry)\n                })\n                .unwrap_or_else(|_| {\n                    let message = create_crash_message(\"Symphonia\", &file_entry_clone.path.to_string_lossy(), \"https://github.com/pdeljanov/Symphonia\");\n                    error!(\"{message}\");\n                    file_entry_clone.error_string = message;\n                    Some(file_entry_clone)\n                })\n            }\n            Err(_inspected) => None,\n        }\n    }\n    fn check_broken_pdf(mut file_entry: BrokenEntry) -> BrokenEntry {\n        let mut file_entry_clone = file_entry.clone();\n        panic::catch_unwind(|| {\n            match File::open(&file_entry.path) {\n                Ok(file) => {\n                    if let Err(e) = Document::load_from(file) {\n                        file_entry.error_string = e.to_string().trim().to_string();\n                    }\n                }\n                Err(e) => {\n                    file_entry.error_string = e.to_string().trim().to_string();\n                }\n            }\n            file_entry\n        })\n        .unwrap_or_else(|_| {\n            let message = create_crash_message(\"lopdf\", &file_entry_clone.path.to_string_lossy(), \"https://github.com/J-F-Liu/lopdf\");\n            error!(\"{message}\");\n            file_entry_clone.error_string = message;\n            file_entry_clone\n        })\n    }\n\n    // None if stopped, otherwise Some\n    fn check_broken_video(mut file_entry: BrokenEntry, stop_flag: &Arc<AtomicBool>) -> Option<BrokenEntry> {\n        let ffprobe_errors = [\n            (\"moov atom not found\", Some(\"broken file structure\")),\n            (\"error reading header\", Some(\"broken file structure\")),\n            (\"EBML header parsing failed\", None),\n            (\"exceeds containing master element\", Some(\"broken file structure\")),\n            (\"invalid frame index table\", Some(\"broken file structure\")),\n            (\"Invalid argument\", Some(\"ffprobe seems to not recognize file format\")),\n        ];\n\n        let mut command = Command::new(\"ffprobe\");\n        command.arg(\"-v\").arg(\"error\").arg(&file_entry.path);\n\n        match run_command_interruptible(command, stop_flag) {\n            None => return None,\n            Some(Err(e)) => {\n                debug!(\"Failed to run ffprobe on {:?}: {}\", file_entry.path, e);\n                file_entry.error_string = format!(\"Failed to run ffprobe: {e}\").trim().to_string();\n                return Some(file_entry);\n            }\n            Some(Ok(output)) => {\n                let combined = format!(\"{}{}\", output.stdout.trim(), output.stderr.trim());\n\n                if let Some((error_message, additional_message)) = ffprobe_errors.iter().find(|(err, _)| combined.contains(err)) {\n                    file_entry.error_string = format!(\"{error_message}{}\", additional_message.map(|e| format!(\" ({e})\")).unwrap_or_default());\n                    return Some(file_entry);\n                } else if !output.status.success() {\n                    // debug_save_file(\"ffprobe_failed_output.txt\", &format!(\"{} --- \\n{}\", file_entry.path.to_string_lossy(), combined));\n                    file_entry.error_string = format!(\"ffprobe exited with non-zero status: {}\", output.status);\n                    return Some(file_entry);\n                }\n            }\n        }\n\n        let ffmpeg_message = [\n            (\"Output file does not contain any stream\", Some(\"cannot find video stream - possible not even video file\")),\n            (\"missing mandatory atoms, broken header\", Some(\"broken file structure\")),\n            (\"Cannot determine format of input\", None),\n            (\"decode_slice_header error\", Some(\"corrupted video data, may be still fully/partially playable\")),\n            (\"Truncating packet\", Some(\"corrupted video data, may be still fully/partially playable\")),\n            (\"Invalid NAL unit size\", Some(\"corrupted video data, may be still fully/partially playable\")),\n            (\n                \"exceeds containing master element ending\",\n                Some(\"corrupted video data, may be still fully/partially playable\"),\n            ),\n            (\"corrupt input packet in stream\", Some(\"Possible corruption in audio/video stream, may be still playable\")),\n            (\n                \"invalid as first byte of an EBML number\",\n                Some(\"corrupted video data, may be still fully/partially playable\"),\n            ),\n            // Last resort for all other errors\n            (\"Invalid data found when processing input\", Some(\"generic error\")), // Must be last to not override more precise errors\n            // Warnings\n            (\"corrupt decoded frame\", Some(\"may be still playable\")),\n        ];\n        let ffmpeg_allowed_messages = [\n            \"Input buffer exhausted before END element found\", // Looks like quite popular message, so ignoring it\n            \"Invalid color space\",                             // https://fftrac-bg.ffmpeg.org/ticket/11020 - seems to be non-fatal\n        ];\n\n        let mut command = Command::new(\"ffmpeg\");\n        command\n            .arg(\"-v\")\n            .arg(\"error\")\n            .arg(\"-xerror\")\n            .arg(\"-threads\")\n            .arg(\"1\")\n            .arg(\"-i\")\n            .arg(&file_entry.path)\n            .arg(\"-f\")\n            .arg(\"null\")\n            .arg(\"-\");\n\n        match run_command_interruptible(command, stop_flag) {\n            None => return None,\n            Some(Err(e)) => {\n                debug!(\"Failed to run ffmpeg on {:?}: {}\", file_entry.path, e);\n                file_entry.error_string = format!(\"Failed to run ffmpeg: {}\", e.trim());\n            }\n            Some(Ok(output)) => {\n                let combined = format!(\"{}{}\", output.stdout.trim(), output.stderr.trim());\n\n                if ffmpeg_allowed_messages.iter().any(|msg| combined.contains(msg)) {\n                    // Allowed message, do nothing\n                } else if let Some((error_message, additional_message)) = ffmpeg_message.iter().find(|(err, _)| combined.contains(err)) {\n                    file_entry.error_string = format!(\"{error_message}{}\", additional_message.map(|e| format!(\" ({e})\")).unwrap_or_default());\n                } else if !output.status.success() {\n                    // debug_save_file(\"ffmpeg_failed_output.txt\", &format!(\"{} --- \\n{}\", file_entry.path.to_string_lossy(), combined));\n                    file_entry.error_string = format!(\"ffmpeg exited with non-zero status: {}\", output.status);\n                }\n            }\n        }\n\n        Some(file_entry)\n    }\n\n    #[fun_time(message = \"load_cache\", level = \"debug\")]\n    fn load_cache(&mut self) -> (BTreeMap<String, BrokenEntry>, BTreeMap<String, BrokenEntry>, BTreeMap<String, BrokenEntry>) {\n        load_and_split_cache_generalized_by_path(&get_broken_files_cache_file(), mem::take(&mut self.files_to_check), self)\n    }\n\n    #[fun_time(message = \"save_to_cache\", level = \"debug\")]\n    fn save_to_cache(&mut self, vec_file_entry: &[BrokenEntry], loaded_hash_map: BTreeMap<String, BrokenEntry>) {\n        save_and_connect_cache_generalized_by_path(&get_broken_files_cache_file(), vec_file_entry, loaded_hash_map, self);\n    }\n\n    fn check_file(file_entry: BrokenEntry, stop_flag: &Arc<AtomicBool>) -> Option<Option<BrokenEntry>> {\n        match check_extension_availability(&file_entry.path) {\n            TypeOfFile::Image => Some(Some(Self::check_broken_image(file_entry))),\n            TypeOfFile::ArchiveZip => Some(Self::check_broken_zip(file_entry)),\n            TypeOfFile::Audio => Some(Self::check_broken_audio(file_entry)),\n            TypeOfFile::Pdf => Some(Some(Self::check_broken_pdf(file_entry))),\n            TypeOfFile::Video => Self::check_broken_video(file_entry, stop_flag).map(Some),\n            TypeOfFile::Unknown => {\n                error!(\"Unknown file type of: {file_entry:?}\");\n                Some(None)\n            }\n        }\n    }\n\n    #[fun_time(message = \"look_for_broken_files\", level = \"debug\")]\n    pub(crate) fn look_for_broken_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.files_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache();\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::BrokenFilesChecking,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            non_cached_files_to_check.values().map(|item| item.size).sum::<u64>(),\n        );\n\n        let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::<Vec<_>>();\n\n        debug!(\"look_for_broken_files - started finding for broken files\");\n        let mut vec_file_entry: Vec<BrokenEntry> = non_cached_files_to_check\n            .into_par_iter()\n            .with_max_len(3)\n            .map(|(_, file_entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let size = file_entry.size;\n                let res = Self::check_file(file_entry, stop_flag);\n\n                progress_handler.increase_items(1);\n                progress_handler.increase_size(size);\n\n                res\n            })\n            .while_some()\n            .flatten()\n            .collect::<Vec<BrokenEntry>>();\n        debug!(\"look_for_broken_files - ended finding for broken files\");\n\n        progress_handler.join_thread();\n\n        // Just connect loaded results with already calculated\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        self.save_to_cache(&vec_file_entry, loaded_hash_map);\n\n        self.broken_files = vec_file_entry.into_iter().filter_map(|f| if f.error_string.is_empty() { None } else { Some(f) }).collect();\n\n        self.information.number_of_broken_files = self.broken_files.len();\n        debug!(\"Found {} broken files.\", self.information.number_of_broken_files);\n        // Clean unused data\n        self.files_to_check = Default::default();\n\n        WorkContinueStatus::Continue\n    }\n}\n\n#[expect(clippy::string_slice)] // Valid, because we address up to the dot, which is known ascii character\nfn check_extension_availability(full_name: &Path) -> TypeOfFile {\n    let Some(file_name) = full_name.file_name() else {\n        error!(\"Missing file name in file - \\\"{}\\\"\", full_name.to_string_lossy());\n        debug_assert!(false, \"Missing file name in file - \\\"{}\\\"\", full_name.to_string_lossy());\n        return TypeOfFile::Unknown;\n    };\n\n    // Faster manual conversion than using Path::extension()\n    let Some(file_name_str) = file_name.to_str() else { return TypeOfFile::Unknown };\n    let Some(extension_idx) = file_name_str.rfind('.') else { return TypeOfFile::Unknown };\n    let extension_str = &file_name_str[extension_idx + 1..];\n\n    let extension_lowercase = extension_str.to_ascii_lowercase();\n\n    if IMAGE_RS_BROKEN_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) {\n        TypeOfFile::Image\n    } else if ZIP_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) {\n        TypeOfFile::ArchiveZip\n    } else if PDF_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) {\n        TypeOfFile::Pdf\n    } else if AUDIO_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) {\n        TypeOfFile::Audio\n    } else if VIDEO_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) {\n        TypeOfFile::Video\n    } else {\n        error!(\"File with unknown extension: \\\"{}\\\" - {extension_lowercase}\", full_name.to_string_lossy());\n        debug_assert!(false, \"File with unknown extension - \\\"{}\\\" - {extension_lowercase}\", full_name.to_string_lossy());\n        TypeOfFile::Unknown\n    }\n}\n\npub fn get_broken_files_cache_file() -> String {\n    format!(\"cache_broken_files_{CACHE_BROKEN_FILES_VERSION}.bin\")\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/broken_files/mod.rs",
    "content": "use bitflags::bitflags;\n\npub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\n#[derive(Clone, Serialize, Deserialize, Debug)]\npub struct BrokenEntry {\n    pub path: PathBuf,\n    pub modified_date: u64,\n    pub size: u64,\n    pub error_string: String,\n}\nimpl ResultEntry for BrokenEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl FileEntry {\n    fn into_broken_entry(self) -> BrokenEntry {\n        BrokenEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n            error_string: String::new(),\n        }\n    }\n}\n\n#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]\npub enum TypeOfFile {\n    Unknown = -1,\n    Image = 0,\n    ArchiveZip,\n    Audio,\n    Pdf,\n    Video,\n}\n\nbitflags! {\n    #[derive(PartialEq, Copy, Clone, Debug)]\n    pub struct CheckedTypes : u32 {\n        const NONE = 0;\n\n        const PDF = 0b1;\n        const AUDIO = 0b10;\n        const IMAGE = 0b100;\n        const ARCHIVE = 0b1000;\n        const VIDEO = 0b10000;\n    }\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_broken_files: usize,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone)]\npub struct BrokenFilesParameters {\n    pub checked_types: CheckedTypes,\n}\n\nimpl BrokenFilesParameters {\n    pub fn new(checked_types: CheckedTypes) -> Self {\n        Self { checked_types }\n    }\n}\n\npub struct BrokenFiles {\n    common_data: CommonToolData,\n    information: Info,\n    files_to_check: BTreeMap<String, BrokenEntry>,\n    broken_files: Vec<BrokenEntry>,\n    params: BrokenFilesParameters,\n}\n\nimpl BrokenFiles {\n    pub const fn get_broken_files(&self) -> &Vec<BrokenEntry> {\n        &self.broken_files\n    }\n\n    pub(crate) fn get_params(&self) -> &BrokenFilesParameters {\n        &self.params\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/broken_files/tests.rs",
    "content": "use std::fs;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes};\n\nfn get_test_resources_path() -> PathBuf {\n    let path = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\")).join(\"test_resources\");\n    assert!(path.exists(), \"Test resources not found at \\\"{}\\\"\", path.to_string_lossy());\n    path\n}\n\nfn corrupt_file(source: &PathBuf, dest: &PathBuf, bytes_to_corrupt: usize) {\n    let mut content = fs::read(source).unwrap();\n    for byte in content.iter_mut().take(bytes_to_corrupt) {\n        *byte = 0x11;\n    }\n    fs::write(dest, content).unwrap();\n}\n\n#[test]\nfn test_find_broken_image() {\n    let temp_dir = TempDir::new().unwrap();\n    let test_resources = get_test_resources_path();\n\n    let source_image = test_resources.join(\"images\").join(\"normal.jpg\");\n    let broken_image = temp_dir.path().join(\"broken.jpg\");\n    corrupt_file(&source_image, &broken_image, 10);\n\n    let params = BrokenFilesParameters::new(CheckedTypes::IMAGE);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    assert_eq!(broken_files.len(), 1, \"Should find 1 broken image file\");\n    assert!(!broken_files[0].error_string.is_empty(), \"Error string should not be empty\");\n}\n\n#[test]\nfn test_valid_image() {\n    let temp_dir = TempDir::new().unwrap();\n    let test_resources = get_test_resources_path();\n\n    let source_image = test_resources.join(\"images\").join(\"normal.jpg\");\n    let valid_image = temp_dir.path().join(\"valid.jpg\");\n    fs::copy(&source_image, &valid_image).unwrap();\n\n    let params = BrokenFilesParameters::new(CheckedTypes::IMAGE);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    assert_eq!(broken_files.len(), 0, \"Should find no broken image files\");\n}\n\n#[test]\nfn test_broken_audio() {\n    let temp_dir = TempDir::new().unwrap();\n    let test_resources = get_test_resources_path();\n\n    let source_audio = test_resources.join(\"audio\").join(\"base.mp3\");\n    let broken_audio = temp_dir.path().join(\"broken.mp3\");\n    let file_len = fs::metadata(&source_audio).unwrap().len();\n    corrupt_file(&source_audio, &broken_audio, file_len as usize);\n\n    let good_audio = temp_dir.path().join(\"good.mp3\");\n    fs::copy(&source_audio, &good_audio).unwrap();\n\n    let params = BrokenFilesParameters::new(CheckedTypes::AUDIO);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    assert_eq!(broken_files.len(), 1, \"Should find 1 broken audio file\");\n    assert!(!broken_files[0].error_string.is_empty(), \"Error string should not be empty\");\n}\n\n#[test]\nfn test_mixed_valid_and_broken_images() {\n    let temp_dir = TempDir::new().unwrap();\n    let test_resources = get_test_resources_path();\n\n    let source_image1 = test_resources.join(\"images\").join(\"normal.jpg\");\n    fs::copy(&source_image1, temp_dir.path().join(\"valid.jpg\")).unwrap();\n\n    let source_image2 = test_resources.join(\"images\").join(\"normal2.jpg\");\n    corrupt_file(&source_image2, &temp_dir.path().join(\"broken.jpg\"), 10);\n\n    let params = BrokenFilesParameters::new(CheckedTypes::IMAGE);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    let info = finder.get_information();\n\n    assert_eq!(broken_files.len(), 1, \"Should find only 1 broken file out of 2 total\");\n    assert_eq!(info.number_of_broken_files, 1, \"Info should report 1 broken file\");\n}\n\n#[test]\nfn test_multiple_file_types() {\n    let temp_dir = TempDir::new().unwrap();\n    let test_resources = get_test_resources_path();\n\n    let source_image = test_resources.join(\"images\").join(\"normal.jpg\");\n    corrupt_file(&source_image, &temp_dir.path().join(\"broken.jpg\"), 10);\n\n    let source_audio = test_resources.join(\"audio\").join(\"base.mp3\");\n    let file_len = fs::metadata(&source_audio).unwrap().len();\n    corrupt_file(&source_audio, &temp_dir.path().join(\"broken.mp3\"), file_len as usize);\n\n    let params = BrokenFilesParameters::new(CheckedTypes::IMAGE | CheckedTypes::AUDIO);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    assert_eq!(broken_files.len(), 2, \"Should find 2 broken files\");\n}\n\n#[test]\nfn test_empty_directory() {\n    let temp_dir = TempDir::new().unwrap();\n\n    let params = BrokenFilesParameters::new(CheckedTypes::IMAGE);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    assert_eq!(broken_files.len(), 0, \"Should find no broken files in empty directory\");\n}\n\n#[test]\nfn test_no_file_types_selected() {\n    let temp_dir = TempDir::new().unwrap();\n    let test_resources = get_test_resources_path();\n\n    let source_image = test_resources.join(\"images\").join(\"normal.jpg\");\n    corrupt_file(&source_image, &temp_dir.path().join(\"broken.jpg\"), 10);\n\n    let params = BrokenFilesParameters::new(CheckedTypes::NONE);\n    let mut finder = BrokenFiles::new(params);\n    finder.set_included_paths(vec![temp_dir.path().to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let broken_files = finder.get_broken_files();\n    assert_eq!(broken_files.len(), 0, \"Should find no files when no types are selected\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/broken_files/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_BROKEN_FILES_EXTENSIONS, PDF_FILES_EXTENSIONS, VIDEO_FILES_EXTENSIONS, ZIP_FILES_EXTENSIONS};\nuse crate::common::ffmpeg_utils::check_if_ffprobe_ffmpeg_exists;\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::flc;\nuse crate::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes, Info};\n\nimpl AllTraits for BrokenFiles {}\n\nimpl Search for BrokenFiles {\n    #[fun_time(message = \"find_broken_files\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.params.checked_types.contains(CheckedTypes::VIDEO) && !check_if_ffprobe_ffmpeg_exists() {\n                self.common_data.text_messages.critical = Some(flc!(\"core_ffmpeg_not_found\"));\n                #[cfg(target_os = \"windows\")]\n                self.common_data.text_messages.errors.push(flc!(\"core_ffmpeg_not_found_windows\"));\n                return;\n            }\n\n            let extension_types = [\n                (CheckedTypes::PDF, PDF_FILES_EXTENSIONS),\n                (CheckedTypes::AUDIO, AUDIO_FILES_EXTENSIONS),\n                (CheckedTypes::ARCHIVE, ZIP_FILES_EXTENSIONS),\n                (CheckedTypes::IMAGE, IMAGE_RS_BROKEN_FILES_EXTENSIONS),\n                (CheckedTypes::VIDEO, VIDEO_FILES_EXTENSIONS),\n            ];\n            let extensions = extension_types\n                .into_iter()\n                .filter(|(checked_type, _)| self.get_params().checked_types.contains(*checked_type))\n                .flat_map(|(_, exts)| exts.to_vec())\n                .collect::<Vec<&str>>();\n\n            if extensions.is_empty() {\n                self.common_data.text_messages.critical = Some(flc!(\"core_needs_to_set_at_least_one_broken_option\"));\n                return;\n            }\n\n            if self.prepare_items(Some(&extensions)).is_err() {\n                return;\n            }\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.look_for_broken_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for BrokenFiles {\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        self.debug_print_common();\n    }\n}\n\nimpl PrintResults for BrokenFiles {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        if !self.broken_files.is_empty() {\n            writeln!(writer, \"Found {} broken files.\", self.information.number_of_broken_files)?;\n            for file_entry in &self.broken_files {\n                writeln!(writer, \"\\\"{}\\\" - {}\", file_entry.path.to_string_lossy(), file_entry.error_string)?;\n            }\n        } else {\n            write!(writer, \"Not found any broken files.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.broken_files, pretty_print)\n    }\n}\nimpl DeletingItems for BrokenFiles {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.broken_files.clone())),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl CommonData for BrokenFiles {\n    type Info = Info;\n    type Parameters = BrokenFilesParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_broken_files > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/duplicate/core.rs",
    "content": "use std::collections::BTreeMap;\nuse std::path::Path;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\nuse std::{mem, thread};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\nuse indexmap::IndexMap;\nuse log::debug;\nuse rayon::prelude::*;\n\nuse crate::common::cache::{CACHE_DUPLICATE_VERSION, load_cache_from_file_generalized_by_size, save_cache_to_file_generalized};\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{CheckingMethod, FileEntry, HashType, ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::common::traits::ResultEntry;\nuse crate::tools::duplicate::{\n    DuplicateEntry, DuplicateFinder, DuplicateFinderParameters, Info, PREHASHING_BUFFER_SIZE, THREAD_BUFFER, filter_hard_links, hash_calculation, hash_calculation_limit,\n};\n\nimpl DuplicateFinder {\n    pub fn new(params: DuplicateFinderParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::Duplicate),\n            information: Info::default(),\n            files_with_identical_names: Default::default(),\n            files_with_identical_size: Default::default(),\n            files_with_identical_size_names: Default::default(),\n            files_with_identical_hashes: Default::default(),\n            files_with_identical_names_referenced: Default::default(),\n            files_with_identical_size_names_referenced: Default::default(),\n            files_with_identical_size_referenced: Default::default(),\n            files_with_identical_hashes_referenced: Default::default(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"check_files_name\", level = \"debug\")]\n    pub(crate) fn check_files_name(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let group_by_func = if self.get_params().case_sensitive_name_comparison {\n            |fe: &FileEntry| {\n                fe.path\n                    .file_name()\n                    .unwrap_or_else(|| panic!(\"Found invalid file_name \\\"{}\\\" (cannot panic, because it is always normal file)\", fe.path.to_string_lossy()))\n                    .to_string_lossy()\n                    .to_string()\n            }\n        } else {\n            |fe: &FileEntry| {\n                fe.path\n                    .file_name()\n                    .unwrap_or_else(|| panic!(\"Found invalid file_name \\\"{}\\\" (cannot panic, because it is always normal file)\", fe.path.to_string_lossy()))\n                    .to_string_lossy()\n                    .to_lowercase()\n            }\n        };\n\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(group_by_func)\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .checking_method(CheckingMethod::Name)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.common_data.text_messages.warnings.extend(warnings);\n\n                // Create new BTreeMap without single size entries(files have not duplicates)\n                self.files_with_identical_names = grouped_file_entries\n                    .into_iter()\n                    .filter_map(|(name, vector)| {\n                        if vector.len() > 1 {\n                            Some((name, vector.into_iter().map(FileEntry::into_duplicate_entry).collect()))\n                        } else {\n                            None\n                        }\n                    })\n                    .collect();\n\n                // Reference - only use in size, because later hash will be counted differently\n                if self.common_data.use_reference_folders {\n                    let vec = mem::take(&mut self.files_with_identical_names)\n                        .into_iter()\n                        .filter_map(|(_name, vec_file_entry)| {\n                            let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry\n                                .into_iter()\n                                .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path()));\n\n                            if normal_files.is_empty() {\n                                None\n                            } else {\n                                files_from_referenced_folders.pop().map(|file| (file, normal_files))\n                            }\n                        })\n                        .collect::<Vec<(DuplicateEntry, Vec<DuplicateEntry>)>>();\n                    for (fe, vec_fe) in vec {\n                        self.files_with_identical_names_referenced.insert(fe.path.to_string_lossy().to_string(), (fe, vec_fe));\n                    }\n                }\n                self.calculate_name_stats();\n\n                WorkContinueStatus::Continue\n            }\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    fn calculate_name_stats(&mut self) {\n        if self.common_data.use_reference_folders {\n            for (_fe, vector) in self.files_with_identical_names_referenced.values() {\n                self.information.number_of_duplicated_files_by_name += vector.len();\n                self.information.number_of_groups_by_name += 1;\n            }\n        } else {\n            for vector in self.files_with_identical_names.values() {\n                self.information.number_of_duplicated_files_by_name += vector.len() - 1;\n                self.information.number_of_groups_by_name += 1;\n            }\n        }\n    }\n\n    #[fun_time(message = \"check_files_size_name\", level = \"debug\")]\n    pub(crate) fn check_files_size_name(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let group_by_func = if self.get_params().case_sensitive_name_comparison {\n            |fe: &FileEntry| {\n                (\n                    fe.size,\n                    fe.path\n                        .file_name()\n                        .unwrap_or_else(|| panic!(\"Found invalid file_name \\\"{}\\\" (cannot panic, because it is always normal file)\", fe.path.to_string_lossy()))\n                        .to_string_lossy()\n                        .to_string(),\n                )\n            }\n        } else {\n            |fe: &FileEntry| {\n                (\n                    fe.size,\n                    fe.path\n                        .file_name()\n                        .unwrap_or_else(|| panic!(\"Found invalid file_name \\\"{}\\\" (cannot panic, because it is always normal file)\", fe.path.to_string_lossy()))\n                        .to_string_lossy()\n                        .to_lowercase(),\n                )\n            }\n        };\n\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(group_by_func)\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .checking_method(CheckingMethod::SizeName)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.common_data.text_messages.warnings.extend(warnings);\n\n                self.files_with_identical_size_names = grouped_file_entries\n                    .into_iter()\n                    .filter_map(|(size_name, vector)| {\n                        if vector.len() > 1 {\n                            Some((size_name, vector.into_iter().map(FileEntry::into_duplicate_entry).collect()))\n                        } else {\n                            None\n                        }\n                    })\n                    .collect();\n\n                // Reference - only use in size, because later hash will be counted differently\n                if self.common_data.use_reference_folders {\n                    let vec = mem::take(&mut self.files_with_identical_size_names)\n                        .into_iter()\n                        .filter_map(|(_size, vec_file_entry)| {\n                            let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry\n                                .into_iter()\n                                .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path()));\n\n                            if normal_files.is_empty() {\n                                None\n                            } else {\n                                files_from_referenced_folders.pop().map(|file| (file, normal_files))\n                            }\n                        })\n                        .collect::<Vec<(DuplicateEntry, Vec<DuplicateEntry>)>>();\n                    for (fe, vec_fe) in vec {\n                        self.files_with_identical_size_names_referenced\n                            .insert((fe.size, fe.path.to_string_lossy().to_string()), (fe, vec_fe));\n                    }\n                }\n                self.calculate_size_name_stats();\n\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    fn calculate_size_name_stats(&mut self) {\n        if self.common_data.use_reference_folders {\n            for ((size, _name), (_fe, vector)) in &self.files_with_identical_size_names_referenced {\n                self.information.number_of_duplicated_files_by_size_name += vector.len();\n                self.information.number_of_groups_by_size_name += 1;\n                self.information.lost_space_by_size += (vector.len() as u64) * size;\n            }\n        } else {\n            for ((size, _name), vector) in &self.files_with_identical_size_names {\n                self.information.number_of_duplicated_files_by_size_name += vector.len() - 1;\n                self.information.number_of_groups_by_size_name += 1;\n                self.information.lost_space_by_size += (vector.len() as u64 - 1) * size;\n            }\n        }\n    }\n\n    #[fun_time(message = \"check_files_size\", level = \"debug\")]\n    pub(crate) fn check_files_size(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(|fe| fe.size)\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .checking_method(self.get_params().check_method)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.common_data.text_messages.warnings.extend(warnings);\n\n                let grouped_file_entries: Vec<(u64, Vec<FileEntry>)> = grouped_file_entries.into_iter().collect();\n                let rayon_max_len = if self.get_hide_hard_links() { 3 } else { 100 };\n\n                let start_time = Instant::now();\n                // We only gather files with more than 1 entry, because only this will be later used\n                let initial_size = grouped_file_entries\n                    .iter()\n                    .map(|(_size, vec)| if vec.len() > 1 { vec.len() as u64 } else { 0 })\n                    .sum::<u64>();\n                self.files_with_identical_size = grouped_file_entries\n                    .into_par_iter()\n                    .with_max_len(rayon_max_len)\n                    .filter_map(|(size, vec)| {\n                        if vec.len() <= 1 {\n                            return None;\n                        }\n\n                        let vector = if self.get_hide_hard_links() { filter_hard_links(vec) } else { vec };\n\n                        if vector.len() > 1 {\n                            Some((size, vector.into_iter().map(FileEntry::into_duplicate_entry).collect()))\n                        } else {\n                            None\n                        }\n                    })\n                    .collect();\n                let filtered_size = self.files_with_identical_size.values().map(|v| v.len() as u64).sum::<u64>();\n                debug!(\n                    \"check_file_size - filtered hard links in {:?}, removed {} hardlinks ({} -> {})\",\n                    start_time.elapsed(),\n                    initial_size - filtered_size,\n                    initial_size,\n                    filtered_size\n                );\n\n                self.filter_reference_folders_by_size();\n                self.calculate_size_stats();\n\n                debug!(\n                    \"check_file_size - after calculating size stats/duplicates, found in {} groups, {} files with same size | referenced {} groups, {} files\",\n                    self.files_with_identical_size.len(),\n                    self.files_with_identical_size.values().map(Vec::len).sum::<usize>(),\n                    self.files_with_identical_size_referenced.len(),\n                    self.files_with_identical_size_referenced.values().map(|(_fe, vec)| vec.len()).sum::<usize>()\n                );\n\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    fn calculate_size_stats(&mut self) {\n        if self.common_data.use_reference_folders {\n            for (size, (_fe, vector)) in &self.files_with_identical_size_referenced {\n                self.information.number_of_duplicated_files_by_size += vector.len();\n                self.information.number_of_groups_by_size += 1;\n                self.information.lost_space_by_size += (vector.len() as u64) * size;\n            }\n        } else {\n            for (size, vector) in &self.files_with_identical_size {\n                self.information.number_of_duplicated_files_by_size += vector.len() - 1;\n                self.information.number_of_groups_by_size += 1;\n                self.information.lost_space_by_size += (vector.len() as u64 - 1) * size;\n            }\n        }\n    }\n\n    #[fun_time(message = \"filter_reference_folders_by_size\", level = \"debug\")]\n    fn filter_reference_folders_by_size(&mut self) {\n        if self.common_data.use_reference_folders && self.get_params().check_method == CheckingMethod::Size {\n            let vec = mem::take(&mut self.files_with_identical_size)\n                .into_iter()\n                .filter_map(|(_size, vec_file_entry)| {\n                    let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry\n                        .into_iter()\n                        .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path()));\n\n                    if normal_files.is_empty() {\n                        None\n                    } else {\n                        files_from_referenced_folders.pop().map(|file| (file, normal_files))\n                    }\n                })\n                .collect::<Vec<(DuplicateEntry, Vec<DuplicateEntry>)>>();\n            for (fe, vec_fe) in vec {\n                self.files_with_identical_size_referenced.insert(fe.size, (fe, vec_fe));\n            }\n        }\n    }\n\n    #[fun_time(message = \"prehash_load_cache_at_start\", level = \"debug\")]\n    fn prehash_load_cache_at_start(&mut self) -> (BTreeMap<u64, Vec<DuplicateEntry>>, BTreeMap<u64, Vec<DuplicateEntry>>, BTreeMap<u64, Vec<DuplicateEntry>>) {\n        // Cache algorithm\n        // - Load data from cache\n        // - Convert from BT<u64,Vec<DuplicateEntry>> to BT<String,DuplicateEntry>\n        // - Save to proper values\n        let loaded_hash_map;\n        let mut records_already_cached: BTreeMap<u64, Vec<DuplicateEntry>> = Default::default();\n        let mut non_cached_files_to_check: BTreeMap<u64, Vec<DuplicateEntry>> = Default::default();\n\n        if self.get_params().use_prehash_cache {\n            let (messages, loaded_items) = load_cache_from_file_generalized_by_size::<DuplicateEntry>(\n                &get_duplicate_cache_file(self.get_params().hash_type, true),\n                self.get_delete_outdated_cache(),\n                &self.files_with_identical_size,\n            );\n            self.get_text_messages_mut().extend_with_another_messages(messages);\n            loaded_hash_map = loaded_items.unwrap_or_default();\n\n            Self::diff_loaded_and_prechecked_files(\n                \"prehash_load_cache_at_start\",\n                mem::take(&mut self.files_with_identical_size),\n                &loaded_hash_map,\n                &mut records_already_cached,\n                &mut non_cached_files_to_check,\n            );\n        } else {\n            loaded_hash_map = Default::default();\n            mem::swap(&mut self.files_with_identical_size, &mut non_cached_files_to_check);\n        }\n        (loaded_hash_map, records_already_cached, non_cached_files_to_check)\n    }\n\n    #[fun_time(message = \"prehash_save_cache_at_exit\", level = \"debug\")]\n    fn prehash_save_cache_at_exit(\n        &mut self,\n        loaded_hash_map: BTreeMap<u64, Vec<DuplicateEntry>>,\n        pre_hash_results: Vec<(u64, BTreeMap<String, Vec<DuplicateEntry>>, Vec<String>)>,\n    ) {\n        if self.get_params().use_prehash_cache {\n            // All results = records already cached + computed results\n            let mut save_cache_to_hashmap: BTreeMap<String, DuplicateEntry> = Default::default();\n\n            for (size, vec_file_entry) in loaded_hash_map {\n                if size >= self.get_params().minimal_prehash_cache_file_size {\n                    for file_entry in vec_file_entry {\n                        save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry);\n                    }\n                }\n            }\n\n            for (size, hash_map, _errors) in pre_hash_results {\n                if size >= self.get_params().minimal_prehash_cache_file_size {\n                    for vec_file_entry in hash_map.into_values() {\n                        for file_entry in vec_file_entry {\n                            save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry);\n                        }\n                    }\n                }\n            }\n\n            let messages = save_cache_to_file_generalized(\n                &get_duplicate_cache_file(self.get_params().hash_type, true),\n                &save_cache_to_hashmap,\n                self.common_data.save_also_as_json,\n                self.get_params().minimal_prehash_cache_file_size,\n            );\n            self.get_text_messages_mut().extend_with_another_messages(messages);\n        }\n    }\n\n    #[fun_time(message = \"prehashing\", level = \"debug\")]\n    fn prehashing(\n        &mut self,\n        stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n        pre_checked_map: &mut BTreeMap<u64, Vec<DuplicateEntry>>,\n    ) -> WorkContinueStatus {\n        if self.files_with_identical_size.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let check_type = self.get_params().hash_type;\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicatePreHashCacheLoading, 0, self.get_test_type(), 0);\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.prehash_load_cache_at_start();\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::DuplicatePreHashing,\n            non_cached_files_to_check.values().map(Vec::len).sum(),\n            self.get_test_type(),\n            non_cached_files_to_check\n                .iter()\n                .map(|(size, items)| items.len() as u64 * PREHASHING_BUFFER_SIZE.min(*size))\n                .sum::<u64>(),\n        );\n\n        // Convert to vector to be able to use with_max_len method from rayon\n        let non_cached_files_to_check: Vec<(u64, Vec<DuplicateEntry>)> = non_cached_files_to_check.into_iter().collect();\n\n        debug!(\"Starting calculating prehash\");\n        #[expect(clippy::type_complexity)]\n        let pre_hash_results: Vec<(u64, BTreeMap<String, Vec<DuplicateEntry>>, Vec<String>)> = non_cached_files_to_check\n            .into_par_iter()\n            .with_max_len(3) // Vectors and BTreeMaps for really big inputs, leave some jobs to 0 thread, to avoid that I minimized max tasks for each thread to 3, which improved performance\n            .map(|(size, vec_file_entry)| {\n                let mut hashmap_with_hash: BTreeMap<String, Vec<DuplicateEntry>> = Default::default();\n                let mut errors: Vec<String> = Vec::new();\n\n                THREAD_BUFFER.with_borrow_mut(|buffer| {\n                    for mut file_entry in vec_file_entry {\n                        if check_if_stop_received(stop_flag) {\n                            return None;\n                        }\n                        match hash_calculation_limit(buffer, &file_entry, check_type, PREHASHING_BUFFER_SIZE, progress_handler.size_counter()) {\n                            Ok(hash_string) => {\n                                file_entry.hash = hash_string.clone();\n                                hashmap_with_hash.entry(hash_string).or_default().push(file_entry);\n                            }\n                            Err(s) => errors.push(s),\n                        }\n                        progress_handler.increase_items(1);\n                    }\n\n                    Some(())\n                })?;\n\n                Some((size, hashmap_with_hash, errors))\n            })\n            .while_some()\n            .collect();\n\n        debug!(\"Completed calculating prehash\");\n\n        progress_handler.join_thread();\n\n        // Saving into cache\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicatePreHashCacheSaving, 0, self.get_test_type(), 0);\n\n        // Add data from cache\n        for (size, mut vec_file_entry) in records_already_cached {\n            pre_checked_map.entry(size).or_default().append(&mut vec_file_entry);\n        }\n\n        // Check results\n        for (size, hash_map, errors) in &pre_hash_results {\n            if !errors.is_empty() {\n                self.common_data.text_messages.warnings.append(&mut errors.clone());\n            }\n            for vec_file_entry in hash_map.values() {\n                if vec_file_entry.len() > 1 {\n                    pre_checked_map.entry(*size).or_default().append(&mut vec_file_entry.clone());\n                }\n            }\n        }\n\n        self.prehash_save_cache_at_exit(loaded_hash_map, pre_hash_results);\n\n        progress_handler.join_thread();\n\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    fn diff_loaded_and_prechecked_files(\n        function_name: &str,\n        used_map: BTreeMap<u64, Vec<DuplicateEntry>>,\n        loaded_hash_map: &BTreeMap<u64, Vec<DuplicateEntry>>,\n        records_already_cached: &mut BTreeMap<u64, Vec<DuplicateEntry>>,\n        non_cached_files_to_check: &mut BTreeMap<u64, Vec<DuplicateEntry>>,\n    ) {\n        debug!(\"{function_name} - started diff between loaded and prechecked files\");\n\n        for (size, mut vec_file_entry) in used_map {\n            if let Some(cached_vec_file_entry) = loaded_hash_map.get(&size) {\n                // TODO maybe hashmap is not needed when using < 4 elements\n                let mut cached_path_entries: IndexMap<&Path, DuplicateEntry> = IndexMap::new();\n                for file_entry in cached_vec_file_entry {\n                    cached_path_entries.insert(&file_entry.path, file_entry.clone());\n                }\n                for file_entry in vec_file_entry {\n                    if let Some(cached_file_entry) = cached_path_entries.swap_remove(file_entry.path.as_path()) {\n                        records_already_cached.entry(size).or_default().push(cached_file_entry);\n                    } else {\n                        non_cached_files_to_check.entry(size).or_default().push(file_entry);\n                    }\n                }\n            } else {\n                non_cached_files_to_check.entry(size).or_default().append(&mut vec_file_entry);\n            }\n        }\n        debug!(\n            \"{function_name} - completed diff between loaded and prechecked files - {}({}) non cached, {}({}) already cached\",\n            non_cached_files_to_check.len(),\n            format_size(non_cached_files_to_check.values().map(|v| v.iter().map(|e| e.size).sum::<u64>()).sum::<u64>(), BINARY),\n            records_already_cached.len(),\n            format_size(records_already_cached.values().map(|v| v.iter().map(|e| e.size).sum::<u64>()).sum::<u64>(), BINARY),\n        );\n    }\n\n    #[fun_time(message = \"full_hashing_load_cache_at_start\", level = \"debug\")]\n    fn full_hashing_load_cache_at_start(\n        &mut self,\n        mut pre_checked_map: BTreeMap<u64, Vec<DuplicateEntry>>,\n    ) -> (BTreeMap<u64, Vec<DuplicateEntry>>, BTreeMap<u64, Vec<DuplicateEntry>>, BTreeMap<u64, Vec<DuplicateEntry>>) {\n        let loaded_hash_map;\n        let mut records_already_cached: BTreeMap<u64, Vec<DuplicateEntry>> = Default::default();\n        let mut non_cached_files_to_check: BTreeMap<u64, Vec<DuplicateEntry>> = Default::default();\n\n        if self.common_data.use_cache {\n            debug!(\"full_hashing_load_cache_at_start - using cache\");\n            let (messages, loaded_items) = load_cache_from_file_generalized_by_size::<DuplicateEntry>(\n                &get_duplicate_cache_file(self.get_params().hash_type, false),\n                self.get_delete_outdated_cache(),\n                &pre_checked_map,\n            );\n            self.get_text_messages_mut().extend_with_another_messages(messages);\n            loaded_hash_map = loaded_items.unwrap_or_default();\n\n            Self::diff_loaded_and_prechecked_files(\n                \"full_hashing_load_cache_at_start\",\n                pre_checked_map,\n                &loaded_hash_map,\n                &mut records_already_cached,\n                &mut non_cached_files_to_check,\n            );\n        } else {\n            debug!(\"full_hashing_load_cache_at_start - not using cache\");\n            loaded_hash_map = Default::default();\n            mem::swap(&mut pre_checked_map, &mut non_cached_files_to_check);\n        }\n        (loaded_hash_map, records_already_cached, non_cached_files_to_check)\n    }\n\n    #[fun_time(message = \"full_hashing_save_cache_at_exit\", level = \"debug\")]\n    fn full_hashing_save_cache_at_exit(\n        &mut self,\n        records_already_cached: BTreeMap<u64, Vec<DuplicateEntry>>,\n        full_hash_results: &mut Vec<(u64, BTreeMap<String, Vec<DuplicateEntry>>, Vec<String>)>,\n        loaded_hash_map: BTreeMap<u64, Vec<DuplicateEntry>>,\n    ) {\n        if !self.common_data.use_cache {\n            return;\n        }\n        'main: for (size, vec_file_entry) in records_already_cached {\n            // Check if size already exists, if exists we must to change it outside because cannot have mut and non mut reference to full_hash_results\n            for (full_size, full_hashmap, _errors) in &mut (*full_hash_results) {\n                if size == *full_size {\n                    for file_entry in vec_file_entry {\n                        full_hashmap.entry(file_entry.hash.clone()).or_default().push(file_entry);\n                    }\n                    continue 'main;\n                }\n            }\n            // Size doesn't exists add results to files\n            let mut temp_hashmap: BTreeMap<String, Vec<DuplicateEntry>> = Default::default();\n            for file_entry in vec_file_entry {\n                temp_hashmap.entry(file_entry.hash.clone()).or_default().push(file_entry);\n            }\n            full_hash_results.push((size, temp_hashmap, Vec::new()));\n        }\n\n        // Must save all results to file, old loaded from file with all currently counted results\n        let mut all_results: BTreeMap<String, DuplicateEntry> = Default::default();\n        for (_size, vec_file_entry) in loaded_hash_map {\n            for file_entry in vec_file_entry {\n                all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry);\n            }\n        }\n        for (_size, hashmap, _errors) in full_hash_results {\n            for vec_file_entry in hashmap.values() {\n                for file_entry in vec_file_entry {\n                    all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());\n                }\n            }\n        }\n\n        let messages = save_cache_to_file_generalized(\n            &get_duplicate_cache_file(self.get_params().hash_type, false),\n            &all_results,\n            self.common_data.save_also_as_json,\n            self.get_params().minimal_cache_file_size,\n        );\n        self.get_text_messages_mut().extend_with_another_messages(messages);\n    }\n\n    #[fun_time(message = \"full_hashing\", level = \"debug\")]\n    fn full_hashing(\n        &mut self,\n        stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n        pre_checked_map: BTreeMap<u64, Vec<DuplicateEntry>>,\n    ) -> WorkContinueStatus {\n        if pre_checked_map.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicateCacheLoading, 0, self.get_test_type(), 0);\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.full_hashing_load_cache_at_start(pre_checked_map);\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::DuplicateFullHashing,\n            non_cached_files_to_check.values().map(Vec::len).sum(),\n            self.get_test_type(),\n            non_cached_files_to_check.iter().map(|(size, items)| (*size) * items.len() as u64).sum::<u64>(),\n        );\n\n        let non_cached_files_to_check: Vec<(u64, Vec<DuplicateEntry>)> = non_cached_files_to_check.into_iter().collect();\n\n        let check_type = self.get_params().hash_type;\n        debug!(\n            \"Starting full hashing of {} files\",\n            non_cached_files_to_check.iter().map(|(_size, v)| v.len() as u64).sum::<u64>()\n        );\n        let mut full_hash_results: Vec<(u64, BTreeMap<String, Vec<DuplicateEntry>>, Vec<String>)> = non_cached_files_to_check\n            .into_par_iter()\n            .with_max_len(3)\n            .map(|(size, vec_file_entry)| {\n                let mut hashmap_with_hash: BTreeMap<String, Vec<DuplicateEntry>> = Default::default();\n                let mut errors: Vec<String> = Vec::new();\n\n                THREAD_BUFFER.with_borrow_mut(|buffer| {\n                    for mut file_entry in vec_file_entry {\n                        if check_if_stop_received(stop_flag) {\n                            return None;\n                        }\n\n                        match hash_calculation(buffer, &file_entry, check_type, progress_handler.size_counter(), stop_flag) {\n                            Ok(hash_string) => {\n                                if let Some(hash_string) = hash_string {\n                                    file_entry.hash = hash_string.clone();\n                                    hashmap_with_hash.entry(hash_string).or_default().push(file_entry);\n                                } else {\n                                    return None;\n                                }\n                            }\n                            Err(s) => errors.push(s),\n                        }\n                        progress_handler.increase_items(1);\n                    }\n                    Some(())\n                })?;\n\n                Some((size, hashmap_with_hash, errors))\n            })\n            .while_some()\n            .collect();\n        debug!(\"Finished full hashing\");\n\n        // Even if clicked stop, save items to cache and show results\n\n        progress_handler.join_thread();\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicateCacheSaving, 0, self.get_test_type(), 0);\n\n        self.full_hashing_save_cache_at_exit(records_already_cached, &mut full_hash_results, loaded_hash_map);\n\n        progress_handler.join_thread();\n\n        for (size, hash_map, mut errors) in full_hash_results {\n            self.common_data.text_messages.warnings.append(&mut errors);\n            for (_hash, vec_file_entry) in hash_map {\n                if vec_file_entry.len() > 1 {\n                    self.files_with_identical_hashes.entry(size).or_default().push(vec_file_entry);\n                }\n            }\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"hash_reference_folders\", level = \"debug\")]\n    fn hash_reference_folders(&mut self) {\n        // Reference - only use in size, because later hash will be counted differently\n        if self.common_data.use_reference_folders {\n            let vec = mem::take(&mut self.files_with_identical_hashes)\n                .into_iter()\n                .filter_map(|(_size, vec_vec_file_entry)| {\n                    let mut all_results_with_same_size = Vec::new();\n                    for vec_file_entry in vec_vec_file_entry {\n                        let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry\n                            .into_iter()\n                            .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path()));\n\n                        if normal_files.is_empty() {\n                            continue;\n                        }\n                        if let Some(file) = files_from_referenced_folders.pop() {\n                            all_results_with_same_size.push((file, normal_files));\n                        }\n                    }\n                    if all_results_with_same_size.is_empty() {\n                        None\n                    } else {\n                        Some(all_results_with_same_size)\n                    }\n                })\n                .collect::<Vec<Vec<(DuplicateEntry, Vec<DuplicateEntry>)>>>();\n            #[expect(clippy::indexing_slicing)] // Safe, because here, empty vectors cannot exist\n            for vec_of_vec in vec {\n                self.files_with_identical_hashes_referenced.insert(vec_of_vec[0].0.size, vec_of_vec);\n            }\n        }\n\n        if self.common_data.use_reference_folders {\n            for (size, vector_vectors) in &self.files_with_identical_hashes_referenced {\n                for (_fe, vector) in vector_vectors {\n                    self.information.number_of_duplicated_files_by_hash += vector.len();\n                    self.information.number_of_groups_by_hash += 1;\n                    self.information.lost_space_by_hash += (vector.len() as u64) * size;\n                }\n            }\n        } else {\n            for (size, vector_vectors) in &self.files_with_identical_hashes {\n                for vector in vector_vectors {\n                    self.information.number_of_duplicated_files_by_hash += vector.len() - 1;\n                    self.information.number_of_groups_by_hash += 1;\n                    self.information.lost_space_by_hash += (vector.len() as u64 - 1) * size;\n                }\n            }\n        }\n    }\n\n    #[fun_time(message = \"check_files_hash\", level = \"debug\")]\n    pub(crate) fn check_files_hash(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        assert_eq!(self.get_params().check_method, CheckingMethod::Hash);\n\n        let mut pre_checked_map: BTreeMap<u64, Vec<DuplicateEntry>> = Default::default();\n        if self.prehashing(stop_flag, progress_sender, &mut pre_checked_map) == WorkContinueStatus::Stop {\n            return WorkContinueStatus::Stop;\n        }\n\n        if self.full_hashing(stop_flag, progress_sender, pre_checked_map) == WorkContinueStatus::Stop {\n            return WorkContinueStatus::Stop;\n        }\n\n        self.hash_reference_folders();\n\n        // Clean unused data\n        let files_with_identical_size = mem::take(&mut self.files_with_identical_size);\n        thread::spawn(move || drop(files_with_identical_size));\n\n        WorkContinueStatus::Continue\n    }\n}\n\npub fn get_duplicate_cache_file(type_of_hash: HashType, is_prehash: bool) -> String {\n    let prehash_str = if is_prehash { \"_prehash\" } else { \"\" };\n    format!(\"cache_duplicates_{type_of_hash:?}{prehash_str}_{CACHE_DUPLICATE_VERSION}.bin\")\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/duplicate/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::cell::RefCell;\nuse std::collections::BTreeMap;\nuse std::fmt::Debug;\n#[cfg(target_family = \"unix\")]\nuse std::fs;\nuse std::fs::File;\nuse std::hash::Hasher;\nuse std::io::prelude::*;\n#[cfg(target_family = \"unix\")]\nuse std::os::unix::fs::MetadataExt;\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::time::Duration;\n\nuse indexmap::IndexSet;\nuse serde::{Deserialize, Serialize};\nuse static_assertions::const_assert;\nuse xxhash_rust::xxh3::Xxh3;\n\nuse crate::common::model::{CheckingMethod, FileEntry, HashType};\nuse crate::common::progress_stop_handler::check_if_stop_received;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\n\npub const PREHASHING_BUFFER_SIZE: u64 = 4 * 1024;\npub const THREAD_BUFFER_SIZE: usize = 2 * 1024 * 1024;\n\nthread_local! {\n    static THREAD_BUFFER: RefCell<Vec<u8>> = RefCell::new(vec![0u8; THREAD_BUFFER_SIZE]);\n}\n\n#[derive(Clone, Serialize, Deserialize, Debug, Default)]\npub struct DuplicateEntry {\n    pub path: PathBuf,\n    pub modified_date: u64,\n    pub size: u64,\n    pub hash: String,\n}\nimpl ResultEntry for DuplicateEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl FileEntry {\n    fn into_duplicate_entry(self) -> DuplicateEntry {\n        DuplicateEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n            hash: String::new(),\n        }\n    }\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_groups_by_size: usize,\n    pub number_of_duplicated_files_by_size: usize,\n    pub number_of_groups_by_hash: usize,\n    pub number_of_duplicated_files_by_hash: usize,\n    pub number_of_groups_by_name: usize,\n    pub number_of_duplicated_files_by_name: usize,\n    pub number_of_groups_by_size_name: usize,\n    pub number_of_duplicated_files_by_size_name: usize,\n    pub lost_space_by_size: u64,\n    pub lost_space_by_hash: u64,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone)]\npub struct DuplicateFinderParameters {\n    pub check_method: CheckingMethod,\n    pub hash_type: HashType,\n    pub use_prehash_cache: bool,\n    pub minimal_cache_file_size: u64,\n    pub minimal_prehash_cache_file_size: u64,\n    pub case_sensitive_name_comparison: bool,\n}\n\nimpl DuplicateFinderParameters {\n    pub fn new(\n        check_method: CheckingMethod,\n        hash_type: HashType,\n        use_prehash_cache: bool,\n        minimal_cache_file_size: u64,\n        minimal_prehash_cache_file_size: u64,\n        case_sensitive_name_comparison: bool,\n    ) -> Self {\n        Self {\n            check_method,\n            hash_type,\n            use_prehash_cache,\n            minimal_cache_file_size,\n            minimal_prehash_cache_file_size,\n            case_sensitive_name_comparison,\n        }\n    }\n}\n\npub struct DuplicateFinder {\n    common_data: CommonToolData,\n    information: Info,\n    // File Size, File Entry\n    files_with_identical_names: BTreeMap<String, Vec<DuplicateEntry>>,\n    // File (Size, Name), File Entry\n    files_with_identical_size_names: BTreeMap<(u64, String), Vec<DuplicateEntry>>,\n    // File Size, File Entry\n    files_with_identical_size: BTreeMap<u64, Vec<DuplicateEntry>>,\n    // File Size, next grouped by file size, next grouped by hash\n    files_with_identical_hashes: BTreeMap<u64, Vec<Vec<DuplicateEntry>>>,\n    // File Size, File Entry\n    files_with_identical_names_referenced: BTreeMap<String, (DuplicateEntry, Vec<DuplicateEntry>)>,\n    // File (Size, Name), File Entry\n    files_with_identical_size_names_referenced: BTreeMap<(u64, String), (DuplicateEntry, Vec<DuplicateEntry>)>,\n    // File Size, File Entry\n    files_with_identical_size_referenced: BTreeMap<u64, (DuplicateEntry, Vec<DuplicateEntry>)>,\n    // File Size, next grouped by file size, next grouped by hash\n    files_with_identical_hashes_referenced: BTreeMap<u64, Vec<(DuplicateEntry, Vec<DuplicateEntry>)>>,\n    params: DuplicateFinderParameters,\n}\n\n#[cfg(target_family = \"windows\")]\nfn filter_hard_links(vec_file_entry: Vec<FileEntry>) -> Vec<FileEntry> {\n    let mut inodes: IndexSet<u128> = IndexSet::with_capacity(vec_file_entry.len());\n    let mut identical: Vec<FileEntry> = Vec::with_capacity(vec_file_entry.len());\n    for f in vec_file_entry {\n        if let Ok(meta) = file_id::get_high_res_file_id(&f.path) {\n            if let file_id::FileId::HighRes { file_id, .. } = meta {\n                if !inodes.insert(file_id) {\n                    continue;\n                }\n            }\n        }\n        identical.push(f);\n    }\n    identical\n}\n\n#[cfg(target_family = \"unix\")]\nfn filter_hard_links(vec_file_entry: Vec<FileEntry>) -> Vec<FileEntry> {\n    let mut inodes: IndexSet<u64> = IndexSet::with_capacity(vec_file_entry.len());\n    let mut identical: Vec<FileEntry> = Vec::with_capacity(vec_file_entry.len());\n    for f in vec_file_entry {\n        if let Ok(meta) = fs::metadata(&f.path)\n            && !inodes.insert(meta.ino())\n        {\n            continue;\n        }\n        identical.push(f);\n    }\n    identical\n}\n\npub trait MyHasher {\n    fn update(&mut self, bytes: &[u8]);\n    fn finalize(&self) -> String;\n}\n\nimpl DuplicateFinder {\n    pub fn get_params(&self) -> &DuplicateFinderParameters {\n        &self.params\n    }\n\n    pub const fn get_files_sorted_by_names(&self) -> &BTreeMap<String, Vec<DuplicateEntry>> {\n        &self.files_with_identical_names\n    }\n\n    pub const fn get_files_sorted_by_size(&self) -> &BTreeMap<u64, Vec<DuplicateEntry>> {\n        &self.files_with_identical_size\n    }\n\n    pub const fn get_files_sorted_by_size_name(&self) -> &BTreeMap<(u64, String), Vec<DuplicateEntry>> {\n        &self.files_with_identical_size_names\n    }\n\n    pub const fn get_files_sorted_by_hash(&self) -> &BTreeMap<u64, Vec<Vec<DuplicateEntry>>> {\n        &self.files_with_identical_hashes\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n\n    pub fn set_dry_run(&mut self, dry_run: bool) {\n        self.common_data.dry_run = dry_run;\n    }\n\n    pub fn get_use_reference(&self) -> bool {\n        self.common_data.use_reference_folders\n    }\n\n    pub fn get_files_with_identical_hashes_referenced(&self) -> &BTreeMap<u64, Vec<(DuplicateEntry, Vec<DuplicateEntry>)>> {\n        &self.files_with_identical_hashes_referenced\n    }\n\n    pub fn get_files_with_identical_name_referenced(&self) -> &BTreeMap<String, (DuplicateEntry, Vec<DuplicateEntry>)> {\n        &self.files_with_identical_names_referenced\n    }\n\n    pub fn get_files_with_identical_size_referenced(&self) -> &BTreeMap<u64, (DuplicateEntry, Vec<DuplicateEntry>)> {\n        &self.files_with_identical_size_referenced\n    }\n\n    pub fn get_files_with_identical_size_names_referenced(&self) -> &BTreeMap<(u64, String), (DuplicateEntry, Vec<DuplicateEntry>)> {\n        &self.files_with_identical_size_names_referenced\n    }\n}\n\npub(crate) fn hash_calculation_limit(buffer: &mut [u8], file_entry: &DuplicateEntry, hash_type: HashType, limit: u64, size_counter: &Arc<AtomicU64>) -> Result<String, String> {\n    // This function is used only to calculate hash of file with limit\n    // We must ensure that buffer is big enough to store all data\n    // We don't need to check that each time\n    const_assert!(PREHASHING_BUFFER_SIZE <= THREAD_BUFFER_SIZE as u64);\n\n    let mut file_handler = match File::open(&file_entry.path) {\n        Ok(t) => t,\n        Err(e) => {\n            size_counter.fetch_add(limit, Ordering::Relaxed);\n            return Err(flc!(\n                \"core_unable_check_hash_of_file\",\n                file = file_entry.path.to_string_lossy().to_string(),\n                reason = e.to_string()\n            ));\n        }\n    };\n    let hasher = &mut *hash_type.hasher();\n    #[expect(clippy::indexing_slicing)] // Safe, because limit is always <= buffer size\n    let n = match file_handler.read(&mut buffer[..limit as usize]) {\n        Ok(t) => t,\n        Err(e) => return Err(flc!(\"core_error_checking_hash_of_file\", file = file_entry.path.to_string_lossy(), reason = e.to_string())),\n    };\n\n    #[expect(clippy::indexing_slicing)] // Safe, because we read only n bytes, which is always <= limit <= buffer size\n    hasher.update(&buffer[..n]);\n    size_counter.fetch_add(n as u64, Ordering::Relaxed);\n    Ok(hasher.finalize())\n}\n\npub fn hash_calculation(\n    buffer: &mut [u8],\n    file_entry: &DuplicateEntry,\n    hash_type: HashType,\n    size_counter: &Arc<AtomicU64>,\n    stop_flag: &Arc<AtomicBool>,\n) -> Result<Option<String>, String> {\n    let mut file_handler = match File::open(&file_entry.path) {\n        Ok(t) => t,\n        Err(e) => {\n            size_counter.fetch_add(file_entry.size, Ordering::Relaxed);\n            return Err(flc!(\"core_unable_check_hash_of_file\", file = file_entry.path.to_string_lossy(), reason = e.to_string()));\n        }\n    };\n    let hasher = &mut *hash_type.hasher();\n    loop {\n        let n = match file_handler.read(buffer) {\n            Ok(0) => break,\n            Ok(t) => t,\n            Err(e) => return Err(flc!(\"core_error_checking_hash_of_file\", file = file_entry.path.to_string_lossy(), reason = e.to_string())),\n        };\n\n        #[expect(clippy::indexing_slicing)] // Safe, because we read only n bytes, which is always <= buffer size\n        hasher.update(&buffer[..n]);\n        size_counter.fetch_add(n as u64, Ordering::Relaxed);\n        if check_if_stop_received(stop_flag) {\n            return Ok(None);\n        }\n    }\n    Ok(Some(hasher.finalize()))\n}\n\nimpl MyHasher for blake3::Hasher {\n    fn update(&mut self, bytes: &[u8]) {\n        self.update(bytes);\n    }\n    fn finalize(&self) -> String {\n        self.finalize().to_hex().to_string()\n    }\n}\n\nimpl MyHasher for crc32fast::Hasher {\n    fn update(&mut self, bytes: &[u8]) {\n        self.write(bytes);\n    }\n    fn finalize(&self) -> String {\n        self.finish().to_string()\n    }\n}\n\nimpl MyHasher for Xxh3 {\n    fn update(&mut self, bytes: &[u8]) {\n        self.write(bytes);\n    }\n    fn finalize(&self) -> String {\n        self.finish().to_string()\n    }\n}\n\n#[cfg(test)]\nmod tests2 {\n    use std::fs::File;\n    use std::io;\n\n    use super::*;\n    use crate::common::model::FileEntry;\n    use crate::tools::duplicate::filter_hard_links;\n\n    #[test]\n    fn test_filter_hard_links_empty() {\n        let expected: Vec<FileEntry> = Default::default();\n        assert_eq!(expected, filter_hard_links(Vec::new()));\n    }\n\n    #[cfg(target_family = \"unix\")]\n    #[test]\n    fn test_filter_hard_links() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let (src, dst) = (dir.path().join(\"a\"), dir.path().join(\"b\"));\n        File::create(&src)?;\n        fs::hard_link(src.clone(), dst.clone())?;\n        let e1 = FileEntry { path: src, ..Default::default() };\n        let e2 = FileEntry { path: dst, ..Default::default() };\n        let actual = filter_hard_links(vec![e1.clone(), e2]);\n        assert_eq!(vec![e1], actual);\n        Ok(())\n    }\n\n    #[test]\n    fn test_filter_hard_links_regular_files() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let (src, dst) = (dir.path().join(\"a\"), dir.path().join(\"b\"));\n        File::create(&src)?;\n        File::create(&dst)?;\n        let e1 = FileEntry { path: src, ..Default::default() };\n        let e2 = FileEntry { path: dst, ..Default::default() };\n        let actual = filter_hard_links(vec![e1.clone(), e2.clone()]);\n        assert_eq!(vec![e1, e2], actual);\n        Ok(())\n    }\n\n    #[test]\n    fn test_hash_calculation() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let mut buf = [0u8; 1 << 10];\n        let src = dir.path().join(\"a\");\n        let mut file = File::create(&src)?;\n        file.write_all(b\"aaAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFFFFFGGGGGGGGG\")?;\n        let e = DuplicateEntry { path: src, ..Default::default() };\n        let size_counter = Arc::new(AtomicU64::new(0));\n        let r = hash_calculation(&mut buf, &e, HashType::Blake3, &size_counter, &Arc::default())\n            .expect(\"hash_calculation failed\")\n            .expect(\"hash_calculation returned None\");\n        assert!(!r.is_empty());\n        assert_eq!(size_counter.load(Ordering::Relaxed), 45);\n        Ok(())\n    }\n\n    #[test]\n    fn test_hash_calculation_limit() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let mut buf = [0u8; 1000];\n        let src = dir.path().join(\"a\");\n        let mut file = File::create(&src)?;\n        file.write_all(b\"aa\")?;\n        let e = DuplicateEntry { path: src, ..Default::default() };\n        let size_counter_1 = Arc::new(AtomicU64::new(0));\n        let size_counter_2 = Arc::new(AtomicU64::new(0));\n        let size_counter_3 = Arc::new(AtomicU64::new(0));\n        let r1 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 1, &size_counter_1).expect(\"hash_calculation failed\");\n        let r2 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 2, &size_counter_2).expect(\"hash_calculation failed\");\n        let r3 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 1000, &size_counter_3).expect(\"hash_calculation failed\");\n        assert_ne!(r1, r2);\n        assert_eq!(r2, r3);\n\n        assert_eq!(1, size_counter_1.load(Ordering::Relaxed));\n        assert_eq!(2, size_counter_2.load(Ordering::Relaxed));\n        assert_eq!(2, size_counter_3.load(Ordering::Relaxed));\n\n        Ok(())\n    }\n\n    #[test]\n    fn test_hash_calculation_invalid_file() -> io::Result<()> {\n        let dir = tempfile::Builder::new().tempdir()?;\n        let mut buf = [0u8; 1 << 10];\n        let src = dir.path().join(\"a\");\n        let e = DuplicateEntry { path: src, ..Default::default() };\n        let r = hash_calculation(&mut buf, &e, HashType::Blake3, &Arc::default(), &Arc::default()).expect_err(\"hash_calculation succeeded\");\n        assert!(!r.is_empty());\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/duplicate/tests.rs",
    "content": "use std::fs;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::model::{CheckingMethod, HashType};\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters};\n\n#[test]\nfn test_find_duplicates_by_hash() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create duplicate files with same content\n    fs::write(path.join(\"file1.txt\"), b\"duplicate content\").unwrap();\n    fs::write(path.join(\"file2.txt\"), b\"duplicate content\").unwrap();\n    fs::write(path.join(\"unique.txt\"), b\"unique content\").unwrap();\n\n    let params = DuplicateFinderParameters::new(CheckingMethod::Hash, HashType::Blake3, false, 0, 0, true);\n\n    let mut finder = DuplicateFinder::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_minimal_file_size(0);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_groups_by_hash, 1, \"Should find 1 group of duplicates\");\n    assert_eq!(info.number_of_duplicated_files_by_hash, 1, \"Should find 1 duplicate file\");\n}\n\n#[test]\nfn test_find_duplicates_by_size() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create files with same size\n    fs::write(path.join(\"file1.txt\"), b\"12345\").unwrap();\n    fs::write(path.join(\"file2.txt\"), b\"abcde\").unwrap();\n    fs::write(path.join(\"unique.txt\"), b\"123\").unwrap();\n\n    let params = DuplicateFinderParameters::new(CheckingMethod::Size, HashType::Blake3, false, 0, 0, true);\n\n    let mut finder = DuplicateFinder::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_minimal_file_size(0);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_groups_by_size, 1, \"Should find 1 group by size\");\n    assert_eq!(info.number_of_duplicated_files_by_size, 1, \"Should find 1 duplicate by size\");\n}\n\n#[test]\nfn test_find_duplicates_by_name() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let dir1 = path.join(\"dir1\");\n    let dir2 = path.join(\"dir2\");\n    fs::create_dir(&dir1).unwrap();\n    fs::create_dir(&dir2).unwrap();\n\n    // Create files with same name in different directories\n    fs::write(dir1.join(\"duplicate.txt\"), b\"content1\").unwrap();\n    fs::write(dir2.join(\"duplicate.txt\"), b\"content2\").unwrap();\n    fs::write(dir1.join(\"unique.txt\"), b\"unique\").unwrap();\n\n    let params = DuplicateFinderParameters::new(CheckingMethod::Name, HashType::Blake3, false, 0, 0, true);\n\n    let mut finder = DuplicateFinder::new(params);\n    finder.set_recursive_search(true);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_minimal_file_size(0);\n    finder.set_use_cache(false);\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_groups_by_name, 1, \"Should find 1 group by name\");\n    assert_eq!(info.number_of_duplicated_files_by_name, 1, \"Should find 1 duplicate by name\");\n}\n\n#[test]\nfn test_no_duplicates_found() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create unique files\n    fs::write(path.join(\"file1.txt\"), b\"content1\").unwrap();\n    fs::write(path.join(\"file2.txt\"), b\"content2\").unwrap();\n\n    let params = DuplicateFinderParameters::new(CheckingMethod::Hash, HashType::Blake3, false, 0, 0, true);\n\n    let mut finder = DuplicateFinder::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n    finder.set_minimal_file_size(0);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_groups_by_hash, 0, \"Should find no duplicate groups\");\n    assert_eq!(info.lost_space_by_hash, 0, \"Should have no lost space\");\n}\n\n#[test]\nfn test_lost_space_calculation() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create 3 files with 100 bytes each, all duplicates\n    let content = vec![b'A'; 100];\n    fs::write(path.join(\"file1.txt\"), &content).unwrap();\n    fs::write(path.join(\"file2.txt\"), &content).unwrap();\n    fs::write(path.join(\"file3.txt\"), &content).unwrap();\n\n    let params = DuplicateFinderParameters::new(CheckingMethod::Hash, HashType::Blake3, false, 0, 0, true);\n\n    let mut finder = DuplicateFinder::new(params);\n    finder.set_minimal_file_size(0);\n    finder.set_use_cache(false);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.lost_space_by_hash, 200, \"Should calculate 200 bytes lost space (2 duplicate files * 100 bytes)\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/duplicate/traits.rs",
    "content": "use std::io::prelude::*;\nuse std::io::{self};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\n\nuse crate::common::model::{CheckingMethod, WorkContinueStatus};\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters, Info};\n\nimpl AllTraits for DuplicateFinder {}\n\nimpl DeletingItems for DuplicateFinder {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.common_data.delete_method == DeleteMethod::None {\n            return WorkContinueStatus::Continue;\n        }\n\n        let files_to_delete = match self.get_params().check_method {\n            CheckingMethod::Name => self.files_with_identical_names.values().cloned().collect::<Vec<_>>(),\n            CheckingMethod::SizeName => self.files_with_identical_size_names.values().cloned().collect::<Vec<_>>(),\n            CheckingMethod::Hash => self.files_with_identical_hashes.values().flatten().cloned().collect::<Vec<_>>(),\n            CheckingMethod::Size => self.files_with_identical_size.values().cloned().collect::<Vec<_>>(),\n            _ => panic!(),\n        };\n        self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete)\n    }\n}\n\nimpl Search for DuplicateFinder {\n    #[fun_time(message = \"find_duplicates\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty();\n\n            match self.get_params().check_method {\n                CheckingMethod::Name => {\n                    self.common_data.stopped_search = self.check_files_name(stop_flag, progress_sender) == WorkContinueStatus::Stop;\n                    if self.common_data.stopped_search {\n                        return;\n                    }\n                }\n                CheckingMethod::SizeName => {\n                    self.common_data.stopped_search = self.check_files_size_name(stop_flag, progress_sender) == WorkContinueStatus::Stop;\n                    if self.common_data.stopped_search {\n                        return;\n                    }\n                }\n                CheckingMethod::Size => {\n                    self.common_data.stopped_search = self.check_files_size(stop_flag, progress_sender) == WorkContinueStatus::Stop;\n                    if self.common_data.stopped_search {\n                        return;\n                    }\n                }\n                CheckingMethod::Hash => {\n                    self.common_data.stopped_search = self.check_files_size(stop_flag, progress_sender) == WorkContinueStatus::Stop;\n                    if self.common_data.stopped_search {\n                        return;\n                    }\n                    self.common_data.stopped_search = self.check_files_hash(stop_flag, progress_sender) == WorkContinueStatus::Stop;\n                    if self.common_data.stopped_search {\n                        return;\n                    }\n                }\n                _ => panic!(),\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        self.debug_print();\n    }\n}\n\nimpl DebugPrint for DuplicateFinder {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        println!(\"---------------DEBUG PRINT---------------\");\n        println!(\n            \"Number of duplicated files by size(in groups) - {} ({})\",\n            self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size\n        );\n        println!(\n            \"Number of duplicated files by hash(in groups) - {} ({})\",\n            self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash\n        );\n        println!(\n            \"Number of duplicated files by name(in groups) - {} ({})\",\n            self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name\n        );\n        println!(\n            \"Lost space by size - {} ({} bytes)\",\n            format_size(self.information.lost_space_by_size, BINARY),\n            self.information.lost_space_by_size\n        );\n        println!(\n            \"Lost space by hash - {} ({} bytes)\",\n            format_size(self.information.lost_space_by_hash, BINARY),\n            self.information.lost_space_by_hash\n        );\n\n        println!(\"### Other\");\n\n        println!(\"Files list size - {}\", self.files_with_identical_size.len());\n        println!(\"Hashed files list size - {}\", self.files_with_identical_hashes.len());\n        println!(\"Files with identical names - {}\", self.files_with_identical_names.len());\n        println!(\"Files with identical size names - {}\", self.files_with_identical_size_names.len());\n        println!(\"Files with identical names referenced - {}\", self.files_with_identical_names_referenced.len());\n        println!(\"Files with identical size names referenced - {}\", self.files_with_identical_size_names_referenced.len());\n        println!(\"Files with identical size referenced - {}\", self.files_with_identical_size_referenced.len());\n        println!(\"Files with identical hashes referenced - {}\", self.files_with_identical_hashes_referenced.len());\n        println!(\"Checking Method - {:?}\", self.get_params().check_method);\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for DuplicateFinder {\n    fn write_results<T: Write>(&self, writer: &mut T) -> io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        match self.get_params().check_method {\n            CheckingMethod::Name => {\n                if !self.files_with_identical_names.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same names-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} files in {} groups with same name(may have different content)\",\n                        self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name,\n                    )?;\n                    for (name, vector) in self.files_with_identical_names.iter().rev() {\n                        writeln!(writer, \"Name - {} - {} files \", name, vector.len())?;\n                        for j in vector {\n                            writeln!(writer, \"\\\"{}\\\"\", j.path.to_string_lossy())?;\n                        }\n                        writeln!(writer)?;\n                    }\n                } else if !self.files_with_identical_names_referenced.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same names in referenced folders-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} files in {} groups with same name(may have different content)\",\n                        self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name,\n                    )?;\n                    for (name, (file_entry, vector)) in self.files_with_identical_names_referenced.iter().rev() {\n                        writeln!(writer, \"Name - {} - {} files \", name, vector.len())?;\n                        writeln!(writer, \"Reference file - \\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                        for j in vector {\n                            writeln!(writer, \"\\\"{}\\\"\", j.path.to_string_lossy())?;\n                        }\n                        writeln!(writer)?;\n                    }\n                } else {\n                    write!(writer, \"Not found any files with same names.\")?;\n                }\n            }\n            CheckingMethod::SizeName => {\n                if !self.files_with_identical_names.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same size and names-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} files in {} groups with same size and name(may have different content)\",\n                        self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name,\n                    )?;\n                    for ((size, name), vector) in self.files_with_identical_size_names.iter().rev() {\n                        writeln!(writer, \"Name - {}, {} - {} files \", name, format_size(*size, BINARY), vector.len())?;\n                        for j in vector {\n                            writeln!(writer, \"\\\"{}\\\"\", j.path.to_string_lossy())?;\n                        }\n                        writeln!(writer)?;\n                    }\n                } else if !self.files_with_identical_names_referenced.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same size and names in referenced folders-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} files in {} groups with same size and name(may have different content)\",\n                        self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name,\n                    )?;\n                    for ((size, name), (file_entry, vector)) in self.files_with_identical_size_names_referenced.iter().rev() {\n                        writeln!(writer, \"Name - {}, {} - {} files \", name, format_size(*size, BINARY), vector.len())?;\n                        writeln!(writer, \"Reference file - \\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                        for j in vector {\n                            writeln!(writer, \"\\\"{}\\\"\", j.path.to_string_lossy())?;\n                        }\n                        writeln!(writer)?;\n                    }\n                } else {\n                    write!(writer, \"Not found any files with same size and names.\")?;\n                }\n            }\n            CheckingMethod::Size => {\n                if !self.files_with_identical_size.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same size-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} duplicated files which in {} groups which takes {}.\",\n                        self.information.number_of_duplicated_files_by_size,\n                        self.information.number_of_groups_by_size,\n                        format_size(self.information.lost_space_by_size, BINARY)\n                    )?;\n                    for (size, vector) in self.files_with_identical_size.iter().rev() {\n                        write!(writer, \"\\n---- Size {} ({}) - {} files \\n\", format_size(*size, BINARY), size, vector.len())?;\n                        for file_entry in vector {\n                            writeln!(writer, \"\\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                        }\n                    }\n                } else if !self.files_with_identical_size_referenced.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same size in referenced folders-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} duplicated files which in {} groups which takes {}.\",\n                        self.information.number_of_duplicated_files_by_size,\n                        self.information.number_of_groups_by_size,\n                        format_size(self.information.lost_space_by_size, BINARY)\n                    )?;\n                    for (size, (file_entry, vector)) in self.files_with_identical_size_referenced.iter().rev() {\n                        writeln!(writer, \"\\n---- Size {} ({}) - {} files\", format_size(*size, BINARY), size, vector.len())?;\n                        writeln!(writer, \"Reference file - \\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                        for file_entry in vector {\n                            writeln!(writer, \"\\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                        }\n                    }\n                } else {\n                    write!(writer, \"Not found any duplicates.\")?;\n                }\n            }\n            CheckingMethod::Hash => {\n                if !self.files_with_identical_hashes.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same hashes-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} duplicated files which in {} groups which takes {}.\",\n                        self.information.number_of_duplicated_files_by_hash,\n                        self.information.number_of_groups_by_hash,\n                        format_size(self.information.lost_space_by_hash, BINARY)\n                    )?;\n                    for (size, vectors_vector) in self.files_with_identical_hashes.iter().rev() {\n                        for vector in vectors_vector {\n                            writeln!(writer, \"\\n---- Size {} ({}) - {} files\", format_size(*size, BINARY), size, vector.len())?;\n                            for file_entry in vector {\n                                writeln!(writer, \"\\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                            }\n                        }\n                    }\n                } else if !self.files_with_identical_hashes_referenced.is_empty() {\n                    writeln!(\n                        writer,\n                        \"-------------------------------------------------Files with same hashes in referenced folders-------------------------------------------------\"\n                    )?;\n                    writeln!(\n                        writer,\n                        \"Found {} duplicated files which in {} groups which takes {}.\",\n                        self.information.number_of_duplicated_files_by_hash,\n                        self.information.number_of_groups_by_hash,\n                        format_size(self.information.lost_space_by_hash, BINARY)\n                    )?;\n                    for (size, vectors_vector) in self.files_with_identical_hashes_referenced.iter().rev() {\n                        for (file_entry, vector) in vectors_vector {\n                            writeln!(writer, \"\\n---- Size {} ({}) - {} files\", format_size(*size, BINARY), size, vector.len())?;\n                            writeln!(writer, \"Reference file - \\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                            for file_entry in vector {\n                                writeln!(writer, \"\\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n                            }\n                        }\n                    }\n                } else {\n                    write!(writer, \"Not found any duplicates.\")?;\n                }\n            }\n            _ => panic!(),\n        }\n\n        Ok(())\n    }\n\n    // TODO - check if is possible to save also data in header about size and name in SizeName mode - https://github.com/qarmin/czkawka/issues/1137\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> io::Result<()> {\n        if self.get_use_reference() {\n            match self.get_params().check_method {\n                CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names_referenced, pretty_print),\n                CheckingMethod::SizeName => {\n                    self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names_referenced.values().collect::<Vec<_>>(), pretty_print)\n                }\n                CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_referenced, pretty_print),\n                CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes_referenced, pretty_print),\n                _ => panic!(),\n            }\n        } else {\n            match self.get_params().check_method {\n                CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names, pretty_print),\n                CheckingMethod::SizeName => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names.values().collect::<Vec<_>>(), pretty_print),\n                CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size, pretty_print),\n                CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes, pretty_print),\n                _ => panic!(),\n            }\n        }\n    }\n}\n\nimpl CommonData for DuplicateFinder {\n    type Info = Info;\n    type Parameters = DuplicateFinderParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn get_check_method(&self) -> CheckingMethod {\n        self.get_params().check_method\n    }\n    fn found_any_items(&self) -> bool {\n        self.get_information().number_of_duplicated_files_by_hash > 0\n            || self.get_information().number_of_duplicated_files_by_name > 0\n            || self.get_information().number_of_duplicated_files_by_size > 0\n            || self.get_information().number_of_duplicated_files_by_size_name > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_files/core.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::debug;\n\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::CommonToolData;\nuse crate::tools::empty_files::{EmptyFiles, Info};\n\nimpl EmptyFiles {\n    pub fn new() -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::EmptyFiles),\n            information: Info::default(),\n            empty_files: Vec::new(),\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .minimal_file_size(0)\n            .maximal_file_size(0)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.empty_files = grouped_file_entries.into_values().flatten().collect();\n                self.information.number_of_empty_files = self.empty_files.len();\n                self.common_data.text_messages.warnings.extend(warnings);\n\n                debug!(\"Found {} empty files.\", self.information.number_of_empty_files);\n\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_files/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::time::Duration;\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_empty_files: usize,\n    pub scanning_time: Duration,\n}\n\npub struct EmptyFiles {\n    common_data: CommonToolData,\n    information: Info,\n    empty_files: Vec<FileEntry>,\n}\n\nimpl Default for EmptyFiles {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl EmptyFiles {\n    pub const fn get_empty_files(&self) -> &Vec<FileEntry> {\n        &self.empty_files\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_files/tests.rs",
    "content": "use std::fs;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::empty_files::EmptyFiles;\n\n#[test]\nfn test_find_empty_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create empty files\n    fs::write(path.join(\"empty1.txt\"), b\"\").unwrap();\n    fs::write(path.join(\"empty2.txt\"), b\"\").unwrap();\n    fs::write(path.join(\"not_empty.txt\"), b\"content\").unwrap();\n\n    let mut finder = EmptyFiles::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_empty_files, 2, \"Should find 2 empty files\");\n    assert_eq!(finder.get_empty_files().len(), 2, \"Empty files list should contain 2 files\");\n}\n\n#[test]\nfn test_no_empty_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create only non-empty files\n    fs::write(path.join(\"file1.txt\"), b\"content1\").unwrap();\n    fs::write(path.join(\"file2.txt\"), b\"content2\").unwrap();\n\n    let mut finder = EmptyFiles::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_empty_files, 0, \"Should find no empty files\");\n    assert!(finder.get_empty_files().is_empty(), \"Empty files list should be empty\");\n}\n\n#[test]\nfn test_recursive_search_empty_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let subdir = path.join(\"subdir\");\n    fs::create_dir(&subdir).unwrap();\n\n    // Create empty files in different directories\n    fs::write(path.join(\"empty1.txt\"), b\"\").unwrap();\n    fs::write(subdir.join(\"empty2.txt\"), b\"\").unwrap();\n\n    let mut finder = EmptyFiles::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_empty_files, 2, \"Should find empty files in subdirectories\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_files/traits.rs",
    "content": "use std::io::prelude::*;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::empty_files::{EmptyFiles, Info};\n\nimpl AllTraits for EmptyFiles {}\n\nimpl Search for EmptyFiles {\n    #[fun_time(message = \"find_empty_files\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for EmptyFiles {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        println!(\"---------------DEBUG PRINT---------------\");\n        println!(\"Empty list size - {}\", self.empty_files.len());\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for EmptyFiles {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        if !self.empty_files.is_empty() {\n            writeln!(writer, \"Found {} empty files.\", self.information.number_of_empty_files)?;\n            for file_entry in &self.empty_files {\n                writeln!(writer, \"\\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n            }\n        } else {\n            write!(writer, \"Not found any empty files.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.empty_files, pretty_print)\n    }\n}\nimpl CommonData for EmptyFiles {\n    type Info = Info;\n    type Parameters = ();\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {}\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_empty_files > 0\n    }\n}\nimpl DeletingItems for EmptyFiles {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.empty_files.clone())),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_folder/core.rs",
    "content": "use std::fs::DirEntry;\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse indexmap::IndexMap;\nuse log::debug;\nuse rayon::prelude::*;\n\nuse crate::common::dir_traversal::{common_get_entry_data, common_get_metadata_dir, common_read_dir, get_modified_time};\nuse crate::common::directories::Directories;\nuse crate::common::items::ExcludedItems;\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::tools::empty_folder::{EmptyFolder, FolderEmptiness, FolderEntry, Info};\n\nimpl EmptyFolder {\n    pub fn new() -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::EmptyFolders),\n            information: Default::default(),\n            empty_folder_list: Default::default(),\n        }\n    }\n\n    pub const fn get_empty_folder_list(&self) -> &IndexMap<String, FolderEntry> {\n        &self.empty_folder_list\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n\n    pub(crate) fn optimize_folders(&mut self) {\n        let mut new_directory_folders: IndexMap<String, FolderEntry> = Default::default();\n\n        for (name, folder_entry) in &self.empty_folder_list {\n            match &folder_entry.parent_path {\n                Some(t) => {\n                    if !self.empty_folder_list.contains_key(t) {\n                        new_directory_folders.insert(name.clone(), folder_entry.clone());\n                    }\n                }\n                None => {\n                    new_directory_folders.insert(name.clone(), folder_entry.clone());\n                }\n            }\n        }\n        self.empty_folder_list = new_directory_folders;\n        self.information.number_of_empty_folders = self.empty_folder_list.len();\n    }\n\n    #[fun_time(message = \"check_for_empty_folders\", level = \"debug\")]\n    pub(crate) fn check_for_empty_folders(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let mut folders_to_check: Vec<PathBuf> = self.common_data.directories.included_directories.clone();\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::CollectingFiles, 0, self.get_test_type(), 0);\n\n        let excluded_items = self.common_data.excluded_items.clone();\n        let directories = self.common_data.directories.clone();\n\n        let mut non_empty_folders: Vec<String> = Vec::new();\n\n        let mut start_folder_entries = Vec::with_capacity(folders_to_check.len());\n        let mut new_folder_entries_list = Vec::new();\n        for dir in &folders_to_check {\n            start_folder_entries.push(FolderEntry {\n                path: dir.clone(),\n                parent_path: None,\n                is_empty: FolderEmptiness::Maybe,\n                modified_date: 0,\n            });\n        }\n\n        while !folders_to_check.is_empty() {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            let segments: Vec<_> = folders_to_check\n                .into_par_iter()\n                .map(|current_folder| {\n                    let mut dir_result = Vec::new();\n                    let mut warnings = Vec::new();\n                    let mut non_empty_folder = None;\n                    let mut folder_entries_list = Vec::new();\n\n                    let current_folder_as_string = current_folder.to_string_lossy().to_string();\n\n                    let Some(read_dir) = common_read_dir(&current_folder, &mut warnings) else {\n                        return (dir_result, warnings, Some(current_folder_as_string), folder_entries_list);\n                    };\n\n                    let mut counter = 0;\n                    // Check every sub folder/file/link etc.\n                    for entry in read_dir {\n                        let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, &current_folder) else {\n                            continue;\n                        };\n                        let Ok(file_type) = entry_data.file_type() else { continue };\n\n                        if file_type.is_dir() {\n                            counter += 1;\n                            Self::process_dir_in_dir_mode(\n                                &current_folder,\n                                &current_folder_as_string,\n                                entry_data,\n                                &directories,\n                                &mut dir_result,\n                                &mut warnings,\n                                &excluded_items,\n                                &mut non_empty_folder,\n                                &mut folder_entries_list,\n                            );\n                        } else if non_empty_folder.is_none() {\n                            non_empty_folder = Some(current_folder_as_string.clone());\n                        }\n                    }\n                    if counter > 0 {\n                        // Increase counter in batch, because usually it may be slow to add multiple times atomic value\n                        progress_handler.increase_items(counter);\n                    }\n\n                    (dir_result, warnings, non_empty_folder, folder_entries_list)\n                })\n                .collect();\n\n            let required_size = segments.iter().map(|(segment, _, _, _)| segment.len()).sum::<usize>();\n            folders_to_check = Vec::with_capacity(required_size);\n\n            // Process collected data\n            for (segment, warnings, non_empty_folder, fe_list) in segments {\n                folders_to_check.extend(segment);\n                if !warnings.is_empty() {\n                    self.common_data.text_messages.warnings.extend(warnings);\n                }\n                if let Some(non_empty_folder) = non_empty_folder {\n                    non_empty_folders.push(non_empty_folder);\n                }\n                new_folder_entries_list.push(fe_list);\n            }\n        }\n\n        let mut folder_entries: IndexMap<String, FolderEntry> = IndexMap::with_capacity(start_folder_entries.len() + new_folder_entries_list.iter().map(Vec::len).sum::<usize>());\n        for fe in start_folder_entries {\n            folder_entries.insert(fe.path.to_string_lossy().to_string(), fe);\n        }\n        for fe_list in new_folder_entries_list {\n            for fe in fe_list {\n                folder_entries.insert(fe.path.to_string_lossy().to_string(), fe);\n            }\n        }\n\n        for current_folder in non_empty_folders.into_iter().rev() {\n            Self::set_as_not_empty_folder(&mut folder_entries, &current_folder);\n        }\n\n        for (name, folder_entry) in folder_entries {\n            if folder_entry.is_empty != FolderEmptiness::No {\n                self.empty_folder_list.insert(name, folder_entry);\n            }\n        }\n\n        debug!(\"Found {} empty folders.\", self.empty_folder_list.len());\n        progress_handler.join_thread();\n        WorkContinueStatus::Continue\n    }\n\n    pub(crate) fn set_as_not_empty_folder(folder_entries: &mut IndexMap<String, FolderEntry>, current_folder: &str) {\n        let mut d = folder_entries\n            .get_mut(current_folder)\n            .unwrap_or_else(|| panic!(\"Folder {current_folder} not found in folder_entries (cannot panic, because we first added parent folders)\"));\n        if d.is_empty == FolderEmptiness::No {\n            return; // Already set as non empty by one of its child\n        }\n\n        // Loop to recursively set as non empty this and all its parent folders\n        loop {\n            d.is_empty = FolderEmptiness::No;\n\n            if let Some(parent_path) = &d.parent_path {\n                let cf = parent_path.clone();\n                d = folder_entries\n                    .get_mut(&cf)\n                    .unwrap_or_else(|| panic!(\"Folder {cf} not found in folder_entries (cannot panic, because we first added parent folders)\"));\n                if d.is_empty == FolderEmptiness::No {\n                    break; // Already set as non empty, so one of child already set it to non empty\n                }\n            } else {\n                break;\n            }\n        }\n    }\n\n    fn process_dir_in_dir_mode(\n        current_folder: &Path,\n        current_folder_as_str: &str,\n        entry_data: &DirEntry,\n        directories: &Directories,\n        dir_result: &mut Vec<PathBuf>,\n        warnings: &mut Vec<String>,\n        excluded_items: &ExcludedItems,\n        non_empty_folder: &mut Option<String>,\n        folder_entries_list: &mut Vec<FolderEntry>,\n    ) {\n        let next_folder = entry_data.path();\n        if excluded_items.is_excluded(&next_folder) || directories.is_excluded_dir(&next_folder) {\n            if non_empty_folder.is_none() {\n                *non_empty_folder = Some(current_folder_as_str.to_string());\n            }\n            return;\n        }\n\n        #[cfg(target_family = \"unix\")]\n        if directories.exclude_other_filesystems() {\n            match directories.is_on_other_filesystems(&next_folder) {\n                Ok(true) => return,\n                Err(e) => warnings.push(e),\n                _ => (),\n            }\n        }\n\n        let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &next_folder) else {\n            if non_empty_folder.is_none() {\n                *non_empty_folder = Some(current_folder_as_str.to_string());\n            }\n            return;\n        };\n\n        dir_result.push(next_folder.clone());\n        folder_entries_list.push(FolderEntry {\n            path: next_folder,\n            parent_path: Some(current_folder_as_str.to_string()),\n            is_empty: FolderEmptiness::Maybe,\n            modified_date: get_modified_time(&metadata, warnings, current_folder, true),\n        });\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_folder/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse indexmap::IndexMap;\n\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\n#[derive(Clone, Debug)]\npub struct FolderEntry {\n    pub path: PathBuf,\n    pub(crate) parent_path: Option<String>,\n    // Usable only when finding\n    pub(crate) is_empty: FolderEmptiness,\n    pub modified_date: u64,\n}\n\nimpl ResultEntry for FolderEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n\n    fn get_size(&self) -> u64 {\n        0\n    }\n}\n\npub struct EmptyFolder {\n    common_data: CommonToolData,\n    information: Info,\n    empty_folder_list: IndexMap<String, FolderEntry>, // Path, FolderEntry\n}\n\n/// Enum with values which show if folder is empty.\n/// In function \"`optimize_folders`\" automatically \"Maybe\" is changed to \"Yes\", so it is not necessary to put it here\n#[derive(Eq, PartialEq, Copy, Clone, Debug)]\npub(crate) enum FolderEmptiness {\n    No,\n    Maybe,\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_empty_folders: usize,\n    pub scanning_time: Duration,\n}\n\nimpl Default for EmptyFolder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_folder/tests.rs",
    "content": "use std::fs;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::empty_folder::EmptyFolder;\n\n#[test]\nfn test_find_empty_folders() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create empty directories\n    fs::create_dir(path.join(\"empty1\")).unwrap();\n    fs::create_dir(path.join(\"empty2\")).unwrap();\n\n    // Create non-empty directory\n    let non_empty = path.join(\"non_empty\");\n    fs::create_dir(&non_empty).unwrap();\n    fs::write(non_empty.join(\"file.txt\"), b\"content\").unwrap();\n\n    let mut finder = EmptyFolder::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_empty_folders, 2, \"Should find 2 empty folders\");\n}\n\n#[test]\nfn test_nested_empty_folders() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create nested empty directories\n    let parent = path.join(\"parent\");\n    let child = parent.join(\"child\");\n    fs::create_dir(&parent).unwrap();\n    fs::create_dir(&child).unwrap();\n\n    let mut finder = EmptyFolder::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    // After optimization, only the deepest empty folder should be counted\n    let info = finder.get_information();\n    assert!(info.number_of_empty_folders > 0, \"Should find empty folders\");\n}\n\n#[test]\nfn test_no_empty_folders() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create directory with file\n    let dir = path.join(\"dir\");\n    fs::create_dir(&dir).unwrap();\n    fs::write(dir.join(\"file.txt\"), b\"content\").unwrap();\n\n    let mut finder = EmptyFolder::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_empty_folders, 0, \"Should find no empty folders\");\n}\n\n#[test]\nfn test_folder_with_only_empty_subfolders() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    // Create parent with only empty subdirectories\n    let parent = path.join(\"parent\");\n    fs::create_dir(&parent).unwrap();\n    fs::create_dir(parent.join(\"empty_child1\")).unwrap();\n    fs::create_dir(parent.join(\"empty_child2\")).unwrap();\n\n    let mut finder = EmptyFolder::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    // Parent and children are all empty\n    assert!(\n        info.number_of_empty_folders == 1,\n        \"Should find 1 empty folder (the parent) - which contains only empty subfolders\"\n    );\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/empty_folder/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse rayon::prelude::*;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::empty_folder::{EmptyFolder, Info};\n\nimpl AllTraits for EmptyFolder {}\n\nimpl Search for EmptyFolder {\n    #[fun_time(message = \"find_empty_folders\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            if self.check_for_empty_folders(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            self.optimize_folders();\n\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for EmptyFolder {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n\n        println!(\"---------------DEBUG PRINT---------------\");\n        println!(\"Number of empty folders - {}\", self.information.number_of_empty_folders);\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for EmptyFolder {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n        if !self.empty_folder_list.is_empty() {\n            writeln!(writer, \"--------------------------Empty folder list--------------------------\")?;\n            writeln!(writer, \"Found {} empty folders\", self.information.number_of_empty_folders)?;\n            let mut empty_folder_list = self.empty_folder_list.keys().collect::<Vec<_>>();\n            empty_folder_list.par_sort_unstable();\n            for name in empty_folder_list {\n                writeln!(writer, \"{name}\")?;\n            }\n        } else {\n            write!(writer, \"Not found any empty folders.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.empty_folder_list.keys().collect::<Vec<_>>(), pretty_print)\n    }\n}\n\nimpl CommonData for EmptyFolder {\n    type Info = Info;\n    type Parameters = ();\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {}\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_empty_folders > 0\n    }\n}\n\nimpl DeletingItems for EmptyFolder {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(\n                stop_flag,\n                progress_sender,\n                DeleteItemType::DeletingFolders(self.empty_folder_list.values().cloned().collect::<Vec<_>>()),\n            ),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/exif_remover/core.rs",
    "content": "use std::collections::BTreeMap;\nuse std::path::Path;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::{fs, mem, panic};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse little_exif::filetype::FileExtension;\nuse little_exif::ifd::ExifTagGroup;\nuse little_exif::metadata::Metadata;\nuse log::{debug, error};\nuse rayon::prelude::*;\n\nuse crate::common::cache::{CACHE_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path};\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::flc;\nuse crate::tools::exif_remover::{ExifEntry, ExifRemover, ExifRemoverParameters, ExifTagInfo, ExifTagsFixerParams, Info};\n\nimpl ExifRemover {\n    pub fn new(params: ExifRemoverParameters) -> Self {\n        let mut additional_excluded_tags = BTreeMap::new();\n\n        let tiff_disabled_tags = vec![\n            \"ImageWidth\",\n            \"ImageHeight\",\n            \"BitsPerSample\",\n            \"Compression\",\n            \"PhotometricInterpretation\",\n            \"StripOffsets\",\n            \"SamplesPerPixel\",\n            \"RowsPerStrip\",\n            \"StripByteCounts\",\n            \"PlanarConfiguration\",\n        ];\n        for i in [\"tif\", \"tiff\"] {\n            additional_excluded_tags.insert(i, tiff_disabled_tags.clone());\n        }\n        Self {\n            common_data: CommonToolData::new(ToolType::ExifRemover),\n            information: Info::default(),\n            exif_files: Vec::new(),\n            files_to_check: Default::default(),\n            params,\n            additional_excluded_tags,\n        }\n    }\n\n    #[fun_time(message = \"find_exif_files\", level = \"debug\")]\n    pub(crate) fn find_exif_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.files_to_check = grouped_file_entries\n                    .into_values()\n                    .flatten()\n                    .map(|fe| {\n                        let exif_entry = ExifEntry {\n                            path: fe.path.clone(),\n                            size: fe.size,\n                            modified_date: fe.modified_date,\n                            exif_tags: Vec::new(),\n                            error: None,\n                        };\n                        (fe.path.to_string_lossy().to_string(), exif_entry)\n                    })\n                    .collect();\n\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"find_exif_files - Found {} files to check.\", self.files_to_check.len());\n\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    #[fun_time(message = \"load_cache\", level = \"debug\")]\n    fn load_cache(\n        &mut self,\n        _stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n    ) -> (BTreeMap<String, ExifEntry>, BTreeMap<String, ExifEntry>, BTreeMap<String, ExifEntry>) {\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::ExifRemoverCacheLoading, 0, self.get_test_type(), 0);\n        let res = load_and_split_cache_generalized_by_path(&get_exif_remover_cache_file(), mem::take(&mut self.files_to_check), self);\n\n        progress_handler.join_thread();\n        res\n    }\n\n    #[fun_time(message = \"save_to_cache\", level = \"debug\")]\n    fn save_to_cache(\n        &mut self,\n        vec_file_entry: &[ExifEntry],\n        loaded_hash_map: BTreeMap<String, ExifEntry>,\n        _stop_flag: &Arc<AtomicBool>,\n        progress_sender: Option<&Sender<ProgressData>>,\n    ) {\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::ExifRemoverCacheSaving, 0, self.get_test_type(), 0);\n\n        save_and_connect_cache_generalized_by_path(&get_exif_remover_cache_file(), vec_file_entry, loaded_hash_map, self);\n\n        progress_handler.join_thread();\n    }\n\n    #[fun_time(message = \"check_exif_in_files\", level = \"debug\")]\n    pub(crate) fn check_exif_in_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.files_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(stop_flag, progress_sender);\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::ExifRemoverExtractingTags,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            non_cached_files_to_check.values().map(|item| item.size).sum::<u64>(),\n        );\n\n        let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::<Vec<_>>();\n\n        debug!(\"check_exif_in_files - started extracting EXIF data\");\n        let mut vec_file_entry: Vec<ExifEntry> = non_cached_files_to_check\n            .into_par_iter()\n            .map(|(_, mut file_entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let size = file_entry.size;\n                let res = extract_exif_tags(&file_entry.path);\n\n                progress_handler.increase_items(1);\n                progress_handler.increase_size(size);\n\n                match res {\n                    Ok(tags) => {\n                        file_entry.exif_tags = tags.into_iter().map(|(name, code, group)| ExifTagInfo { name, code, group }).collect();\n                    }\n                    Err(e) => {\n                        file_entry.error = Some(format!(\"Failed to extract Exif data for file \\\"{}\\\": {}\", file_entry.path.to_string_lossy(), e));\n                    }\n                }\n\n                Some(file_entry)\n            })\n            .while_some()\n            .collect();\n        debug!(\"check_exif_in_files - finished extracting EXIF data\");\n\n        progress_handler.join_thread();\n\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        self.save_to_cache(&vec_file_entry, loaded_hash_map, stop_flag, progress_sender);\n\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        // After saving to cache, remove ignored tags - because in cache we need to store full info about tags\n        for entry in &mut vec_file_entry {\n            let extension = entry.path.extension().and_then(|ext| ext.to_str()).unwrap_or(\"\").to_lowercase();\n            if let Some(additional_ignored_tags) = self.additional_excluded_tags.get(&extension.as_str()) {\n                entry.exif_tags.retain(|tag_item| !additional_ignored_tags.contains(&tag_item.name.as_str()));\n            }\n            if self.params.ignored_tags.is_empty() {\n                continue;\n            }\n\n            entry.exif_tags.retain(|tag_item| !self.params.ignored_tags.contains(&tag_item.name));\n        }\n\n        self.exif_files = vec_file_entry.into_iter().filter(|f| f.error.is_none() && !f.exif_tags.is_empty()).collect();\n        self.exif_files.iter_mut().for_each(|file| file.exif_tags.sort_unstable_by(|a, b| a.name.cmp(&b.name)));\n\n        self.information.number_of_files_with_exif = self.exif_files.len();\n        debug!(\"Found {} files with EXIF data.\", self.information.number_of_files_with_exif);\n\n        self.files_to_check = Default::default();\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"fix_files\", level = \"debug\")]\n    pub(crate) fn fix_files(&mut self, stop_flag: &Arc<AtomicBool>, _progress_sender: Option<&Sender<ProgressData>>, fix_params: ExifTagsFixerParams) {\n        let warnings: Vec<_> = mem::take(&mut self.exif_files)\n            .into_par_iter()\n            .map(|entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let exif_data_to_remove: Vec<(u16, String)> = entry.exif_tags.iter().map(|item_tag| (item_tag.code, item_tag.group.clone())).collect();\n                match clean_exif_tags(&entry.path.to_string_lossy(), &exif_data_to_remove, fix_params.override_file) {\n                    Ok(_number_removed_tags) => Some(None),\n                    Err(e) => Some(Some(format!(\"Failed to clean EXIF tags for file \\\"{}\\\": {}\", entry.path.to_string_lossy(), e))),\n                }\n            })\n            .while_some()\n            .flatten()\n            .collect();\n\n        self.common_data.text_messages.warnings.extend(warnings);\n    }\n}\n\npub fn clean_exif_tags(file_path: &str, tags_to_remove: &[(u16, String)], override_file: bool) -> Result<u32, String> {\n    panic::catch_unwind(|| {\n        let file_path = Path::new(file_path);\n        let mut file_data = fs::read(file_path).map_err(|e| e.to_string())?;\n        let mut cursor = std::io::Cursor::new(&file_data);\n        let ext = FileExtension::auto_detect(&mut cursor).ok_or_else(|| \"Failed to detect file type\".to_string())?;\n        let metadata = Metadata::new_from_vec(&file_data, ext).map_err(|e| format!(\"Failed to read EXIF: {e}\"))?;\n\n        let mut new_metadata = metadata;\n        let mut tags_removed: u32 = 0;\n        for (tag_u16, tag_group) in tags_to_remove {\n            let Ok(tag_group) = string_to_exif_tag_group(tag_group) else {\n                error!(\"Unknown EXIF tag group string: {tag_group}, skipping tag removal.\");\n                continue;\n            };\n\n            new_metadata.remove_tag_by_hex_group(*tag_u16, tag_group);\n            tags_removed += 1;\n        }\n\n        new_metadata.write_to_vec(&mut file_data, ext).map_err(|e| e.to_string())?;\n        if override_file {\n            fs::write(file_path, file_data).map_err(|e| e.to_string())?;\n        } else {\n            let extension = file_path.extension().and_then(|ext| ext.to_str()).unwrap_or(\"\");\n            let new_file_path = file_path.with_extension(format!(\"czkawka_cleaned_exif.{extension}\"));\n            fs::write(new_file_path, file_data).map_err(|e| e.to_string())?;\n        }\n\n        Ok(tags_removed)\n    })\n    .map_err(|e| format!(\"Panic occurred while reading EXIF: {e:?}\"))?\n    .map_err(|e: String| format!(\"Failed to remove EXIF from file {file_path} - {e}\"))\n}\n\npub fn extract_exif_tags_public(path: &Path) -> Result<Vec<(u16, String)>, String> {\n    let tags = extract_exif_tags(path)?;\n    Ok(tags.into_iter().map(|(_, code, group)| (code, group)).collect())\n}\n\nfn extract_exif_tags(path: &Path) -> Result<Vec<(String, u16, String)>, String> {\n    panic::catch_unwind(|| {\n        let file_path = Path::new(path);\n        let data = fs::read(file_path).map_err(|e| e.to_string())?;\n        let mut cursor = std::io::Cursor::new(&data);\n        let ext = FileExtension::auto_detect(&mut cursor).ok_or_else(|| \"Failed to detect file type\".to_string())?;\n        let metadata = Metadata::new_from_vec(&data, ext).map_err(|e| format!(\"Failed to read EXIF: {e}\"))?;\n\n        let mut tags = Vec::new();\n\n        for tag in &metadata {\n            let tag_name = format!(\"{tag:?}\");\n            let tag_u16 = tag.as_u16();\n            let tag_group = exif_tag_group_to_string(tag.get_group());\n            if let Some(pos) = tag_name.find('(') {\n                #[expect(clippy::string_slice)] // Safe, because pos is from find\n                tags.push((tag_name[..pos].to_string(), tag_u16, tag_group));\n            } else {\n                tags.push((tag_name, tag_u16, tag_group));\n            }\n        }\n\n        Ok(tags)\n    })\n    .map_err(|e| format!(\"Panic occurred while reading \\\"{}\\\" - EXIF: {e:?}\", path.to_string_lossy()))?\n}\n\npub fn file_extension_to_string(extension: FileExtension) -> &'static str {\n    match extension {\n        FileExtension::PNG { .. } => \"PNG\",\n        FileExtension::JPEG => \"JPEG\",\n        FileExtension::TIFF => \"TIFF\",\n        FileExtension::WEBP => \"WEBP\",\n        FileExtension::NAKED_JXL => \"NAKED_JXL\",\n        FileExtension::JXL => \"JXL\",\n        FileExtension::HEIF => \"HEIF\",\n    }\n}\npub fn string_to_file_extension(s: &str) -> FileExtension {\n    match s {\n        \"PNG\" => FileExtension::PNG { as_zTXt_chunk: true },\n        \"JPEG\" => FileExtension::JPEG,\n        \"TIFF\" => FileExtension::TIFF,\n        \"WEBP\" => FileExtension::WEBP,\n        \"NAKED_JXL\" => FileExtension::NAKED_JXL,\n        \"JXL\" => FileExtension::JXL,\n        \"HEIF\" => FileExtension::HEIF,\n        _ => {\n            error!(\"Unknown file extension string: {s}, defaulting to JPEG\");\n            FileExtension::JPEG\n        } // Default to JPEG\n    }\n}\n\n// Nom-exif implementation\n// Probably will use this version in future\n// fn extract_exif_tags2(path: &Path) -> Result<Vec<String>, String> {\n//     let res = panic::catch_unwind(|| {\n//         let mut parser = nom_exif::MediaParser::new();\n//         let ms = nom_exif::MediaSource::file_path(path).map_err(|e| format!(\"Failed to open file: {e}\"))?;\n//         let mut results = Vec::new();\n//         if !ms.has_exif() {\n//             return Ok(results);\n//         }\n//         let exif_iter: nom_exif::ExifIter = parser.parse(ms).map_err(|e| format!(\"Failed to parse EXIF data: {e}\"))?;\n//         for exif_entry in exif_iter {\n//             results.push(exif_entry.tag().map_or_else(|| \"Unknown\".to_string(), |t| format!(\"{t:?}\")));\n//         }\n//\n//         Ok(results)\n//     });\n//\n//     res.unwrap_or_else(|_| {\n//         let message = crate::common::create_crash_message(\"nom-exif\", path.to_string_lossy().as_ref(), \"https://github.com/mindeng/nom-exif\");\n//         error!(\"{message}\");\n//         Err(\"Panic in get_rotation_from_exif\".to_string())\n//     })\n// }\n\npub fn string_to_exif_tag_group(tag: &str) -> Result<ExifTagGroup, String> {\n    match tag {\n        \"EXIF\" => Ok(ExifTagGroup::EXIF),\n        \"INTEROP\" => Ok(ExifTagGroup::INTEROP),\n        \"GPS\" => Ok(ExifTagGroup::GPS),\n        \"GENERIC\" => Ok(ExifTagGroup::GENERIC),\n        _ => Err(flc!(\"core_unknown_exif_tag_group\", tag = tag)),\n    }\n}\n\npub fn exif_tag_group_to_string(tag_group: ExifTagGroup) -> String {\n    match tag_group {\n        ExifTagGroup::EXIF => \"EXIF\".to_string(),\n        ExifTagGroup::INTEROP => \"INTEROP\".to_string(),\n        ExifTagGroup::GPS => \"GPS\".to_string(),\n        ExifTagGroup::GENERIC => \"GENERIC\".to_string(),\n    }\n}\n\npub fn get_exif_remover_cache_file() -> String {\n    format!(\"cache_exif_remover_{CACHE_VERSION}.bin\")\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/exif_remover/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::collections::BTreeMap;\nuse std::path::PathBuf;\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\n#[derive(Debug, Default, Clone, Copy)]\npub struct Info {\n    pub number_of_files_with_exif: usize,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone, Default)]\npub struct ExifRemoverParameters {\n    pub ignored_tags: Vec<String>,\n}\n\nimpl ExifRemoverParameters {\n    pub fn new(ignored_tags: Vec<String>) -> Self {\n        Self { ignored_tags }\n    }\n}\n\n#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct ExifEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub modified_date: u64,\n    pub exif_tags: Vec<ExifTagInfo>,\n    pub error: Option<String>,\n}\n\n#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct ExifTagInfo {\n    pub name: String,\n    pub code: u16,\n    pub group: String,\n}\n\n#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct ExifTagsFixerParams {\n    pub override_file: bool,\n}\n\nimpl ResultEntry for ExifEntry {\n    fn get_path(&self) -> &std::path::Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\npub struct ExifRemover {\n    common_data: CommonToolData,\n    information: Info,\n    exif_files: Vec<ExifEntry>,\n    files_to_check: BTreeMap<String, ExifEntry>,\n    params: ExifRemoverParameters,\n    additional_excluded_tags: BTreeMap<&'static str, Vec<&'static str>>,\n}\n\nimpl ExifRemover {\n    pub const fn get_exif_files(&self) -> &Vec<ExifEntry> {\n        &self.exif_files\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/exif_remover/tests.rs",
    "content": "use std::fs;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::exif_remover::{ExifRemover, ExifRemoverParameters};\n\nfn get_test_resources_path() -> PathBuf {\n    PathBuf::from(env!(\"CARGO_MANIFEST_DIR\")).join(\"test_resources\").join(\"images\")\n}\n\n#[test]\nfn test_find_exif_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let source_image = get_test_resources_path().join(\"normal.jpg\");\n    let dest_image = path.join(\"test.jpg\");\n    fs::copy(&source_image, &dest_image).unwrap();\n\n    let mut finder = ExifRemover::new(ExifRemoverParameters::default());\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_files_with_exif, 1, \"Should find at least one file with EXIF data\");\n\n    let exif_files = finder.get_exif_files();\n    assert_eq!(exif_files.len(), 1, \"Should find exactly one file with EXIF\");\n    assert!(!exif_files[0].exif_tags.is_empty(), \"EXIF tags should not be empty\");\n}\n\n#[test]\nfn test_empty_directory() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let mut finder = ExifRemover::new(ExifRemoverParameters::default());\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let exif_files = finder.get_exif_files();\n    assert_eq!(exif_files.len(), 0, \"Should find no files with EXIF in empty directory\");\n}\n\n#[test]\nfn test_non_image_files() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    fs::write(path.join(\"test.txt\"), b\"This is not an image\").unwrap();\n\n    let mut finder = ExifRemover::new(ExifRemoverParameters::default());\n    finder.set_included_paths(vec![path.to_path_buf()]);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let exif_files = finder.get_exif_files();\n    assert_eq!(exif_files.len(), 0, \"Should not find EXIF in non-image files\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/exif_remover/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::BINARY;\n\nuse crate::common::consts::EXIF_FILES_EXTENSIONS;\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search};\nuse crate::tools::exif_remover::{ExifEntry, ExifRemover, ExifRemoverParameters, ExifTagsFixerParams, Info};\n\nimpl AllTraits for ExifRemover {}\n\nimpl DeletingItems for ExifRemover {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => {\n                let files_to_delete: Vec<ExifEntry> = self.exif_files.clone();\n                self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(files_to_delete))\n            }\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl FixingItems for ExifRemover {\n    type FixParams = ExifTagsFixerParams;\n    #[fun_time(message = \"fix_items\", level = \"debug\")]\n    fn fix_items(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>, fix_params: Self::FixParams) {\n        self.fix_files(stop_flag, progress_sender, fix_params);\n    }\n}\n\nimpl DebugPrint for ExifRemover {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n\n        println!(\"### INDIVIDUAL DEBUG PRINT ###\");\n        println!(\"Info: {:?}\", self.information);\n        println!(\"Number of files with EXIF: {}\", self.information.number_of_files_with_exif);\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for ExifRemover {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        if self.information.number_of_files_with_exif != 0 {\n            writeln!(writer, \"Found {} files with EXIF data.\\n\", self.information.number_of_files_with_exif)?;\n\n            for exif_entry in &self.exif_files {\n                writeln!(\n                    writer,\n                    \"\\nFile: \\\"{}\\\" - {} - {} - {:?}\",\n                    exif_entry.path.to_string_lossy(),\n                    humansize::format_size(exif_entry.size, BINARY),\n                    exif_entry.modified_date,\n                    exif_entry.exif_tags.iter().map(|item_tag| item_tag.name.clone()).collect::<Vec<_>>()\n                )?;\n            }\n        } else {\n            writeln!(writer, \"Not found any files with EXIF data.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.exif_files, pretty_print)\n    }\n}\n\nimpl Search for ExifRemover {\n    #[fun_time(message = \"find_exif_data\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(Some(EXIF_FILES_EXTENSIONS)).is_err() {\n                return;\n            }\n            if self.find_exif_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n\n            if self.check_exif_in_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl CommonData for ExifRemover {\n    type Info = Info;\n    type Parameters = ExifRemoverParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_files_with_exif > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/invalid_symlinks/core.rs",
    "content": "use std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::debug;\n\nuse crate::common::dir_traversal::{Collect, DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::CommonToolData;\nuse crate::tools::invalid_symlinks::{ErrorType, Info, InvalidSymlinks, MAX_NUMBER_OF_SYMLINK_JUMPS, SymlinkInfo};\n\nimpl InvalidSymlinks {\n    pub fn new() -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::InvalidSymlinks),\n            information: Info::default(),\n            invalid_symlinks: Vec::new(),\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .common_data(&self.common_data)\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .collect(Collect::InvalidSymlinks)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.invalid_symlinks = grouped_file_entries\n                    .into_values()\n                    .flatten()\n                    .filter_map(|e| {\n                        let (destination_path, type_of_error) = Self::check_invalid_symlinks(&e.path)?;\n                        Some(e.into_symlinks_entry(SymlinkInfo { destination_path, type_of_error }))\n                    })\n                    .collect();\n                self.information.number_of_invalid_symlinks = self.invalid_symlinks.len();\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"Found {} invalid symlinks.\", self.information.number_of_invalid_symlinks);\n                WorkContinueStatus::Continue\n            }\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    fn check_invalid_symlinks(current_file_name: &Path) -> Option<(PathBuf, ErrorType)> {\n        let mut destination_path = PathBuf::new();\n        let type_of_error;\n\n        match current_file_name.read_link() {\n            Ok(t) => {\n                destination_path.push(t);\n                let mut loop_count = 0;\n                let mut current_path = current_file_name.to_path_buf();\n                loop {\n                    if loop_count == 0 && !current_path.exists() {\n                        type_of_error = ErrorType::NonExistentFile;\n                        break;\n                    }\n                    if loop_count == MAX_NUMBER_OF_SYMLINK_JUMPS {\n                        type_of_error = ErrorType::InfiniteRecursion;\n                        break;\n                    }\n\n                    current_path = match current_path.read_link() {\n                        Ok(t) => t,\n                        Err(_inspected) => {\n                            // Looks that some next symlinks are broken, but we do nothing with it - TODO why they are broken\n                            return None;\n                        }\n                    };\n\n                    loop_count += 1;\n                }\n            }\n            Err(_inspected) => {\n                // Failed to load info about it\n                type_of_error = ErrorType::NonExistentFile;\n            }\n        }\n        Some((destination_path, type_of_error))\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/invalid_symlinks/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::fmt::Display;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_invalid_symlinks: usize,\n    pub scanning_time: Duration,\n}\n\nconst MAX_NUMBER_OF_SYMLINK_JUMPS: i32 = 20;\n\n#[derive(Clone, Debug, PartialEq, Eq, Copy, Deserialize, Serialize)]\npub enum ErrorType {\n    InfiniteRecursion,\n    NonExistentFile,\n}\n\nimpl ErrorType {\n    pub fn translate(self) -> String {\n        match self {\n            Self::InfiniteRecursion => flc!(\"core_invalid_symlink_infinite_recursion\"),\n            Self::NonExistentFile => flc!(\"core_invalid_symlink_non_existent_destination\"),\n        }\n    }\n}\n\nimpl Display for ErrorType {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::InfiniteRecursion => write!(f, \"Infinite recursion\"),\n            Self::NonExistentFile => write!(f, \"Non existent file\"),\n        }\n    }\n}\n\n#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]\npub struct SymlinkInfo {\n    pub destination_path: PathBuf,\n    pub type_of_error: ErrorType,\n}\n\n#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\npub struct SymlinksFileEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub modified_date: u64,\n    pub symlink_info: SymlinkInfo,\n}\n\nimpl ResultEntry for SymlinksFileEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl FileEntry {\n    fn into_symlinks_entry(self, symlink_info: SymlinkInfo) -> SymlinksFileEntry {\n        SymlinksFileEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n\n            symlink_info,\n        }\n    }\n}\n\npub struct InvalidSymlinks {\n    common_data: CommonToolData,\n    information: Info,\n    invalid_symlinks: Vec<SymlinksFileEntry>,\n}\n\nimpl Default for InvalidSymlinks {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl InvalidSymlinks {\n    pub const fn get_invalid_symlinks(&self) -> &Vec<SymlinksFileEntry> {\n        &self.invalid_symlinks\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/invalid_symlinks/tests.rs",
    "content": "#[cfg(target_family = \"unix\")]\nuse std::fs;\n#[cfg(target_family = \"unix\")]\nuse std::sync::Arc;\n#[cfg(target_family = \"unix\")]\nuse std::sync::atomic::AtomicBool;\n\n#[cfg(target_family = \"unix\")]\nuse tempfile::TempDir;\n\n#[cfg(target_family = \"unix\")]\nuse crate::common::tool_data::CommonData;\n#[cfg(target_family = \"unix\")]\nuse crate::common::traits::Search;\n#[cfg(target_family = \"unix\")]\nuse crate::tools::invalid_symlinks::InvalidSymlinks;\n\n#[test]\n#[cfg(target_family = \"unix\")]\nfn test_find_invalid_symlinks() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let valid_target = path.join(\"valid_target.txt\");\n    fs::write(&valid_target, b\"content\").unwrap();\n\n    let valid_link = path.join(\"valid_link\");\n    std::os::unix::fs::symlink(&valid_target, &valid_link).unwrap();\n\n    let invalid_link = path.join(\"invalid_link\");\n    std::os::unix::fs::symlink(path.join(\"non_existent.txt\"), &invalid_link).unwrap();\n\n    let mut finder = InvalidSymlinks::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_invalid_symlinks, 1, \"Should find 1 invalid symlink\");\n}\n\n#[test]\n#[cfg(target_family = \"unix\")]\nfn test_no_invalid_symlinks() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let target = path.join(\"target.txt\");\n    fs::write(&target, b\"content\").unwrap();\n\n    let link = path.join(\"link\");\n    std::os::unix::fs::symlink(&target, &link).unwrap();\n\n    let mut finder = InvalidSymlinks::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_invalid_symlinks, 0, \"Should find no invalid symlinks\");\n}\n\n#[test]\n#[cfg(target_family = \"unix\")]\nfn test_deleted_target_creates_invalid_symlink() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let target = path.join(\"target.txt\");\n    fs::write(&target, b\"content\").unwrap();\n\n    let link = path.join(\"link\");\n    std::os::unix::fs::symlink(&target, &link).unwrap();\n\n    fs::remove_file(&target).unwrap();\n\n    let mut finder = InvalidSymlinks::new();\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_invalid_symlinks, 1, \"Should find the broken symlink\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/invalid_symlinks/traits.rs",
    "content": "use std::io::prelude::*;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::invalid_symlinks::{ErrorType, Info, InvalidSymlinks};\n\nimpl AllTraits for InvalidSymlinks {}\n\nimpl Search for InvalidSymlinks {\n    #[fun_time(message = \"find_invalid_links\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for InvalidSymlinks {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        println!(\"---------------DEBUG PRINT---------------\");\n        println!(\"Invalid symlinks list size - {}\", self.invalid_symlinks.len());\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for InvalidSymlinks {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n        if !self.invalid_symlinks.is_empty() {\n            writeln!(writer, \"Found {} invalid symlinks.\", self.information.number_of_invalid_symlinks)?;\n            for file_entry in &self.invalid_symlinks {\n                writeln!(\n                    writer,\n                    \"\\\"{}\\\"\\t\\t\\\"{}\\\"\\t\\t{}\",\n                    file_entry.path.to_string_lossy(),\n                    file_entry.symlink_info.destination_path.to_string_lossy(),\n                    match file_entry.symlink_info.type_of_error {\n                        ErrorType::InfiniteRecursion => \"Infinite Recursion\",\n                        ErrorType::NonExistentFile => \"Non Existent File\",\n                    }\n                )?;\n            }\n        } else {\n            write!(writer, \"Not found any invalid symlinks.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.invalid_symlinks, pretty_print)\n    }\n}\n\nimpl CommonData for InvalidSymlinks {\n    type Info = Info;\n    type Parameters = ();\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {}\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_invalid_symlinks > 0\n    }\n}\nimpl DeletingItems for InvalidSymlinks {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.common_data.delete_method {\n            DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.invalid_symlinks.clone())),\n            DeleteMethod::None => WorkContinueStatus::Continue,\n            _ => unreachable!(),\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/mod.rs",
    "content": "pub mod bad_extensions;\npub mod bad_names;\npub mod big_file;\npub mod broken_files;\npub mod duplicate;\npub mod empty_files;\npub mod empty_folder;\npub mod exif_remover;\npub mod invalid_symlinks;\npub mod same_music;\npub mod similar_images;\npub mod similar_videos;\npub mod temporary;\npub mod video_optimizer;\n"
  },
  {
    "path": "czkawka_core/src/tools/same_music/core.rs",
    "content": "use std::collections::BTreeMap;\nuse std::fs::File;\nuse std::path::Path;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\nuse std::{mem, panic};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse indexmap::IndexSet;\nuse lofty::file::{AudioFile, TaggedFileExt};\nuse lofty::prelude::*;\nuse lofty::read_from;\nuse log::{debug, error};\nuse rayon::prelude::*;\nuse rusty_chromaprint::{Configuration, Fingerprinter, match_fingerprints};\nuse symphonia::core::audio::SampleBuffer;\nuse symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions};\nuse symphonia::core::formats::FormatOptions;\nuse symphonia::core::io::MediaSourceStream;\nuse symphonia::core::meta::MetadataOptions;\nuse symphonia::core::probe::Hint;\n\nuse crate::common::cache::{CACHE_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path};\nuse crate::common::create_crash_message;\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\nuse crate::tools::same_music::{GroupedFilesToCheck, Info, MusicEntry, MusicSimilarity, SameMusic, SameMusicParameters};\n\nimpl SameMusic {\n    pub fn new(params: SameMusicParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::SameMusic),\n            information: Info::default(),\n            music_entries: Vec::with_capacity(2048),\n            duplicated_music_entries: Vec::new(),\n            music_to_check: Default::default(),\n            duplicated_music_entries_referenced: Vec::new(),\n            hash_preset_config: Configuration::preset_test1(), // TODO allow to change this and move to parameters\n            params,\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .checking_method(self.params.check_type)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.music_to_check = grouped_file_entries\n                    .into_values()\n                    .flatten()\n                    .map(|fe| (fe.path.to_string_lossy().to_string(), fe.into_music_entry()))\n                    .collect();\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"check_files - Found {} music files.\", self.music_to_check.len());\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    #[fun_time(message = \"load_cache\", level = \"debug\")]\n    fn load_cache(&mut self, checking_tags: bool) -> (BTreeMap<String, MusicEntry>, BTreeMap<String, MusicEntry>, BTreeMap<String, MusicEntry>) {\n        load_and_split_cache_generalized_by_path(&get_similar_music_cache_file(checking_tags), mem::take(&mut self.music_to_check), self)\n    }\n\n    #[fun_time(message = \"save_cache\", level = \"debug\")]\n    fn save_cache(&mut self, vec_file_entry: &[MusicEntry], loaded_hash_map: BTreeMap<String, MusicEntry>, checking_tags: bool) {\n        save_and_connect_cache_generalized_by_path(&get_similar_music_cache_file(checking_tags), vec_file_entry, loaded_hash_map, self);\n    }\n\n    #[fun_time(message = \"calculate_fingerprint\", level = \"debug\")]\n    pub(crate) fn calculate_fingerprint(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.music_entries.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        // We only calculate fingerprints, for files with similar titles\n        // This saves a lot of time, because we don't need to calculate and later compare fingerprints for files with different titles\n\n        if self.params.compare_fingerprints_only_with_similar_titles {\n            let grouped_by_title: BTreeMap<String, Vec<MusicEntry>> = Self::get_entries_grouped_by_title(mem::take(&mut self.music_entries));\n            self.music_to_check = grouped_by_title\n                .into_iter()\n                .filter_map(|(_title, entries)| if entries.len() >= 2 { Some(entries) } else { None })\n                .flatten()\n                .map(|e| (e.path.to_string_lossy().to_string(), e))\n                .collect();\n        } else {\n            self.music_to_check = mem::take(&mut self.music_entries).into_iter().map(|e| (e.path.to_string_lossy().to_string(), e)).collect();\n        }\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheLoadingFingerprints, 0, self.get_test_type(), 0);\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(false);\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::SameMusicCalculatingFingerprints,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            non_cached_files_to_check.values().map(|e| e.size).sum::<u64>(),\n        );\n        let configuration = &self.hash_preset_config;\n\n        let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::<Vec<_>>();\n\n        debug!(\"calculate_fingerprint - starting fingerprinting\");\n        let mut vec_file_entry = non_cached_files_to_check\n            .into_par_iter()\n            .with_max_len(2)\n            .map(|(path, mut music_entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let res = calc_fingerprint_helper(path, configuration);\n                progress_handler.increase_size(music_entry.size);\n                progress_handler.increase_items(1);\n\n                let Ok(fingerprint) = res else {\n                    return Some(None);\n                };\n\n                music_entry.fingerprint = fingerprint;\n\n                Some(Some(music_entry))\n            })\n            .while_some()\n            .flatten()\n            .collect::<Vec<_>>();\n        debug!(\"calculate_fingerprint - ended fingerprinting\");\n\n        progress_handler.join_thread();\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheSavingFingerprints, 0, self.get_test_type(), 0);\n\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        self.save_cache(&vec_file_entry, loaded_hash_map, false);\n\n        self.music_entries = vec_file_entry;\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"read_tags\", level = \"debug\")]\n    pub(crate) fn read_tags(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.music_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheLoadingTags, 0, self.get_test_type(), 0);\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(true);\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::SameMusicReadingTags,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            0,\n        );\n\n        debug!(\"read_tags - starting reading tags\");\n        // Clean for duplicate files\n        let mut vec_file_entry = non_cached_files_to_check\n            .into_par_iter()\n            .map(|(path, music_entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let res = read_single_file_tags(&path, music_entry);\n                progress_handler.increase_items(1);\n                Some(res)\n            })\n            .while_some()\n            .flatten()\n            .collect::<Vec<_>>();\n        debug!(\"read_tags - ended reading tags\");\n\n        progress_handler.join_thread();\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheSavingTags, 0, self.get_test_type(), 0);\n\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        self.save_cache(&vec_file_entry, loaded_hash_map, true);\n\n        self.music_entries = vec_file_entry;\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"check_for_duplicate_tags\", level = \"debug\")]\n    pub(crate) fn check_for_duplicate_tags(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.music_entries.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicComparingTags, self.music_entries.len(), self.get_test_type(), 0);\n\n        let mut old_duplicates: Vec<Vec<MusicEntry>> = vec![self.music_entries.clone()];\n        let mut new_duplicates: Vec<Vec<MusicEntry>> = Vec::new();\n\n        if (self.params.music_similarity & MusicSimilarity::TRACK_TITLE) == MusicSimilarity::TRACK_TITLE {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            old_duplicates = self.check_music_item(\n                old_duplicates,\n                progress_handler.items_counter(),\n                |fe| fe.track_title.clone(),\n                self.params.approximate_comparison,\n            );\n        }\n        if (self.params.music_similarity & MusicSimilarity::TRACK_ARTIST) == MusicSimilarity::TRACK_ARTIST {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            old_duplicates = self.check_music_item(\n                old_duplicates,\n                progress_handler.items_counter(),\n                |fe| fe.track_artist.clone(),\n                self.params.approximate_comparison,\n            );\n        }\n        if (self.params.music_similarity & MusicSimilarity::YEAR) == MusicSimilarity::YEAR {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| fe.year.clone(), false);\n        }\n        if (self.params.music_similarity & MusicSimilarity::LENGTH) == MusicSimilarity::LENGTH {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| format_audio_duration(fe.length), false);\n        }\n        if (self.params.music_similarity & MusicSimilarity::GENRE) == MusicSimilarity::GENRE {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| fe.genre.clone(), false);\n        }\n        if (self.params.music_similarity & MusicSimilarity::BITRATE) == MusicSimilarity::BITRATE {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n            let old_duplicates_len = old_duplicates.len();\n            for vec_file_entry in old_duplicates {\n                let mut hash_map: BTreeMap<String, Vec<MusicEntry>> = Default::default();\n                for file_entry in vec_file_entry {\n                    if file_entry.bitrate != 0 {\n                        let thing = file_entry.bitrate.to_string();\n\n                        hash_map.entry(thing).or_default().push(file_entry);\n                    }\n                }\n                for (_title, vec_file_entry) in hash_map {\n                    if vec_file_entry.len() > 1 {\n                        new_duplicates.push(vec_file_entry);\n                    }\n                }\n            }\n            progress_handler.increase_items(old_duplicates_len);\n            old_duplicates = new_duplicates;\n        }\n\n        progress_handler.join_thread();\n\n        self.duplicated_music_entries = old_duplicates;\n\n        if self.common_data.use_reference_folders {\n            self.duplicated_music_entries_referenced = self.common_data.directories.filter_reference_folders(mem::take(&mut self.duplicated_music_entries));\n        }\n\n        if self.common_data.use_reference_folders {\n            for (_fe, vector) in &self.duplicated_music_entries_referenced {\n                self.information.number_of_duplicates += vector.len();\n                self.information.number_of_groups += 1;\n            }\n        } else {\n            for vector in &self.duplicated_music_entries {\n                self.information.number_of_duplicates += vector.len() - 1;\n                self.information.number_of_groups += 1;\n            }\n        }\n\n        // Clear unused data\n        self.music_entries.clear();\n\n        WorkContinueStatus::Continue\n    }\n\n    fn split_fingerprints_to_base_and_files_to_compare(&self, music_data: Vec<MusicEntry>) -> (Vec<MusicEntry>, Vec<MusicEntry>) {\n        if self.common_data.use_reference_folders {\n            music_data.into_iter().partition(|f| self.common_data.directories.is_in_referenced_directory(f.get_path()))\n        } else {\n            (music_data.clone(), music_data)\n        }\n    }\n\n    fn get_entries_grouped_by_title(music_data: Vec<MusicEntry>) -> BTreeMap<String, Vec<MusicEntry>> {\n        let mut entries_grouped_by_title: BTreeMap<String, Vec<MusicEntry>> = BTreeMap::new();\n        for entry in music_data {\n            let simplified_track_title = get_simplified_name(&entry.track_title);\n            // TODO maybe add as option to check for empty titles?\n            if simplified_track_title.is_empty() {\n                continue;\n            }\n            entries_grouped_by_title.entry(simplified_track_title).or_default().push(entry);\n        }\n        entries_grouped_by_title\n    }\n\n    fn split_fingerprints_to_check(&mut self) -> Vec<GroupedFilesToCheck> {\n        if self.params.compare_fingerprints_only_with_similar_titles {\n            let entries_grouped_by_title: BTreeMap<String, Vec<MusicEntry>> = Self::get_entries_grouped_by_title(mem::take(&mut self.music_entries));\n\n            entries_grouped_by_title\n                .into_iter()\n                .filter_map(|(_title, entries)| {\n                    let (base_files, files_to_compare) = self.split_fingerprints_to_base_and_files_to_compare(entries);\n\n                    // When there is 0 files in base files or files to compare there will be no comparison, so removing it from the list\n                    // Also when there is only one file in base files and files to compare and they are the same file, there will be no comparison\n\n                    #[expect(clippy::indexing_slicing)] // Validated that base_files/files_to_compare are not empty\n                    if base_files.is_empty()\n                        || files_to_compare.is_empty()\n                        || (base_files.len() == 1 && files_to_compare.len() == 1 && (base_files[0].path == files_to_compare[0].path))\n                    {\n                        return None;\n                    }\n\n                    Some(GroupedFilesToCheck { base_files, files_to_compare })\n                })\n                .collect()\n        } else {\n            let entries = mem::take(&mut self.music_entries);\n            let (base_files, files_to_compare) = self.split_fingerprints_to_base_and_files_to_compare(entries);\n\n            vec![GroupedFilesToCheck { base_files, files_to_compare }]\n        }\n    }\n\n    fn compare_fingerprints(\n        &mut self,\n        stop_flag: &Arc<AtomicBool>,\n        items_counter: &Arc<AtomicUsize>,\n        base_files: Vec<MusicEntry>,\n        files_to_compare: &[MusicEntry],\n    ) -> Option<Vec<Vec<MusicEntry>>> {\n        let mut used_paths: IndexSet<String> = Default::default();\n\n        let configuration = &self.hash_preset_config;\n        let minimum_segment_duration = self.params.minimum_segment_duration;\n        let maximum_difference = self.params.maximum_difference;\n\n        let mut duplicated_music_entries = Vec::new();\n\n        for f_entry in base_files {\n            items_counter.fetch_add(1, Ordering::Relaxed);\n            if check_if_stop_received(stop_flag) {\n                return None;\n            }\n\n            let f_string = f_entry.path.to_string_lossy().to_string();\n            if used_paths.contains(&f_string) {\n                continue;\n            }\n\n            let (mut collected_similar_items, errors): (Vec<_>, Vec<_>) = files_to_compare\n                .par_iter()\n                .map(|e_entry| {\n                    let e_string = e_entry.path.to_string_lossy().to_string();\n                    if used_paths.contains(&e_string) || e_string == f_string {\n                        return None;\n                    }\n                    let mut segments = match match_fingerprints(&f_entry.fingerprint, &e_entry.fingerprint, configuration) {\n                        Ok(segments) => segments,\n                        Err(e) => return Some(Err(flc!(\"core_error_comparing_fingerprints\", reason = e.to_string()))),\n                    };\n                    segments.retain(|s| s.duration(configuration) > minimum_segment_duration && s.score < maximum_difference);\n                    if segments.is_empty() { None } else { Some(Ok((e_string, e_entry))) }\n                })\n                .flatten()\n                .partition_map(|res| match res {\n                    Ok(entry) => itertools::Either::Left(entry),\n                    Err(err) => itertools::Either::Right(err),\n                });\n\n            self.common_data.text_messages.errors.extend(errors);\n\n            collected_similar_items.retain(|(path, _entry)| !used_paths.contains(path));\n            if !collected_similar_items.is_empty() {\n                let mut music_entries = Vec::new();\n                for (path, entry) in collected_similar_items {\n                    used_paths.insert(path);\n                    music_entries.push(entry.clone());\n                }\n                used_paths.insert(f_string);\n                music_entries.push(f_entry);\n                duplicated_music_entries.push(music_entries);\n            }\n        }\n        Some(duplicated_music_entries)\n    }\n\n    #[fun_time(message = \"check_for_duplicate_fingerprints\", level = \"debug\")]\n    pub(crate) fn check_for_duplicate_fingerprints(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.music_entries.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let grouped_files_to_check = self.split_fingerprints_to_check();\n        let base_files_number = grouped_files_to_check.iter().map(|g| g.base_files.len()).sum::<usize>();\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicComparingFingerprints, base_files_number, self.get_test_type(), 0);\n\n        let mut duplicated_music_entries = Vec::new();\n        for group in grouped_files_to_check {\n            let GroupedFilesToCheck { base_files, files_to_compare } = group;\n            let Some(temp_music_entries) = self.compare_fingerprints(stop_flag, progress_handler.items_counter(), base_files, &files_to_compare) else {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            };\n            duplicated_music_entries.extend(temp_music_entries);\n        }\n\n        progress_handler.join_thread();\n\n        self.duplicated_music_entries = duplicated_music_entries;\n\n        if self.common_data.use_reference_folders {\n            self.duplicated_music_entries_referenced = self.common_data.directories.filter_reference_folders(mem::take(&mut self.duplicated_music_entries));\n        }\n\n        if self.common_data.use_reference_folders {\n            for (_fe, vector) in &self.duplicated_music_entries_referenced {\n                self.information.number_of_duplicates += vector.len();\n                self.information.number_of_groups += 1;\n            }\n        } else {\n            for vector in &self.duplicated_music_entries {\n                self.information.number_of_duplicates += vector.len() - 1;\n                self.information.number_of_groups += 1;\n            }\n        }\n\n        // Clear unused data\n        self.music_entries.clear();\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"check_music_item\", level = \"debug\")]\n    fn check_music_item(\n        &self,\n        old_duplicates: Vec<Vec<MusicEntry>>,\n        items_counter: &Arc<AtomicUsize>,\n        get_item: fn(&MusicEntry) -> String,\n        approximate_comparison: bool,\n    ) -> Vec<Vec<MusicEntry>> {\n        let mut new_duplicates: Vec<_> = Default::default();\n        let old_duplicates_len = old_duplicates.len();\n        for vec_file_entry in old_duplicates {\n            let mut hash_map: BTreeMap<String, Vec<MusicEntry>> = Default::default();\n            for file_entry in vec_file_entry {\n                let mut thing = get_item(&file_entry).trim().to_lowercase();\n                if approximate_comparison {\n                    thing = get_simplified_name(&thing);\n                }\n                if !thing.is_empty() {\n                    hash_map.entry(thing).or_default().push(file_entry);\n                }\n            }\n            for (_title, vec_file_entry) in hash_map {\n                if vec_file_entry.len() > 1 {\n                    new_duplicates.push(vec_file_entry);\n                }\n            }\n        }\n        items_counter.fetch_add(old_duplicates_len, Ordering::Relaxed);\n\n        new_duplicates\n    }\n}\n\n// TODO this should be taken from rusty-chromaprint repo, not reimplemented here\nfn calc_fingerprint_helper<P: AsRef<Path>>(path: P, config: &Configuration) -> Result<Vec<u32>, String> {\n    let path = path.as_ref().to_path_buf();\n    panic::catch_unwind(|| {\n        let path = &path;\n\n        let src = File::open(path).map_err(|_| \"failed to open file\".to_string())?;\n        let mss = MediaSourceStream::new(Box::new(src), Default::default());\n\n        let mut hint = Hint::new();\n        if let Some(ext) = path.extension().and_then(std::ffi::OsStr::to_str) {\n            hint.with_extension(ext);\n        }\n\n        let meta_opts: MetadataOptions = Default::default();\n        let fmt_opts: FormatOptions = Default::default();\n\n        let probed = symphonia::default::get_probe()\n            .format(&hint, mss, &fmt_opts, &meta_opts)\n            .map_err(|_| \"unsupported format\".to_string())?;\n\n        let mut format = probed.format;\n\n        let track = format\n            .tracks()\n            .iter()\n            .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)\n            .ok_or_else(|| \"no supported audio tracks\".to_string())?;\n\n        let dec_opts: DecoderOptions = Default::default();\n\n        let mut decoder = symphonia::default::get_codecs()\n            .make(&track.codec_params, &dec_opts)\n            .map_err(|_| \"unsupported codec\".to_string())?;\n\n        let track_id = track.id;\n\n        let mut printer = Fingerprinter::new(config);\n        let sample_rate = track.codec_params.sample_rate.ok_or_else(|| \"missing sample rate\".to_string())?;\n        let channels = track.codec_params.channels.ok_or_else(|| \"missing audio channels\".to_string())?.count() as u32;\n        printer.start(sample_rate, channels).map_err(|_| \"initializing fingerprinter\".to_string())?;\n\n        let mut sample_buf = None;\n\n        loop {\n            let Ok(packet) = format.next_packet() else {\n                break;\n            };\n\n            if packet.track_id() != track_id {\n                continue;\n            }\n\n            match decoder.decode(&packet) {\n                Ok(audio_buf) => {\n                    if sample_buf.is_none() {\n                        let spec = *audio_buf.spec();\n                        let duration = audio_buf.capacity() as u64;\n                        sample_buf = Some(SampleBuffer::<i16>::new(duration, spec));\n                    }\n\n                    if let Some(buf) = &mut sample_buf {\n                        buf.copy_interleaved_ref(audio_buf);\n                        printer.consume(buf.samples());\n                    }\n                }\n                Err(symphonia::core::errors::Error::DecodeError(_)) => (),\n                Err(_) => break,\n            }\n        }\n\n        printer.finish();\n        Ok(printer.fingerprint().to_vec())\n    })\n    .unwrap_or_else(|_| {\n        let message = create_crash_message(\"Symphonia\", &path.to_string_lossy(), \"https://github.com/pdeljanov/Symphonia\");\n        error!(\"{message}\");\n        Err(message)\n    })\n}\n\nfn read_single_file_tags(path: &str, mut music_entry: MusicEntry) -> Option<MusicEntry> {\n    let Ok(mut file) = File::open(path) else {\n        return None;\n    };\n\n    let Ok(possible_tagged_file) = panic::catch_unwind(move || read_from(&mut file).ok()) else {\n        let message = create_crash_message(\"Lofty\", path, \"https://github.com/Serial-ATA/lofty-rs\");\n        error!(\"{message}\");\n        return None;\n    };\n\n    let Some(tagged_file) = possible_tagged_file else { return Some(music_entry) };\n\n    let properties = tagged_file.properties();\n\n    let mut track_title = String::new();\n    let mut track_artist = String::new();\n    let mut year = String::new();\n    let mut genre = String::new();\n\n    let bitrate = properties.audio_bitrate().unwrap_or(0);\n\n    if let Some(tag) = tagged_file.primary_tag() {\n        track_title = tag.get_string(ItemKey::TrackTitle).unwrap_or_default().to_string();\n        track_artist = tag.get_string(ItemKey::TrackArtist).unwrap_or_default().to_string();\n        year = tag.get_string(ItemKey::Year).unwrap_or_default().to_string();\n        genre = tag.get_string(ItemKey::Genre).unwrap_or_default().to_string();\n    }\n\n    for tag in tagged_file.tags() {\n        if track_title.is_empty()\n            && let Some(tag_value) = tag.get_string(ItemKey::TrackTitle)\n        {\n            track_title = tag_value.to_string();\n        }\n        if track_artist.is_empty()\n            && let Some(tag_value) = tag.get_string(ItemKey::TrackArtist)\n        {\n            track_artist = tag_value.to_string();\n        }\n        if year.is_empty()\n            && let Some(tag_value) = tag.get_string(ItemKey::Year)\n        {\n            year = tag_value.to_string();\n        }\n        if genre.is_empty()\n            && let Some(tag_value) = tag.get_string(ItemKey::Genre)\n        {\n            genre = tag_value.to_string();\n        }\n    }\n\n    let length_milliseconds = properties.duration().as_millis();\n    let length_in_seconds = if length_milliseconds == 0 {\n        0\n    } else {\n        let secs = properties.duration().as_secs() as u32;\n        if secs == 0 { 1 } else { secs }\n    };\n\n    music_entry.track_title = track_title;\n    music_entry.track_artist = track_artist;\n    music_entry.year = year;\n    music_entry.length = length_in_seconds;\n    music_entry.genre = genre;\n    music_entry.bitrate = bitrate;\n\n    Some(music_entry)\n}\n\npub fn format_audio_duration(duration: u32) -> String {\n    let hours = duration / 3600;\n    let minutes = (duration % 3600) / 60;\n    let seconds = duration % 60;\n    if hours > 0 {\n        format!(\"{hours}:{minutes:02}:{seconds:02}\")\n    } else {\n        format!(\"{minutes}:{seconds:02}\")\n    }\n}\n\nfn get_simplified_name_internal(what: &str, ignore_numbers: bool) -> String {\n    let mut new_what = String::with_capacity(what.len());\n    let mut tab_number = 0;\n    let mut space_before = true;\n    for character in what.chars().map(|e| if e.is_whitespace() { ' ' } else { e }) {\n        match character {\n            '(' | '[' => {\n                tab_number += 1;\n            }\n            ')' | ']' => {\n                if tab_number == 0 {\n                    // Nothing to do, not even save it to output\n                } else {\n                    tab_number -= 1;\n                }\n            }\n            ' ' => {\n                if !space_before {\n                    new_what.push(' ');\n                    space_before = true;\n                }\n            }\n            ch => {\n                if tab_number == 0 {\n                    if ch.is_ascii_alphabetic() || (!ignore_numbers && ch.is_numeric()) {\n                        space_before = false;\n                        new_what.push(ch);\n                    } else {\n                        let new_items = deunicode::deunicode_char(character).map_or_else(|| vec![character; 1], |e| e.trim().to_string().chars().collect::<Vec<_>>());\n\n                        // If is equal, then we're trying to deunicode e.g. dot, comma etc.\n                        // We just ignore char, because it is mostly useless, but we add space instead it if it wasn't added already\n                        if new_items.first() == Some(&character) {\n                            if !space_before {\n                                new_what.push(' ');\n                                space_before = true;\n                            }\n                        } else {\n                            new_what.extend(new_items.into_iter());\n                            space_before = false;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    if new_what.ends_with(' ') {\n        new_what.pop();\n    }\n    new_what\n}\nfn get_simplified_name(what: &str) -> String {\n    let new_what = get_simplified_name_internal(what, true);\n    if !new_what.is_empty() {\n        return new_what;\n    }\n    let new_what = get_simplified_name_internal(what, false);\n    if !new_what.is_empty() {\n        return new_what;\n    }\n    let simplified_unicode = deunicode::deunicode(what).trim().to_string();\n    if !simplified_unicode.is_empty() {\n        return simplified_unicode;\n    }\n    // If everything failed, we return original string\n    // this is more useful than returning empty string, which is ignored by other functions\n    what.trim().to_string()\n}\n\npub fn get_similar_music_cache_file(checking_tags: bool) -> String {\n    if checking_tags {\n        format!(\"cache_same_music_tags_{CACHE_VERSION}.bin\")\n    } else {\n        format!(\"cache_same_music_fingerprints_{CACHE_VERSION}.bin\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    #[test]\n    fn test_simplified_names() {\n        let cases = [\n            (\"roman ( ziemniak ) \", \"roman\"),\n            (\"  HH)    \", \"HH\"),\n            (\"  fsf.f.  \", \"fsf f\"),\n            (\"  śśśśćććć  \", \"sssscccc\"),\n            (\"rr\\t\", \"rr\"),\n            (\"Kekistan (feat. roman) [Mix on Mix]\", \"Kekistan\"),\n            (\"23\", \"23\"),\n            (\"23 (random)\", \"23\"),\n            (\"(23)\", \"(23)\"),\n        ];\n\n        for (input, expected) in cases {\n            let res = get_simplified_name(input);\n            assert_eq!(res, expected, \"Input: {input}, Expected: {expected}, Got: {res}\");\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/same_music/mod.rs",
    "content": "use bitflags::bitflags;\npub mod core;\npub mod traits;\n\n#[cfg(test)]\nmod tests;\n\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse rusty_chromaprint::Configuration;\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::model::{CheckingMethod, FileEntry};\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\nbitflags! {\n    #[derive(PartialEq, Copy, Clone, Debug)]\n    pub struct MusicSimilarity : u32 {\n        const NONE = 0;\n\n        const TRACK_TITLE = 0b1;\n        const TRACK_ARTIST = 0b10;\n        const YEAR = 0b100;\n        const LENGTH = 0b1000;\n        const GENRE = 0b10000;\n        const BITRATE = 0b10_0000;\n    }\n}\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct MusicEntry {\n    pub size: u64,\n\n    pub path: PathBuf,\n    pub modified_date: u64,\n    pub fingerprint: Vec<u32>,\n\n    pub track_title: String,\n    pub track_artist: String,\n    pub year: String,\n    pub length: u32,\n    pub genre: String,\n    pub bitrate: u32,\n}\n\nimpl ResultEntry for MusicEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl FileEntry {\n    fn into_music_entry(self) -> MusicEntry {\n        MusicEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n\n            fingerprint: Vec::new(),\n            track_title: String::new(),\n            track_artist: String::new(),\n            year: String::new(),\n            length: 0,\n            genre: String::new(),\n            bitrate: 0,\n        }\n    }\n}\n\nstruct GroupedFilesToCheck {\n    pub base_files: Vec<MusicEntry>,\n    pub files_to_compare: Vec<MusicEntry>,\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_duplicates: usize,\n    pub number_of_groups: usize,\n    pub scanning_time: Duration,\n}\n\n#[derive(Clone)]\npub struct SameMusicParameters {\n    pub music_similarity: MusicSimilarity,\n    pub approximate_comparison: bool,\n    pub check_type: CheckingMethod,\n    pub minimum_segment_duration: f32,\n    pub maximum_difference: f64,\n    pub compare_fingerprints_only_with_similar_titles: bool,\n}\n\nimpl SameMusicParameters {\n    pub fn new(\n        music_similarity: MusicSimilarity,\n        approximate_comparison: bool,\n        check_type: CheckingMethod,\n        minimum_segment_duration: f32,\n        maximum_difference: f64,\n        compare_fingerprints_only_with_similar_titles: bool,\n    ) -> Self {\n        assert!(!music_similarity.is_empty());\n        assert!([CheckingMethod::AudioTags, CheckingMethod::AudioContent].contains(&check_type));\n        Self {\n            music_similarity,\n            approximate_comparison,\n            check_type,\n            minimum_segment_duration,\n            maximum_difference,\n            compare_fingerprints_only_with_similar_titles,\n        }\n    }\n}\n\npub struct SameMusic {\n    common_data: CommonToolData,\n    information: Info,\n    music_to_check: BTreeMap<String, MusicEntry>,\n    music_entries: Vec<MusicEntry>,\n    duplicated_music_entries: Vec<Vec<MusicEntry>>,\n    duplicated_music_entries_referenced: Vec<(MusicEntry, Vec<MusicEntry>)>,\n    hash_preset_config: Configuration,\n    params: SameMusicParameters,\n}\n\nimpl SameMusic {\n    pub const fn get_duplicated_music_entries(&self) -> &Vec<Vec<MusicEntry>> {\n        &self.duplicated_music_entries\n    }\n\n    pub fn get_params(&self) -> &SameMusicParameters {\n        &self.params\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n\n    pub fn get_similar_music_referenced(&self) -> &Vec<(MusicEntry, Vec<MusicEntry>)> {\n        &self.duplicated_music_entries_referenced\n    }\n\n    pub fn get_number_of_base_duplicated_files(&self) -> usize {\n        if self.common_data.use_reference_folders {\n            self.duplicated_music_entries_referenced.len()\n        } else {\n            self.duplicated_music_entries.len()\n        }\n    }\n\n    pub fn get_use_reference(&self) -> bool {\n        self.common_data.use_reference_folders\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/same_music/tests.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crate::common::model::CheckingMethod;\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::same_music::{MusicSimilarity, SameMusic, SameMusicParameters};\n\nfn get_test_resources_path() -> PathBuf {\n    let path = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\")).join(\"test_resources\").join(\"audio\");\n\n    assert!(path.exists(), \"Test resources not found at \\\"{}\\\"\", path.to_string_lossy());\n\n    path\n}\n\n#[test]\nfn test_same_music_by_content_high_similarity() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioContent, 10.0, 0.2, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 1);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates.iter().map(|e| e.len()).sum::<usize>(), 2);\n}\n\n#[test]\nfn test_same_music_by_content_medium_similarity() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioContent, 10.0, 0.5, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 1);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates.iter().map(|e| e.len()).sum::<usize>(), 2);\n}\n\n#[test]\nfn test_same_music_by_content_low_similarity() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioContent, 10.0, 0.8, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 3);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates.iter().map(|e| e.len()).sum::<usize>(), 4);\n}\n\n#[test]\nfn test_same_music_by_tags_title_artist() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(\n        MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST,\n        false,\n        CheckingMethod::AudioTags,\n        10.0,\n        0.2,\n        false,\n    );\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 4);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates[0].len(), 5);\n}\n\n#[test]\nfn test_same_music_by_tags_year() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::YEAR, false, CheckingMethod::AudioTags, 10.0, 0.2, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 0);\n    assert_eq!(info.number_of_groups, 0);\n    assert_eq!(duplicates.len(), 0);\n}\n\n#[test]\nfn test_same_music_by_tags_genre() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::GENRE, false, CheckingMethod::AudioTags, 10.0, 0.2, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 4);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates[0].len(), 5);\n}\n\n#[test]\nfn test_same_music_by_tags_bitrate() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::BITRATE, false, CheckingMethod::AudioTags, 10.0, 0.2, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 2);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates.iter().map(|e| e.len()).sum::<usize>(), 3);\n}\n\n#[test]\nfn test_same_music_by_tags_all_criteria() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(\n        MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST | MusicSimilarity::YEAR | MusicSimilarity::GENRE,\n        false,\n        CheckingMethod::AudioTags,\n        10.0,\n        0.2,\n        false,\n    );\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 0);\n    assert_eq!(info.number_of_groups, 0);\n    assert_eq!(duplicates.len(), 0);\n}\n\n#[test]\nfn test_same_music_approximate_comparison() {\n    let test_path = get_test_resources_path();\n\n    let params = SameMusicParameters::new(\n        MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST,\n        true,\n        CheckingMethod::AudioTags,\n        10.0,\n        0.2,\n        false,\n    );\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 4);\n    assert_eq!(info.number_of_groups, 1);\n    assert_eq!(duplicates.len(), 1);\n    assert_eq!(duplicates[0].len(), 5);\n}\n\n#[test]\nfn test_same_music_empty_directory() {\n    use tempfile::TempDir;\n\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioTags, 10.0, 0.2, false);\n\n    let mut finder = SameMusic::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let duplicates = finder.get_duplicated_music_entries();\n\n    assert_eq!(info.number_of_duplicates, 0);\n    assert_eq!(info.number_of_groups, 0);\n    assert_eq!(duplicates.len(), 0);\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/same_music/traits.rs",
    "content": "use std::io::prelude::*;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::consts::AUDIO_FILES_EXTENSIONS;\nuse crate::common::model::{CheckingMethod, WorkContinueStatus};\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::flc;\nuse crate::tools::same_music::core::format_audio_duration;\nuse crate::tools::same_music::{Info, MusicEntry, MusicSimilarity, SameMusic, SameMusicParameters};\n\nimpl AllTraits for SameMusic {}\n\nimpl Search for SameMusic {\n    #[fun_time(message = \"find_same_music\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(Some(AUDIO_FILES_EXTENSIONS)).is_err() {\n                return;\n            }\n            self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty();\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            match self.params.check_type {\n                CheckingMethod::AudioTags => {\n                    if self.params.music_similarity == MusicSimilarity::NONE {\n                        self.common_data.text_messages.critical = flc!(\"core_no_similarity_method_selected\").into();\n                        return;\n                    }\n\n                    if self.read_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                        self.common_data.stopped_search = true;\n                        return;\n                    }\n                    if self.check_for_duplicate_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                        self.common_data.stopped_search = true;\n                        return;\n                    }\n                }\n                CheckingMethod::AudioContent => {\n                    if self.read_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                        self.common_data.stopped_search = true;\n                        return;\n                    }\n                    if self.calculate_fingerprint(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                        self.common_data.stopped_search = true;\n                        return;\n                    }\n                    if self.check_for_duplicate_fingerprints(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                        self.common_data.stopped_search = true;\n                        return;\n                    }\n                }\n                _ => panic!(),\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for SameMusic {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        println!(\"---------------DEBUG PRINT---------------\");\n        println!(\"Found files music - {}\", self.music_entries.len());\n        println!(\"Found duplicated files music - {}\", self.duplicated_music_entries.len());\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for SameMusic {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n        if !self.duplicated_music_entries.is_empty() {\n            writeln!(writer, \"{} music files which have similar friends\\n\\n.\", self.duplicated_music_entries.len())?;\n\n            for vec_file_entry in &self.duplicated_music_entries {\n                writeln!(writer, \"Found {} music files which have similar friends\", vec_file_entry.len())?;\n                for file_entry in vec_file_entry {\n                    write_music_entry(writer, file_entry)?;\n                }\n                writeln!(writer)?;\n            }\n        } else if !self.duplicated_music_entries_referenced.is_empty() {\n            writeln!(writer, \"{} music files which have similar friends\\n\\n.\", self.duplicated_music_entries_referenced.len())?;\n            for (file_entry, vec_file_entry) in &self.duplicated_music_entries_referenced {\n                writeln!(writer, \"Found {} music files which have similar friends\", vec_file_entry.len())?;\n                writeln!(writer)?;\n                write_music_entry(writer, file_entry)?;\n                for file_entry in vec_file_entry {\n                    write_music_entry(writer, file_entry)?;\n                }\n                writeln!(writer)?;\n            }\n        } else {\n            write!(writer, \"Not found any similar music files.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        if self.get_use_reference() {\n            self.save_results_to_file_as_json_internal(file_name, &self.duplicated_music_entries_referenced, pretty_print)\n        } else {\n            self.save_results_to_file_as_json_internal(file_name, &self.duplicated_music_entries, pretty_print)\n        }\n    }\n}\n\nfn write_music_entry<T: Write>(writer: &mut T, file_entry: &MusicEntry) -> std::io::Result<()> {\n    writeln!(\n        writer,\n        \"TT: {}  -  TA: {}  -  Y: {}  -  L: {}  -  G: {}  -  B: {}  -  P: \\\"{}\\\"\",\n        file_entry.track_title,\n        file_entry.track_artist,\n        file_entry.year,\n        format_audio_duration(file_entry.length),\n        file_entry.genre,\n        file_entry.bitrate,\n        file_entry.path.to_string_lossy()\n    )\n}\n\nimpl CommonData for SameMusic {\n    type Info = Info;\n    type Parameters = SameMusicParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn get_check_method(&self) -> CheckingMethod {\n        self.get_params().check_type\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_duplicates > 0\n    }\n}\n\nimpl DeletingItems for SameMusic {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.get_cd().delete_method == DeleteMethod::None {\n            return WorkContinueStatus::Continue;\n        }\n        let files_to_delete = self.duplicated_music_entries.clone();\n        self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete)\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_images/core.rs",
    "content": "use std::collections::{BTreeMap, BTreeSet};\nuse std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::{mem, panic};\n\nuse bk_tree::BKTree;\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse image::GenericImageView;\nuse image_hasher::{FilterType, HashAlg, HasherConfig};\nuse indexmap::{IndexMap, IndexSet};\nuse log::{debug, error};\nuse rayon::prelude::*;\n\nuse crate::common::cache::{CACHE_IMAGE_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path};\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult, inode, take_1_per_inode};\nuse crate::common::image::get_dynamic_image_from_path;\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\nuse crate::tools::similar_images::{Hamming, ImHash, ImagesEntry, SIMILAR_VALUES, SimilarImages, SimilarImagesParameters, SimilarityPreset};\n\nimpl SimilarImages {\n    pub fn new(params: SimilarImagesParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::SimilarImages),\n            information: Default::default(),\n            bktree: BKTree::new(Hamming),\n            similar_vectors: Vec::new(),\n            similar_referenced_vectors: Vec::new(),\n            params,\n            images_to_check: Default::default(),\n            image_hashes: Default::default(),\n        }\n    }\n\n    #[fun_time(message = \"check_for_similar_images\", level = \"debug\")]\n    pub(crate) fn check_for_similar_images(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(inode)\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.images_to_check = grouped_file_entries\n                    .into_par_iter()\n                    .flat_map(if self.get_hide_hard_links() { |(_, fes)| fes } else { take_1_per_inode })\n                    .map(|fe| {\n                        let fe_str = fe.path.to_string_lossy().to_string();\n                        let image_entry = fe.into_images_entry();\n\n                        (fe_str, image_entry)\n                    })\n                    .collect();\n\n                self.information.initial_found_files = self.images_to_check.len();\n\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"check_files - Found {} image files.\", self.images_to_check.len());\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    #[fun_time(message = \"hash_images_load_cache\", level = \"debug\")]\n    fn hash_images_load_cache(&mut self) -> (BTreeMap<String, ImagesEntry>, BTreeMap<String, ImagesEntry>, BTreeMap<String, ImagesEntry>) {\n        load_and_split_cache_generalized_by_path(\n            &get_similar_images_cache_file(self.get_params().hash_size, self.get_params().hash_alg, self.get_params().image_filter),\n            mem::take(&mut self.images_to_check),\n            self,\n        )\n    }\n\n    #[fun_time(message = \"save_to_cache\", level = \"debug\")]\n    fn save_to_cache(&mut self, vec_file_entry: &[ImagesEntry], loaded_hash_map: BTreeMap<String, ImagesEntry>) {\n        save_and_connect_cache_generalized_by_path(\n            &get_similar_images_cache_file(self.get_params().hash_size, self.get_params().hash_alg, self.get_params().image_filter),\n            vec_file_entry,\n            loaded_hash_map,\n            self,\n        );\n    }\n\n    #[fun_time(message = \"hash_images\", level = \"debug\")]\n    pub(crate) fn hash_images(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.images_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.hash_images_load_cache();\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::SimilarImagesCalculatingHashes,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            non_cached_files_to_check.values().map(|entry| entry.size).sum(),\n        );\n\n        debug!(\"hash_images - start hashing images\");\n        let (mut vec_file_entry, errors): (Vec<ImagesEntry>, Vec<String>) = non_cached_files_to_check\n            .into_par_iter()\n            .map(|(_s, file_entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n                let size = file_entry.size;\n                let res = self.collect_image_file_entry(file_entry);\n                progress_handler.increase_items(1);\n                progress_handler.increase_size(size);\n\n                Some(res)\n            })\n            .while_some()\n            .partition_map(|res| match res {\n                Ok(entry) => itertools::Either::Left(entry),\n                Err(err) => itertools::Either::Right(err),\n            });\n\n        self.common_data.text_messages.errors.extend(errors);\n        debug!(\"hash_images - end hashing {} images\", vec_file_entry.len());\n\n        progress_handler.join_thread();\n\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        self.save_to_cache(&vec_file_entry, loaded_hash_map);\n\n        // All valid entries are used to create bktree used to check for hash similarity\n        for file_entry in vec_file_entry {\n            // Only use to comparing, non broken hashes(all 0 or 255 hashes means that algorithm fails to decode them because e.g. contains a lot of alpha channel)\n            if !(file_entry.hash.is_empty() || file_entry.hash.iter().all(|e| *e == 0) || file_entry.hash.iter().all(|e| *e == 255)) {\n                self.image_hashes.entry(file_entry.hash.clone()).or_default().push(file_entry);\n            }\n        }\n\n        // Break if stop was clicked after saving to cache\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    fn collect_image_file_entry(&self, mut file_entry: ImagesEntry) -> Result<ImagesEntry, String> {\n        let img = get_dynamic_image_from_path(&file_entry.path.to_string_lossy(), None)?.image;\n\n        let dimensions = img.dimensions();\n\n        file_entry.width = dimensions.0;\n        file_entry.height = dimensions.1;\n\n        let hasher_config = HasherConfig::new()\n            .hash_size(self.get_params().hash_size as u32, self.get_params().hash_size as u32)\n            .hash_alg(self.get_params().hash_alg)\n            .resize_filter(self.get_params().image_filter);\n        let hasher = hasher_config.to_hasher();\n        let hash = hasher.hash_image(&img);\n        file_entry.hash = hash.as_bytes().to_vec();\n\n        Ok(file_entry)\n    }\n\n    // Split hashes at 2 parts, base hashes and hashes to compare, 3 argument is set of hashes with multiple images\n    #[fun_time(message = \"split_hashes\", level = \"debug\")]\n    fn split_hashes(&mut self, all_hashed_images: &IndexMap<ImHash, Vec<ImagesEntry>>) -> (Vec<ImHash>, IndexSet<ImHash>) {\n        let hashes_with_multiple_images: IndexSet<ImHash> = all_hashed_images\n            .iter()\n            .filter_map(|(hash, vec_file_entry)| {\n                if vec_file_entry.len() >= 2 {\n                    return Some(hash.clone());\n                }\n                None\n            })\n            .collect();\n        let mut base_hashes = Vec::new(); // Initial hashes\n        if self.common_data.use_reference_folders {\n            let mut files_from_referenced_folders: IndexMap<ImHash, Vec<ImagesEntry>> = IndexMap::new();\n            let mut normal_files: IndexMap<ImHash, Vec<ImagesEntry>> = IndexMap::new();\n\n            all_hashed_images.clone().into_iter().for_each(|(hash, vec_file_entry)| {\n                for file_entry in vec_file_entry {\n                    if is_in_reference_folder(&self.common_data.directories.reference_directories, &file_entry.path) {\n                        files_from_referenced_folders.entry(hash.clone()).or_default().push(file_entry);\n                    } else {\n                        normal_files.entry(hash.clone()).or_default().push(file_entry);\n                    }\n                }\n            });\n\n            for hash in normal_files.into_keys() {\n                self.bktree.add(hash);\n            }\n\n            for hash in files_from_referenced_folders.into_keys() {\n                base_hashes.push(hash);\n            }\n        } else {\n            for original_hash in all_hashed_images.keys() {\n                self.bktree.add(original_hash.clone());\n            }\n            base_hashes = all_hashed_images.keys().cloned().collect::<Vec<_>>();\n        }\n        (base_hashes, hashes_with_multiple_images)\n    }\n\n    #[fun_time(message = \"collect_hash_compare_result\", level = \"debug\")]\n    fn collect_hash_compare_result(\n        &self,\n        hashes_parents: IndexMap<ImHash, u32>,\n        hashes_with_multiple_images: &IndexSet<ImHash>,\n        all_hashed_images: &IndexMap<ImHash, Vec<ImagesEntry>>,\n        collected_similar_images: &mut IndexMap<ImHash, Vec<ImagesEntry>>,\n        hashes_similarity: IndexMap<ImHash, (ImHash, u32)>,\n    ) {\n        // Collecting results to vector\n        for (parent_hash, child_number) in hashes_parents {\n            // If hash contains other hasher OR multiple images are available for checked hash\n            if child_number > 0 || hashes_with_multiple_images.contains(&parent_hash) {\n                let vec_fe = all_hashed_images[&parent_hash].clone();\n                collected_similar_images.insert(parent_hash.clone(), vec_fe);\n            }\n        }\n\n        for (child_hash, (parent_hash, similarity)) in hashes_similarity {\n            let mut vec_fe = all_hashed_images[&child_hash].clone();\n            for fe in &mut vec_fe {\n                fe.difference = similarity;\n            }\n            collected_similar_images\n                .get_mut(&parent_hash)\n                .expect(\"Cannot find parent hash - this should be added in previous step\")\n                .append(&mut vec_fe);\n        }\n    }\n\n    #[fun_time(message = \"compare_hashes_with_non_zero_tolerance\", level = \"debug\")]\n    fn compare_hashes_with_non_zero_tolerance(\n        &mut self,\n        all_hashed_images: &IndexMap<ImHash, Vec<ImagesEntry>>,\n        collected_similar_images: &mut IndexMap<ImHash, Vec<ImagesEntry>>,\n        progress_sender: Option<&Sender<ProgressData>>,\n        stop_flag: &Arc<AtomicBool>,\n        tolerance: u32,\n    ) -> WorkContinueStatus {\n        // Don't use hashes with multiple images in bktree, because they will always be master of group and cannot be find by other hashes\n        let (base_hashes, hashes_with_multiple_images) = self.split_hashes(all_hashed_images);\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SimilarImagesComparingHashes, base_hashes.len(), self.get_test_type(), 0);\n\n        let mut hashes_parents: IndexMap<ImHash, u32> = Default::default(); // Hashes used as parent (hash, children_number_of_hash)\n        let mut hashes_similarity: IndexMap<ImHash, (ImHash, u32)> = Default::default(); // Hashes used as child, (parent_hash, similarity)\n\n        // Check them in chunks, to decrease number of used memory\n        // Without chunks, every single hash would be compared to every other hash and generate really big amount of results\n        // With chunks we can save results to variables and later use such variables, to skip ones with too big difference\n        // Not really helpful, when not finding almost any duplicates, but with bigger amount of them, this should help a lot\n        let base_hashes_chunks = base_hashes.chunks(1000);\n        for chunk in base_hashes_chunks {\n            let partial_results = chunk\n                .into_par_iter()\n                .map(|hash_to_check| {\n                    progress_handler.increase_items(1);\n\n                    if check_if_stop_received(stop_flag) {\n                        return None;\n                    }\n                    let mut found_items = self\n                        .bktree\n                        .find(hash_to_check, tolerance)\n                        .filter(|(similarity, compared_hash)| {\n                            *similarity != 0 && !hashes_parents.contains_key(*compared_hash) && !hashes_with_multiple_images.contains(*compared_hash)\n                        })\n                        .filter(|(similarity, compared_hash)| {\n                            if let Some((_, other_similarity_with_parent)) = hashes_similarity.get(*compared_hash) {\n                                // If current hash is more similar to other hash than to current parent hash, then skip check earlier\n                                // Because there is no way to be more similar to other hash than to current parent hash\n                                if *similarity >= *other_similarity_with_parent {\n                                    return false;\n                                }\n                            }\n                            true\n                        })\n                        .collect::<Vec<_>>();\n\n                    // Sort by tolerance\n                    found_items.sort_unstable_by_key(|f| f.0);\n                    Some((hash_to_check, found_items))\n                })\n                .while_some()\n                // TODO - this filter move to into_par_iter above\n                .filter(|(original_hash, vec_similar_hashes)| !vec_similar_hashes.is_empty() || hashes_with_multiple_images.contains(*original_hash))\n                .collect::<Vec<_>>();\n\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            SimilarImages::connect_results_simplified(partial_results, &mut hashes_parents, &mut hashes_similarity, &hashes_with_multiple_images);\n        }\n        // To avoid situations in simplified connector we don't add such hashes to results\n        for multiple_image_hash in &hashes_with_multiple_images {\n            if !hashes_parents.contains_key(multiple_image_hash) {\n                hashes_parents.insert(multiple_image_hash.clone(), 0);\n            }\n        }\n\n        progress_handler.join_thread();\n\n        debug_check_for_duplicated_things(self.common_data.use_reference_folders, &hashes_parents, &hashes_similarity, all_hashed_images, \"LATTER\");\n        self.collect_hash_compare_result(hashes_parents, &hashes_with_multiple_images, all_hashed_images, collected_similar_images, hashes_similarity);\n\n        WorkContinueStatus::Continue\n    }\n\n    fn connect_results_simplified<'a>(\n        partial_results: Vec<(&'a ImHash, Vec<(u32, &'a ImHash)>)>,\n        hashes_parents: &mut IndexMap<ImHash, u32>,\n        hashes_similarity: &mut IndexMap<ImHash, (ImHash, u32)>,\n        hashes_with_multiple_images: &IndexSet<ImHash>,\n    ) {\n        // To simplify later logic, we sort all results by similarity\n        // To be able to do this, we need to flatten structure, which will increase memory usage a bit, but should improve a little logic(algorithm is a little broken and works better with sorted data)\n        // There can be hashes with multiple similar images, without any similar hashes, so we need to keep them too and add to final results without even checking for parents etc.\n        let mut flattened_partial_results: Vec<(&'a ImHash, (u32, &'a ImHash))> = partial_results\n            .into_iter()\n            .filter_map(|(parent, similar)| {\n                if similar.is_empty() {\n                    assert!(hashes_with_multiple_images.contains(parent)); // We expect, that only hashes with multiple images can have no similar hashes\n                    assert!(!hashes_parents.contains_key(parent)); // We expect, that this hash is not already in parents list - this would be strange, because it have no similar hashes\n                    None\n                } else {\n                    Some(similar.into_iter().map(move |sim| (parent, sim)))\n                }\n            })\n            .flatten()\n            .collect::<Vec<_>>();\n\n        flattened_partial_results.sort_by_key(|(_parent, (similarity, _compared_hash))| *similarity);\n\n        // Original hash means, that we check this hash and we can easily find this hash a new parent\n        // Compared hash cannot be changed if it is already parent to different hash, because it would be too complex to handle this properly\n        for (original_hash, (similarity, compared_hash)) in flattened_partial_results {\n            // If compared hash already is parent to different hash, skip it\n            // This may be not optimal, because we may miss better parent for such hash, but I have no idea how to properly reparent it\n            // This would be hard, because we would need to track all similar hashes for reparented childrens, to find them better parents\n            if hashes_parents.contains_key(compared_hash) {\n                continue;\n            }\n\n            let compared_hash_parent = if let Some((other_parent_hash, other_similarity)) = hashes_similarity.get(compared_hash) {\n                if *other_similarity > similarity {\n                    Some(other_parent_hash.clone())\n                } else {\n                    // Have parent, but with lower similarity, so skipping this one\n                    continue;\n                }\n            } else {\n                None\n            };\n\n            // If current checked hash, have parent, first we must check if similarity between them is lower than checked item\n            if let Some((current_parent_hash, current_similarity_with_parent)) = hashes_similarity.get(original_hash) {\n                if *current_similarity_with_parent <= similarity {\n                    // Have more similar parent, so skip this one\n                    continue;\n                }\n\n                let children_count = hashes_parents.get_mut(current_parent_hash).expect(\"Cannot find parent hash\");\n                *children_count -= 1;\n                let left_any_children = *children_count != 0;\n\n                // We can remove entirely previous parent from hashes_parents if it will not have any other children\n                // Of course, only if hash applies to single image, because hashes with multiple images must stay in parents list\n                if !left_any_children && !hashes_with_multiple_images.contains(current_parent_hash) {\n                    hashes_parents.swap_remove(current_parent_hash);\n                }\n                hashes_similarity\n                    .swap_remove(original_hash)\n                    .expect(\"This should never fail, because we are iterating over this hash\");\n\n                let parent = hashes_parents.insert((*original_hash).clone(), 1);\n                assert!(parent.is_none(), \"Parent hash should not exist here\");\n            } else {\n                *hashes_parents.entry(original_hash.clone()).or_insert(0) += 1;\n            }\n\n            // This overwrites parent hash if there was any\n            // or just adds new record if there was no parent\n            hashes_similarity.insert(compared_hash.clone(), (original_hash.clone(), similarity));\n\n            if let Some(compared_hash_parent) = compared_hash_parent {\n                *hashes_parents.get_mut(&compared_hash_parent).expect(\"Cannot find parent hash\") -= 1;\n            }\n        }\n    }\n\n    #[fun_time(message = \"find_similar_hashes\", level = \"debug\")]\n    pub(crate) fn find_similar_hashes(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.image_hashes.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let tolerance = self.get_params().max_difference;\n\n        // Results\n        let mut collected_similar_images: IndexMap<ImHash, Vec<ImagesEntry>> = Default::default();\n\n        let all_hashed_images = mem::take(&mut self.image_hashes);\n\n        // Checking entries with tolerance 0 is really easy and fast, because only entries with same hashes needs to be checked\n        if tolerance == 0 {\n            for (hash, vec_file_entry) in all_hashed_images {\n                if vec_file_entry.len() >= 2 {\n                    collected_similar_images.insert(hash, vec_file_entry);\n                }\n            }\n        } else if self.compare_hashes_with_non_zero_tolerance(&all_hashed_images, &mut collected_similar_images, progress_sender, stop_flag, tolerance) == WorkContinueStatus::Stop\n        {\n            return WorkContinueStatus::Stop;\n        }\n\n        Self::verify_duplicated_items(&collected_similar_images);\n\n        // Info about hashes is not needed anymore, so we drop this info\n        self.similar_vectors = collected_similar_images.into_values().collect();\n\n        self.exclude_items_with_same_size();\n\n        self.remove_multiple_records_from_reference_folders();\n\n        if self.common_data.use_reference_folders {\n            for (_fe, vector) in &self.similar_referenced_vectors {\n                self.information.number_of_duplicates += vector.len();\n                self.information.number_of_groups += 1;\n            }\n        } else {\n            for vector in &self.similar_vectors {\n                self.information.number_of_duplicates += vector.len() - 1;\n                self.information.number_of_groups += 1;\n            }\n        }\n\n        // Clean unused data to save ram\n        self.image_hashes = Default::default();\n        self.images_to_check = Default::default();\n        self.bktree = BKTree::new(Hamming);\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"exclude_items_with_same_size\", level = \"debug\")]\n    fn exclude_items_with_same_size(&mut self) {\n        if self.get_params().exclude_images_with_same_size {\n            for vec_file_entry in mem::take(&mut self.similar_vectors) {\n                let mut bt_sizes: BTreeSet<u64> = Default::default();\n                let mut vec_values = Vec::new();\n                for file_entry in vec_file_entry {\n                    if bt_sizes.insert(file_entry.size) {\n                        vec_values.push(file_entry);\n                    }\n                }\n                if vec_values.len() > 1 {\n                    self.similar_vectors.push(vec_values);\n                }\n            }\n        }\n    }\n\n    #[fun_time(message = \"remove_multiple_records_from_reference_folders\", level = \"debug\")]\n    fn remove_multiple_records_from_reference_folders(&mut self) {\n        if self.common_data.use_reference_folders {\n            self.similar_referenced_vectors = mem::take(&mut self.similar_vectors)\n                .into_iter()\n                .filter_map(|vec_file_entry| {\n                    let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry\n                        .into_iter()\n                        .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path()));\n\n                    if normal_files.is_empty() {\n                        None\n                    } else {\n                        files_from_referenced_folders.pop().map(|file| (file, normal_files))\n                    }\n                })\n                .collect::<Vec<(ImagesEntry, Vec<ImagesEntry>)>>();\n        }\n    }\n\n    // TODO this probably not works good when reference folders are used\n    pub(crate) fn verify_duplicated_items(collected_similar_images: &IndexMap<ImHash, Vec<ImagesEntry>>) {\n        if !cfg!(debug_assertions) {\n            return;\n        }\n        // Validating if group contains duplicated results\n        let mut result_hashset: IndexSet<String> = Default::default();\n        let mut found = false;\n\n        for vec_file_entry in collected_similar_images.values() {\n            if vec_file_entry.is_empty() {\n                error!(\"Found empty group\");\n                found = true;\n                continue;\n            }\n            if vec_file_entry.len() == 1 {\n                error!(\"Found simple element {vec_file_entry:?}\");\n                found = true;\n                continue;\n            }\n            for file_entry in vec_file_entry {\n                let st = file_entry.path.to_string_lossy().to_string();\n                if result_hashset.contains(&st) {\n                    found = true;\n                    error!(\"Duplicated Element {st}\");\n                } else {\n                    result_hashset.insert(st);\n                }\n            }\n        }\n        assert!(!found, \"Found Invalid entries, verify errors before\");\n    }\n}\n\nfn is_in_reference_folder(reference_directories: &[PathBuf], path: &Path) -> bool {\n    reference_directories.iter().any(|e| path.starts_with(e))\n}\n\n#[expect(clippy::indexing_slicing)] // Because hash size is validated before\npub fn get_string_from_similarity(similarity: u32, hash_size: u8) -> String {\n    let index_preset = match hash_size {\n        8 => 0,\n        16 => 1,\n        32 => 2,\n        64 => 3,\n        _ => panic!(\"Invalid hash size {hash_size} (caller is responsible for validating this)\"),\n    };\n\n    if similarity == 0 {\n        flc!(\"core_similarity_original\")\n    } else if similarity <= SIMILAR_VALUES[index_preset][0] {\n        flc!(\"core_similarity_very_high\")\n    } else if similarity <= SIMILAR_VALUES[index_preset][1] {\n        flc!(\"core_similarity_high\")\n    } else if similarity <= SIMILAR_VALUES[index_preset][2] {\n        flc!(\"core_similarity_medium\")\n    } else if similarity <= SIMILAR_VALUES[index_preset][3] {\n        flc!(\"core_similarity_small\")\n    } else if similarity <= SIMILAR_VALUES[index_preset][4] {\n        flc!(\"core_similarity_very_small\")\n    } else if similarity <= SIMILAR_VALUES[index_preset][5] {\n        flc!(\"core_similarity_minimal\")\n    } else {\n        panic!(\"Invalid similarity value {similarity} for hash size {hash_size} (index {index_preset}) (caller is responsible for validating this)\");\n    }\n}\n\n#[expect(clippy::indexing_slicing)] // Because hash size is validated before\npub fn return_similarity_from_similarity_preset(similarity_preset: SimilarityPreset, hash_size: u8) -> u32 {\n    let index_preset = match hash_size {\n        8 => 0,\n        16 => 1,\n        32 => 2,\n        64 => 3,\n        _ => panic!(\"Invalid hash size {hash_size} (caller is responsible for validating this)\"),\n    };\n    match similarity_preset {\n        SimilarityPreset::Original => 0,\n        SimilarityPreset::VeryHigh => SIMILAR_VALUES[index_preset][0],\n        SimilarityPreset::High => SIMILAR_VALUES[index_preset][1],\n        SimilarityPreset::Medium => SIMILAR_VALUES[index_preset][2],\n        SimilarityPreset::Small => SIMILAR_VALUES[index_preset][3],\n        SimilarityPreset::VerySmall => SIMILAR_VALUES[index_preset][4],\n        SimilarityPreset::Minimal => SIMILAR_VALUES[index_preset][5],\n        SimilarityPreset::None => panic!(\"Invalid similarity preset None (caller is responsible for validating this)\"),\n    }\n}\n\npub(crate) fn convert_filters_to_string(image_filter: FilterType) -> String {\n    match image_filter {\n        FilterType::Lanczos3 => \"Lanczos3\",\n        FilterType::Nearest => \"Nearest\",\n        FilterType::Triangle => \"Triangle\",\n        FilterType::Gaussian => \"Gaussian\",\n        FilterType::CatmullRom => \"CatmullRom\",\n    }\n    .to_string()\n}\n\npub(crate) fn convert_algorithm_to_string(hash_alg: HashAlg) -> String {\n    match hash_alg {\n        HashAlg::Mean => \"Mean\",\n        HashAlg::Gradient => \"Gradient\",\n        HashAlg::Blockhash => \"Blockhash\",\n        HashAlg::VertGradient => \"VertGradient\",\n        HashAlg::DoubleGradient => \"DoubleGradient\",\n        HashAlg::Median => \"Median\",\n    }\n    .to_string()\n}\n\n#[allow(clippy::allow_attributes)]\n#[allow(unfulfilled_lint_expectations)] // Happens only on release build\n#[expect(dead_code)]\n#[expect(unreachable_code)]\n#[expect(unused_variables)]\n// Function to validate if after first check there are any duplicated entries\n// E.g. /a.jpg is used also as master and similar image which is forbidden, because may\n// cause accidentally delete more pictures that user wanted\nfn debug_check_for_duplicated_things(\n    use_reference_folders: bool,\n    hashes_parents: &IndexMap<ImHash, u32>,\n    hashes_similarity: &IndexMap<ImHash, (ImHash, u32)>,\n    all_hashed_images: &IndexMap<ImHash, Vec<ImagesEntry>>,\n    numm: &str,\n) {\n    if !cfg!(debug_assertions) {\n        return;\n    }\n\n    if use_reference_folders {\n        return;\n    }\n\n    let mut found_broken_thing = false;\n    let mut hashmap_hashes: IndexSet<_> = Default::default();\n    let mut hashmap_names: IndexSet<_> = Default::default();\n    for (hash, number_of_children) in hashes_parents {\n        if *number_of_children > 0 {\n            if hashmap_hashes.contains(hash) {\n                debug!(\"------1--HASH--{}  {:?}\", numm, all_hashed_images[hash]);\n                found_broken_thing = true;\n            }\n            hashmap_hashes.insert((*hash).clone());\n\n            for i in &all_hashed_images[hash] {\n                let name = i.path.to_string_lossy().to_string();\n                if hashmap_names.contains(&name) {\n                    debug!(\"------1--NAME--{numm}  {name:?}\");\n                    found_broken_thing = true;\n                }\n                hashmap_names.insert(name);\n            }\n        }\n    }\n    for hash in hashes_similarity.keys() {\n        if hashmap_hashes.contains(hash) {\n            debug!(\"------2--HASH--{}  {:?}\", numm, all_hashed_images[hash]);\n            found_broken_thing = true;\n        }\n        hashmap_hashes.insert((*hash).clone());\n\n        for i in &all_hashed_images[hash] {\n            let name = i.path.to_string_lossy().to_string();\n            if hashmap_names.contains(&name) {\n                debug!(\"------2--NAME--{numm}  {name:?}\");\n                found_broken_thing = true;\n            }\n            hashmap_names.insert(name);\n        }\n    }\n\n    assert!(!found_broken_thing);\n}\n\npub fn get_similar_images_cache_file(hash_size: u8, hash_alg: HashAlg, image_filter: FilterType) -> String {\n    format!(\n        \"cache_similar_images_{hash_size}_{}_{}_{CACHE_IMAGE_VERSION}.bin\",\n        convert_algorithm_to_string(hash_alg),\n        convert_filters_to_string(image_filter),\n    )\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::PathBuf;\n\n    use bk_tree::BKTree;\n    use image::imageops::FilterType;\n    use image_hasher::HashAlg;\n    use indexmap::IndexMap;\n\n    use super::*;\n    use crate::common::tool_data::CommonData;\n    use crate::tools::similar_images::{Hamming, ImHash, ImagesEntry, SimilarImages, SimilarImagesParameters};\n\n    fn get_default_parameters() -> SimilarImagesParameters {\n        SimilarImagesParameters {\n            hash_alg: HashAlg::Gradient,\n            hash_size: 8,\n            max_difference: 0,\n            image_filter: FilterType::Lanczos3,\n            exclude_images_with_same_size: false,\n        }\n    }\n\n    // Just to debug changes to algorithms\n    // #[test]\n    // fn test_fuzzer() {\n    //     for _ in 0..100 {\n    //         let mut parameters = get_default_parameters();\n    //         parameters.similarity = rand::random::<u32>() % 40;\n    //         let mut similar_images = SimilarImages::new(parameters);\n    //\n    //         for i in 0..(rand::random::<u32>() % 2000) {\n    //             let mut entry = vec![1u8; 8];\n    //             entry[1] = rand::random::<u8>();\n    //             if rand::random::<bool>() {\n    //                 entry[2] = rand::random::<u8>();\n    //             }\n    //             if rand::random::<bool>() {\n    //                 entry[3] = rand::random::<u8>();\n    //             }\n    //             if rand::random::<bool>() {\n    //                 entry[4] = rand::random::<u8>();\n    //             }\n    //             let fe = create_random_file_entry(entry, &format!(\"file_{i}.txt\"));\n    //             add_hashes(&mut similar_images.image_hashes, vec![fe]);\n    //         }\n    //\n    //         similar_images.find_similar_hashes(&Arc::default(), None);\n    //     }\n    // }\n\n    #[test]\n    fn test_compare_no_images() {\n        use crate::common::traits::Search;\n        for _ in 0..100 {\n            let mut similar_images = SimilarImages::new(get_default_parameters());\n            similar_images.search(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images().len(), 0);\n        }\n    }\n\n    #[test]\n    fn test_compare_tolerance_0_normal_mode() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 0;\n            let mut similar_images = SimilarImages::new(parameters);\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"bcd.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], \"cde.txt\");\n            let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], \"rrt.txt\");\n            let fe5 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], \"bld.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1.clone(), fe2.clone(), fe3.clone(), fe4.clone(), fe5.clone()]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images().len(), 2);\n            let first_group = similar_images.get_similar_images()[0].iter().map(|e| &e.path).collect::<Vec<_>>();\n            let second_group = similar_images.get_similar_images()[1].iter().map(|e| &e.path).collect::<Vec<_>>();\n            // Initial order is not guaranteed, so we need to check both options\n            if similar_images.get_similar_images()[0][0].hash == fe1.hash {\n                assert_eq!(first_group, vec![&fe1.path, &fe2.path]);\n                assert_eq!(second_group, vec![&fe3.path, &fe4.path, &fe5.path]);\n            } else {\n                assert_eq!(first_group, vec![&fe3.path, &fe4.path, &fe5.path]);\n                assert_eq!(second_group, vec![&fe1.path, &fe2.path]);\n            }\n        }\n    }\n\n    #[test]\n    fn test_simple_normal_one_group() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 1;\n            let mut similar_images = SimilarImages::new(parameters);\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"bcd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images().len(), 1);\n        }\n    }\n\n    #[test]\n    fn test_2000_hashes() {\n        let mut parameters = get_default_parameters();\n        parameters.max_difference = 10;\n        let mut similar_images = SimilarImages::new(parameters);\n\n        for i in 0..2000 {\n            let mut entry = vec![1u8; 8];\n            entry[7] = (i as u32 % 256) as u8;\n            entry[6] = (i as u32 / 256 % 256) as u8;\n            entry[5] = (i as u32 / 256 / 256 % 256) as u8;\n            let fe = create_random_file_entry(entry, &format!(\"file_{i}.txt\"));\n            add_hashes(&mut similar_images.image_hashes, vec![fe]);\n        }\n\n        similar_images.find_similar_hashes(&Arc::default(), None);\n        assert!(!similar_images.get_similar_images().is_empty());\n    }\n\n    #[test]\n    fn test_simple_normal_one_group_extended() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 2;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(false);\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"bcd.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], \"rrd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images().len(), 1);\n            assert_eq!(similar_images.get_similar_images()[0].len(), 3);\n        }\n    }\n\n    #[test]\n    fn test_simple_normal_one_group_extended2() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 222222;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(false);\n\n            let fe1 = create_random_file_entry(vec![59, 41, 53, 27, 19, 143, 228, 228], \"abc.txt\");\n            let fe2 = create_random_file_entry(vec![57, 41, 60, 155, 51, 173, 204, 228], \"bcd.txt\");\n            let fe3 = create_random_file_entry(vec![28, 222, 206, 192, 203, 157, 25, 24], \"rrd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images().len(), 1);\n            assert_eq!(similar_images.get_similar_images()[0].len(), 3);\n        }\n    }\n\n    #[test]\n    fn test_simple_referenced_same_group() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 0;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/rr/bcd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images().len(), 0);\n        }\n    }\n\n    #[test]\n    fn test_simple_referenced_group_extended() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 0;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/kk/bcd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            assert_eq!(similar_images.get_similar_images_referenced().len(), 1);\n            assert_eq!(similar_images.get_similar_images_referenced()[0].1.len(), 1);\n        }\n    }\n\n    #[test]\n    fn test_simple_referenced_group_extended2() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 0;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/rr/abc2.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/kk/bcd.txt\");\n            let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/kk/bcd2.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images_referenced();\n            assert_eq!(res.len(), 1);\n            assert_eq!(res[0].1.len(), 2);\n            assert!(res[0].1.iter().all(|e| e.path.starts_with(\"/home/kk/\")));\n        }\n    }\n\n    #[test]\n    fn test_simple_normal_too_small_similarity() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 1;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(false);\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b00001], \"abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b00100], \"bcd.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b10000], \"rrd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images();\n            assert!(res.is_empty());\n        }\n    }\n\n    #[test]\n    fn test_simple_normal_union_of_similarity() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 4;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(false);\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0001], \"abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_1111], \"bcd.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0111_1111], \"rrd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images();\n            assert_eq!(res.len(), 1);\n            let mut path = res[0].iter().map(|e| e.path.to_string_lossy().to_string()).collect::<Vec<_>>();\n            path.sort();\n            if res[0].len() == 3 {\n                assert_eq!(path, vec![\"abc.txt\".to_string(), \"bcd.txt\".to_string(), \"rrd.txt\".to_string()]);\n            } else if res[0].len() == 2 {\n                assert!(path == vec![\"abc.txt\".to_string(), \"bcd.txt\".to_string()] || path == vec![\"bcd.txt\".to_string(), \"rrd.txt\".to_string()]);\n            } else {\n                panic!(\"Invalid number of items\");\n            }\n        }\n    }\n\n    #[test]\n    fn test_reference_similarity_only_one() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 1;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0011], \"/home/kk/bcd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images_referenced();\n            assert_eq!(res.len(), 1);\n            assert_eq!(res[0].1.len(), 1);\n            assert_eq!(res[0].0.path, PathBuf::from(\"/home/rr/abc.txt\"));\n            assert_eq!(res[0].1[0].path, PathBuf::from(\"/home/kk/bcd.txt\"));\n        }\n    }\n\n    #[test]\n    fn test_reference_too_small_similarity() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 1;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0010], \"/home/kk/bcd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images_referenced();\n            assert_eq!(res.len(), 0);\n        }\n    }\n\n    #[test]\n    fn test_reference_minimal() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 1;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0011], \"/home/kk/bcd.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0100], \"/home/kk/bcd2.txt\");\n            let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1100], \"/home/rr/krkr.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images_referenced();\n            assert_eq!(res.len(), 2);\n            assert_eq!(res[0].1.len(), 1);\n            assert_eq!(res[1].1.len(), 1);\n            #[allow(clippy::allow_attributes)]\n            #[allow(clippy::cmp_owned)] // TODO Bug in nightly\n            if res[0].1[0].path == PathBuf::from(\"/home/kk/bcd.txt\") {\n                assert_eq!(res[0].0.path, PathBuf::from(\"/home/rr/abc.txt\"));\n                assert_eq!(res[1].0.path, PathBuf::from(\"/home/rr/krkr.txt\"));\n            } else if res[0].1[0].path == PathBuf::from(\"/home/kk/bcd2.txt\") {\n                assert_eq!(res[0].0.path, PathBuf::from(\"/home/rr/krkr.txt\"));\n                assert_eq!(res[1].0.path, PathBuf::from(\"/home/rr/abc.txt\"));\n            }\n        }\n    }\n\n    #[test]\n    fn test_reference_same() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 1;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], \"/home/kk/bcd.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images_referenced();\n            assert_eq!(res.len(), 1);\n            assert_eq!(res[0].1.len(), 1);\n        }\n    }\n\n    #[test]\n    fn test_reference_union() {\n        for _ in 0..100 {\n            let mut parameters = get_default_parameters();\n            parameters.max_difference = 10;\n            let mut similar_images = SimilarImages::new(parameters);\n            similar_images.set_use_reference_folders(true);\n            // Not using special method, because it validates if path exists\n            similar_images.common_data.directories.reference_directories = vec![PathBuf::from(\"/home/rr/\")];\n\n            let fe0 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1000], \"/home/rr/abc2.txt\");\n            let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], \"/home/rr/abc.txt\");\n            let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1110], \"/home/kk/bcd.txt\");\n            let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0100], \"/home/kk/bcd2.txt\");\n            let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1100], \"/home/rr/krkr.txt\");\n\n            add_hashes(&mut similar_images.image_hashes, vec![fe0, fe1, fe2, fe3, fe4]);\n\n            similar_images.find_similar_hashes(&Arc::default(), None);\n            let res = similar_images.get_similar_images_referenced();\n            assert_eq!(res.len(), 1);\n            assert_eq!(res[0].1.len(), 2);\n            assert_eq!(res[0].0.path, PathBuf::from(\"/home/rr/krkr.txt\"));\n        }\n    }\n\n    #[test]\n    fn test_tolerance() {\n        // This test not really tests anything, but shows that current hamming distance works\n        // in bits instead of bytes\n        // I tried to make it work in bytes, but it was terrible, so Hamming should be really Ok\n\n        let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 1];\n        let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 2];\n        let mut bktree = BKTree::new(Hamming);\n        bktree.add(fe1);\n        let (similarity, _hash) = bktree.find(&fe2, 100).next().expect(\"No similar images found\");\n        assert_eq!(similarity, 2);\n\n        let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 1];\n        let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 3];\n        let mut bktree = BKTree::new(Hamming);\n        bktree.add(fe1);\n        let (similarity, _hash) = bktree.find(&fe2, 100).next().expect(\"No similar images found\");\n        assert_eq!(similarity, 1);\n\n        let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0000];\n        let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 0b0000_1000];\n        let mut bktree = BKTree::new(Hamming);\n        bktree.add(fe1);\n        let (similarity, _hash) = bktree.find(&fe2, 100).next().expect(\"No similar images found\");\n        assert_eq!(similarity, 1);\n    }\n\n    fn add_hashes(hashmap: &mut IndexMap<ImHash, Vec<ImagesEntry>>, file_entries: Vec<ImagesEntry>) {\n        for fe in file_entries {\n            hashmap.entry(fe.hash.clone()).or_default().push(fe);\n        }\n    }\n\n    fn create_random_file_entry(hash: Vec<u8>, name: &str) -> ImagesEntry {\n        ImagesEntry {\n            path: PathBuf::from(name.to_string()),\n            size: 0,\n            width: 100,\n            height: 100,\n            modified_date: 0,\n            hash,\n            difference: 0,\n        }\n    }\n}\n\n#[cfg(test)]\nmod connect_results_tests {\n    use image_hasher::{FilterType, HashAlg};\n    use indexmap::{IndexMap, IndexSet};\n\n    use super::*;\n\n    #[test]\n    fn test_connect_results_real_case() {\n        let params = SimilarImagesParameters::new(10, 8, HashAlg::Gradient, FilterType::Lanczos3, false);\n        let _finder = SimilarImages::new(params);\n\n        let hash1: ImHash = vec![59, 41, 53, 27, 19, 143, 228, 228];\n        let hash2: ImHash = vec![57, 41, 60, 155, 51, 173, 204, 228];\n        let hash3: ImHash = vec![28, 222, 206, 192, 203, 157, 25, 24];\n\n        let partial_results = vec![\n            (&hash1, vec![(9, &hash2), (43, &hash3)]),\n            (&hash2, vec![(9, &hash1), (38, &hash3)]),\n            (&hash3, vec![(38, &hash2), (43, &hash1)]),\n        ];\n\n        let mut hashes_parents: IndexMap<ImHash, u32> = IndexMap::new();\n        let mut hashes_similarity: IndexMap<ImHash, (ImHash, u32)> = IndexMap::new();\n        let hashes_with_multiple_images: IndexSet<ImHash> = IndexSet::new();\n\n        assert_eq!(hashes_parents.len(), 0);\n        assert_eq!(hashes_similarity.len(), 0);\n\n        SimilarImages::connect_results_simplified(partial_results, &mut hashes_parents, &mut hashes_similarity, &hashes_with_multiple_images);\n\n        assert_eq!(hashes_parents.len(), 1);\n        assert_eq!(hashes_similarity.len(), 2);\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_images/mod.rs",
    "content": "pub mod core;\npub mod traits;\n\npub use core::return_similarity_from_similarity_preset;\n\n#[cfg(test)]\nmod tests;\n\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse bk_tree::BKTree;\nuse hamming_bitwise_fast::hamming_bitwise_fast;\nuse image_hasher::{FilterType, HashAlg};\nuse indexmap::IndexMap;\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\ntype ImHash = Vec<u8>;\n\n// 40 is a little useless in 8 similarity - but this value is kept to simplify harder Krokiet max value calculations\npub const SIMILAR_VALUES: [[u32; 6]; 4] = [\n    [1, 2, 5, 7, 14, 40],    // 8\n    [2, 5, 15, 30, 40, 40],  // 16\n    [4, 10, 20, 40, 40, 40], // 32\n    [6, 20, 40, 40, 40, 40], // 64\n];\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct ImagesEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub width: u32,\n    pub height: u32,\n    pub modified_date: u64,\n    pub hash: ImHash,\n    pub difference: u32,\n}\n\nimpl ResultEntry for ImagesEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\nimpl FileEntry {\n    fn into_images_entry(self) -> ImagesEntry {\n        ImagesEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n\n            width: 0,\n            height: 0,\n            hash: Vec::new(),\n            difference: 0,\n        }\n    }\n}\n\n#[derive(Clone, Debug, Copy)]\npub enum SimilarityPreset {\n    Original,\n    VeryHigh,\n    High,\n    Medium,\n    Small,\n    VerySmall,\n    Minimal,\n    None,\n}\n\nstruct Hamming;\n\nimpl bk_tree::Metric<ImHash> for Hamming {\n    fn distance(&self, a: &ImHash, b: &ImHash) -> u32 {\n        hamming_bitwise_fast(a, b)\n    }\n\n    fn threshold_distance(&self, a: &ImHash, b: &ImHash, _threshold: u32) -> Option<u32> {\n        Some(self.distance(a, b))\n    }\n}\n\n#[derive(Clone)]\npub struct SimilarImagesParameters {\n    pub max_difference: u32,\n    pub hash_size: u8,\n    pub hash_alg: HashAlg,\n    pub image_filter: FilterType,\n    pub exclude_images_with_same_size: bool,\n}\n\nimpl SimilarImagesParameters {\n    pub fn new(max_difference: u32, hash_size: u8, hash_alg: HashAlg, image_filter: FilterType, exclude_images_with_same_size: bool) -> Self {\n        assert!([8, 16, 32, 64].contains(&hash_size));\n        Self {\n            max_difference,\n            hash_size,\n            hash_alg,\n            image_filter,\n            exclude_images_with_same_size,\n        }\n    }\n}\n\npub struct SimilarImages {\n    common_data: CommonToolData,\n    information: Info,\n    bktree: BKTree<ImHash, Hamming>,\n    similar_vectors: Vec<Vec<ImagesEntry>>,\n    similar_referenced_vectors: Vec<(ImagesEntry, Vec<ImagesEntry>)>,\n    // Hashmap with image hashes and Vector with names of files\n    image_hashes: IndexMap<ImHash, Vec<ImagesEntry>>,\n    images_to_check: BTreeMap<String, ImagesEntry>,\n    params: SimilarImagesParameters,\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub initial_found_files: usize,\n    pub number_of_duplicates: usize,\n    pub number_of_groups: usize,\n    pub scanning_time: Duration,\n}\n\nimpl SimilarImages {\n    pub fn get_params(&self) -> &SimilarImagesParameters {\n        &self.params\n    }\n\n    pub const fn get_similar_images(&self) -> &Vec<Vec<ImagesEntry>> {\n        &self.similar_vectors\n    }\n\n    pub fn get_similar_images_referenced(&self) -> &Vec<(ImagesEntry, Vec<ImagesEntry>)> {\n        &self.similar_referenced_vectors\n    }\n\n    pub fn get_use_reference(&self) -> bool {\n        self.common_data.use_reference_folders\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_images/tests.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse image_hasher::{FilterType, HashAlg};\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::similar_images::{SimilarImages, SimilarImagesParameters};\n\nfn get_test_resources_path() -> PathBuf {\n    let path = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\")).join(\"test_resources\").join(\"images\");\n\n    assert!(path.exists(), \"Test resources not found at \\\"{}\\\"\", path.to_string_lossy());\n\n    path\n}\n\n#[test]\nfn test_similar_images() {\n    let test_path = get_test_resources_path();\n\n    let algo_filter_hash_sim_found = [\n        (HashAlg::Gradient, FilterType::Lanczos3, 8, 222240, 2, 1, 3),\n        (HashAlg::Gradient, FilterType::Lanczos3, 8, 15, 1, 1, 2),\n        (HashAlg::Gradient, FilterType::Lanczos3, 8, 8, 0, 0, 0),\n        (HashAlg::Blockhash, FilterType::Lanczos3, 8, 40, 2, 1, 3),\n        (HashAlg::Blockhash, FilterType::Lanczos3, 8, 15, 1, 1, 2),\n        (HashAlg::Blockhash, FilterType::Lanczos3, 8, 2, 0, 0, 0),\n        (HashAlg::Mean, FilterType::Lanczos3, 8, 40, 2, 1, 3),\n        (HashAlg::Mean, FilterType::Lanczos3, 8, 15, 1, 1, 2),\n        (HashAlg::Mean, FilterType::Lanczos3, 8, 2, 0, 0, 0),\n        (HashAlg::DoubleGradient, FilterType::Lanczos3, 8, 40, 2, 1, 3),\n        (HashAlg::DoubleGradient, FilterType::Lanczos3, 8, 15, 1, 1, 2),\n        (HashAlg::DoubleGradient, FilterType::Lanczos3, 8, 2, 0, 0, 0),\n        (HashAlg::VertGradient, FilterType::Lanczos3, 8, 40, 2, 1, 3),\n        (HashAlg::VertGradient, FilterType::Lanczos3, 8, 15, 1, 1, 2),\n        (HashAlg::VertGradient, FilterType::Lanczos3, 8, 2, 0, 0, 0),\n        (HashAlg::Gradient, FilterType::Gaussian, 16, 15, 0, 0, 0),\n        (HashAlg::Gradient, FilterType::Gaussian, 16, 32, 1, 1, 2),\n        (HashAlg::VertGradient, FilterType::Nearest, 16, 32, 1, 1, 2),\n    ];\n\n    for (idx, (hash_alg, filter_type, hash_size, similarity, duplicates, groups, all_in_similar)) in algo_filter_hash_sim_found.into_iter().enumerate() {\n        let params = SimilarImagesParameters::new(similarity, hash_size, hash_alg, filter_type, false);\n\n        let mut finder = SimilarImages::new(params);\n        finder.set_included_paths(vec![test_path.clone()]);\n        finder.set_recursive_search(true);\n        finder.set_use_cache(false);\n\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        finder.search(&stop_flag, None);\n\n        let info = finder.get_information();\n        let similar_images = finder.get_similar_images();\n\n        let msg = format!(\"Failed for algo/filter/hash/similarity set {idx}: {hash_alg:?}/{filter_type:?}/{hash_size}/{similarity}\");\n\n        assert_eq!(info.initial_found_files, 3, \"{msg}\");\n        assert_eq!(info.number_of_duplicates, duplicates, \"{msg}\");\n        assert_eq!(info.number_of_groups, groups, \"{msg}\");\n        assert_eq!(similar_images.len(), groups, \"{msg}\");\n        assert_eq!(similar_images.iter().map(|e| e.len()).sum::<usize>(), all_in_similar, \"{msg}\");\n    }\n}\n\n#[test]\nfn test_similar_images_exclude_same_size() {\n    let test_path = get_test_resources_path();\n\n    let params = SimilarImagesParameters::new(10, 8, HashAlg::Gradient, FilterType::Lanczos3, true);\n\n    let mut finder = SimilarImages::new(params);\n    finder.set_included_paths(vec![test_path]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let similar_images = finder.get_similar_images();\n    let info = finder.get_information();\n\n    assert!(info.number_of_groups > 0);\n    for group in similar_images {\n        if group.len() > 1 {\n            let first_size = group[0].size;\n            let all_same_size = group.iter().all(|img| img.size == first_size);\n            assert!(!all_same_size);\n        }\n    }\n}\n\n#[test]\nfn test_similar_images_empty_directory() {\n    use tempfile::TempDir;\n\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let params = SimilarImagesParameters::new(10, 8, HashAlg::Gradient, FilterType::Lanczos3, false);\n\n    let mut finder = SimilarImages::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    let similar_images = finder.get_similar_images();\n\n    assert_eq!(info.number_of_duplicates, 0);\n    assert_eq!(info.number_of_groups, 0);\n    assert_eq!(similar_images.len(), 0);\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_images/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\n\nuse crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS};\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::similar_images::core::get_string_from_similarity;\nuse crate::tools::similar_images::{Info, SimilarImages, SimilarImagesParameters};\n\nimpl AllTraits for SimilarImages {}\n\nimpl Search for SimilarImages {\n    #[fun_time(message = \"find_similar_images\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            let extensions = if cfg!(feature = \"heif\") {\n                [IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat()\n            } else {\n                [IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat()\n            };\n\n            if self.prepare_items(Some(&extensions)).is_err() {\n                return;\n            }\n            self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty();\n            if self.check_for_similar_images(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.hash_images(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.find_similar_hashes(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DebugPrint for SimilarImages {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n\n        println!(\"---------------DEBUG PRINT---------------\");\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for SimilarImages {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n        if !self.similar_vectors.is_empty() {\n            write!(writer, \"{} images which have similar friends\\n\\n\", self.similar_vectors.len())?;\n\n            for struct_similar in &self.similar_vectors {\n                writeln!(writer, \"Found {} images which have similar friends\", struct_similar.len())?;\n                for file_entry in struct_similar {\n                    writeln!(\n                        writer,\n                        \"\\\"{}\\\" - {}x{} - {} - {}\",\n                        file_entry.path.to_string_lossy(),\n                        file_entry.width,\n                        file_entry.height,\n                        format_size(file_entry.size, BINARY),\n                        get_string_from_similarity(file_entry.difference, self.get_params().hash_size)\n                    )?;\n                }\n                writeln!(writer)?;\n            }\n        } else if !self.similar_referenced_vectors.is_empty() {\n            writeln!(writer, \"{} images which have similar friends\\n\\n\", self.similar_referenced_vectors.len())?;\n\n            for (file_entry, vec_file_entry) in &self.similar_referenced_vectors {\n                writeln!(writer, \"Found {} images which have similar friends\", vec_file_entry.len())?;\n                writeln!(writer)?;\n                writeln!(\n                    writer,\n                    \"\\\"{}\\\" - {}x{} - {} - {}\",\n                    file_entry.path.to_string_lossy(),\n                    file_entry.width,\n                    file_entry.height,\n                    format_size(file_entry.size, BINARY),\n                    get_string_from_similarity(file_entry.difference, self.get_params().hash_size)\n                )?;\n                for file_entry in vec_file_entry {\n                    writeln!(\n                        writer,\n                        \"\\\"{}\\\" - {}x{} - {} - {}\",\n                        file_entry.path.to_string_lossy(),\n                        file_entry.width,\n                        file_entry.height,\n                        format_size(file_entry.size, BINARY),\n                        get_string_from_similarity(file_entry.difference, self.get_params().hash_size)\n                    )?;\n                }\n                writeln!(writer)?;\n            }\n        } else {\n            write!(writer, \"Not found any similar images.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        if self.get_use_reference() {\n            self.save_results_to_file_as_json_internal(file_name, &self.similar_referenced_vectors, pretty_print)\n        } else {\n            self.save_results_to_file_as_json_internal(file_name, &self.similar_vectors, pretty_print)\n        }\n    }\n}\nimpl CommonData for SimilarImages {\n    type Info = Info;\n    type Parameters = SimilarImagesParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_duplicates > 0\n    }\n}\n\nimpl DeletingItems for SimilarImages {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.get_cd().delete_method == DeleteMethod::None {\n            return WorkContinueStatus::Continue;\n        }\n        let files_to_delete = self.similar_vectors.clone();\n        self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete)\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_videos/core.rs",
    "content": "use std::collections::{BTreeMap, BTreeSet};\nuse std::mem;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse indexmap::IndexMap;\nuse log::debug;\nuse rayon::prelude::*;\nuse vid_dup_finder_lib::{CreationOptions, Cropdetect, VideoHash, VideoHashBuilder};\n\nuse crate::common::cache::{CACHE_VIDEO_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path};\nuse crate::common::config_cache_path::get_config_cache_path;\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult, inode, take_1_per_inode};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::common::traits::ResultEntry;\nuse crate::common::video_utils::{VIDEO_THUMBNAILS_SUBFOLDER, VideoMetadata, generate_thumbnail};\nuse crate::tools::similar_videos::{SimilarVideos, SimilarVideosParameters, VideosEntry};\n\nimpl SimilarVideos {\n    pub fn new(params: SimilarVideosParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::SimilarVideos),\n            information: Default::default(),\n            similar_vectors: Vec::new(),\n            videos_hashes: Default::default(),\n            videos_to_check: Default::default(),\n            similar_referenced_vectors: Vec::new(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"check_for_similar_videos\", level = \"debug\")]\n    pub(crate) fn check_for_similar_videos(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(inode)\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                self.videos_to_check = grouped_file_entries\n                    .into_par_iter()\n                    .flat_map(if self.get_hide_hard_links() { |(_, fes)| fes } else { take_1_per_inode })\n                    .map(|fe| (fe.path.to_string_lossy().to_string(), fe.into_videos_entry()))\n                    .collect();\n                self.common_data.text_messages.warnings.extend(warnings);\n                debug!(\"check_files - Found {} video files.\", self.videos_to_check.len());\n                WorkContinueStatus::Continue\n            }\n\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    fn check_video_file_entry(&self, mut file_entry: VideosEntry) -> VideosEntry {\n        let creation_options = CreationOptions {\n            skip_forward_amount: self.params.skip_forward_amount as f64,\n            duration: self.params.duration as f64,\n            cropdetect: self.params.crop_detect,\n        };\n        let vhash = match VideoHashBuilder::from_options(creation_options).hash(file_entry.path.clone()) {\n            Ok(t) => t,\n            Err(e) => {\n                let path = file_entry.path.to_string_lossy();\n                file_entry.error = format!(\"Failed to hash file \\\"{path}\\\": reason {e}\");\n                return file_entry;\n            }\n        };\n\n        file_entry.vhash = vhash;\n\n        file_entry\n    }\n\n    fn read_video_properties(mut file_entry: VideosEntry) -> VideosEntry {\n        match VideoMetadata::from_path(&file_entry.path) {\n            Ok(metadata) => {\n                file_entry.fps = metadata.fps;\n                file_entry.codec = metadata.codec;\n                file_entry.bitrate = metadata.bitrate;\n                file_entry.width = metadata.width;\n                file_entry.height = metadata.height;\n                file_entry.duration = metadata.duration;\n            }\n            Err(e) => {\n                let path = file_entry.path.to_string_lossy();\n                file_entry.error = format!(\"Failed to read properties for file \\\"{path}\\\": reason {e}\");\n            }\n        }\n\n        file_entry\n    }\n\n    #[fun_time(message = \"sort_videos\", level = \"debug\")]\n    pub(crate) fn sort_videos(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.videos_to_check.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache_at_start();\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::SimilarVideosCalculatingHashes,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            0, // non_cached_files_to_check.values().map(|e| e.size).sum(), // Looks, that at least for now, there is no big difference between checking big and small files, so at least for now, only tracking number of files is enough\n        );\n\n        let non_cached_files_to_check: Vec<_> = non_cached_files_to_check.into_iter().map(|f| f.1).collect();\n        let mut vec_file_entry: Vec<VideosEntry> = non_cached_files_to_check\n            .into_par_iter()\n            .with_max_len(2)\n            .map(|file_entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                // Currently size is not too much relevant\n                // let size = file_entry.size;\n                let res = self.check_video_file_entry(file_entry);\n                let res = Self::read_video_properties(res);\n\n                progress_handler.increase_items(1);\n                // progress_handler.increase_size(size);\n\n                Some(res)\n            })\n            .while_some()\n            .collect::<Vec<VideosEntry>>();\n\n        progress_handler.join_thread();\n\n        // Just connect loaded results with already calculated hashes\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        self.save_cache(&vec_file_entry, loaded_hash_map);\n\n        let mut hashmap_with_file_entries: IndexMap<String, VideosEntry> = Default::default();\n        let mut vector_of_hashes: Vec<VideoHash> = Vec::new();\n        for file_entry in vec_file_entry {\n            if file_entry.error.is_empty() {\n                vector_of_hashes.push(file_entry.vhash.clone());\n                hashmap_with_file_entries.insert(file_entry.vhash.src_path().to_string_lossy().to_string(), file_entry);\n            } else {\n                self.common_data.text_messages.warnings.push(file_entry.error);\n            }\n        }\n\n        // Break if stop was clicked after saving to cache\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        self.match_groups_of_videos(vector_of_hashes, &hashmap_with_file_entries);\n\n        if self.create_thumbnails(progress_sender, stop_flag) == WorkContinueStatus::Stop {\n            return WorkContinueStatus::Stop;\n        }\n\n        self.remove_from_reference_folders();\n\n        if self.common_data.use_reference_folders {\n            for (_fe, vector) in &self.similar_referenced_vectors {\n                self.information.number_of_duplicates += vector.len();\n                self.information.number_of_groups += 1;\n            }\n        } else {\n            for vector in &self.similar_vectors {\n                self.information.number_of_duplicates += vector.len() - 1;\n                self.information.number_of_groups += 1;\n            }\n        }\n\n        // Clean unused data\n        self.videos_hashes = Default::default();\n        self.videos_to_check = Default::default();\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"create_thumbnails\", level = \"debug\")]\n    fn create_thumbnails(&mut self, progress_sender: Option<&Sender<ProgressData>>, stop_flag: &Arc<AtomicBool>) -> WorkContinueStatus {\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::SimilarVideosCreatingThumbnails,\n            self.similar_vectors.iter().map(|e| e.len()).sum::<usize>(),\n            self.get_test_type(),\n            0,\n        );\n\n        let Some(config_cache_path) = get_config_cache_path() else {\n            return WorkContinueStatus::Continue;\n        };\n\n        let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER);\n        if let Err(e) = std::fs::create_dir_all(&thumbnails_dir) {\n            debug!(\"Failed to create thumbnails directory: {e}\");\n            return WorkContinueStatus::Continue;\n        }\n        let thumbnail_video_percentage_from_start = self.params.thumbnail_video_percentage_from_start;\n        let generate_grid_instead_of_single = self.params.generate_thumbnail_grid_instead_of_single;\n        let thumbnail_grid_tiles_per_side = self.params.thumbnail_grid_tiles_per_side;\n        let errors = self\n            .similar_vectors\n            .par_iter_mut()\n            .with_max_len(2)\n            .map(|vec_file_entry| {\n                let mut errs = Vec::new();\n                for file_entry in vec_file_entry {\n                    if check_if_stop_received(stop_flag) {\n                        return errs;\n                    }\n\n                    match generate_thumbnail(\n                        stop_flag,\n                        &file_entry.path,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        file_entry.duration,\n                        &thumbnails_dir,\n                        thumbnail_video_percentage_from_start,\n                        generate_grid_instead_of_single,\n                        thumbnail_grid_tiles_per_side,\n                        self.params.generate_thumbnails,\n                    ) {\n                        Ok(Some(thumbnail_path)) => {\n                            file_entry.thumbnail_path = Some(thumbnail_path);\n                        }\n                        Ok(None) => {}\n                        Err(e) => errs.push(e),\n                    }\n\n                    progress_handler.increase_items(1);\n                }\n\n                errs\n            })\n            .flatten()\n            .collect::<Vec<String>>();\n\n        self.common_data.text_messages.warnings.extend(errors);\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"save_cache\", level = \"debug\")]\n    fn save_cache(&mut self, vec_file_entry: &[VideosEntry], loaded_hash_map: BTreeMap<String, VideosEntry>) {\n        save_and_connect_cache_generalized_by_path(\n            &get_similar_videos_cache_file(self.params.skip_forward_amount, self.params.duration, self.params.crop_detect),\n            vec_file_entry,\n            loaded_hash_map,\n            self,\n        );\n    }\n\n    #[fun_time(message = \"load_cache_at_start\", level = \"debug\")]\n    fn load_cache_at_start(&mut self) -> (BTreeMap<String, VideosEntry>, BTreeMap<String, VideosEntry>, BTreeMap<String, VideosEntry>) {\n        load_and_split_cache_generalized_by_path(\n            &get_similar_videos_cache_file(self.params.skip_forward_amount, self.params.duration, self.params.crop_detect),\n            mem::take(&mut self.videos_to_check),\n            self,\n        )\n    }\n\n    #[fun_time(message = \"match_groups_of_videos\", level = \"debug\")]\n    fn match_groups_of_videos(&mut self, vector_of_hashes: Vec<VideoHash>, hashmap_with_file_entries: &IndexMap<String, VideosEntry>) {\n        // Tolerance in library is a value between 0 and 1\n        // Tolerance in this app is a value between 0 and 20\n        // Default tolerance in library is 0.30\n        // We need to allow to set value in range 0 - 0.5\n        let match_group = vid_dup_finder_lib::search(vector_of_hashes, self.get_params().tolerance as f64 / 40.0f64);\n\n        let mut collected_similar_videos: Vec<Vec<VideosEntry>> = Default::default();\n        for i in match_group {\n            let mut temp_vector: Vec<VideosEntry> = Vec::new();\n            let mut bt_size: BTreeSet<u64> = Default::default();\n            for j in i.duplicates() {\n                let file_entry = &hashmap_with_file_entries[&j.to_string_lossy().to_string()];\n                if self.get_params().exclude_videos_with_same_size {\n                    if bt_size.insert(file_entry.size) {\n                        temp_vector.push(file_entry.clone());\n                    }\n                } else {\n                    temp_vector.push(file_entry.clone());\n                }\n            }\n            if temp_vector.len() > 1 {\n                collected_similar_videos.push(temp_vector);\n            }\n        }\n\n        self.similar_vectors = collected_similar_videos;\n    }\n\n    #[fun_time(message = \"remove_from_reference_folders\", level = \"debug\")]\n    fn remove_from_reference_folders(&mut self) {\n        if self.common_data.use_reference_folders {\n            self.similar_referenced_vectors = mem::take(&mut self.similar_vectors)\n                .into_iter()\n                .filter_map(|vec_file_entry| {\n                    let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry\n                        .into_iter()\n                        .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path()));\n\n                    if normal_files.is_empty() {\n                        None\n                    } else {\n                        files_from_referenced_folders.pop().map(|file| (file, normal_files))\n                    }\n                })\n                .collect::<Vec<(VideosEntry, Vec<VideosEntry>)>>();\n        }\n    }\n}\npub fn get_similar_videos_cache_file(skip_forward_amount: u32, duration: u32, crop_detect: Cropdetect) -> String {\n    let crop_detect_str = match crop_detect {\n        Cropdetect::None => \"none\",\n        Cropdetect::Letterbox => \"letterbox\",\n        Cropdetect::Motion => \"motion\",\n    };\n    format!(\"cache_similar_videos_{CACHE_VIDEO_VERSION}__skip_{skip_forward_amount}__dur_{duration}__cd_{crop_detect_str}.bin\")\n}\npub fn format_bitrate_opt(bitrate: Option<u64>) -> String {\n    match bitrate {\n        Some(b) => {\n            if b >= 1_000_000 {\n                format!(\"{:.1} Mbps\", b as f64 / 1_000_000.0)\n            } else if b >= 1000 {\n                format!(\"{:.0} kbps\", b as f64 / 1000.0)\n            } else {\n                format!(\"{b} bps\")\n            }\n        }\n        None => String::from(\"\"),\n    }\n}\n\npub fn format_duration_opt(duration: Option<f64>) -> String {\n    duration\n        .map(|d| {\n            let hours = (d / 3600.0) as u32;\n            let minutes = ((d % 3600.0) / 60.0) as u32;\n            let seconds = (d % 60.0) as u32;\n            if hours > 0 {\n                format!(\"{hours:02}:{minutes:02}:{seconds:02}\")\n            } else {\n                format!(\"{minutes:02}:{seconds:02}\")\n            }\n        })\n        .unwrap_or_default()\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_videos/mod.rs",
    "content": "pub mod core;\npub mod traits;\n\n#[cfg(test)]\nmod tests;\n\nuse std::collections::BTreeMap;\nuse std::ops::RangeInclusive;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\nuse vid_dup_finder_lib::{Cropdetect, VideoHash};\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\npub const MAX_TOLERANCE: i32 = 20;\n\npub const DEFAULT_CROP_DETECT: Cropdetect = Cropdetect::Letterbox;\n\npub const ALLOWED_SKIP_FORWARD_AMOUNT: RangeInclusive<u32> = 0..=300;\npub const DEFAULT_SKIP_FORWARD_AMOUNT: u32 = 15;\n\npub const ALLOWED_VID_HASH_DURATION: RangeInclusive<u32> = 2..=60;\npub const DEFAULT_VID_HASH_DURATION: u32 = 10;\n\npub const DEFAULT_VIDEO_PERCENTAGE_FOR_THUMBNAIL: u8 = 10;\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct VideosEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub modified_date: u64,\n    pub vhash: VideoHash,\n    pub error: String,\n\n    // Properties extracted from video\n    pub fps: Option<f64>,\n    pub codec: Option<String>,\n    pub bitrate: Option<u64>,\n    pub width: Option<u32>,\n    pub height: Option<u32>,\n    pub duration: Option<f64>,\n\n    #[serde(skip)] // Saving it to cache is bad idea, because cache can be moved to another locations\n    pub thumbnail_path: Option<PathBuf>,\n}\n\nimpl ResultEntry for VideosEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl FileEntry {\n    fn into_videos_entry(self) -> VideosEntry {\n        VideosEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n\n            vhash: Default::default(),\n            error: String::new(),\n            fps: None,\n            codec: None,\n            bitrate: None,\n            width: None,\n            height: None,\n            duration: None,\n            thumbnail_path: None,\n        }\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct SimilarVideosParameters {\n    pub tolerance: i32,\n    pub exclude_videos_with_same_size: bool,\n    pub skip_forward_amount: u32,\n    pub duration: u32,\n    pub crop_detect: Cropdetect,\n    pub generate_thumbnails: bool,\n    pub thumbnail_video_percentage_from_start: u8,\n    pub generate_thumbnail_grid_instead_of_single: bool,\n    pub thumbnail_grid_tiles_per_side: u8,\n}\n\npub fn crop_detect_from_str_opt(s: &str) -> Option<Cropdetect> {\n    match s.to_lowercase().as_str() {\n        \"none\" => Some(Cropdetect::None),\n        \"letterbox\" => Some(Cropdetect::Letterbox),\n        \"motion\" => Some(Cropdetect::Motion),\n        _ => None,\n    }\n}\n\nimpl SimilarVideosParameters {\n    pub fn new(\n        tolerance: i32,\n        exclude_videos_with_same_size: bool,\n        skip_forward_amount: u32,\n        duration: u32,\n        crop_detect: Cropdetect,\n        generate_thumbnails: bool,\n        thumbnail_video_percentage_from_start: u8,\n        generate_thumbnail_grid_instead_of_single: bool,\n        thumbnail_grid_tiles_per_side: u8,\n    ) -> Self {\n        assert!((0..=MAX_TOLERANCE).contains(&tolerance));\n        assert!(ALLOWED_SKIP_FORWARD_AMOUNT.contains(&skip_forward_amount));\n        assert!(ALLOWED_VID_HASH_DURATION.contains(&duration));\n        Self {\n            tolerance,\n            exclude_videos_with_same_size,\n            skip_forward_amount,\n            duration,\n            crop_detect,\n            generate_thumbnails,\n            thumbnail_video_percentage_from_start,\n            generate_thumbnail_grid_instead_of_single,\n            thumbnail_grid_tiles_per_side,\n        }\n    }\n}\n\npub struct SimilarVideos {\n    common_data: CommonToolData,\n    information: Info,\n    similar_vectors: Vec<Vec<VideosEntry>>,\n    similar_referenced_vectors: Vec<(VideosEntry, Vec<VideosEntry>)>,\n    videos_hashes: BTreeMap<Vec<u8>, Vec<VideosEntry>>,\n    videos_to_check: BTreeMap<String, VideosEntry>,\n    params: SimilarVideosParameters,\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_duplicates: usize,\n    pub number_of_groups: usize,\n    pub scanning_time: Duration,\n}\n\nimpl SimilarVideos {\n    pub fn get_params(&self) -> &SimilarVideosParameters {\n        &self.params\n    }\n\n    pub const fn get_similar_videos(&self) -> &Vec<Vec<VideosEntry>> {\n        &self.similar_vectors\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n\n    pub fn get_similar_videos_referenced(&self) -> &Vec<(VideosEntry, Vec<VideosEntry>)> {\n        &self.similar_referenced_vectors\n    }\n\n    pub fn get_number_of_base_duplicated_files(&self) -> usize {\n        if self.common_data.use_reference_folders {\n            self.similar_referenced_vectors.len()\n        } else {\n            self.similar_vectors.len()\n        }\n    }\n\n    pub fn get_use_reference(&self) -> bool {\n        self.common_data.use_reference_folders\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_videos/tests.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse tempfile::TempDir;\nuse vid_dup_finder_lib::Cropdetect;\n\nuse crate::common::tool_data::CommonData;\nuse crate::common::traits::Search;\nuse crate::tools::similar_videos::{SimilarVideos, SimilarVideosParameters};\n\n// Tests are quite limited here, due to the needing of external ffmpeg libraries and video files.\n// Just tested is that searching in an empty directory works as expected - no found similar videos\n\n#[test]\nfn test_similar_videos_empty_directory() {\n    let temp_dir = TempDir::new().unwrap();\n    let path = temp_dir.path();\n\n    let params = SimilarVideosParameters::new(10, false, 15, 10, Cropdetect::Letterbox, false, 0, false, 2);\n\n    let mut finder = SimilarVideos::new(params);\n    finder.set_included_paths(vec![path.to_path_buf()]);\n    finder.set_recursive_search(true);\n    finder.set_use_cache(false);\n\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    finder.search(&stop_flag, None);\n\n    let info = finder.get_information();\n    assert_eq!(info.number_of_duplicates, 0, \"Should find no duplicates in empty directory\");\n    assert_eq!(info.number_of_groups, 0, \"Should find no groups in empty directory\");\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/similar_videos/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\n\nuse crate::common::consts::VIDEO_FILES_EXTENSIONS;\nuse crate::common::ffmpeg_utils::check_if_ffprobe_ffmpeg_exists;\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::flc;\nuse crate::tools::similar_videos::core::{format_bitrate_opt, format_duration_opt};\nuse crate::tools::similar_videos::{Info, SimilarVideos, SimilarVideosParameters};\n\nimpl AllTraits for SimilarVideos {}\n\nimpl Search for SimilarVideos {\n    #[fun_time(message = \"find_similar_videos\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if !check_if_ffprobe_ffmpeg_exists() {\n                self.common_data.text_messages.critical = Some(flc!(\"core_ffmpeg_not_found\"));\n                #[cfg(target_os = \"windows\")]\n                self.common_data.text_messages.errors.push(flc!(\"core_ffmpeg_not_found_windows\"));\n                return;\n            }\n\n            if self.prepare_items(Some(VIDEO_FILES_EXTENSIONS)).is_err() {\n                return;\n            }\n            self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty();\n            if self.check_for_similar_videos(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.sort_videos(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DeletingItems for SimilarVideos {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.get_cd().delete_method == DeleteMethod::None {\n            return WorkContinueStatus::Continue;\n        }\n        let files_to_delete = self.similar_vectors.clone();\n        self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete)\n    }\n}\n\nimpl DebugPrint for SimilarVideos {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n\n        println!(\"---------------DEBUG PRINT---------------\");\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for SimilarVideos {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        fn write_video_entry<T: Write>(writer: &mut T, file_entry: &crate::tools::similar_videos::VideosEntry) -> std::io::Result<()> {\n            let bitrate = format_bitrate_opt(file_entry.bitrate);\n            let fps = file_entry.fps.map(|e| format!(\"{e:.2}\")).unwrap_or_default();\n            let codec = file_entry.codec.clone().unwrap_or_default();\n            let dimensions = if let (Some(w), Some(h)) = (file_entry.width, file_entry.height) {\n                format!(\"{w}x{h}\")\n            } else {\n                \"\".to_string()\n            };\n            let duration = format_duration_opt(file_entry.duration);\n\n            writeln!(\n                writer,\n                \"\\\"{}\\\" - {} - {} - {} - {} - {} - {}\",\n                file_entry.path.to_string_lossy(),\n                format_size(file_entry.size, BINARY),\n                bitrate,\n                fps,\n                codec,\n                dimensions,\n                duration\n            )\n        }\n\n        if !self.similar_vectors.is_empty() {\n            write!(writer, \"{} videos which have similar friends\\n\\n\", self.similar_vectors.len())?;\n\n            for struct_similar in &self.similar_vectors {\n                writeln!(\n                    writer,\n                    \"Found {} videos which have similar friends (path, size, bitrate, fps, codec, dimensions, duration)\",\n                    struct_similar.len()\n                )?;\n                for file_entry in struct_similar {\n                    write_video_entry(writer, file_entry)?;\n                }\n                writeln!(writer)?;\n            }\n        } else if !self.similar_referenced_vectors.is_empty() {\n            write!(\n                writer,\n                \"{} videos which have similar friends (path, size, bitrate, fps, codec, dimensions, duration)\\n\\n\",\n                self.similar_referenced_vectors.len()\n            )?;\n\n            for (fe, struct_similar) in &self.similar_referenced_vectors {\n                writeln!(writer, \"Found {} videos which have similar friends\", struct_similar.len())?;\n                writeln!(writer)?;\n                write_video_entry(writer, fe)?;\n                for file_entry in struct_similar {\n                    write_video_entry(writer, file_entry)?;\n                }\n                writeln!(writer)?;\n            }\n        } else {\n            write!(writer, \"Not found any similar videos.\")?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        if self.get_use_reference() {\n            self.save_results_to_file_as_json_internal(file_name, &self.similar_referenced_vectors, pretty_print)\n        } else {\n            self.save_results_to_file_as_json_internal(file_name, &self.similar_vectors, pretty_print)\n        }\n    }\n}\n\nimpl CommonData for SimilarVideos {\n    type Info = Info;\n    type Parameters = SimilarVideosParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_duplicates > 0\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/temporary/core.rs",
    "content": "use std::fs::DirEntry;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse rayon::prelude::*;\n\nuse crate::common::dir_traversal::{common_read_dir, get_modified_time};\nuse crate::common::directories::Directories;\nuse crate::common::items::ExcludedItems;\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::tools::temporary::{Info, TEMP_EXTENSIONS, Temporary, TemporaryFileEntry};\n\nimpl Temporary {\n    pub fn new() -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::TemporaryFiles),\n            information: Info::default(),\n            temporary_files: Vec::new(),\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let mut folders_to_check: Vec<PathBuf> = self.common_data.directories.included_directories.clone();\n\n        let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::CollectingFiles, 0, self.get_test_type(), 0);\n\n        while !folders_to_check.is_empty() {\n            if check_if_stop_received(stop_flag) {\n                progress_handler.join_thread();\n                return WorkContinueStatus::Stop;\n            }\n\n            let segments: Vec<_> = folders_to_check\n                .into_par_iter()\n                .map(|current_folder| {\n                    let mut dir_result = Vec::new();\n                    let mut warnings = Vec::new();\n                    let mut fe_result = Vec::new();\n\n                    let Some(read_dir) = common_read_dir(&current_folder, &mut warnings) else {\n                        return (dir_result, warnings, fe_result);\n                    };\n\n                    // Check every sub folder/file/link etc.\n                    for entry in read_dir {\n                        let Ok(entry_data) = entry else {\n                            continue;\n                        };\n                        let Ok(file_type) = entry_data.file_type() else {\n                            continue;\n                        };\n\n                        if file_type.is_dir() {\n                            check_folder_children(\n                                &mut dir_result,\n                                &mut warnings,\n                                &entry_data,\n                                self.common_data.recursive_search,\n                                &self.common_data.directories,\n                                &self.common_data.excluded_items,\n                            );\n                        } else if file_type.is_file()\n                            && let Some(file_entry) = self.get_file_entry(progress_handler.items_counter(), &entry_data, &mut warnings)\n                        {\n                            fe_result.push(file_entry);\n                        }\n                    }\n                    (dir_result, warnings, fe_result)\n                })\n                .collect();\n\n            let required_size = segments.iter().map(|(segment, _, _)| segment.len()).sum::<usize>();\n            folders_to_check = Vec::with_capacity(required_size);\n\n            // Process collected data\n            for (segment, warnings, fe_result) in segments {\n                folders_to_check.extend(segment);\n                self.common_data.text_messages.warnings.extend(warnings);\n                for fe in fe_result {\n                    self.temporary_files.push(fe);\n                }\n            }\n        }\n\n        progress_handler.join_thread();\n        self.information.number_of_temporary_files = self.temporary_files.len();\n\n        WorkContinueStatus::Continue\n    }\n\n    pub(crate) fn get_file_entry(&self, items_counter: &Arc<AtomicUsize>, entry_data: &DirEntry, warnings: &mut Vec<String>) -> Option<TemporaryFileEntry> {\n        items_counter.fetch_add(1, Ordering::Relaxed);\n\n        let current_file_name = entry_data.path();\n        if self.common_data.excluded_items.is_excluded(&current_file_name) {\n            return None;\n        }\n\n        let file_name = entry_data.file_name();\n        let file_name_ascii_lowercase = file_name.to_ascii_lowercase();\n        let file_name_lowercase = file_name_ascii_lowercase.to_string_lossy();\n        if !TEMP_EXTENSIONS.iter().any(|f| file_name_lowercase.ends_with(f)) {\n            return None;\n        }\n\n        let Ok(metadata) = entry_data.metadata() else {\n            return None;\n        };\n\n        // Creating new file entry\n        Some(TemporaryFileEntry {\n            modified_date: get_modified_time(&metadata, warnings, &current_file_name, false),\n            size: metadata.len(),\n            path: current_file_name,\n        })\n    }\n}\n\npub(crate) fn check_folder_children(\n    dir_result: &mut Vec<PathBuf>,\n    warnings: &mut Vec<String>,\n    entry_data: &DirEntry,\n    recursive_search: bool,\n    directories: &Directories,\n    excluded_items: &ExcludedItems,\n) {\n    if !recursive_search {\n        return;\n    }\n\n    let next_item = entry_data.path();\n    if directories.is_excluded_dir(&next_item) {\n        return;\n    }\n\n    if excluded_items.is_excluded(&next_item) {\n        return;\n    }\n\n    #[cfg(target_family = \"unix\")]\n    if directories.exclude_other_filesystems() {\n        match directories.is_on_other_filesystems(&next_item) {\n            Ok(true) => return,\n            Err(e) => warnings.push(e),\n            _ => (),\n        }\n    }\n\n    #[cfg(target_family = \"windows\")]\n    let _ = warnings; // Silence unused variable warning on Windows\n\n    dir_result.push(next_item);\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/temporary/mod.rs",
    "content": "pub mod core;\npub mod traits;\n\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::Serialize;\n\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\n\nconst TEMP_EXTENSIONS: &[&str] = &[\n    \"#\",\n    \"thumbs.db\",\n    \".bak\",\n    \"~\",\n    \".tmp\",\n    \".temp\",\n    \".ds_store\",\n    \".crdownload\",\n    \".part\",\n    \".cache\",\n    \".dmp\",\n    \".download\",\n    \".partial\",\n];\n\n#[derive(Clone, Serialize, Debug)]\npub struct TemporaryFileEntry {\n    pub path: PathBuf,\n    pub modified_date: u64,\n    pub size: u64,\n}\n\nimpl ResultEntry for TemporaryFileEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\n#[derive(Default, Clone, Copy)]\npub struct Info {\n    pub number_of_temporary_files: usize,\n    pub scanning_time: Duration,\n}\n\npub struct Temporary {\n    common_data: CommonToolData,\n    information: Info,\n    temporary_files: Vec<TemporaryFileEntry>,\n}\n\nimpl Default for Temporary {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl Temporary {\n    pub const fn get_temporary_files(&self) -> &Vec<TemporaryFileEntry> {\n        &self.temporary_files\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/temporary/traits.rs",
    "content": "use std::io::prelude::*;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\n\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search};\nuse crate::tools::temporary::{Info, Temporary};\n\nimpl AllTraits for Temporary {}\n\nimpl Search for Temporary {\n    #[fun_time(message = \"find_temporary_files\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if self.prepare_items(None).is_err() {\n                return;\n            }\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl DeletingItems for Temporary {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.get_cd().delete_method == DeleteMethod::None {\n            return WorkContinueStatus::Continue;\n        }\n        let files_to_delete = self.temporary_files.clone();\n        self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(files_to_delete))\n    }\n}\n\nimpl PrintResults for Temporary {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n        writeln!(writer, \"Found {} temporary files.\\n\", self.information.number_of_temporary_files)?;\n\n        for file_entry in &self.temporary_files {\n            writeln!(writer, \"\\\"{}\\\"\", file_entry.path.to_string_lossy())?;\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        self.save_results_to_file_as_json_internal(file_name, &self.temporary_files, pretty_print)\n    }\n}\n\nimpl CommonData for Temporary {\n    type Info = Info;\n    type Parameters = ();\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n    fn get_params(&self) -> Self::Parameters {}\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n    fn found_any_items(&self) -> bool {\n        self.information.number_of_temporary_files > 0\n    }\n}\n\nimpl DebugPrint for Temporary {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n        println!(\"### Information's\");\n        println!(\"Temporary list size - {}\", self.temporary_files.len());\n        self.debug_print_common();\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/video_optimizer/core/video_converter.rs",
    "content": "use std::fs;\nuse std::path::Path;\nuse std::process::Command;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse log::error;\n\nuse crate::common::process_utils::run_command_interruptible;\nuse crate::common::video_utils::VideoMetadata;\nuse crate::flc;\nuse crate::tools::video_optimizer::{VideoTranscodeEntry, VideoTranscodeFixParams};\n\npub fn check_video(mut entry: VideoTranscodeEntry) -> VideoTranscodeEntry {\n    let metadata = match VideoMetadata::from_path(&entry.path) {\n        Ok(metadata) => metadata,\n        Err(e) => {\n            entry.error = Some(flc!(\"core_failed_to_get_video_metadata\", file = entry.path.to_string_lossy(), reason = e));\n            return entry;\n        }\n    };\n\n    let Some(current_codec) = metadata.codec.clone() else {\n        entry.error = Some(flc!(\"core_failed_to_get_video_codec\", file = entry.path.to_string_lossy()));\n        return entry;\n    };\n\n    let Some(duration) = metadata.duration else {\n        entry.error = Some(flc!(\"core_failed_to_get_video_duration\", file = entry.path.to_string_lossy()));\n        return entry;\n    };\n\n    entry.codec = current_codec;\n    entry.duration = duration;\n    match (metadata.width, metadata.height) {\n        (Some(width), Some(height)) => {\n            entry.width = width;\n            entry.height = height;\n        }\n        _ => {\n            entry.error = Some(flc!(\"core_failed_to_get_video_dimensions\", file = entry.path.to_string_lossy()));\n            return entry;\n        }\n    }\n\n    entry\n}\n\npub fn process_video(stop_flag: &Arc<AtomicBool>, video_path: &str, original_size: u64, params: VideoTranscodeFixParams) -> Result<(), String> {\n    let temp_output = Path::new(video_path).with_extension(\"czkawka_optimized.mp4\");\n\n    let mut command = Command::new(\"ffmpeg\");\n    command\n        .arg(\"-i\")\n        .arg(video_path)\n        .arg(\"-nostdin\")\n        .arg(\"-c:v\")\n        .arg(params.codec.as_str())\n        .arg(\"-crf\")\n        .arg(params.quality.to_string());\n\n    if params.limit_video_size {\n        let scale_filter = format!(\"scale='min({},iw):min({},ih):force_original_aspect_ratio=decrease'\", params.max_width, params.max_height);\n        command.arg(\"-vf\").arg(scale_filter);\n    }\n\n    command.arg(\"-c:a\").arg(\"copy\").arg(\"-y\").arg(&temp_output);\n\n    match run_command_interruptible(command, stop_flag) {\n        None => {\n            let _ = fs::remove_file(&temp_output);\n            return Err(flc!(\"core_video_processing_stopped_by_user\"));\n        }\n        Some(Err(e)) => {\n            let _ = fs::remove_file(&temp_output);\n            return Err(flc!(\"core_failed_to_process_video\", file = video_path, reason = e));\n        }\n        Some(Ok(output)) => {\n            if !output.status.success() {\n                let connected = format!(\"{} - {}\", output.stdout, output.stderr);\n                if connected.to_lowercase().contains(\"unknown encoder\") {\n                    return Err(flc!(\"core_ffmpeg_unknown_encoder\", file = video_path, encoder = params.codec.as_ffprobe_codec_name()));\n                }\n                error!(\n                    \"FFmpeg failed to transcode video \\\"{}\\\" with status {}. Stdout: {}, Stderr: {}\",\n                    video_path, output.status, output.stdout, output.stderr\n                );\n                return Err(flc!(\"core_ffmpeg_error\", file = video_path, code = output.status.to_string(), reason = output.stderr));\n            }\n        }\n    }\n\n    let metadata = fs::metadata(&temp_output).map_err(|e| {\n        let _ = fs::remove_file(&temp_output);\n        flc!(\n            \"core_failed_to_get_metadata_of_optimized_file\",\n            file = temp_output.to_string_lossy(),\n            reason = e.to_string()\n        )\n    })?;\n\n    let new_size = metadata.len();\n\n    if params.fail_if_not_smaller && new_size >= original_size {\n        let _ = fs::remove_file(&temp_output);\n        return Err(flc!(\n            \"core_optimized_file_larger\",\n            optimized = temp_output.to_string_lossy(),\n            new_size = new_size,\n            original = video_path,\n            original_size = original_size\n        ));\n    }\n\n    if params.overwrite_original {\n        fs::rename(&temp_output, video_path).map_err(|e| {\n            let _ = fs::remove_file(&temp_output);\n            flc!(\"core_failed_to_replace_with_optimized\", file = video_path, reason = e.to_string())\n        })?;\n        return Ok(());\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/video_optimizer/core/video_cropper.rs",
    "content": "use std::path::Path;\nuse std::process::Command;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\n\nuse image::RgbImage;\nuse log::error;\n\nuse crate::common::consts::VIDEO_RESOLUTION_LIMIT;\nuse crate::common::process_utils::run_command_interruptible;\nuse crate::common::video_utils::{VideoMetadata, extract_frame_ffmpeg};\nuse crate::flc;\nuse crate::tools::video_optimizer::{VideoCropEntry, VideoCropParams, VideoCropSingleFixParams, VideoCroppingMechanism};\n\nconst MIN_SAMPLES: usize = 3;\nconst MIN_SAMPLE_INTERVAL: f32 = 0.1;\n\n#[derive(Debug, Clone, Copy, PartialEq)]\nstruct Rectangle {\n    top: u32,\n    bottom: u32,\n    left: u32,\n    right: u32,\n}\n\nimpl Rectangle {\n    fn new(top: u32, bottom: u32, left: u32, right: u32) -> Self {\n        let s = Self { top, bottom, left, right };\n        s.validate();\n        s\n    }\n\n    fn union(&self, other: &Self) -> Self {\n        let s = Self {\n            top: self.top.min(other.top),\n            bottom: self.bottom.max(other.bottom),\n            left: self.left.min(other.left),\n            right: self.right.max(other.right),\n        };\n        s.validate();\n        s\n    }\n\n    fn validate(&self) {\n        assert!(\n            self.left <= self.right && self.top <= self.bottom,\n            \"Invalid rectangle coordinates: top={}, bottom={}, left={}, right={}. Expected: left <= right && top <= bottom (critical algorithm error, please report an issue)\",\n            self.top,\n            self.bottom,\n            self.left,\n            self.right\n        );\n    }\n    fn validate_image_size(&self, width: u32, height: u32) {\n        assert!(\n            self.right <= width && self.bottom <= height,\n            \"Rectangle exceeds image dimensions: image_width={}, image_height={}, rectangle_right={}, rectangle_bottom={}. Expected: right <= image_width && bottom <= image_height (critical algorithm error, please report an issue)\",\n            width,\n            height,\n            self.right,\n            self.bottom\n        );\n    }\n\n    fn is_cropping_needed(&self, width: u32, height: u32, min_crop_size: u32) -> bool {\n        let right_margin = width - self.right;\n        let bottom_margin = height - self.bottom;\n        self.left > min_crop_size || right_margin > min_crop_size || self.top > min_crop_size || bottom_margin > min_crop_size\n    }\n}\n\nfn is_pixel_black(img: &image::RgbImage, x: u32, y: u32, black_pixel_threshold: u8) -> bool {\n    let pixel = img.get_pixel(x, y);\n    pixel.0.iter().all(|&channel| channel <= black_pixel_threshold)\n}\n\n#[derive(Debug)]\nenum BlackBarResult {\n    NoBlackBars,\n    BlackBarsDetected(Rectangle),\n    FullBlackImage,\n}\n\nfn detect_black_bars(rgb_img: &RgbImage, params: &VideoCropParams) -> BlackBarResult {\n    let (width, height) = rgb_img.dimensions();\n    let min_percentage = params.black_bar_min_percentage as f32 / 100.0;\n\n    let mut left_crop = 0u32;\n    for x in 0..width {\n        let black_pixels = (0..height).filter(|&y| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count();\n        if (black_pixels as f32 / height as f32) < min_percentage {\n            break;\n        }\n        left_crop = x + 1;\n    }\n\n    let mut right_pos = width;\n    for x in (0..width).rev() {\n        let black_pixels = (0..height).filter(|&y| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count();\n        if (black_pixels as f32 / height as f32) < min_percentage {\n            right_pos = x + 1;\n            break;\n        }\n    }\n\n    if left_crop >= right_pos {\n        return BlackBarResult::FullBlackImage;\n    }\n\n    let mut top_crop = 0u32;\n    for y in 0..height {\n        let black_pixels = (0..width).filter(|&x| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count();\n        if (black_pixels as f32 / width as f32) < min_percentage {\n            break;\n        }\n        top_crop = y + 1;\n    }\n\n    let mut bottom_pos = height;\n    for y in (0..height).rev() {\n        let black_pixels = (0..width).filter(|&x| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count();\n        if (black_pixels as f32 / width as f32) < min_percentage {\n            bottom_pos = y + 1;\n            break;\n        }\n    }\n\n    if top_crop >= bottom_pos {\n        return BlackBarResult::FullBlackImage;\n    }\n\n    let rect = Rectangle::new(top_crop, bottom_pos, left_crop, right_pos);\n    if rect.is_cropping_needed(width, height, params.min_crop_size) {\n        BlackBarResult::BlackBarsDetected(rect)\n    } else {\n        BlackBarResult::NoBlackBars\n    }\n}\n\nfn analyze_black_bars<F>(\n    duration: f32,\n    get_frame: &F,\n    stop_flag: &Arc<AtomicBool>,\n    first_frame: &RgbImage,\n    params: &VideoCropParams,\n    path: &Path,\n) -> Option<Result<Option<Rectangle>, String>>\nwhere\n    F: Fn(f32) -> Result<RgbImage, String>,\n{\n    if stop_flag.load(Ordering::Relaxed) {\n        return None;\n    }\n\n    let mut rectangle = match detect_black_bars(first_frame, params) {\n        BlackBarResult::BlackBarsDetected(rect) => Some(rect),\n        BlackBarResult::NoBlackBars => {\n            return Some(Ok(None));\n        }\n        BlackBarResult::FullBlackImage => None,\n    };\n\n    let num_samples = ((duration / MIN_SAMPLE_INTERVAL).floor() as usize).clamp(MIN_SAMPLES, params.max_samples);\n\n    for i in 1..num_samples {\n        if stop_flag.load(Ordering::Relaxed) {\n            return None;\n        }\n\n        let timestamp = (i as f32 / num_samples as f32) * duration;\n\n        let tmp_frame = match get_frame(timestamp) {\n            Ok(frame) => frame,\n            Err(e) => {\n                return Some(Err(flc!(\n                    \"core_failed_get_frame_at_timestamp\",\n                    file = path.to_string_lossy().to_string(),\n                    timestamp = timestamp,\n                    reason = e\n                )));\n            }\n        };\n        if tmp_frame.dimensions() != first_frame.dimensions() {\n            return Some(Err(flc!(\n                \"core_frame_dimensions_mismatch\",\n                timestamp = timestamp,\n                first_w = first_frame.width(),\n                first_h = first_frame.height()\n            )));\n        }\n\n        match detect_black_bars(&tmp_frame, params) {\n            BlackBarResult::BlackBarsDetected(tmp_rect) => {\n                rectangle = match rectangle {\n                    Some(current_rect) => Some(current_rect.union(&tmp_rect)),\n                    None => Some(tmp_rect),\n                };\n            }\n            BlackBarResult::NoBlackBars => {\n                return Some(Ok(None));\n            }\n            BlackBarResult::FullBlackImage => {\n                // Do nothing - leave the current rectangle as is\n            }\n        }\n    }\n    if let Some(rectangle) = rectangle {\n        rectangle.validate();\n        // Rectangle may extend step by step to full image size, so that is why previous checks are not enough\n        if !rectangle.is_cropping_needed(first_frame.width(), first_frame.height(), params.min_crop_size) {\n            return Some(Ok(None));\n        }\n        Some(Ok(Some(rectangle)))\n    } else {\n        Some(Ok(None)) // All frames were fully black\n    }\n}\n\nfn diff_between_dynamic_images(img_original: &RgbImage, mut consumed_temp_img: RgbImage) -> RgbImage {\n    assert_eq!(\n        img_original.dimensions(),\n        consumed_temp_img.dimensions(),\n        \"Image dimensions do not match for diffing (critical algorithm error, please report an issue)\"\n    );\n    img_original.pixels().zip(consumed_temp_img.pixels_mut()).for_each(|(img_original_pixel, consumed_pixel)| {\n        consumed_pixel\n            .0\n            .iter_mut()\n            .zip(img_original_pixel.0.iter())\n            .for_each(|(consumed_channel, &original_channel)| {\n                *consumed_channel = original_channel.abs_diff(*consumed_channel);\n            });\n    });\n    consumed_temp_img\n}\n\nfn analyze_static_image_parts<F>(\n    duration: f32,\n    get_frame: &F,\n    stop_flag: &Arc<AtomicBool>,\n    first_frame: &RgbImage,\n    params: &VideoCropParams,\n    path: &Path,\n) -> Option<Result<Option<Rectangle>, String>>\nwhere\n    F: Fn(f32) -> Result<RgbImage, String>,\n{\n    if stop_flag.load(Ordering::Relaxed) {\n        return None;\n    }\n    // Initial rectangle is empty, because with only one frame we cannot determine static parts\n    let mut rectangle: Option<Rectangle> = None;\n\n    let num_samples = ((duration / MIN_SAMPLE_INTERVAL).floor() as usize).clamp(MIN_SAMPLES, params.max_samples);\n\n    for i in 1..num_samples {\n        if stop_flag.load(Ordering::Relaxed) {\n            return None;\n        }\n\n        let timestamp = (i as f32 / num_samples as f32) * duration;\n\n        let tmp_frame = match get_frame(timestamp) {\n            Ok(frame) => frame,\n            Err(e) => {\n                return Some(Err(flc!(\n                    \"core_failed_get_frame_from_file\",\n                    file = path.to_string_lossy().to_string(),\n                    timestamp = timestamp,\n                    reason = e\n                )));\n            }\n        };\n        if tmp_frame.dimensions() != first_frame.dimensions() {\n            return Some(Err(flc!(\n                \"core_frame_dimensions_mismatch\",\n                timestamp = timestamp,\n                first_w = first_frame.width(),\n                first_h = first_frame.height()\n            )));\n        }\n        let dynamic_image_diff: RgbImage = diff_between_dynamic_images(first_frame, tmp_frame);\n\n        match detect_black_bars(&dynamic_image_diff, params) {\n            BlackBarResult::FullBlackImage => {\n                // Do nothing - leave the current rectangle as is\n            }\n            BlackBarResult::NoBlackBars => {\n                return Some(Ok(None));\n            }\n            BlackBarResult::BlackBarsDetected(tmp_rect) => {\n                rectangle = match rectangle {\n                    Some(current_rect) => Some(current_rect.union(&tmp_rect)),\n                    None => Some(tmp_rect),\n                };\n            }\n        }\n    }\n\n    if let Some(rectangle) = rectangle {\n        rectangle.validate();\n        // Rectangle may extend step by step to full image size, so that is why previous checks are not enough\n        if !rectangle.is_cropping_needed(first_frame.width(), first_frame.height(), params.min_crop_size) {\n            return Some(Ok(None));\n        }\n        Some(Ok(Some(rectangle)))\n    } else {\n        Some(Ok(None)) // All frames were fully static\n    }\n}\n\nfn extract_video_metadata_for_crop(entry: &mut VideoCropEntry) -> Result<(u32, u32, f64, f64), ()> {\n    let metadata = match VideoMetadata::from_path(&entry.path) {\n        Ok(metadata) => metadata,\n        Err(e) => {\n            entry.error = Some(format!(\"Failed to get video metadata for file \\\"{}\\\": {}\", entry.path.to_string_lossy(), e));\n            return Err(());\n        }\n    };\n\n    let Some(current_codec) = metadata.codec.clone() else {\n        entry.error = Some(format!(\"Failed to get video codec from metadata for file \\\"{}\\\"\", entry.path.to_string_lossy()));\n        return Err(());\n    };\n\n    entry.codec = current_codec;\n\n    let (width, height) = match (metadata.width, metadata.height) {\n        (Some(width), Some(height)) => {\n            entry.width = width;\n            entry.height = height;\n            (width, height)\n        }\n        _ => {\n            entry.error = Some(format!(\"Failed to get video dimensions from metadata for file \\\"{}\\\"\", entry.path.to_string_lossy()));\n            return Err(());\n        }\n    };\n\n    let Some(duration) = metadata.duration else {\n        entry.error = Some(format!(\"Failed to get video duration from metadata, for file \\\"{}\\\"\", entry.path.to_string_lossy()));\n        return Err(());\n    };\n\n    entry.duration = duration;\n\n    let fps = metadata.fps.unwrap_or(25.0);\n\n    Ok((width, height, duration, fps))\n}\n\npub fn check_video_crop(mut entry: VideoCropEntry, params: &VideoCropParams, stop_flag: &Arc<AtomicBool>) -> Option<VideoCropEntry> {\n    let Ok((_width, _height, duration, _fps)) = extract_video_metadata_for_crop(&mut entry) else {\n        return Some(entry);\n    };\n\n    let video_path = entry.path.clone();\n    let get_frame = |timestamp: f32| -> Result<RgbImage, String> { extract_frame_ffmpeg(&video_path, timestamp, None) };\n\n    // TODO - metadata are broken? Not proper?\n    // Metadata shows different dimensions than actual frames extracted - quite strange, probably rotated data -\n    let first_frame = match get_frame(0.0) {\n        Ok(frame) => frame,\n        Err(e) => {\n            entry.error = Some(format!(\"Failed to extract first frame for video \\\"{}\\\": {}\", entry.path.to_string_lossy(), e));\n            return Some(entry);\n        }\n    };\n\n    let (width, height) = first_frame.dimensions();\n    entry.height = height;\n    entry.width = width;\n\n    if entry.width > VIDEO_RESOLUTION_LIMIT || entry.height > VIDEO_RESOLUTION_LIMIT {\n        entry.error = Some(format!(\n            \"Image dimensions for video \\\"{}\\\" exceed the limit: {}x{} > {}x{}\",\n            entry.path.to_string_lossy(),\n            entry.width,\n            entry.height,\n            VIDEO_RESOLUTION_LIMIT,\n            VIDEO_RESOLUTION_LIMIT\n        ));\n        return Some(entry);\n    }\n\n    match params.crop_detect {\n        VideoCroppingMechanism::BlackBars => match analyze_black_bars(duration as f32, &get_frame, stop_flag, &first_frame, params, &entry.path) {\n            Some(Ok(Some(rectangle))) => {\n                rectangle.validate_image_size(width, height);\n                entry.new_image_dimensions = (rectangle.left, rectangle.top, rectangle.right, rectangle.bottom);\n            }\n            Some(Ok(None)) => { // No black bars\n            }\n            Some(Err(e)) => {\n                entry.error = Some(e);\n                return Some(entry);\n            }\n            None => return None,\n        },\n        VideoCroppingMechanism::StaticContent => match analyze_static_image_parts(duration as f32, &get_frame, stop_flag, &first_frame, params, &entry.path) {\n            Some(Ok(Some(rectangle))) => {\n                rectangle.validate_image_size(width, height);\n                entry.new_image_dimensions = (rectangle.left, rectangle.top, rectangle.right, rectangle.bottom);\n            }\n            Some(Ok(None)) => {}\n            Some(Err(e)) => {\n                entry.error = Some(e);\n                return Some(entry);\n            }\n            None => return None,\n        },\n    }\n\n    Some(entry)\n}\n\npub fn fix_video_crop(video_path: &Path, params: &VideoCropSingleFixParams, stop_flag: &Arc<AtomicBool>, current_codec: &str) -> Result<(), String> {\n    if stop_flag.load(Ordering::Relaxed) {\n        return Err(\"Video processing was stopped by user\".to_string());\n    }\n\n    let (left, top, right, bottom) = params.crop_rectangle;\n\n    if left >= right || top >= bottom {\n        return Err(flc!(\"core_invalid_crop_rectangle\", left = left, top = top, right = right, bottom = bottom));\n    }\n\n    let crop_width = right - left;\n    let crop_height = bottom - top;\n\n    let crop_type_suffix = match params.crop_mechanism {\n        VideoCroppingMechanism::BlackBars => \"blackbars\",\n        VideoCroppingMechanism::StaticContent => \"staticcontent\",\n    };\n\n    let extension = video_path.extension().and_then(|ext| ext.to_str()).unwrap_or(\"\");\n    let temp_output = video_path.with_extension(format!(\"czkawka_cropped_{crop_type_suffix}.{extension}\"));\n\n    let mut command = Command::new(\"ffmpeg\");\n    command.arg(\"-i\").arg(video_path).arg(\"-vf\").arg(format!(\"crop={crop_width}:{crop_height}:{left}:{top}\"));\n\n    match (params.target_codec, params.quality) {\n        (None, None) => {\n            // Do nothing, do not convert video to different codec\n        }\n        (Some(target_codec), Some(quality)) => {\n            command.arg(\"-c:v\").arg(target_codec.as_str()).arg(\"-crf\").arg(quality.to_string());\n        }\n        _ => {\n            return Err(\"Both target_codec and quality must be specified together\".to_string());\n        }\n    }\n\n    command.arg(\"-c:a\").arg(\"copy\");\n    command.arg(\"-y\").arg(&temp_output);\n\n    match run_command_interruptible(command, stop_flag) {\n        None => {\n            let _ = std::fs::remove_file(&temp_output);\n            return Err(String::from(\"Video cropping was stopped by user\"));\n        }\n        Some(Err(e)) => {\n            let _ = std::fs::remove_file(&temp_output);\n            return Err(flc!(\"core_failed_to_crop_video_file\", file = video_path.to_string_lossy(), reason = e));\n        }\n        Some(Ok(output)) => {\n            if !output.status.success() {\n                let connected = format!(\"{} - {}\", output.stdout, output.stderr);\n                if connected.to_lowercase().contains(\"unknown encoder\") {\n                    let missing_codec = match params.target_codec {\n                        Some(target_codec) => target_codec.as_ffprobe_codec_name(),\n                        None => current_codec,\n                    };\n                    return Err(flc!(\"core_ffmpeg_unknown_encoder\", file = video_path.to_string_lossy(), encoder = missing_codec));\n                }\n                error!(\n                    \"FFmpeg failed to crop video \\\"{}\\\" with status {}. Stdout: {}, Stderr: {}\",\n                    video_path.to_string_lossy(),\n                    output.status,\n                    output.stdout,\n                    output.stderr\n                );\n                return Err(flc!(\n                    \"core_ffmpeg_error\",\n                    file = video_path.to_string_lossy(),\n                    code = output.status.to_string(),\n                    reason = output.stderr\n                ));\n            }\n        }\n    }\n\n    if !temp_output.exists() {\n        error!(\"Cropped video file was not created: {temp_output:?}\");\n        return Err(flc!(\"core_cropped_video_not_created\", temp = format!(\"{:?}\", temp_output)));\n    }\n\n    if params.overwrite_original {\n        std::fs::rename(&temp_output, video_path).map_err(|e| format!(\"Failed to replace original file: {e}\"))?;\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use std::sync::Arc;\n    use std::sync::atomic::AtomicBool;\n\n    use image::RgbImage;\n\n    use super::*;\n\n    fn default_test_params() -> VideoCropParams {\n        VideoCropParams {\n            crop_detect: VideoCroppingMechanism::BlackBars,\n            black_pixel_threshold: 20,\n            black_bar_min_percentage: 90,\n            max_samples: 60,\n            min_crop_size: 5,\n            generate_thumbnails: false,\n            thumbnail_video_percentage_from_start: 0,\n            generate_thumbnail_grid_instead_of_single: false,\n            thumbnail_grid_tiles_per_side: 2,\n        }\n    }\n\n    fn create_colored_frame(width: u32, height: u32, r: u8, g: u8, b: u8) -> RgbImage {\n        let mut img = RgbImage::new(width, height);\n        for pixel in img.pixels_mut() {\n            *pixel = image::Rgb([r, g, b]);\n        }\n        img\n    }\n\n    fn create_frame_with_black_bars(width: u32, height: u32, bar_size: u32) -> RgbImage {\n        let mut img = RgbImage::new(width, height);\n        for (x, y, pixel) in img.enumerate_pixels_mut() {\n            if x < bar_size || x >= width - bar_size || y < bar_size || y >= height - bar_size {\n                *pixel = image::Rgb([0, 0, 0]);\n            } else {\n                *pixel = image::Rgb([100, 150, 200]);\n            }\n        }\n        img\n    }\n\n    #[test]\n    fn test_is_pixel_black() {\n        let params = default_test_params();\n\n        let black_img = RgbImage::from_pixel(10, 10, image::Rgb([0, 0, 0]));\n        assert!(is_pixel_black(&black_img, 5, 5, params.black_pixel_threshold));\n\n        let light_gray_img = RgbImage::from_pixel(10, 10, image::Rgb([20, 20, 20]));\n        assert!(is_pixel_black(&light_gray_img, 5, 5, params.black_pixel_threshold));\n\n        let dark_gray_img = RgbImage::from_pixel(10, 10, image::Rgb([21, 21, 21]));\n        assert!(!is_pixel_black(&dark_gray_img, 5, 5, params.black_pixel_threshold));\n\n        let white_img = RgbImage::from_pixel(10, 10, image::Rgb([255, 255, 255]));\n        assert!(!is_pixel_black(&white_img, 5, 5, params.black_pixel_threshold));\n    }\n\n    #[test]\n    fn test_detect_black_bars_no_bars() {\n        let params = default_test_params();\n        let img = create_colored_frame(100, 100, 100, 150, 200);\n        let result = detect_black_bars(&img, &params);\n        assert!(matches!(result, BlackBarResult::NoBlackBars));\n    }\n\n    #[test]\n    fn test_detect_black_bars_with_bars() {\n        let params = default_test_params();\n        let img = create_frame_with_black_bars(200, 200, 20);\n        let result = detect_black_bars(&img, &params);\n        if let BlackBarResult::BlackBarsDetected(rect) = result {\n            assert!(rect.left >= 15 && rect.left <= 25, \"Left crop: {}\", rect.left);\n            assert!(rect.top >= 15 && rect.top <= 25, \"Top crop: {}\", rect.top);\n            assert!(rect.right >= 175 && rect.right <= 185, \"Right position: {}\", rect.right);\n            assert!(rect.bottom >= 175 && rect.bottom <= 185, \"Bottom position: {}\", rect.bottom);\n        } else {\n            panic!(\"Expected BlackBarsDetected, got {result:?}\");\n        }\n    }\n\n    #[test]\n    fn test_detect_black_bars_small_bars() {\n        let params = default_test_params();\n        let img = create_frame_with_black_bars(200, 200, 3);\n        let result = detect_black_bars(&img, &params);\n        assert!(matches!(result, BlackBarResult::NoBlackBars));\n    }\n\n    #[test]\n    fn test_rectangle_union() {\n        let rect1 = Rectangle::new(10, 10, 10, 10);\n        let rect2 = Rectangle::new(5, 15, 8, 12);\n        let union = rect1.union(&rect2);\n\n        assert_eq!(union.top, 5);\n        assert_eq!(union.bottom, 15);\n        assert_eq!(union.left, 8);\n        assert_eq!(union.right, 12);\n    }\n\n    #[test]\n    fn test_rectangle_is_cropping_needed() {\n        let params = default_test_params();\n\n        // Image 100x100, cropped to (10, 10) -> (90, 90), so 10px margin on each side\n        let cropping_needed = Rectangle::new(10, 90, 10, 90);\n        assert!(cropping_needed.is_cropping_needed(100, 100, params.min_crop_size));\n\n        // Image 100x100, no cropping: (0, 0) -> (100, 100)\n        let no_cropping_needed = Rectangle::new(0, 100, 0, 100);\n        assert!(!no_cropping_needed.is_cropping_needed(100, 100, params.min_crop_size));\n\n        // Image 100x100, small crop (3px on each side) - below threshold\n        let small_crop = Rectangle::new(3, 97, 3, 97);\n        assert!(!small_crop.is_cropping_needed(100, 100, params.min_crop_size));\n    }\n\n    #[test]\n    fn test_analyze_black_bars_consistent_bars() {\n        let params = default_test_params();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let duration = 10.0;\n\n        let get_frame = |_timestamp: f32| -> Result<RgbImage, String> { Ok(create_frame_with_black_bars(200, 200, 20)) };\n\n        let result = analyze_black_bars(\n            duration,\n            &get_frame,\n            &stop_flag,\n            &create_frame_with_black_bars(200, 200, 20),\n            &params,\n            Path::new(\"text.txt\"),\n        );\n        assert!(result.expect(\"Expected Result\").unwrap().is_some());\n    }\n\n    #[test]\n    fn test_analyze_black_bars_no_bars() {\n        let params = default_test_params();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let duration = 10.0;\n\n        let get_frame = |_timestamp: f32| -> Result<RgbImage, String> { Ok(create_colored_frame(200, 200, 100, 150, 200)) };\n\n        let result = analyze_black_bars(\n            duration,\n            &get_frame,\n            &stop_flag,\n            &create_colored_frame(200, 200, 100, 150, 200),\n            &params,\n            Path::new(\"text.txt\"),\n        );\n        assert!(result.expect(\"Expected Result\").unwrap().is_none());\n    }\n\n    #[test]\n    fn test_analyze_black_bars_inconsistent_bars() {\n        let params = default_test_params();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let duration = 10.0;\n\n        let get_frame = |timestamp: f32| -> Result<RgbImage, String> {\n            if timestamp < 5.0 {\n                Ok(create_frame_with_black_bars(200, 200, 20))\n            } else {\n                Ok(create_colored_frame(200, 200, 100, 150, 200))\n            }\n        };\n\n        let result = analyze_black_bars(\n            duration,\n            &get_frame,\n            &stop_flag,\n            &create_frame_with_black_bars(200, 200, 20),\n            &params,\n            Path::new(\"text.txt\"),\n        );\n        assert!(result.expect(\"Expected Result\").unwrap().is_none());\n    }\n\n    #[test]\n    fn test_analyze_black_bars_variable_rectangles() {\n        let params = default_test_params();\n        let stop_flag = Arc::new(AtomicBool::new(false));\n        let duration = 10.0;\n\n        let get_frame = |timestamp: f32| -> Result<RgbImage, String> {\n            if timestamp < 3.0 {\n                Ok(create_frame_with_black_bars(200, 200, 20))\n            } else if timestamp < 7.0 {\n                Ok(create_frame_with_black_bars(200, 200, 18))\n            } else {\n                Ok(create_frame_with_black_bars(200, 200, 22))\n            }\n        };\n\n        let result = analyze_black_bars(\n            duration,\n            &get_frame,\n            &stop_flag,\n            &create_frame_with_black_bars(200, 200, 20),\n            &params,\n            Path::new(\"text.txt\"),\n        );\n        let rect = result.expect(\"Expected Result\").unwrap().unwrap();\n        assert_eq!(rect.left, 18);\n        assert_eq!(rect.top, 18);\n        assert_eq!(rect.right, 200 - 18);\n        assert_eq!(rect.bottom, 200 - 18);\n    }\n\n    #[test]\n    fn test_detect_black_bars_fuzzer() {\n        let params = default_test_params();\n        let test_cases = vec![\n            (1, 1, \"1x1 image\"),\n            (1, 100, \"1 pixel wide\"),\n            (100, 1, \"1 pixel tall\"),\n            (2, 2, \"2x2 minimum\"),\n            (10, 10, \"10x10 small\"),\n            (100, 100, \"100x100 medium\"),\n            (1920, 1080, \"1920x1080 Full HD\"),\n            (3840, 2160, \"3840x2160 4K\"),\n        ];\n\n        for (width, height, desc) in test_cases {\n            // Test 1: All black image\n            let mut all_black = RgbImage::new(width, height);\n            for pixel in all_black.pixels_mut() {\n                *pixel = image::Rgb([0, 0, 0]);\n            }\n            let result = detect_black_bars(&all_black, &params);\n            assert!(matches!(result, BlackBarResult::FullBlackImage), \"All black image should return FullBlackImage for {desc}\");\n\n            // Test 2: All white image\n            let mut all_white = RgbImage::new(width, height);\n            for pixel in all_white.pixels_mut() {\n                *pixel = image::Rgb([255, 255, 255]);\n            }\n            let result = detect_black_bars(&all_white, &params);\n            assert!(matches!(result, BlackBarResult::NoBlackBars), \"All white image should return NoBlackBars for {desc}\");\n\n            // Test 4: Checkerboard pattern (no black bars)\n            if width > 4 && height > 4 {\n                let mut checkerboard = RgbImage::new(width, height);\n                for (x, y, pixel) in checkerboard.enumerate_pixels_mut() {\n                    let color = if (x + y) % 2 == 0 { 255 } else { 0 };\n                    *pixel = image::Rgb([color, color, color]);\n                }\n                let result = detect_black_bars(&checkerboard, &params);\n                assert!(matches!(result, BlackBarResult::NoBlackBars), \"Checkerboard should return NoBlackBars for {desc}\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/video_optimizer/core.rs",
    "content": "use std::collections::BTreeMap;\nuse std::mem;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse log::{debug, info};\nuse rayon::prelude::*;\n\nuse crate::common::cache::{load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path};\nuse crate::common::config_cache_path::get_config_cache_path;\nuse crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult};\nuse crate::common::model::{ToolType, WorkContinueStatus};\nuse crate::common::progress_data::{CurrentStage, ProgressData};\nuse crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common};\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::common::video_utils::{VIDEO_THUMBNAILS_SUBFOLDER, generate_thumbnail};\nuse crate::tools::video_optimizer::{\n    Info, VideoCropEntry, VideoCropParams, VideoCropSingleFixParams, VideoOptimizer, VideoOptimizerFixParams, VideoOptimizerParameters, VideoTranscodeEntry, VideoTranscodeParams,\n};\n\nmod video_converter;\nmod video_cropper;\n\npub use video_converter::process_video;\npub use video_cropper::fix_video_crop;\n\nuse crate::common::cache::CACHE_VIDEO_OPTIMIZE_VERSION;\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\n\nimpl VideoOptimizer {\n    pub fn new(params: VideoOptimizerParameters) -> Self {\n        Self {\n            common_data: CommonToolData::new(ToolType::VideoOptimizer),\n            information: Info::default(),\n            video_transcode_test_entries: Default::default(),\n            video_crop_test_entries: Default::default(),\n            video_transcode_result_entries: Vec::new(),\n            video_crop_result_entries: Vec::new(),\n            params,\n        }\n    }\n\n    #[fun_time(message = \"scan_files\", level = \"debug\")]\n    pub(crate) fn scan_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        let result = DirTraversalBuilder::new()\n            .group_by(|_fe| ())\n            .stop_flag(stop_flag)\n            .progress_sender(progress_sender)\n            .common_data(&self.common_data)\n            .build()\n            .run();\n\n        match result {\n            DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => {\n                match &self.params {\n                    VideoOptimizerParameters::VideoTranscode(_) => {\n                        self.video_transcode_test_entries = grouped_file_entries\n                            .into_values()\n                            .flatten()\n                            .map(|fe| (fe.get_path().to_string_lossy().to_string(), fe.into_video_transcode_entry()))\n                            .collect();\n                        info!(\"Found {} files to check\", self.video_transcode_test_entries.len());\n                    }\n                    VideoOptimizerParameters::VideoCrop(_) => {\n                        self.video_crop_test_entries = grouped_file_entries\n                            .into_values()\n                            .flatten()\n                            .map(|fe| (fe.get_path().to_string_lossy().to_string(), fe.into_video_crop_entry()))\n                            .collect();\n                        info!(\"Found {} files to check\", self.video_crop_test_entries.len());\n                    }\n                }\n\n                self.common_data.text_messages.warnings.extend(warnings);\n\n                WorkContinueStatus::Continue\n            }\n            DirTraversalResult::Stopped => WorkContinueStatus::Stop,\n        }\n    }\n\n    #[fun_time(message = \"check_files\", level = \"debug\")]\n    pub(crate) fn check_files(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        match self.params.clone() {\n            VideoOptimizerParameters::VideoTranscode(params) => self.process_video_transcode(stop_flag, progress_sender, params),\n            VideoOptimizerParameters::VideoCrop(_) => self.process_video_crop(stop_flag, progress_sender),\n        }\n    }\n\n    #[fun_time(message = \"process_video_transcode\", level = \"debug\")]\n    fn process_video_transcode(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>, params: VideoTranscodeParams) -> WorkContinueStatus {\n        if self.video_transcode_test_entries.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_video_transcode_cache();\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::VideoOptimizerProcessingVideos,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            non_cached_files_to_check.values().map(|entry| entry.size).sum(),\n        );\n\n        let mut entries: Vec<VideoTranscodeEntry> = non_cached_files_to_check\n            .into_par_iter()\n            .map(|(_path, entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n                let size = entry.size;\n                let res = video_converter::check_video(entry);\n                progress_handler.increase_items(1);\n                progress_handler.increase_size(size);\n                Some(res)\n            })\n            .while_some()\n            .collect();\n\n        self.common_data.text_messages.warnings.extend(entries.iter().filter_map(|e| e.error.as_ref()).cloned());\n        entries.extend(records_already_cached.into_values());\n\n        progress_handler.join_thread();\n\n        self.save_video_transcode_cache(&entries, loaded_hash_map);\n\n        entries.retain(|e| e.error.is_none() && !params.excluded_codecs.contains(&e.codec));\n\n        self.video_transcode_result_entries = entries;\n        self.information.number_of_videos_to_transcode = self.video_transcode_result_entries.len();\n\n        if self.create_transcode_thumbnails(progress_sender, stop_flag, &params) == WorkContinueStatus::Stop {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"process_video_crop\", level = \"debug\")]\n    fn process_video_crop(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        if self.video_crop_test_entries.is_empty() {\n            return WorkContinueStatus::Continue;\n        }\n\n        let VideoOptimizerParameters::VideoCrop(params) = self.params.clone() else {\n            unreachable!(\"process_video_crop called with non VideoCrop parameters, caller is responsible for that\");\n        };\n\n        let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_video_crop_cache(&params);\n\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::VideoOptimizerProcessingVideos,\n            non_cached_files_to_check.len(),\n            self.get_test_type(),\n            non_cached_files_to_check.values().map(|entry| entry.size).sum(),\n        );\n\n        let mut vec_file_entry: Vec<VideoCropEntry> = non_cached_files_to_check\n            .into_par_iter()\n            .map(|(_path, entry)| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n                let size = entry.size;\n                let res = video_cropper::check_video_crop(entry, &params, stop_flag);\n                progress_handler.increase_items(1);\n                progress_handler.increase_size(size);\n                res\n            })\n            .while_some()\n            .collect();\n\n        self.common_data\n            .text_messages\n            .warnings\n            .extend(vec_file_entry.iter().filter_map(|e| e.error.as_ref()).cloned());\n        vec_file_entry.extend(records_already_cached.into_values());\n\n        progress_handler.join_thread();\n\n        self.save_video_crop_cache(&vec_file_entry, &params, loaded_hash_map);\n\n        vec_file_entry.retain(|e| e.error.is_none() && e.new_image_dimensions != (0, 0, 0, 0));\n\n        self.video_crop_result_entries = vec_file_entry;\n        self.information.number_of_videos_to_crop = self.video_crop_result_entries.len();\n\n        if self.create_crop_thumbnails(progress_sender, stop_flag, &params) == WorkContinueStatus::Stop {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"create_transcode_thumbnails\", level = \"debug\")]\n    fn create_transcode_thumbnails(&mut self, progress_sender: Option<&Sender<ProgressData>>, stop_flag: &Arc<AtomicBool>, params: &VideoTranscodeParams) -> WorkContinueStatus {\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::VideoOptimizerCreatingThumbnails,\n            self.video_transcode_result_entries.len(),\n            self.get_test_type(),\n            0,\n        );\n\n        let Some(config_cache_path) = get_config_cache_path() else {\n            return WorkContinueStatus::Continue;\n        };\n\n        let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER);\n        if let Err(e) = std::fs::create_dir_all(&thumbnails_dir) {\n            debug!(\"Failed to create thumbnails directory: {e}\");\n            return WorkContinueStatus::Continue;\n        }\n\n        let thumbnail_video_percentage_from_start = params.thumbnail_video_percentage_from_start;\n        let generate_grid_instead_of_single = params.generate_thumbnail_grid_instead_of_single;\n        let thumbnail_grid_tiles_per_side = params.thumbnail_grid_tiles_per_side;\n\n        let errors = self\n            .video_transcode_result_entries\n            .par_iter_mut()\n            .map(|entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                match generate_thumbnail(\n                    stop_flag,\n                    &entry.path,\n                    entry.size,\n                    entry.modified_date,\n                    Some(entry.duration),\n                    &thumbnails_dir,\n                    thumbnail_video_percentage_from_start,\n                    generate_grid_instead_of_single,\n                    thumbnail_grid_tiles_per_side,\n                    params.generate_thumbnails,\n                ) {\n                    Ok(Some(thumbnail_path)) => {\n                        entry.thumbnail_path = Some(thumbnail_path);\n                        progress_handler.increase_items(1);\n                        Some(None)\n                    }\n                    Ok(None) => {\n                        progress_handler.increase_items(1);\n                        Some(None)\n                    }\n                    Err(e) => {\n                        progress_handler.increase_items(1);\n                        Some(Some(e))\n                    }\n                }\n            })\n            .while_some()\n            .flatten()\n            .collect::<Vec<String>>();\n\n        self.common_data.text_messages.warnings.extend(errors);\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"create_crop_thumbnails\", level = \"debug\")]\n    fn create_crop_thumbnails(&mut self, progress_sender: Option<&Sender<ProgressData>>, stop_flag: &Arc<AtomicBool>, params: &VideoCropParams) -> WorkContinueStatus {\n        let progress_handler = prepare_thread_handler_common(\n            progress_sender,\n            CurrentStage::VideoOptimizerCreatingThumbnails,\n            self.video_crop_result_entries.len(),\n            self.get_test_type(),\n            self.video_crop_result_entries.iter().map(|e| e.size).sum(),\n        );\n\n        let Some(config_cache_path) = get_config_cache_path() else {\n            return WorkContinueStatus::Continue;\n        };\n\n        let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER);\n        if let Err(e) = std::fs::create_dir_all(&thumbnails_dir) {\n            debug!(\"Failed to create thumbnails directory: {e}\");\n            return WorkContinueStatus::Continue;\n        }\n\n        let thumbnail_video_percentage_from_start = params.thumbnail_video_percentage_from_start;\n        let generate_grid_instead_of_single = params.generate_thumbnail_grid_instead_of_single;\n        let thumbnail_grid_tiles_per_side = params.thumbnail_grid_tiles_per_side;\n\n        let errors = self\n            .video_crop_result_entries\n            .par_iter_mut()\n            .map(|entry| {\n                if check_if_stop_received(stop_flag) {\n                    return None;\n                }\n\n                let result = generate_thumbnail(\n                    stop_flag,\n                    &entry.path,\n                    entry.size,\n                    entry.modified_date,\n                    Some(entry.duration),\n                    &thumbnails_dir,\n                    thumbnail_video_percentage_from_start,\n                    generate_grid_instead_of_single,\n                    thumbnail_grid_tiles_per_side,\n                    params.generate_thumbnails,\n                );\n\n                match result {\n                    Ok(Some(thumbnail_path)) => {\n                        entry.thumbnail_path = Some(thumbnail_path);\n                        progress_handler.increase_items(1);\n                        Some(None)\n                    }\n                    Ok(None) => {\n                        progress_handler.increase_items(1);\n                        Some(None)\n                    }\n                    Err(e) => {\n                        progress_handler.increase_items(1);\n                        Some(Some(e))\n                    }\n                }\n            })\n            .while_some()\n            .flatten()\n            .collect::<Vec<String>>();\n\n        self.common_data.text_messages.warnings.extend(errors);\n\n        progress_handler.join_thread();\n        if check_if_stop_received(stop_flag) {\n            return WorkContinueStatus::Stop;\n        }\n\n        WorkContinueStatus::Continue\n    }\n\n    #[fun_time(message = \"load_video_transcode_cache\", level = \"debug\")]\n    fn load_video_transcode_cache(\n        &mut self,\n    ) -> (\n        BTreeMap<String, VideoTranscodeEntry>,\n        BTreeMap<String, VideoTranscodeEntry>,\n        BTreeMap<String, VideoTranscodeEntry>,\n    ) {\n        load_and_split_cache_generalized_by_path(&get_video_transcode_cache_file(), mem::take(&mut self.video_transcode_test_entries), self)\n    }\n\n    #[fun_time(message = \"load_video_crop_cache\", level = \"debug\")]\n    fn load_video_crop_cache(&mut self, params: &VideoCropParams) -> (BTreeMap<String, VideoCropEntry>, BTreeMap<String, VideoCropEntry>, BTreeMap<String, VideoCropEntry>) {\n        load_and_split_cache_generalized_by_path(&get_video_crop_cache_file(params), mem::take(&mut self.video_crop_test_entries), self)\n    }\n\n    #[fun_time(message = \"save_video_transcode_cache\", level = \"debug\")]\n    fn save_video_transcode_cache(&mut self, vec_file_entry: &[VideoTranscodeEntry], loaded_hash_map: BTreeMap<String, VideoTranscodeEntry>) {\n        save_and_connect_cache_generalized_by_path(&get_video_transcode_cache_file(), vec_file_entry, loaded_hash_map, self);\n    }\n\n    #[fun_time(message = \"save_video_crop_cache\", level = \"debug\")]\n    fn save_video_crop_cache(&mut self, vec_file_entry: &[VideoCropEntry], params: &VideoCropParams, loaded_hash_map: BTreeMap<String, VideoCropEntry>) {\n        save_and_connect_cache_generalized_by_path(&get_video_crop_cache_file(params), vec_file_entry, loaded_hash_map, self);\n    }\n\n    #[fun_time(message = \"fix_files\", level = \"debug\")]\n    pub(crate) fn fix_files(&mut self, stop_flag: &Arc<AtomicBool>, _progress_sender: Option<&Sender<ProgressData>>, fix_params: VideoOptimizerFixParams) {\n        match self.params.clone() {\n            VideoOptimizerParameters::VideoTranscode(_) => {\n                let VideoOptimizerFixParams::VideoTranscode(video_transcode_params) = fix_params else {\n                    unreachable!(\"VideoTranscode mode should have VideoTranscode fix_params(caller is responsible for that)\");\n                };\n\n                let transcode_warnings: Vec<_> = mem::take(&mut self.video_transcode_result_entries)\n                    .into_par_iter()\n                    .map(|entry| {\n                        if check_if_stop_received(stop_flag) {\n                            return None;\n                        }\n\n                        match process_video(stop_flag, &entry.path.to_string_lossy(), entry.size, video_transcode_params) {\n                            Ok(_new_size) => Some(None),\n                            Err(e) => Some(Some(flc!(\"core_failed_to_optimize_video\", file = entry.path.to_string_lossy(), reason = e))),\n                        }\n                    })\n                    .while_some()\n                    .flatten()\n                    .collect();\n\n                self.common_data.text_messages.warnings.extend(transcode_warnings);\n            }\n            VideoOptimizerParameters::VideoCrop(_) => {\n                let VideoOptimizerFixParams::VideoCrop(video_crop_params) = fix_params else {\n                    unreachable!(\"VideoCrop mode should have VideoCrop fix_params(caller is responsible for that)\");\n                };\n\n                let crop_warnings: Vec<_> = mem::take(&mut self.video_crop_result_entries)\n                    .into_par_iter()\n                    .map(|entry| {\n                        if check_if_stop_received(stop_flag) {\n                            return None;\n                        }\n\n                        let (left, top, right, bottom) = entry.new_image_dimensions;\n                        let entry_crop_params = VideoCropSingleFixParams {\n                            overwrite_original: video_crop_params.overwrite_original,\n                            target_codec: video_crop_params.target_codec,\n                            quality: video_crop_params.quality,\n                            crop_rectangle: (left, top, right, bottom),\n                            crop_mechanism: video_crop_params.crop_mechanism,\n                        };\n\n                        match fix_video_crop(&entry.path, &entry_crop_params, stop_flag, &entry.codec) {\n                            Ok(()) => Some(None),\n                            Err(e) => Some(Some(flc!(\"core_failed_to_crop_video\", file = entry.path.to_string_lossy(), reason = e))),\n                        }\n                    })\n                    .while_some()\n                    .flatten()\n                    .collect();\n\n                self.common_data.text_messages.warnings.extend(crop_warnings);\n            }\n        }\n    }\n}\n\npub fn get_video_transcode_cache_file() -> String {\n    format!(\"cache_video_transcode_{CACHE_VIDEO_OPTIMIZE_VERSION}.bin\")\n}\n\npub fn get_video_crop_cache_file(params: &VideoCropParams) -> String {\n    format!(\n        \"cache_video_crop_{CACHE_VIDEO_OPTIMIZE_VERSION}_{:?}_t{}_p{}_s{}_c{}.bin\",\n        params.crop_detect, params.black_pixel_threshold, params.black_bar_min_percentage, params.max_samples, params.min_crop_size\n    )\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/video_optimizer/mod.rs",
    "content": "pub mod core;\n#[cfg(test)]\nmod tests;\npub mod traits;\n\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::common::model::FileEntry;\nuse crate::common::tool_data::CommonToolData;\nuse crate::common::traits::ResultEntry;\nuse crate::flc;\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum VideoCodec {\n    H264,\n    H265,\n    Av1,\n    Vp9,\n}\n\nimpl VideoCodec {\n    pub const fn as_str(&self) -> &str {\n        match self {\n            Self::H264 => \"libx264\",\n            Self::H265 => \"libx265\",\n            Self::Av1 => \"libaom-av1\",\n            Self::Vp9 => \"libvpx-vp9\",\n        }\n    }\n\n    pub const fn as_ffprobe_codec_name(self) -> &'static str {\n        match self {\n            Self::H264 => \"h264\",\n            Self::H265 => \"h265\",\n            Self::Av1 => \"av1\",\n            Self::Vp9 => \"vp9\",\n        }\n    }\n}\n\nimpl std::str::FromStr for VideoCodec {\n    type Err = String;\n\n    fn from_str(codec: &str) -> Result<Self, Self::Err> {\n        match codec.to_lowercase().as_str() {\n            \"h264\" | \"libx264\" => Ok(Self::H264),\n            \"h265\" | \"hevc\" | \"libx265\" => Ok(Self::H265),\n            \"av1\" | \"libaom-av1\" => Ok(Self::Av1),\n            \"vp9\" | \"libvpx-vp9\" => Ok(Self::Vp9),\n            _ => Err(flc!(\"core_unknown_codec\", codec = codec)),\n        }\n    }\n}\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum VideoCroppingMechanism {\n    BlackBars,\n    StaticContent,\n}\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum VideoOptimizerMode {\n    VideoTranscode,\n    VideoCrop,\n}\n\nimpl std::str::FromStr for VideoOptimizerMode {\n    type Err = String;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s.to_lowercase().as_str() {\n            \"transcode\" | \"videotranscode\" => Ok(Self::VideoTranscode),\n            \"crop\" | \"videocrop\" => Ok(Self::VideoCrop),\n            _ => Err(flc!(\"core_invalid_video_optimizer_mode\", mode = s)),\n        }\n    }\n}\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub enum VideoOptimizerFixParams {\n    VideoTranscode(VideoTranscodeFixParams),\n    VideoCrop(VideoCropFixParams),\n}\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub struct VideoTranscodeFixParams {\n    pub codec: VideoCodec,\n    pub quality: u32,\n    pub fail_if_not_smaller: bool,\n    pub overwrite_original: bool,\n    pub limit_video_size: bool,\n    pub max_width: u32,\n    pub max_height: u32,\n}\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub struct VideoCropSingleFixParams {\n    pub overwrite_original: bool,\n    pub target_codec: Option<VideoCodec>,\n    pub quality: Option<u32>,\n    pub crop_rectangle: (u32, u32, u32, u32),\n    pub crop_mechanism: VideoCroppingMechanism,\n}\n\n#[derive(Copy, Clone, Eq, PartialEq, Debug)]\npub struct VideoCropFixParams {\n    pub overwrite_original: bool,\n    pub target_codec: Option<VideoCodec>,\n    pub quality: Option<u32>,\n    pub crop_mechanism: VideoCroppingMechanism,\n}\n\n#[derive(Debug, Default, Clone, Copy)]\npub struct Info {\n    pub scanning_time: Duration,\n    pub number_of_videos_to_transcode: usize,\n    pub number_of_videos_to_crop: usize,\n}\n\n#[derive(Clone, PartialEq, Debug)]\npub enum VideoOptimizerParameters {\n    VideoTranscode(VideoTranscodeParams),\n    VideoCrop(VideoCropParams),\n}\n\nimpl VideoOptimizerParameters {\n    pub fn get_generate_number_of_items_in_thumbnail_grid(&self) -> u8 {\n        let (generate_thumbnail_grid_instead_of_single, thumbnail_grid_tiles_per_side) = match self {\n            Self::VideoTranscode(params) => (params.generate_thumbnail_grid_instead_of_single, params.thumbnail_grid_tiles_per_side),\n            Self::VideoCrop(params) => (params.generate_thumbnail_grid_instead_of_single, params.thumbnail_grid_tiles_per_side),\n        };\n\n        if generate_thumbnail_grid_instead_of_single { thumbnail_grid_tiles_per_side } else { 1 }\n    }\n}\n\n#[derive(Clone, Eq, PartialEq, Debug)]\npub struct VideoTranscodeParams {\n    pub(crate) excluded_codecs: Vec<String>,\n    pub(crate) generate_thumbnails: bool,\n    pub(crate) thumbnail_video_percentage_from_start: u8,\n    pub(crate) generate_thumbnail_grid_instead_of_single: bool,\n    pub(crate) thumbnail_grid_tiles_per_side: u8,\n}\n#[derive(Clone, PartialEq, Debug)]\npub struct VideoCropParams {\n    pub(crate) crop_detect: VideoCroppingMechanism,\n    pub(crate) black_pixel_threshold: u8,\n    pub(crate) black_bar_min_percentage: u8,\n    pub(crate) max_samples: usize,\n    pub(crate) min_crop_size: u32,\n    pub(crate) generate_thumbnails: bool,\n    pub(crate) thumbnail_video_percentage_from_start: u8,\n    pub(crate) generate_thumbnail_grid_instead_of_single: bool,\n    pub(crate) thumbnail_grid_tiles_per_side: u8,\n}\n\nimpl VideoTranscodeParams {\n    pub fn new(\n        excluded_codecs: Vec<String>,\n        generate_thumbnails: bool,\n        thumbnail_video_percentage_from_start: u8,\n        generate_thumbnail_grid_instead_of_single: bool,\n        thumbnail_grid_tiles_per_side: u8,\n    ) -> Self {\n        Self {\n            excluded_codecs,\n            generate_thumbnails,\n            thumbnail_video_percentage_from_start,\n            generate_thumbnail_grid_instead_of_single,\n            thumbnail_grid_tiles_per_side,\n        }\n    }\n}\nimpl Default for VideoTranscodeParams {\n    fn default() -> Self {\n        Self {\n            excluded_codecs: vec![\"hevc\".to_string(), \"h265\".to_string(), \"av1\".to_string(), \"vp9\".to_string()],\n            generate_thumbnails: false,\n            thumbnail_video_percentage_from_start: 10,\n            generate_thumbnail_grid_instead_of_single: false,\n            thumbnail_grid_tiles_per_side: 2,\n        }\n    }\n}\n\nimpl VideoCropParams {\n    pub fn with_custom_params(\n        crop_detect: VideoCroppingMechanism,\n        black_pixel_threshold: u8,\n        black_bar_min_percentage: u8,\n        max_samples: usize,\n        min_crop_size: u32,\n        generate_thumbnails: bool,\n        thumbnail_video_percentage_from_start: u8,\n        generate_thumbnail_grid_instead_of_single: bool,\n        thumbnail_grid_tiles_per_side: u8,\n    ) -> Self {\n        assert!(black_pixel_threshold <= 128, \"black_pixel_threshold must be 0-128, got {black_pixel_threshold}\");\n        assert!(\n            (50..=100).contains(&black_bar_min_percentage),\n            \"black_bar_min_percentage must be 50-100, got {black_bar_min_percentage}\"\n        );\n        assert!((5..=1000).contains(&max_samples), \"max_samples must be 5-1000, got {max_samples}\");\n        assert!((1..=1000).contains(&min_crop_size), \"min_crop_size must be 1-1000, got {min_crop_size}\");\n\n        Self {\n            crop_detect,\n            black_pixel_threshold,\n            black_bar_min_percentage,\n            max_samples,\n            min_crop_size,\n            generate_thumbnails,\n            thumbnail_video_percentage_from_start,\n            generate_thumbnail_grid_instead_of_single,\n            thumbnail_grid_tiles_per_side,\n        }\n    }\n}\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct VideoTranscodeEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub modified_date: u64,\n    pub error: Option<String>,\n\n    pub codec: String,\n    pub width: u32,\n    pub height: u32,\n    pub duration: f64,\n\n    #[serde(skip)] // Saving it to cache is bad idea, because cache can be moved to another locations\n    pub thumbnail_path: Option<PathBuf>,\n}\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct VideoCropEntry {\n    pub path: PathBuf,\n    pub size: u64,\n    pub modified_date: u64,\n    pub error: Option<String>,\n\n    pub codec: String,\n    pub width: u32,\n    pub height: u32,\n    pub new_image_dimensions: (u32, u32, u32, u32),\n    pub duration: f64,\n\n    #[serde(skip)] // Saving it to cache is bad idea, because cache can be moved to another locations\n    pub thumbnail_path: Option<PathBuf>,\n}\n\nimpl ResultEntry for VideoTranscodeEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl ResultEntry for VideoCropEntry {\n    fn get_path(&self) -> &Path {\n        &self.path\n    }\n    fn get_modified_date(&self) -> u64 {\n        self.modified_date\n    }\n    fn get_size(&self) -> u64 {\n        self.size\n    }\n}\n\nimpl FileEntry {\n    fn into_video_transcode_entry(self) -> VideoTranscodeEntry {\n        VideoTranscodeEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n            error: None,\n            codec: String::new(),\n            width: 0,\n            height: 0,\n            duration: 0.0,\n            thumbnail_path: None,\n        }\n    }\n\n    fn into_video_crop_entry(self) -> VideoCropEntry {\n        VideoCropEntry {\n            size: self.size,\n            path: self.path,\n            modified_date: self.modified_date,\n            error: None,\n            codec: String::new(),\n            width: 0,\n            height: 0,\n            new_image_dimensions: (0, 0, 0, 0),\n            duration: 0.0,\n            thumbnail_path: None,\n        }\n    }\n}\n\npub enum VideoOptimizerEntry {\n    VideoTranscode(VideoTranscodeEntry),\n    VideoCrop(VideoCropEntry),\n}\n\npub struct VideoOptimizer {\n    common_data: CommonToolData,\n    information: Info,\n    video_transcode_test_entries: BTreeMap<String, VideoTranscodeEntry>,\n    video_crop_test_entries: BTreeMap<String, VideoCropEntry>,\n    video_transcode_result_entries: Vec<VideoTranscodeEntry>,\n    video_crop_result_entries: Vec<VideoCropEntry>,\n    params: VideoOptimizerParameters,\n}\n\nimpl VideoOptimizer {\n    pub const fn get_video_transcode_entries(&self) -> &Vec<VideoTranscodeEntry> {\n        &self.video_transcode_result_entries\n    }\n\n    pub const fn get_video_crop_entries(&self) -> &Vec<VideoCropEntry> {\n        &self.video_crop_result_entries\n    }\n\n    pub const fn get_params(&self) -> &VideoOptimizerParameters {\n        &self.params\n    }\n\n    pub const fn get_information(&self) -> Info {\n        self.information\n    }\n}\n"
  },
  {
    "path": "czkawka_core/src/tools/video_optimizer/tests.rs",
    "content": "\n"
  },
  {
    "path": "czkawka_core/src/tools/video_optimizer/traits.rs",
    "content": "use std::io::Write;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\nuse std::time::Instant;\n\nuse crossbeam_channel::Sender;\nuse fun_time::fun_time;\nuse humansize::{BINARY, format_size};\n\nuse crate::common::consts::VIDEO_FILES_EXTENSIONS;\nuse crate::common::ffmpeg_utils::check_if_ffprobe_ffmpeg_exists;\nuse crate::common::model::WorkContinueStatus;\nuse crate::common::progress_data::ProgressData;\nuse crate::common::tool_data::{CommonData, CommonToolData};\nuse crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search};\nuse crate::flc;\nuse crate::tools::video_optimizer::{Info, VideoOptimizer, VideoOptimizerFixParams, VideoOptimizerParameters};\n\nimpl AllTraits for VideoOptimizer {}\n\nimpl DeletingItems for VideoOptimizer {\n    #[fun_time(message = \"delete_files\", level = \"debug\")]\n    fn delete_files(&mut self, _stop_flag: &Arc<AtomicBool>, _progress_sender: Option<&Sender<ProgressData>>) -> WorkContinueStatus {\n        unreachable!(\"VideoOptimizer does not support deleting files\");\n    }\n}\n\nimpl FixingItems for VideoOptimizer {\n    type FixParams = VideoOptimizerFixParams;\n    #[fun_time(message = \"fix_items\", level = \"debug\")]\n    fn fix_items(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>, fix_params: Self::FixParams) {\n        self.fix_files(stop_flag, progress_sender, fix_params);\n    }\n}\n\nimpl DebugPrint for VideoOptimizer {\n    #[expect(clippy::print_stdout)]\n    fn debug_print(&self) {\n        if !cfg!(debug_assertions) || cfg!(test) {\n            return;\n        }\n\n        println!(\"### INDIVIDUAL DEBUG PRINT ###\");\n        println!(\"Info: {:?}\", self.information);\n        println!(\"Mode: {:?}\", self.params);\n        println!(\"Video transcode entries: {}\", self.video_transcode_result_entries.len());\n        println!(\"Video crop entries: {}\", self.video_crop_result_entries.len());\n        self.debug_print_common();\n        println!(\"-----------------------------------------\");\n    }\n}\n\nimpl PrintResults for VideoOptimizer {\n    fn write_results<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {\n        self.write_base_search_paths(writer)?;\n\n        match self.params.clone() {\n            VideoOptimizerParameters::VideoTranscode(_) => {\n                writeln!(writer)?;\n\n                let total_entries = self.video_transcode_result_entries.len();\n                let entries_needing_optimization = self.video_transcode_result_entries.iter().filter(|e| !e.codec.is_empty() && e.error.is_none()).count();\n                let failed_entries = self.video_transcode_result_entries.iter().filter(|e| e.error.is_some()).count();\n\n                writeln!(writer, \"Total files found: {total_entries}\")?;\n                writeln!(writer, \"Files needing optimization: {entries_needing_optimization}\")?;\n                writeln!(writer, \"Failed to analyze: {failed_entries}\")?;\n                writeln!(writer)?;\n\n                for entry in &self.video_transcode_result_entries {\n                    if !entry.codec.is_empty() {\n                        writeln!(\n                            writer,\n                            \"\\\"{}\\\" - Codec: {} - Dimensions: {}x{} - Size: {}\",\n                            entry.path.to_string_lossy(),\n                            entry.codec,\n                            entry.width,\n                            entry.height,\n                            format_size(entry.size, BINARY)\n                        )?;\n                    }\n                }\n            }\n            VideoOptimizerParameters::VideoCrop(_) => {\n                writeln!(writer)?;\n\n                let total_entries = self.video_crop_result_entries.len();\n                let entries_with_crop_info = self.video_crop_result_entries.iter().filter(|e| !e.codec.is_empty() && e.error.is_none()).count();\n                let failed_entries = self.video_crop_result_entries.iter().filter(|e| e.error.is_some()).count();\n\n                writeln!(writer, \"Total files found: {total_entries}\")?;\n                writeln!(writer, \"Files with crop information: {entries_with_crop_info}\")?;\n                writeln!(writer, \"Failed to analyze: {failed_entries}\")?;\n                writeln!(writer)?;\n\n                for entry in &self.video_crop_result_entries {\n                    if !entry.codec.is_empty() {\n                        let (lt, rt, rb, lb) = entry.new_image_dimensions;\n                        let new_image_dimensions = format!(\"  New dimensions: LT:{lt}, RT:{rt}, RB:{rb}, LB:{lb}\");\n                        writeln!(\n                            writer,\n                            \"\\\"{}\\\" - Codec: {} - Dimensions: {}x{} - Size: {}{new_image_dimensions}\",\n                            entry.path.to_string_lossy(),\n                            entry.codec,\n                            entry.width,\n                            entry.height,\n                            format_size(entry.size, BINARY)\n                        )?;\n                    }\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> {\n        match &self.params {\n            VideoOptimizerParameters::VideoTranscode(_) => self.save_results_to_file_as_json_internal(file_name, &self.video_transcode_result_entries, pretty_print),\n            VideoOptimizerParameters::VideoCrop(_) => self.save_results_to_file_as_json_internal(file_name, &self.video_crop_result_entries, pretty_print),\n        }\n    }\n}\n\nimpl Search for VideoOptimizer {\n    #[fun_time(message = \"scan_media_files\", level = \"info\")]\n    fn search(&mut self, stop_flag: &Arc<AtomicBool>, progress_sender: Option<&Sender<ProgressData>>) {\n        let start_time = Instant::now();\n\n        let () = (|| {\n            if !check_if_ffprobe_ffmpeg_exists() {\n                self.common_data.text_messages.critical = Some(flc!(\"core_ffmpeg_not_found\"));\n                #[cfg(target_os = \"windows\")]\n                self.common_data.text_messages.errors.push(flc!(\"core_ffmpeg_not_found_windows\"));\n                return;\n            }\n\n            if self.prepare_items(Some(VIDEO_FILES_EXTENSIONS)).is_err() {\n                return;\n            }\n            if self.scan_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n                return;\n            }\n            if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop {\n                self.common_data.stopped_search = true;\n            }\n        })();\n\n        self.information.scanning_time = start_time.elapsed();\n\n        if !self.common_data.stopped_search {\n            self.debug_print();\n        }\n    }\n}\n\nimpl CommonData for VideoOptimizer {\n    type Info = Info;\n    type Parameters = VideoOptimizerParameters;\n\n    fn get_information(&self) -> Self::Info {\n        self.information\n    }\n\n    fn get_params(&self) -> Self::Parameters {\n        self.params.clone()\n    }\n\n    fn get_cd(&self) -> &CommonToolData {\n        &self.common_data\n    }\n\n    fn get_cd_mut(&mut self) -> &mut CommonToolData {\n        &mut self.common_data\n    }\n\n    fn found_any_items(&self) -> bool {\n        match &self.params {\n            VideoOptimizerParameters::VideoTranscode(_) => self.information.number_of_videos_to_transcode > 0,\n            VideoOptimizerParameters::VideoCrop(_) => self.information.number_of_videos_to_crop > 0,\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/Cargo.toml",
    "content": "[package]\nname = \"czkawka_gui\"\nversion = \"11.0.1\"\nauthors = [\"Rafał Mikrut <mikrutrafal@protonmail.com>\"]\nedition = \"2024\"\nrust-version = \"1.92.0\"\ndescription = \"GTK frontend of Czkawka\"\nlicense = \"MIT\"\nhomepage = \"https://github.com/qarmin/czkawka\"\nrepository = \"https://github.com/qarmin/czkawka\"\n\n[dependencies]\ngdk4 = { version = \"0.11.0\", default-features = false, features = [\"v4_6\"] }\nglib = \"0.22.0\"\ngtk4 = { version = \"0.11.0\", default-features = false, features = [\"v4_6\"] }\n\nhumansize = \"2.1\"\nchrono = \"0.4.38\"\ncrossbeam-channel = \"0.5\"\ndirectories-next = \"2.0\"\nopen = \"5.3\"\nimage = \"0.25\"\nregex = \"1.11\"\nfs_extra = \"1.3\"\ndunce = \"1.0.5\"\n\ni18n-embed = { version = \"0.16\", features = [\"fluent-system\", \"desktop-requester\"] }\ni18n-embed-fl = \"0.10\"\nrust-embed = { version = \"8.5\", features = [\"debug-embed\"] }\nonce_cell = \"1.20\"\n\nlog = \"0.4.22\"\nfun_time = { version = \"0.3\", features = [\"log\"] }\nrayon = \"1.10\"\n\nczkawka_core = { path = \"../czkawka_core\", version = \"11.0.1\", features = [] }\n\nresvg = { version = \"0.47.0\", default-features = false }\nserde_json = \"1.0.142\"\nserde = { version = \"1.0.219\", features = [\"derive\"] }\nitertools = \"0.14.0\"\nrand = \"0.10.0\"\n\n[dev-dependencies]\n\n[target.'cfg(windows)'.dependencies]\nwinapi = { version = \"0.3.9\", features = [\"combaseapi\", \"objbase\", \"shobjidl_core\", \"windef\", \"winerror\", \"wtypesbase\", \"winuser\"] }\n\n[features]\ndefault = []\nheif = [\"czkawka_core/heif\"]\nlibraw = [\"czkawka_core/libraw\"]\nlibavif = [\"czkawka_core/libavif\"]\n# Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails\n# No-op on other OSes, it is slower and provides less helpful error messages\nxdg_portal_trash = [\"czkawka_core/xdg_portal_trash\"]\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "czkawka_gui/LICENSE_CC_BY_4_ICONS",
    "content": "All icons, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).\n\nCopyright (c) 2020 [jannuary](https://github.com/jannuary)\n- icons/icon_about.png\n- icons/icon.ico\n\nCopyright (c) 2020-2026 Rafał Mikrut\n- icons/*.svg\n\nLicense: CC-BY-4.0\n Creative Commons Attribution 4.0 International Public License\n .\n By exercising the Licensed Rights (defined below), You accept and agree\n to be bound by the terms and conditions of this Creative Commons\n Attribution 4.0 International Public License (\"Public\n License\"). To the extent this Public License may be interpreted as a\n contract, You are granted the Licensed Rights in consideration of Your\n acceptance of these terms and conditions, and the Licensor grants You\n such rights in consideration of benefits the Licensor receives from\n making the Licensed Material available under these terms and\n conditions.\n .\n Section 1 -- Definitions.\n .\n a. Adapted Material means material subject to Copyright and Similar\n Rights that is derived from or based upon the Licensed Material\n and in which the Licensed Material is translated, altered,\n arranged, transformed, or otherwise modified in a manner requiring\n permission under the Copyright and Similar Rights held by the\n Licensor. For purposes of this Public License, where the Licensed\n Material is a musical work, performance, or sound recording,\n Adapted Material is always produced where the Licensed Material is\n synched in timed relation with a moving image.\n .\n b. Adapter's License means the license You apply to Your Copyright\n and Similar Rights in Your contributions to Adapted Material in\n accordance with the terms and conditions of this Public License.\n .\n c. Copyright and Similar Rights means copyright and/or similar rights\n closely related to copyright including, without limitation,\n performance, broadcast, sound recording, and Sui Generis Database\n Rights, without regard to how the rights are labeled or\n categorized. For purposes of this Public License, the rights\n specified in Section 2(b)(1)-(2) are not Copyright and Similar\n Rights.\n .\n d. Effective Technological Measures means those measures that, in the\n absence of proper authority, may not be circumvented under laws\n fulfilling obligations under Article 11 of the WIPO Copyright\n Treaty adopted on December 20, 1996, and/or similar international\n agreements.\n .\n e. Exceptions and Limitations means fair use, fair dealing, and/or\n any other exception or limitation to Copyright and Similar Rights\n that applies to Your use of the Licensed Material.\n .\n f. Licensed Material means the artistic or literary work, database,\n or other material to which the Licensor applied this Public\n License.\n .\n g. Licensed Rights means the rights granted to You subject to the\n terms and conditions of this Public License, which are limited to\n all Copyright and Similar Rights that apply to Your use of the\n Licensed Material and that the Licensor has authority to license.\n .\n h. Licensor means the individual(s) or entity(ies) granting rights\n under this Public License.\n .\n i. Share means to provide material to the public by any means or\n process that requires permission under the Licensed Rights, such\n as reproduction, public display, public performance, distribution,\n dissemination, communication, or importation, and to make material\n available to the public including in ways that members of the\n public may access the material from a place and at a time\n individually chosen by them.\n .\n j. Sui Generis Database Rights means rights other than copyright\n resulting from Directive 96/9/EC of the European Parliament and of\n the Council of 11 March 1996 on the legal protection of databases,\n as amended and/or succeeded, as well as other essentially\n equivalent rights anywhere in the world.\n .\n k. You means the individual or entity exercising the Licensed Rights\n under this Public License. Your has a corresponding meaning.\n .\n Section 2 -- Scope.\n .\n a. License grant.\n .\n 1. Subject to the terms and conditions of this Public License,\n the Licensor hereby grants You a worldwide, royalty-free,\n non-sublicensable, non-exclusive, irrevocable license to\n exercise the Licensed Rights in the Licensed Material to:\n .\n a. reproduce and Share the Licensed Material, in whole or\n in part; and\n .\n b. produce, reproduce, and Share Adapted Material.\n .\n 2. Exceptions and Limitations. For the avoidance of doubt, where\n Exceptions and Limitations apply to Your use, this Public\n License does not apply, and You do not need to comply with\n its terms and conditions.\n .\n 3. Term. The term of this Public License is specified in Section\n 6(a).\n .\n 4. Media and formats; technical modifications allowed. The\n Licensor authorizes You to exercise the Licensed Rights in\n all media and formats whether now known or hereafter created,\n and to make technical modifications necessary to do so. The\n Licensor waives and/or agrees not to assert any right or\n authority to forbid You from making technical modifications\n necessary to exercise the Licensed Rights, including\n technical modifications necessary to circumvent Effective\n Technological Measures. For purposes of this Public License,\n simply making modifications authorized by this Section 2(a)\n (4) never produces Adapted Material.\n .\n 5. Downstream recipients.\n .\n a. Offer from the Licensor -- Licensed Material. Every\n recipient of the Licensed Material automatically\n receives an offer from the Licensor to exercise the\n Licensed Rights under the terms and conditions of this\n Public License.\n .\n b. No downstream restrictions. You may not offer or impose\n any additional or different terms or conditions on, or\n apply any Effective Technological Measures to, the\n Licensed Material if doing so restricts exercise of the\n Licensed Rights by any recipient of the Licensed\n Material.\n .\n 6. No endorsement. Nothing in this Public License constitutes or\n may be construed as permission to assert or imply that You\n are, or that Your use of the Licensed Material is, connected\n with, or sponsored, endorsed, or granted official status by,\n the Licensor or others designated to receive attribution as\n provided in Section 3(a)(1)(A)(i).\n .\n b. Other rights.\n .\n 1. Moral rights, such as the right of integrity, are not\n licensed under this Public License, nor are publicity,\n privacy, and/or other similar personality rights; however, to\n the extent possible, the Licensor waives and/or agrees not to\n assert any such rights held by the Licensor to the limited\n extent necessary to allow You to exercise the Licensed\n Rights, but not otherwise.\n .\n 2. Patent and trademark rights are not licensed under this\n Public License.\n .\n 3. To the extent possible, the Licensor waives any right to\n collect royalties from You for the exercise of the Licensed\n Rights, whether directly or through a collecting society\n under any voluntary or waivable statutory or compulsory\n licensing scheme. In all other cases the Licensor expressly\n reserves any right to collect such royalties.\n .\n Section 3 -- License Conditions.\n .\n Your exercise of the Licensed Rights is expressly made subject to the\n following conditions.\n .\n a. Attribution.\n .\n 1. If You Share the Licensed Material (including in modified\n form), You must:\n .\n a. retain the following if it is supplied by the Licensor\n with the Licensed Material:\n .\n i. identification of the creator(s) of the Licensed\n Material and any others designated to receive\n attribution, in any reasonable manner requested by\n the Licensor (including by pseudonym if\n designated);\n .\n ii. a copyright notice;\n .\n iii. a notice that refers to this Public License;\n .\n iv. a notice that refers to the disclaimer of\n warranties;\n .\n v. a URI or hyperlink to the Licensed Material to the\n extent reasonably practicable;\n .\n b. indicate if You modified the Licensed Material and\n retain an indication of any previous modifications; and\n .\n c. indicate the Licensed Material is licensed under this\n Public License, and include the text of, or the URI or\n hyperlink to, this Public License.\n .\n 2. You may satisfy the conditions in Section 3(a)(1) in any\n reasonable manner based on the medium, means, and context in\n which You Share the Licensed Material. For example, it may be\n reasonable to satisfy the conditions by providing a URI or\n hyperlink to a resource that includes the required\n information.\n .\n 3. If requested by the Licensor, You must remove any of the\n information required by Section 3(a)(1)(A) to the extent\n reasonably practicable.\n .\n 4. If You Share Adapted Material You produce, the Adapter's\n License You apply must not prevent recipients of the Adapted\n Material from complying with this Public License.\n .\n Section 4 -- Sui Generis Database Rights.\n .\n Where the Licensed Rights include Sui Generis Database Rights that\n apply to Your use of the Licensed Material:\n .\n a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n to extract, reuse, reproduce, and Share all or a substantial\n portion of the contents of the database;\n .\n b. if You include all or a substantial portion of the database\n contents in a database in which You have Sui Generis Database\n Rights, then the database in which You have Sui Generis Database\n Rights (but not its individual contents) is Adapted Material; and\n .\n c. You must comply with the conditions in Section 3(a) if You Share\n all or a substantial portion of the contents of the database.\n .\n For the avoidance of doubt, this Section 4 supplements and does not\n replace Your obligations under this Public License where the Licensed\n Rights include other Copyright and Similar Rights.\n .\n Section 5 -- Disclaimer of Warranties and Limitation of Liability.\n .\n a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n .\n b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n .\n c. The disclaimer of warranties and limitation of liability provided\n above shall be interpreted in a manner that, to the extent\n possible, most closely approximates an absolute disclaimer and\n waiver of all liability.\n .\n Section 6 -- Term and Termination.\n .\n a. This Public License applies for the term of the Copyright and\n Similar Rights licensed here. However, if You fail to comply with\n this Public License, then Your rights under this Public License\n terminate automatically.\n .\n b. Where Your right to use the Licensed Material has terminated under\n Section 6(a), it reinstates:\n .\n 1. automatically as of the date the violation is cured, provided\n it is cured within 30 days of Your discovery of the\n violation; or\n .\n 2. upon express reinstatement by the Licensor.\n .\n For the avoidance of doubt, this Section 6(b) does not affect any\n right the Licensor may have to seek remedies for Your violations\n of this Public License.\n .\n c. For the avoidance of doubt, the Licensor may also offer the\n Licensed Material under separate terms or conditions or stop\n distributing the Licensed Material at any time; however, doing so\n will not terminate this Public License.\n .\n d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n License.\n .\n Section 7 -- Other Terms and Conditions.\n .\n a. The Licensor shall not be bound by any additional or different\n terms or conditions communicated by You unless expressly agreed.\n .\n b. Any arrangements, understandings, or agreements regarding the\n Licensed Material not stated herein are separate from and\n independent of the terms and conditions of this Public License.\n .\n Section 8 -- Interpretation.\n .\n a. For the avoidance of doubt, this Public License does not, and\n shall not be interpreted to, reduce, limit, restrict, or impose\n conditions on any use of the Licensed Material that could lawfully\n be made without permission under this Public License.\n .\n b. To the extent possible, if any provision of this Public License is\n deemed unenforceable, it shall be automatically reformed to the\n minimum extent necessary to make it enforceable. If the provision\n cannot be reformed, it shall be severed from this Public License\n without affecting the enforceability of the remaining terms and\n conditions.\n .\n c. No term or condition of this Public License will be waived and no\n failure to comply consented to unless expressly agreed to by the\n Licensor.\n .\n d. Nothing in this Public License constitutes or may be interpreted\n as a limitation upon, or waiver of, any privileges and immunities\n that apply to the Licensor or You, including from the legal\n processes of any jurisdiction or authority.\n"
  },
  {
    "path": "czkawka_gui/LICENSE_MIT_APP_CODE",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "czkawka_gui/LICENSE_MIT_WINDOWS_THEME",
    "content": "(Used only in prebuild-binaries)\n\nMIT License\n\nCopyright (c) 2019-2020 Nick Rhodes\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "czkawka_gui/README.md",
    "content": "![czkawka_logo](https://user-images.githubusercontent.com/41945903/102616149-66490400-4137-11eb-9cd6-813b2b070834.png)\n\nCzkawka GUI is a graphical user interface for Czkawka Core, built with GTK 4.\n\n![Screenshot from 2023-11-26 12-43-32](https://github.com/qarmin/czkawka/assets/41945903/722ed490-0be1-4dac-bcfc-182a4d0787dc)\n\n## Maintenance Mode\n\nCzkawka GTK is currently in maintenance mode.  \nThis means that new features will be kept to an absolute minimum, and only critical bugs will be fixed.  Compatibility updates with the Czkawka core package will still be provided to ensure that the application continues to compile correctly.  \nActive development is now focused on the Krokiet GUI.\n\n## Requirements\n\nRequirements depend on your platform.\n\nPrebuilt binaries are available here: https://github.com/qarmin/czkawka/releases/\n\nAdditional features such as HEIF, libraw, and libavif require extra libraries to be installed, which may increase the number of dependencies.\n\n### Linux\n\n#### Prebuilt binaries / Self-compiled\n\nUbuntu:  \n`sudo apt install libgtk-4-bin libheif1 libraw-bin ffmpeg -y`\n\n### Mac\n\n```\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\nbrew install gtk4 ffmpeg librsvg libheif libraw dav1d\n```\n\n### Windows\n\n#### Prebuilt binaries\nAll required libraries are bundled in the zip (except ffmpeg, which you can install manually and place `ffmpeg.exe` in a directory included in your system PATH).\n\n## Installation\n\n### Prebuilt binaries (All OS)\nAfter installing the required dependencies, download the prebuilt binaries for your platform from the [releases page](https://github.com/qarmin/czkawka/releases).\n\n### Linux\n\n#### Flatpak\n```\nflatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo\nflatpak install flathub com.github.qarmin.czkawka\n```\n\n#### Debian package (Unofficial) \nRequires Debian 13 (or derivatives) or later.\n```\nsudo apt install czkawka_gui\n```\n\n#### PPA (Unofficial) - Debian-based distributions (Ubuntu, Linux Mint, etc.)\n```\nsudo add-apt-repository ppa:xtradeb/apps\nsudo apt update\nsudo apt install czkawka\n```\n[PPA page](https://launchpad.net/~xtradeb/+archive/ubuntu/apps)\n\n### Mac\n\n#### Homebrew (Unofficial)\n```\nbrew install czkawka\n```\n[Formula page](https://formulae.brew.sh/formula/czkawka)\n\n### Windows\n\n#### MSYS2 (Unofficial)\n```\npacman -S mingw-w64-x86_64-czkawka-gui\n```\n[Package link](https://packages.msys2.org/base/mingw-w64-czkawka)\n\nThe file should be installed to `C:\\msys64\\mingw64\\bin\\czkawka_gui.exe` and can be run from there.  \nThis version is likely the most feature-complete on Windows, as it is compiled with optional features enabled.\n\n## Compilation\n\nCompiling the GUI is more complex than compiling the CLI, core, or Krokiet, because it uses GTK4 (written in C) and requires many build and runtime dependencies.\n\n### Requirements\n\n| Program | Minimal version |\n|:-------:|:---------------:|\n|  Rust   |     1.92.0      | \n|   GTK   |       4.6       |\n\nThe Rust version corresponds to the latest rustc available in Debian Sid: https://packages.debian.org/sid/rustc\n\n### Linux (Ubuntu; similar steps apply to other distributions)\n\n```shell\nsudo apt install libgtk-4-dev -y # Base\nsudo apt install libgtk-4-dev libheif-dev libraw-dev libavif-dev libdav1d-dev -y # With features\ncargo run --release --bin czkawka_gui\n# Or with support for heif, libraw, libavif\ncargo run --release --bin czkawka_gui --features \"heif,libraw,libavif\"\n```\n\n### Mac\n\n```shell\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\nbrew install rustup gtk4 adwaita-icon-theme ffmpeg librsvg libheif libraw dav1d pkg-config\nrustup-init\ncargo run --release --bin czkawka_gui\n# Or with support for heif, libraw, libavif\ncargo run --release --bin czkawka_gui --features \"heif,libraw,libavif\"\n```\n\n### Windows\n\nCurrently, there are no instructions for compiling the app natively on Windows.</br>\nYou can check the CI for instructions on how to cross-compile the app from Linux to Windows (using a prebuilt Docker image): [CI Instructions](../.github/workflows/windows.yml)</br>\nThere is also a mingw recipe you can try to adapt for your needs: https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-czkawka/PKGBUILD\n\n## Limitations\n\nNot all features and components are implemented here. The main limitations are:\n\n- The Windows version does not support HEIF and WebP files with prebuilt binaries (the MSYS2 version supports them).\n- On Windows, text may appear very small on high-resolution displays. You can manually change DPI scaling for this app:\n    - [Recommended fix](https://github.com/qarmin/czkawka/issues/787#issuecomment-1292253437) (modify gtk.css)\n    - [Alternative workaround](https://github.com/qarmin/czkawka/issues/863#issuecomment-1416761308) (modify Windows DPI settings for this app; this works too, but the text may be a bit blurry).\n\n## License\n\nThe code is distributed under the MIT license.\n\nThe icon was created by [jannuary](https://github.com/jannuary) and is licensed under CC-BY-4.0.\n\nThe Windows dark theme is from the [WhiteSur](https://github.com/slypy/whitesur-gtk4-theme) project, licensed under MIT.\n\nThe program is completely free to use.\n\n\"Gratis to uczciwa cena\" - \"Free is a fair price\"\n\n## Name\n\nCzkawka is a Polish word meaning _hiccup_.\n\nI chose this name because I wanted to hear people speaking other languages pronounce it, so feel free to say it however you like.\n\nThis name is not as difficult as it seems; I also considered words like _żółć_, _gżegżółka_, or _żołądź_, but decided against them because they contain Polish characters, which would make searching for the project harder.\n\nAt the beginning of the project, if the response to the name was unanimously negative, I was prepared to change it, but the opinions were extremely mixed.\n"
  },
  {
    "path": "czkawka_gui/i18n/ar/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = الإعدادات\nwindow_main_title = Czkawka\nwindow_progress_title = المسح\nwindow_compare_images = مقارنة الصور\n# General\ngeneral_ok_button = حسناً\ngeneral_close_button = أغلق\n# Krokiet info dialog\nkrokiet_info_title = تقديم Krokiet - نسخة جديدة من Czkawka\nkrokiet_info_message = \n        كروكيت هو الإصدار الجديد والمحسّن والأسرع والأكثر موثوقية لـ Czkawka GTK GUI!\n\n        إنه أسهل في التشغيل وأكثر مقاومة للتغييرات في النظام، لأنه يعتمد فقط على المكتبات الأساسية المتاحة افتراضيًا على معظم الأنظمة.\n\n        كروكيت أيضًا يقدم ميزات يفتقر إليها Czkawka، بما في ذلك الصور المصغرة في وضع مقارنة الفيديو، ومسحّف EXIF، وخيارات تقدم نقل/نسخ/حذف الملفات أو ترتيب موسع.\n\n        جربه بنفسك وشاهد الفرق!\n\n        ستواصل Czkawka تلقي إصلاحات الأخطاء والتحديثات الصغيرة مني، ولكن جميع الميزات الجديدة ستتم تطويرها حصريًا لكروكيت، وأي شخص حر في المساهمة بميزات جديدة أو إضافة أوضاع مفقودة أو توسيع Czkawka بشكل أكبر.\n\n        ملاحظة: يجب أن يظهر هذا الرسالة مرة واحدة فقط. إذا ظهر مرة أخرى، قم بتعيين متغير البيئة CZKAWKA_DONT_ANNOY_ME إلى أي قيمة غير فارغة.\n# Main window\nmusic_title_checkbox = العنوان\nmusic_artist_checkbox = الفنان\nmusic_year_checkbox = السنة\nmusic_bitrate_checkbox = معدل\nmusic_genre_checkbox = النوع\nmusic_length_checkbox = طول\nmusic_comparison_checkbox = مقارنة تقريبية\nmusic_checking_by_tags = الوسوم\nmusic_checking_by_content = محتوى\nsame_music_seconds_label = الحد الأدنى من مدة التجزئة الثانية\nsame_music_similarity_label = الفرق الأقصى\nmusic_compare_only_in_title_group = مقارنة داخل مجموعات من العناوين المتشابهة\nmusic_compare_only_in_title_group_tooltip =\n    عند تمكينه، يتم تجميع الملفات حسب العنوان ومن ثم مقارنتها ببعضها البعض.\n    \n    بمليون ملف من أصل عشرة آلاف ملف، بدلاً من حوالي مليارية مقارنات عادةً ستكون حول 20000 مقارنة.\nsame_music_tooltip =\n    يمكن تكوين البحث عن ملفات موسيقية مشابهة بواسطة محتواها عن طريق الإعداد:\n    \n    - الحد الأدنى لوقت الشظايا الذي يمكن بعدها تحديد ملفات الموسيقى على أنها\n    - الحد الأقصى للفارق بين جزأين تم اختبارهما\n    \n    والمفتاح إلى النتائج الجيدة هو العثور على مجموعات معقولة من هذه المعلمات، عن تقديمه.\n    \n    تحديد الحد الأدنى من الوقت إلى 5 ثوان والحد الأقصى للفرق إلى 1.0، سيبحث عن أجزاء متطابقة تقريبا في الملفات.\n    وقت 20 ثانية وفارق أقصى قدره 6.0، من ناحية أخرى، يعمل بشكل جيد من أجل العثور على تعديلات أو إصدارات حية وما إلى ذلك.\n    \n    بشكل افتراضي، يتم مقارنة كل ملف موسيقي بآخر وقد يستغرق ذلك الكثير من الوقت عند اختبار العديد من الملفات، لذلك من الأفضل عادة استخدام المجلدات المرجعية وتحديد الملفات التي يجب مقارنتها مع بعضها البعض (مع نفس كمية الملفات)، مقارنة بصمات الأصابع ستكون أسرع من 4 × على الأقل من دون مجلدات مرجعية).\nmusic_comparison_checkbox_tooltip =\n    يبحث عن ملفات الموسيقى المشابهة باستخدام الذكاء الاصطناعي، الذي يستخدم التعلم الآلي لإزالة الأقواس من الجملة. على سبيل المثال، مع تمكين هذا الخيار، سيتم النظر في الملفات محل النقاش كتردّدات:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = حالة حساسة\nduplicate_case_sensitive_name_tooltip =\n    عند تمكين هذا الخيار، قم بتجميع السجلات فقط عندما يكون لديها اسمًا متطابقًا تمامًا مثل: Żołd <-> Żołd\n    \n    تعطيل هذا الخيار سيرتبب الأسماء دون التحقق من كون كل حرف بنفس الحجم مثل: żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = الحجم والاسم\nduplicate_mode_name_combo_box = الاسم\nduplicate_mode_size_combo_box = الحجم\nduplicate_mode_hash_combo_box = التجزئة\nduplicate_hash_type_tooltip =\n    يقدم Czkawka 3 أنواع من التجزئة:\n    \n    Blake3 - دالة التجزئة المشفرة. هذا هو الافتراضي لأنه سريع جدا.\n    \n    CRC32 - دالة التجزئة البسيطة. وينبغي أن يكون هذا أسرع من بليك 3، ولكن نادرا ما تحدث بعض الاصطدام.\n    \n    XXH3 - مشابهة جدا في الأداء وجودة التجزئة للـ Blake3 (ولكن غير مشفرة). لذلك يمكن بسهولة تبادل مثل هذه الأوضاع.\nduplicate_check_method_tooltip =\n    في الوقت الحالي، تقدم Czkawka ثلاثة أنواع من الطرق للعثور على التكرارات:\n    \n    Name - Finds الملفات التي تحمل نفس الاسم.\n    \n    الحجم - العثور على الملفات التي لها نفس الحجم.\n    \n    Hash - العثور على الملفات التي لها نفس المحتوى. هذا الوضع يقوم بتجزئة الملف ثم يقارن هذا التجزئة للعثور على التكرار. هذا الوضع هو أكثر الطرق أماناً للعثور على التكرار. يستخدم التطبيق بكثافة ذاكرة التخزين المؤقت، لذا يجب أن تكون المسح الثاني والمزيد لنفس البيانات أسرع بكثير من الأول.\nimage_hash_size_tooltip =\n    كل صورة تم فحصها تنتج تجزئة خاصة يمكن مقارنتها مع بعضها البعض، والاختلاف الصغير بينهما يعني أن هذه الصور متشابهة.\n    \n    8 حجم التجزئة جيد جدا للعثور على صور تشبه قليلا فقط الصور الأصلية. مع مجموعة أكبر من الصور (>1000)، هذا سوف ينتج كمية كبيرة من الإيجابيات الكاذبة، لذا أوصي باستخدام حجم تجزئة أكبر في هذه الحالة.\n    \n    16 هو حجم التجزئة الافتراضي الذي يمثل حلاً وسطاً جيداً بين العثور على صور مشابهة قليلاً فقط وبين حدوث عدد صغير من تصادم التجزئة.\n    \n    32 و64 تجزئة لا تجد سوى صور مشابهة جداً، ولكن ينبغي ألا يكون لها تقريباً إيجابيات كاذبة (ربما باستثناء بعض الصور مع قناة ألفا).\nimage_resize_filter_tooltip =\n    لحساب تشفير الصورة، يجب أولاً أن تقوم المكتبة بإعادة حجمها.\n    \n    تعتمد على الخوارزمية المختارة، ستحظى الصورة الناتجة المستخدمة لحساب التشفير بمظهر قليلاً ما يختلف.\n    \n    الخوارزمية الأسرع للاستخدام، ولكن أيضًا تلك التي تعطي أسوأ النتائج، هي Nearest. يتم تمكينها افتراضيًا، لأن مع حجم التشفير 16x16، فإن الجودة المنخفضة غير مرئية حقاً.\n    \n    مع حجم التشفير 8x8، يُنصح باستخدام خوارزمية مختلفة عن Nearest لتحسين مجموعات الصور.\nimage_hash_alg_tooltip =\n    يمكن للمستخدمين الاختيار من واحدة من خوارزميات عديدة لحساب التجزئة.\n    \n    لكل منها نقاط قوية وأضعف وسوف تعطي أحيانا نتائج أفضل وأحيانا أسوأ لصور مختلفة.\n    \n    لذلك ، لتحديد أفضل واحد لك، يتطلب الاختبار اليدوي.\nbig_files_mode_combobox_tooltip = يسمح بالبحث عن ملفات أصغر/أكبر\nbig_files_mode_label = الملفات المحددة\nbig_files_mode_smallest_combo_box = الأصغر حجماً\nbig_files_mode_biggest_combo_box = الاكبر\nmain_notebook_duplicates = الملفات المكررة\nmain_notebook_empty_directories = دلائل فارغة\nmain_notebook_big_files = الملفات الكبيرة\nmain_notebook_empty_files = الملفات الفارغة\nmain_notebook_temporary = ملفات مؤقتة\nmain_notebook_similar_images = صور مشابهة\nmain_notebook_similar_videos = مقاطع فيديو مماثلة\nmain_notebook_same_music = مكرر الموسيقى\nmain_notebook_symlinks = الروابط الرمزية غير صالحة\nmain_notebook_broken_files = الملفات المكسورة\nmain_notebook_bad_extensions = ملحقات سيئة\nmain_tree_view_column_file_name = اسم الملف\nmain_tree_view_column_folder_name = اسم المجلد\nmain_tree_view_column_path = المسار\nmain_tree_view_column_modification = تاريخ التعديل\nmain_tree_view_column_size = الحجم\nmain_tree_view_column_similarity = تماثل\nmain_tree_view_column_dimensions = الأبعاد\nmain_tree_view_column_title = العنوان\nmain_tree_view_column_artist = الفنان\nmain_tree_view_column_year = السنة\nmain_tree_view_column_bitrate = معدل\nmain_tree_view_column_length = طول\nmain_tree_view_column_genre = النوع\nmain_tree_view_column_symlink_file_name = اسم ملف الرابط الرمزي\nmain_tree_view_column_symlink_folder = مجلد الرابط الرمزي\nmain_tree_view_column_destination_path = مسار الوجهة\nmain_tree_view_column_type_of_error = نوع الخطأ\nmain_tree_view_column_current_extension = التمديد الحالي\nmain_tree_view_column_proper_extensions = التمديد الصحيح\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = ترميز\nmain_label_check_method = طريقة التحقق\nmain_label_hash_type = نوع التجزئة\nmain_label_hash_size = حجم التجزئة\nmain_label_size_bytes = الحجم (بايت)\nmain_label_min_size = الحد الأدنى\nmain_label_max_size = الحد الأقصى\nmain_label_shown_files = عدد الملفات المعروضة\nmain_label_resize_algorithm = تغيير حجم الخوارزمية\nmain_label_similarity = مشابهة{ \" \" }\nmain_check_box_broken_files_audio = الصوت\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = أرشيف\nmain_check_box_broken_files_image = صورة\nmain_check_box_broken_files_video = فيديو\nmain_check_box_broken_files_video_tooltip = يستخدم ffmpeg/ffprobe للتحقق من صحة ملفات الفيديو. بطيء جداً وقد يكتشف الأخطاء الضوئية حتى لو كان الملف يعمل بشكل جيد.\ncheck_button_general_same_size = تجاهل نفس الحجم\ncheck_button_general_same_size_tooltip = تجاهل الملفات ذات الحجم المتطابق في النتائج - عادة ما تكون هذه المكررة 1:1\nmain_label_size_bytes_tooltip = حجم الملفات التي سيتم استخدامها في المسح\n# Upper window\nupper_tree_view_included_folder_column_title = مجلدات للبحث\nupper_tree_view_included_reference_column_title = المجلدات المرجعية\nupper_recursive_button = متكرر\nupper_recursive_button_tooltip = إذا تم تحديده، ابحث أيضا عن الملفات التي لم توضع مباشرة تحت المجلدات المختارة.\nupper_manual_add_included_button = إضافة يدوي\nupper_add_included_button = إضافة\nupper_remove_included_button = إزالة\nupper_manual_add_excluded_button = إضافة يدوي\nupper_add_excluded_button = إضافة\nupper_remove_excluded_button = إزالة\nupper_manual_add_included_button_tooltip =\n    إضافة اسم الدليل للبحث باليد.\n    \n    لإضافة مسارات متعددة في وقت واحد، قم بفصلها بواسطة ؛\n    \n    /home/rozkaz سيضيف دليلين /home/rozkaz و /home/rozkaz\nupper_add_included_button_tooltip = إضافة دليل جديد للبحث.\nupper_remove_included_button_tooltip = حذف الدليل من البحث.\nupper_manual_add_excluded_button_tooltip =\n    إضافة اسم الدليل المستبعد يدوياً.\n    \n    لإضافة مسارات متعددة في وقت واحد، قم بفصلها بواسطة ؛\n    \n    /home/roman;/home/krokiet سيضيف دليلين / home/roman و /home/keokiet\nupper_add_excluded_button_tooltip = إضافة دليل ليتم استبعاده في البحث.\nupper_remove_excluded_button_tooltip = حذف الدليل من المستبعد.\nupper_notebook_items_configuration = تكوين العناصر\nupper_notebook_excluded_directories = المسارات المستبعدة\nupper_notebook_included_directories = المسارات المضمنة\nupper_allowed_extensions_tooltip =\n    يجب أن تكون الملحقات المسموح بها مفصولة بفواصل (بشكل افتراضي كلها متاحة).\n    \n    أجهزة الماكرو التالية، التي تضيف ملحقات متعددة في وقت واحد، متاحة أيضا: IMAGE، VIDEO، MUSIC، TEXT.\n    \n    مثال استخدام \".exe, IMAGE, VIDEO, .rar, 7z\" - وهذا يعني أن الصور (e. .jpg, png) الفيديوهات (مثلاً: avi, mp4) و ex, rar و 7z سيتم مسح الملفات.\nupper_excluded_extensions_tooltip =\n    قائمة الملفات المعطلة التي سيتم تجاهلها في المسح.\n    \n    عند استخدام الملحقات المسموح بها والمعطلة على حد سواء، هذه واحدة لها أولوية أعلى، لذلك لن يتم تحديد الملف.\nupper_excluded_items_tooltip = \n        يجب أن تتضمن العناصر المستبعدة * ويفصل بينها الفواصل.\n        هذا أبطأ من المسارات المستبعدة، لذا استخدمه بحذر.\nupper_excluded_items = البنود المستثناة:\nupper_allowed_extensions = الإضافات المسموح بها:\nupper_excluded_extensions = الملحقات المعطّلة:\n# Popovers\npopover_select_all = حدد الكل\npopover_unselect_all = إلغاء تحديد الكل\npopover_reverse = الاختيار العكسي\npopover_select_all_except_shortest_path = حدد الكل باستثناء أقصر مسار\npopover_select_all_except_longest_path = حدد الكل باستثناء أطول مسار\npopover_select_all_except_oldest = حدد الكل باستثناء الأقدم\npopover_select_all_except_newest = حدد الكل باستثناء الأحدث\npopover_select_one_oldest = حدد أقدم واحد\npopover_select_one_newest = حدد واحد أحدث\npopover_select_custom = تحديد مخصص\npopover_unselect_custom = إلغاء تحديد مخصص\npopover_select_all_images_except_biggest = حدد الكل باستثناء أكبر\npopover_select_all_images_except_smallest = حدد الكل باستثناء الأصغر\npopover_custom_path_check_button_entry_tooltip =\n    اختر السجلات بواسطة المسار.\n    \n    예시 استخدام:\n    /home/pimpek/rzecz.txt يمكن العثور عليه باستخدام /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    حدد السجلات حسب أسماء الملفات.\n    \n    استخدام مثال:\n    /usr/ping/pong.txt يمكن العثور عليه مع *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    حدد السجلات بواسطة Regex.\n    \n    مع هذا الوضع، النص الذي تم البحث عنه هو المسار بالاسم.\n    \n    مثال الاستخدام:\n    /usr/bin/ziemniak. يمكن العثور على xt مع /ziem[a-z]+\n    \n    يستخدم هذا التطبيق الافتراضي Rust regex . يمكنك قراءة المزيد عنه هنا: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    تمكين الكشف الحساس لحالة الأحرف.\n    \n    عند تعطيل / المنزل/* يجد كلا من /HoMe/roman و /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    تمنع اختيار جميع السجلات في المجموعة.\n    \n    هذا مفعل بالطبيعة، لأن في معظم الحالات لا تريد حذف كلاً من الملفات الأصلية والمكررة، ولكنك ترغب في ترك على الأقل ملف واحد.\n    \n    تحذير: هذه الإعداد لا يعمل إذا كنت قد اخترت يدويًا جميع النتائج في مجموعة محددة بالفعل.\npopover_custom_regex_path_label = المسار\npopover_custom_regex_name_label = الاسم\npopover_custom_regex_regex_label = مسار Regex + اسم\npopover_custom_case_sensitive_check_button = حساسية الحالة\npopover_custom_all_in_group_label = عدم تحديد جميع السجلات في المجموعة\npopover_custom_mode_unselect = إلغاء تحديد مخصص\npopover_custom_mode_select = تحديد مخصص\npopover_sort_file_name = اسم الملف\npopover_sort_folder_name = اسم المجلد\npopover_sort_full_name = الاسم الكامل\npopover_sort_size = الحجم\npopover_sort_selection = التحديد\npopover_invalid_regex = Regex غير صحيح\npopover_valid_regex = Regex صالح\n# Bottom buttons\nbottom_search_button = البحث\nbottom_select_button = حدد\nbottom_delete_button = حذف\nbottom_save_button = حفظ\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = نقل\nbottom_sort_button = فرز\nbottom_compare_button = قارن\nbottom_search_button_tooltip = بدء البحث\nbottom_select_button_tooltip = حدد السجلات. يمكن معالجة الملفات/المجلدات المحددة في وقت لاحق.\nbottom_delete_button_tooltip = حذف الملفات/المجلدات المحددة.\nbottom_save_button_tooltip = حفظ البيانات حول البحث في الملف\nbottom_symlink_button_tooltip =\n    إنشاء روابط رمزية.\n    يعمل فقط عندما يتم تحديد نتيجتين على الأقل في المجموعة.\n    أولا لم يتغير و الثاني و اللاحق مرتبطين بالأول.\nbottom_hardlink_button_tooltip =\n    إنشاء روابط صلبة.\n    يعمل فقط عندما يتم تحديد نتيجتين على الأقل في المجموعة.\n    أولا لم يتغير و الثاني و اللاحق متصلين بالأول.\nbottom_hardlink_button_not_available_tooltip =\n    قم بخلق روابط صعبة.\n    الزر معدم، لأن روابط صعبة لا يمكن إنشاؤها.\n    تworks فقط مع صلاحيات مدير في ويندوز، لذا تأكد من تشغيل التطبيق كمدير.\n    إذا كان التطبيق يعمل بالفعل بصلاحية مثل هذه، فقم بفحص مشاكل مماثلة على جيت هاب.\nbottom_move_button_tooltip =\n    ينقل الملفات إلى الدليل المختار.\n    ينسخ جميع الملفات إلى الدليل دون الحفاظ على شجرة الدليل.\n    عند محاولة نقل ملفين مع نفس الاسم إلى مجلد، سيتم فشل الثانية وإظهار الخطأ.\nbottom_sort_button_tooltip = ترتيب الملفات/المجلدات وفقا للطريقة المحددة.\nbottom_compare_button_tooltip = قارن الصور في المجموعة.\nbottom_show_errors_tooltip = إظهار/إخفاء لوحة النص السفلية.\nbottom_show_upper_notebook_tooltip = إظهار/إخفاء لوحة دفتر الملاحظات العلوية.\n# Progress Window\nprogress_stop_button = توقف\nprogress_stop_additional_message = إيقاف الطلب\n# About Window\nabout_repository_button_tooltip = رابط لصفحة المستودع مع رمز المصدر.\nabout_donation_button_tooltip = رابط لصفحة التبرع.\nabout_instruction_button_tooltip = رابط لصفحة التعليمات.\nabout_translation_button_tooltip = رابط إلى صفحة كراودِن مع ترجمة التطبيق. يتم دعم البولندية الرسمية والإنجليزية.\nabout_repository_button = المستودع\nabout_donation_button = تبرع\nabout_instruction_button = تعليمات\nabout_translation_button = الترجمة\n# Header\nheader_setting_button_tooltip = فتح مربع حوار الإعدادات.\nheader_about_button_tooltip = فتح مربع الحوار مع معلومات حول التطبيق.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = عدد المواضيع المستخدمة\nsettings_number_of_threads_tooltip = عدد المواضيع المستخدمة، 0 يعني أن جميع المواضيع المتاحة سيتم استخدامها.\nsettings_use_rust_preview = استخدام المكتبات الخارجية بدلاً من gtk لتحميل المعاينات\nsettings_use_rust_preview_tooltip =\n    وفي بعض الأحيان سيكون استخدام معاينات gtk أسرع ويدعم صيغا أكثر، ولكن في بعض الأحيان قد يكون الأمر على العكس تماما.\n    \n    إذا كان لديك مشاكل في تحميل المعاينات، فيمكنك محاولة تغيير هذا الإعداد.\n    \n    على أنظمة غير لينوكس، يوصى باستخدام هذا الخيار، لأن gtk-pixbuf غير متوفر دائمًا هناك لذلك فإن تعطيل هذا الخيار لن يقوم بتحميل المعاينات لبعض الصور.\nsettings_label_restart = تحتاج إلى إعادة تشغيل التطبيق لتطبيق الإعدادات!\nsettings_ignore_other_filesystems = تجاهل نظم الملفات الأخرى (Linux)\nsettings_ignore_other_filesystems_tooltip =\n    يتجاهل الملفات التي ليست في نفس نظام الملفات مثل الدلائل التي تم بحثها. يعمل\n    \n    مثل خيار -xdev في العثور على أمر على Linux\nsettings_save_at_exit_button_tooltip = حفظ التكوين إلى الملف عند إغلاق التطبيق.\nsettings_load_at_start_button_tooltip =\n    تحميل التكوين من الملف عند فتح التطبيق.\n    \n    إذا لم يتم تمكينه، سيتم استخدام الإعدادات الافتراضية.\nsettings_confirm_deletion_button_tooltip = إظهار مربع حوار التأكيد عند النقر على زر الحذف.\nsettings_confirm_link_button_tooltip = إظهار مربع حوار التأكيد عند النقر على زر الارتباط الصلب/الرمزي.\nsettings_confirm_group_deletion_button_tooltip = إظهار مربع حوار التحذير عند محاولة حذف جميع السجلات من المجموعة.\nsettings_show_text_view_button_tooltip = إظهار لوحة النص في أسفل واجهة المستخدم.\nsettings_use_cache_button_tooltip = استخدام ذاكرة التخزين المؤقت للملف.\nsettings_save_also_as_json_button_tooltip = حفظ ذاكرة التخزين المؤقت إلى تنسيق JSON (قابل للقراءة البشرية). من الممكن تعديل محتواه. الذاكرة المؤقتة من هذا الملف سيتم قراءتها تلقائيًا بواسطة التطبيق إذا كان مخبأ تنسيق ثنائي (مع امتداد بن ) مفقود.\nsettings_use_trash_button_tooltip = نقل الملفات إلى سلة المهملات بدلاً من حذفها بشكل دائم.\nsettings_language_label_tooltip = لغة واجهة المستخدم.\nsettings_save_at_exit_button = حفظ التكوين عند إغلاق التطبيق\nsettings_load_at_start_button = تحميل التكوين عند فتح التطبيق\nsettings_confirm_deletion_button = إظهار تأكيد مربع الحوار عند حذف أي ملفات\nsettings_confirm_link_button = إظهار مربع حوار تأكيد عند ربط أي ملفات بصعوبة/رموز\nsettings_confirm_group_deletion_button = إظهار تأكيد مربع الحوار عند حذف جميع الملفات في المجموعة\nsettings_show_text_view_button = إظهار لوحة النص السفلي\nsettings_use_cache_button = استخدام ذاكرة التخزين المؤقت\nsettings_save_also_as_json_button = حفظ ذاكرة التخزين المؤقت أيضا كملف JSON\nsettings_use_trash_button = نقل الملفات المحذوفة إلى سلة المهملات\nsettings_language_label = اللغة\nsettings_multiple_delete_outdated_cache_checkbutton = حذف إدخالات ذاكرة التخزين المؤقت القديمة تلقائياً\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    حذف نتائج التخزين المؤقت القديمة التي تشير إلى ملفات غير موجودة.\n    \n    عند تمكينها، تقوم التطبيق بضمان أن جميع السجلات تشير إلى ملفات صالحة عند تحميل السجلات (يتم تجاهل تلك المعطوبة).\n    \n    تعطيل هذا الخيار سيساعد في فحص الملفات على الأقراص الخارجية، بحيث لن يتم مسح دخول التخزين المؤقت المتعلقة بها في الفحص التالي.\n    \n    في حالة وجود مئات الآلاف من السجلات في التخزين المؤقت، يُنصح بتمكين هذا الخيار، مما سيسرع تحميل/حفظ التخزين المؤقت في بداية/نهاية الفحص.\nsettings_notebook_general = عمومي\nsettings_notebook_duplicates = مكرر\nsettings_notebook_images = صور مشابهة\nsettings_notebook_videos = فيديو مشابه\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = عرض المعاينة على الجانب الأيمن (عند تحديد ملف صورة).\nsettings_multiple_image_preview_checkbutton = عرض معاينة الصورة\nsettings_multiple_clear_cache_button_tooltip =\n    قم بإزالة ذاكرة التخزين المؤقت يدويًا للعناصر القديمة.\n    يجب استخدام هذا فقط إذا تم تعطيل الإزالة التلقائية.\nsettings_multiple_clear_cache_button = إزالة النتائج القديمة من ذاكرة التخزين المؤقت.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip = \n    يختبئ جميع الملفات باستثناء واحد، إذا أشار كل منها إلى نفس البيانات (وهو متصل بشكل صلب).  \n    \n    مثال: في حالة وجود سبع ملفات على дисك مترابطة ببيانات معينة وملف مختلف يحتوي على نفس البيانات ولكن inode مختلف，则继续翻译剩下的部分：\n    ملف inode، ثم في مستكشف الملفات المكرر، سيتم عرض只有一个唯一文件和一个来自硬链接的文件。.\nsettings_duplicates_minimal_size_entry_tooltip =\n    설정할 최소 파일 크기를 캐시에 저장할 것입니다.\n    작은 값을 선택하면 더 많은 기록이 생성됩니다. 이는 검색 속도가 빨라질 것이지만 캐시 로드/저장은 느려질 수 있습니다.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    تمكين التخزين المؤقت للتجزئة (تجزئة محسوبة من جزء صغير من الملف) مما يسمح برفض النتائج غير المكررة في وقت سابق.\n    \n    يتم تعطيله بشكل افتراضي لأنه يمكن أن يتسبب في تباطؤ في بعض الحالات.\n    \n    يوصى بشدة باستخدامها عند مسح مئات الألوف أو الملايين من الملفات، لأنه يمكن تسريع البحث عدة مرات.\nsettings_duplicates_prehash_minimal_entry_tooltip = الحجم الأدنى للإدخال المخبئ.\nsettings_duplicates_hide_hard_link_button = إخفاء الروابط الصلبة\nsettings_duplicates_prehash_checkbutton = استخدام ذاكرة التخزين المؤقت\nsettings_duplicates_minimal_size_cache_label = الحجم الأدنى للملفات (بالبايت) المحفوظة إلى ذاكرة التخزين المؤقت\nsettings_duplicates_minimal_size_cache_prehash_label = الحجم الأدنى للملفات (بالبايت) المحفوظة في ذاكرة التخزين المؤقت\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = حفظ الإعدادات الحالية إلى الملف.\nsettings_loading_button_tooltip = تحميل الإعدادات من الملف واستبدل الإعدادات الحالية بها.\nsettings_reset_button_tooltip = إعادة تعيين الإعدادات الحالية إلى الإعدادات الافتراضية.\nsettings_saving_button = حفظ التكوين\nsettings_loading_button = تحميل التكوين\nsettings_reset_button = إعادة ضبط الإعدادات\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    يفتح المجلد الذي تخزن فيه ملفات الكاش النصية.\n    \n    يمكن أن يؤدي تعديل ملفات الكاش إلى ظهور نتائج غير صالحة. ومع ذلك، يمكن أن يوفر تغيير المسار الوقت عند تحريك عدد كبير من الملفات إلى موقع مختلف.\n    \n    في حالة وجود مشاكل مع الكاش، يمكن إزالة هذه الملفات. التطبيق سيعيد إنشاءها تلقائيًا.\n    \n    يمكنك نسخ هذه الملفات بين الحواسيب للاستفادة من توفير الوقت في عملية المسح مرة أخرى للملفات (بالطبع إذا كانت لديهم هيكلة مجلدات مشابهة).\nsettings_folder_settings_open_tooltip =\n    يفتح المجلد الذي يحتوي على إعدادات Czkawka.\n    \n    تحذير: تعديل الإعدادات يدويًا قد يتعكر دفق العمل الخاص بك.\nsettings_folder_cache_open = فتح مجلد التخزين المؤقت\nsettings_folder_settings_open = فتح مجلد الإعدادات\n# Compute results\ncompute_stopped_by_user = تم إيقاف البحث من قبل المستخدم\ncompute_found_duplicates_hash_size = تم العثور على { $number_files } مكررة في { $number_groups } مجموعات أخذت { $size } في { $time }\ncompute_found_duplicates_name = تم العثور على { $number_files } مكررة في { $number_groups } مجموعات في { $time }\ncompute_found_empty_folders = تم العثور على { $number_files } مجلدات فارغة في { $time }\ncompute_found_empty_files = تم العثور على { $number_files } ملفات فارغة في { $time }\ncompute_found_big_files = تم العثور على { $number_files } ملفات كبيرة في { $time }\ncompute_found_temporary_files = تم العثور على { $number_files } ملفات مؤقتة في { $time }\ncompute_found_images = تم العثور على { $number_files } صور مماثلة في { $number_groups } مجموعات في { $time }\ncompute_found_videos = تم العثور على { $number_files } مقاطع فيديو مماثلة في { $number_groups } مجموعات في { $time }\ncompute_found_music = تم العثور على { $number_files } ملفات موسيقية مماثلة في { $number_groups } مجموعات في { $time }\ncompute_found_invalid_symlinks = تم العثور على { $number_files } روابط رموز غير صالحة في { $time }\ncompute_found_broken_files = تم العثور على { $number_files } ملفات مكسورة في { $time }\ncompute_found_bad_extensions = تم العثور على { $number_files } ملفات ذات ملحقات غير صالحة في { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] تم فحص ملف { $file_number }\n       *[other] تم فحص { $file_number } ملفًا\n    }\nprogress_scanning_extension_of_files = تم التحقق من ملحق من ملف { $file_checked }/{ $all_files }\nprogress_scanning_broken_files = تم التحقق من الملف { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_video = تم تجزئة فيديو { $file_checked }/{ $all_files }\nprogress_creating_video_thumbnails = تم إنشاء مصغرات للفيديو { $file_checked }/{ $all_files }\nprogress_scanning_image = تجزئة من { $file_checked }/{ $all_files } صورة ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = مقارنة { $file_checked }/{ $all_files } هاش الصورة\nprogress_scanning_music_tags_end = مقارنة العلامات { $file_checked }/{ $all_files } ملف الموسيقى\nprogress_scanning_music_tags = قراءة العلامات { $file_checked }/{ $all_files } ملف الموسيقى\nprogress_scanning_music_content_end = مقارنة بصمة الإصبع من { $file_checked }/{ $all_files } ملف موسيقي\nprogress_scanning_music_content = تم حساب بصمة الإصبع { $file_checked }/{ $all_files } ملف موسيقي ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] تم فحص مجلد { $folder_number }\n       *[other] تم فحص { $folder_number } مجلدًا\n    }\nprogress_scanning_size = حجم ملف { $file_number } المسح الضوئي\nprogress_scanning_size_name = اسم وحجم الملف { $file_number } الذي تم فحصه\nprogress_scanning_name = تم فحص اسم الملف { $file_number }\nprogress_analyzed_partial_hash = تم تحليل التجزئة الجزئية ل { $file_checked }/{ $all_files } ملفات ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = تم تحليل التجزئة الكاملة من ملفات { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = تحميل ذاكرة التخزين المؤقت\nprogress_prehash_cache_saving = حفظ ذاكرة التخزين المؤقت\nprogress_hash_cache_loading = تحميل ذاكرة التخزين المؤقت للتجزئة\nprogress_hash_cache_saving = حفظ ذاكرة التخزين المؤقت\nprogress_cache_loading = تحميل ذاكرة التخزين المؤقت\nprogress_cache_saving = حفظ ذاكرة التخزين المؤقت\nprogress_current_stage = المرحلة الحالية:{ \"\" }\nprogress_all_stages = جميع المراحل:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = حفظ التكوين إلى ملف { $name }.\nsaving_loading_saving_failure = فشل في حفظ بيانات التكوين إلى الملف { $name }، السبب { $reason }.\nsaving_loading_reset_configuration = تم مسح التكوين الحالي.\nsaving_loading_loading_success = تم تحميل إعدادات التطبيق بشكل صحيح.\nsaving_loading_failed_to_create_config_file = فشل في إنشاء ملف الإعداد\"{ $path }\"، السبب\"{ $reason }\".\nsaving_loading_failed_to_read_config_file = لا يمكن تحميل التكوين من \"{ $path }\" لأنه غير موجود أو ليس ملفا.\nsaving_loading_failed_to_read_data_from_file = لا يمكن قراءة البيانات من الملف\"{ $path }\"، السبب\"{ $reason }\".\n# Other\nselected_all_reference_folders = لا يمكن بدء البحث، عندما يتم تعيين جميع الدلائل كمجلدات مرجعية\nsearching_for_data = البحث عن البيانات، قد يستغرق بعض الوقت، يرجى الانتظار...\ntext_view_messages = الرسائل\ntext_view_warnings = التحذيرات\ntext_view_errors = أخطاء\nabout_window_motto = هذا البرنامج حر في الاستخدام وسوف يكون دائماً.\nkrokiet_new_app = Czkawka في وضع الصيانة، مما يعني أنه سيتم إصلاح الأخطاء الحرجة فقط ولن يتم إضافة أي ميزات جديدة. للحصول على ميزات جديدة، يرجى التحقق من تطبيق كروكييت الجديد، الذي أكثر استقراراً وأداء ولا يزال قيد التطوير النشط.\n# Various dialog\ndialogs_ask_next_time = اسأل المرة القادمة\nsymlink_failed = فشل الربط التكافلي { $name } إلى { $target }، السبب { $reason }\ndelete_title_dialog = تأكيد حذف\ndelete_question_label = هل أنت متأكد من أنك تريد حذف الملفات؟\ndelete_all_files_in_group_title = تأكيد حذف جميع الملفات في المجموعة\ndelete_all_files_in_group_label1 = ويتم اختيار جميع السجلات في بعض المجموعات.\ndelete_all_files_in_group_label2 = هل أنت متأكد من أنك تريد حذفهم؟\ndelete_items_label = { $items } سيتم حذف الملفات.\ndelete_items_groups_label = { $items } ملفات من { $groups } سيتم حذف المجموعات.\nhardlink_failed = فشل الربط { $name } إلى { $target }، السبب { $reason }\nhard_sym_invalid_selection_title_dialog = إختيار غير صالح مع بعض المجموعات\nhard_sym_invalid_selection_label_1 = في بعض المجموعات هناك رقم قياسي واحد تم اختياره وسيتم تجاهله.\nhard_sym_invalid_selection_label_2 = لتتمكن من صلابة / ربط هذه الملفات، يجب اختيار نتيجتين على الأقل في المجموعة.\nhard_sym_invalid_selection_label_3 = الأول في المجموعة معترف به على أنه أصلي ولا يتغير ولكن الثاني ثم يتم تعديله.\nhard_sym_link_title_dialog = تأكيد الرابط\nhard_sym_link_label = هل أنت متأكد من أنك تريد ربط هذه الملفات؟\nmove_folder_failed = فشل في نقل المجلد { $name }، السبب { $reason }\nmove_file_failed = فشل نقل الملف { $name }، السبب { $reason }\nmove_files_title_dialog = اختر مجلد تريد نقل الملفات المكررة إليه\nmove_files_choose_more_than_1_path = يمكن تحديد مسار واحد فقط لتكون قادرة على نسخ الملفات المكررة، المحددة { $path_number }.\nmove_stats = نقل بشكل صحيح { $num_files }/{ $all_files } عناصر\nsave_results_to_file = حفظت النتائج إلى ملفات txt و json في \"{ $name }\" مجلد.\nsearch_not_choosing_any_music = خطأ: يجب عليك تحديد مربع اختيار واحد على الأقل مع أنواع البحث عن الموسيقى.\nsearch_not_choosing_any_broken_files = خطأ: يجب عليك تحديد مربع اختيار واحد على الأقل مع نوع الملفات المحددة المكسورة.\ninclude_folders_dialog_title = مجلدات لتضمينها\nexclude_folders_dialog_title = مجلدات للاستبعاد\ninclude_manually_directories_dialog_title = إضافة دليل يدوياً\ncache_properly_cleared = مسح ذاكرة التخزين المؤقت بشكل صحيح\ncache_clear_duplicates_title = مسح ذاكرة التخزين المؤقت التكراري\ncache_clear_similar_images_title = مسح ذاكرة التخزين المؤقت مشابهة للصور\ncache_clear_similar_videos_title = مسح ذاكرة التخزين المؤقت المماثلة للفيديوهات\ncache_clear_message_label_1 = هل تريد مسح ذاكرة التخزين المؤقت للإدخالات العتيقة؟\ncache_clear_message_label_2 = هذه العملية ستزيل جميع إدخالات ذاكرة التخزين المؤقت التي تشير إلى ملفات غير صالحة.\ncache_clear_message_label_3 = قد يؤدي هذا إلى تسريع التحميل/الحفظ إلى ذاكرة التخزين المؤقت.\ncache_clear_message_label_4 = تحذير: العملية ستزيل جميع البيانات المخزنة مؤقتاً من الأقراص الخارجية الغير موصولة. لذلك سوف تحتاج كل تجزئة إلى التجديد.\n# Show preview\npreview_image_resize_failure = فشل تغيير حجم الصورة { $name }.\npreview_image_opening_failure = فشل في فتح الصورة { $name }، السبب { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = المجموعة { $current_group }/{ $all_groups } ({ $images_in_group } صورة)\ncompare_move_left_button = ل\ncompare_move_right_button = ر\n"
  },
  {
    "path": "czkawka_gui/i18n/bg/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Настройки\nwindow_main_title = Czkawka (Хълцук)\nwindow_progress_title = Сканиране\nwindow_compare_images = Сравни изображения\n# General\ngeneral_ok_button = Ок\ngeneral_close_button = Затвори\n# Krokiet info dialog\nkrokiet_info_title = Представяме ви Krokiet - Нова версия на Czkawka\nkrokiet_info_message = \n        Krokiet е новата, подобрена, по-бърза и по-надеждна версия на Czkawka GTK GUI!\n\n        По-лесно се изпълнява и е по-устойчив на системни промени, тъй като разчита само на основни библиотеки, налични по подразбиране на повечето системи.\n\n        Krokiet също така предлага функции, които Czkawka няма, включително миниатюри в режим на сравнение на видео, EXIF почистване, прогрес при преместване/копиране/изтриване на файлове или разширени опции за сортиране.\n\n        Опитайте го и вижте разликата!\n\n        Czkawka ще продължи да получава поправки на грешки и малки актуализации от мен, но всички нови функции ще бъдат разработени изключително за Krokiet, а всеки е свободен да допринася с нови функции, да добавя липсващи режими или да разширява Czkawka допълнително.\n\n        ПС: Това съобщение трябва да се появи само веднъж. Ако се появи отново, задайте променливата на средата CZKAWKA_DONT_ANNOY_ME на всяка непразна стойност.\n# Main window\nmusic_title_checkbox = Заглавие\nmusic_artist_checkbox = Изпълнител\nmusic_year_checkbox = Година\nmusic_bitrate_checkbox = Битрейт\nmusic_genre_checkbox = Жанр\nmusic_length_checkbox = Продължителност\nmusic_comparison_checkbox = Приблизително сравнение\nmusic_checking_by_tags = Етикети\nmusic_checking_by_content = Съдържание\nsame_music_seconds_label = Минимална продължителност на фрагмента в секунди\nsame_music_similarity_label = Максимална разлика\nmusic_compare_only_in_title_group = Сравни в групи от подобни заглавия\nmusic_compare_only_in_title_group_tooltip =\n    Когато е включено, файловете са групирани по заглавие и след това се сравняват едно с друго.\n    \n    С 10000 файла, вместо почти 100 милиона сравнения, обикновено ще има около 20000 сравнения.\nsame_music_tooltip =\n    Търсенето на подобни музикални файлове по съдържание може да се конфигурира чрез настройка:\n    \n    - Минималното време на фрагмента, след което музикалните файлове могат да бъдат идентифицирани като подобни\n    - Максимална разлика между два тествани фрагмента\n    \n    Ключът към добрите резултати е да се намерят разумни комбинации от тези параметри, например.\n    \n    Ако зададете минималното време на 5 s, а максималната разлика на 1,0, ще търсите почти идентични фрагменти във файловете.\n    От друга страна, време от 20 s и максимална разлика от 6,0 работят добре за намиране на ремикси/живи версии и т. н.\n    \n    По подразбиране всеки музикален файл се сравнява един с друг и това може да отнеме много време при тестване на много файлове, така че обикновено е по-добре да се използват референтни папки и да се укаже кои файлове да се сравняват един с друг(при същото количество файлове сравняването на отпечатъци ще бъде по-бързо поне 4 пъти, отколкото без референтни папки).\nmusic_comparison_checkbox_tooltip =\n    Програмата търси подобни музикални файлове с помощта на изкуствен интелект, който използва машинно обучение за премахване на скоби от фраза. Например, при активирана тази опция въпросните файлове ще се считат за дубликати:\n    \n    Świędziżłób --- Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Чувствително изписване\nduplicate_case_sensitive_name_tooltip =\n    Когато е разрешено, групата записва само записи с едно и също име, напр. Żołd <-> Żołd\n    \n    При деактивиране на тази опция имената ще се групират, без да се проверява дали всяка буква е с еднакъв размер, напр. żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Размер и име\nduplicate_mode_name_combo_box = Име\nduplicate_mode_size_combo_box = Размер\nduplicate_mode_hash_combo_box = Хеш\nduplicate_hash_type_tooltip =\n    Czkawka предлага 3 вида хешове:\n    \n    Blake3 - криптографска хеш функция. Тя е избрана по подразбиране, тъй като е много бърза.\n    \n    CRC32 - проста хеш функция. Тя би трябвало да е по-бърза от Blake3, но много рядко може да има някои колизии.\n    \n    XXH3 - много подобна по производителност и качество на хеширане на Blake3 (но некриптографска). Така че тези режими могат лесно да се сменят.\nduplicate_check_method_tooltip =\n    Засега Czkawka предлага три вида методи за намиране на дубликати чрез:\n    \n    Име - Намира файлове с еднакво име.\n    \n    Размер - Намира файлове с еднакъв размер.\n    \n    Hash - Намира файлове с еднакво съдържание. Този режим хешира файла и по-късно сравнява този хеш, за да намери дубликати. Този режим е най-сигурният начин за намиране на дубликати. Приложението използва силно кеша, така че второто и следващите сканирания на едни и същи данни би трябвало да са много по-бързи от първото.\nimage_hash_size_tooltip =\n    Всяко сравнено изображение дава специален хеш, който може да бъде сравнен с другите и малка разлика между тях означава че изображенията са близки.\n    \n    Размер 8 хеш е сравнително добър за намиране на изображения, които са близки до оригинала. С по-голям набор изображения (>1000), това ще доведе до голяма бройка фалшиви позитивни, така че препоръчвам да се ползва по-голям размер на хеша в този случай.\n    \n    16 е размер по подразбиране, който е сравнително добър компромис между намиране на малки разлики в изображенията и имайки малко хеш колизии.\n    \n    32 и 64 хешове намират само много сходни изображения, но ще имат почти никакви фалшиви позитивни (с изключение на някой изображения с алфа канал).\nimage_resize_filter_tooltip =\n    За да изчисли хеша на изображението, библиотеката трябва първо да го оразмери.\n    \n    В зависимост от избрания алгоритъм, крайното изображение използвано за изчисляване на хеша може да изглежда леко различно.\n    \n    Най-бързият алгоритъм, но и даващ най-лоши резултати е Най-Близък. Използва се по-подразбиране, защото хеш с размер 16х16 с ниско качество не е толкова видимо.\n    \n    С 8х8 хеш, се препоръчва да се използва различен алгоритъм от Най-Близък за да има по-добри групи изображения.\nimage_hash_alg_tooltip =\n    Потребителите могат да изберат един от многото алгоритми за изчисляване на хеша.\n    \n    Всеки от тях има както силни, така и слаби страни и понякога дава по-добри, а понякога по-лоши резултати за различни изображения.\n    \n    Затова, за да определите най-добрия за вас, е необходимо ръчно тестване.\nbig_files_mode_combobox_tooltip = Позволява търсене на най-малките/най-големите файлове\nbig_files_mode_label = Проверени файлове\nbig_files_mode_smallest_combo_box = Най-малкия\nbig_files_mode_biggest_combo_box = Най-големия\nmain_notebook_duplicates = Повтарящи се файлове\nmain_notebook_empty_directories = Празни директории\nmain_notebook_big_files = Големи файлове\nmain_notebook_empty_files = Празни файлове\nmain_notebook_temporary = Временни файлове\nmain_notebook_similar_images = Подобни изображения\nmain_notebook_similar_videos = Подобни видеа\nmain_notebook_same_music = Музикални дубликати\nmain_notebook_symlinks = Невалидни симлинкове\nmain_notebook_broken_files = Повредени файлове\nmain_notebook_bad_extensions = Повредени разширения\nmain_tree_view_column_file_name = Име на файла\nmain_tree_view_column_folder_name = Име на папката\nmain_tree_view_column_path = Път\nmain_tree_view_column_modification = Дата на промяна\nmain_tree_view_column_size = Размер\nmain_tree_view_column_similarity = Прилика\nmain_tree_view_column_dimensions = Размери\nmain_tree_view_column_title = Заглавие\nmain_tree_view_column_artist = Изпълнител\nmain_tree_view_column_year = Година\nmain_tree_view_column_bitrate = Битрейт\nmain_tree_view_column_length = Дължина\nmain_tree_view_column_genre = Жанр\nmain_tree_view_column_symlink_file_name = Име на файла на Symlink\nmain_tree_view_column_symlink_folder = Symlink папка\nmain_tree_view_column_destination_path = Път за местоположение\nmain_tree_view_column_type_of_error = Тип на грешка\nmain_tree_view_column_current_extension = Избрано разширение\nmain_tree_view_column_proper_extensions = Правилно разширение\nmain_tree_view_column_fps = БПС\nmain_tree_view_column_codec = Кодек\nmain_label_check_method = Провери метод\nmain_label_hash_type = Хеш тип\nmain_label_hash_size = Хеш размер\nmain_label_size_bytes = Размер (байтове)\nmain_label_min_size = Мин\nmain_label_max_size = Макс\nmain_label_shown_files = Брой на показани файлове\nmain_label_resize_algorithm = Преоразмери алгоритъма\nmain_label_similarity = Сходство{ \" \" }\nmain_check_box_broken_files_audio = Аудио\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = Архив\nmain_check_box_broken_files_image = Изображение\nmain_check_box_broken_files_video = Видео\nmain_check_box_broken_files_video_tooltip = Използва ffmpeg/ffprobe за валидиране на видео файлове. Доста бавно и може да открие педантични грешки дори ако файлът се възпроизвежда добре.\ncheck_button_general_same_size = Игнорирай еднакъв размер\ncheck_button_general_same_size_tooltip = Игнорирай файлове с идентичен размер в резултата - обикновено това са 1:1 дубликати\nmain_label_size_bytes_tooltip = Размер на файловете, които ще се използват при сканиране\n# Upper window\nupper_tree_view_included_folder_column_title = Папки за търсене\nupper_tree_view_included_reference_column_title = Папки за справка\nupper_recursive_button = Рекурсивен\nupper_recursive_button_tooltip = Ако е избрано, се търсят и файлове, които не са поставени директно в избраните папки.\nupper_manual_add_included_button = Ръчно добавяне\nupper_add_included_button = Добави\nupper_remove_included_button = Премахни\nupper_manual_add_excluded_button = Ръчно добавяне\nupper_add_excluded_button = Добави\nupper_remove_excluded_button = Премахни\nupper_manual_add_included_button_tooltip =\n    Добавяне на име на директория за ръчно търсене.\n    \n    За да добавите няколко пътища наведнъж, разделете ги с ;\n    \n    /home/roman;/home/rozkaz ще добави две директории /home/roman и /home/rozkaz\nupper_add_included_button_tooltip = Добавяне на нова директория за търсене.\nupper_remove_included_button_tooltip = Изтриване на директорията от търсенето.\nupper_manual_add_excluded_button_tooltip =\n    Добавете името на изключената директория на ръка.\n    \n    За да добавите няколко пътя наведнъж, разделете ги с ;\n    \n    /home/roman;/home/krokiet ще добави две директории /home/roman и /home/keokiet\nupper_add_excluded_button_tooltip = Добавяне на директория, която да бъде изключена при търсене.\nupper_remove_excluded_button_tooltip = Изтриване на директория от изключените.\nupper_notebook_items_configuration = Конфигурация на елементите\nupper_notebook_excluded_directories = Изключени пътища\nupper_notebook_included_directories = Включени пътища\nupper_allowed_extensions_tooltip =\n    Разрешените разширения трябва да бъдат разделени със запетаи (по подразбиране са налични всички).\n    \n    Налични са и следните макроси, които добавят няколко разширения наведнъж: ИЗОБРАЖЕНИЕ, ВИДЕО, МУЗИКА, ТЕКСТ.\n    \n    Пример за използване \".exe, IMAGE, VIDEO, .rar, 7z\" - това означава, че ще бъдат сканирани изображения (напр. jpg, png), видеоклипове (напр. avi, mp4), файлове exe, rar и 7z.\nupper_excluded_extensions_tooltip =\n    Списък с изключени от търсенето файлове.\n    \n    Когато се ползват едновременно включени и изключени разширения, този тук има по-голям приоритет и файла няма да бъде проверен.\nupper_excluded_items_tooltip = \n        Изключените елементи трябва да съдържат * wildcard и да бъдат разделени с ками.\n        Това е по-бавно от Excluded Paths, така че използвайте внимателно.\nupper_excluded_items = Изключени елементи:\nupper_allowed_extensions = Разрешени разширения:\nupper_excluded_extensions = Изключени разширения:\n# Popovers\npopover_select_all = Избери всички\npopover_unselect_all = Размаркирайте всички\npopover_reverse = Избери обратното\npopover_select_all_except_shortest_path = Изберете всички, освен най-краткия път\npopover_select_all_except_longest_path = Изберете всички, освен най-дълъг път\npopover_select_all_except_oldest = Избери всички освен най-старото\npopover_select_all_except_newest = Избери всички освен най-новото\npopover_select_one_oldest = Избери най-старото\npopover_select_one_newest = Избери най-новото\npopover_select_custom = Избери по избор\npopover_unselect_custom = Размаркирай по избор\npopover_select_all_images_except_biggest = Избери всички освен най-големия\npopover_select_all_images_except_smallest = Избери всички освен най-малкия\npopover_custom_path_check_button_entry_tooltip =\n    Изберете записи по път.\n    \n    Пример за използване:\n    /home/pimpek/rzecz.txt може да бъде намерен с /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Изберете записи по имена на файлове.\n    \n    Пример за използване:\n    /usr/ping/pong.txt може да бъде намерен с *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Избиране на записи по зададен Regex.\n    \n    В този режим търсеният текст е Path with Name.\n    \n    Пример за използване:\n    /usr/bin/ziemniak.txt може да бъде намерен с /ziem[a-z]+\n    \n    В този случай се използва имплементацията на regex по подразбиране на Rust. Можете да прочетете повече за нея тук: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Активира откриването с отчитане на големи и малки букви.\n    \n    Когато е изключено, /home/* намира както /HoMe/roman, така и /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Предотвратява избирането на всички записи в групата.\n    \n    Това е разрешено по подразбиране, тъй като в повечето ситуации не искате да изтривате и оригиналните, и дублираните файлове, а искате да оставите поне един файл.\n    \n    ПРЕДУПРЕЖДЕНИЕ: Тази настройка не работи, ако вече сте избрали ръчно всички резултати в групата.\npopover_custom_regex_path_label = Път\npopover_custom_regex_name_label = Име\npopover_custom_regex_regex_label = Regex Път + Име\npopover_custom_case_sensitive_check_button = Чувствителност на буквите\npopover_custom_all_in_group_label = Да не се избират всички записи в групата\npopover_custom_mode_unselect = Премахване на избора по избор\npopover_custom_mode_select = Избери по избор\npopover_sort_file_name = Име на файла\npopover_sort_folder_name = Име на папката\npopover_sort_full_name = Пълно име\npopover_sort_size = Размер\npopover_sort_selection = Избор\npopover_invalid_regex = Regex е невалиден\npopover_valid_regex = Regex е валиден\n# Bottom buttons\nbottom_search_button = Търсене\nbottom_select_button = Избери\nbottom_delete_button = Изтрий\nbottom_save_button = Запази\nbottom_symlink_button = Симлинк\nbottom_hardlink_button = Хардлинк\nbottom_move_button = Премести\nbottom_sort_button = Сортирай\nbottom_compare_button = Сравни\nbottom_search_button_tooltip = Започни търсене\nbottom_select_button_tooltip = Изберете записи. Само избраните файлове/папки могат да бъдат обработени по-късно.\nbottom_delete_button_tooltip = Изтрий избрани файлове/папки.\nbottom_save_button_tooltip = Записване на данни за търсенето във файл\nbottom_symlink_button_tooltip =\n    Създаване на символни връзки.\n    Работи само когато са избрани поне два резултата в група.\n    Първият е непроменен, а вторият и по-късните са символни връзки към първия.\nbottom_hardlink_button_tooltip =\n    Създаване на твърди връзки.\n    Работи само когато са избрани поне два резултата в група.\n    Първият е непроменен, а вторият и по-късните са свързани с първия.\nbottom_hardlink_button_not_available_tooltip =\n    Създаване на твърди връзки.\n    Бутонът е деактивиран, тъй като не могат да се създават твърди връзки.\n    Хардлинковете работят само с администраторски права в Windows, затова не забравяйте да стартирате приложението като администратор.\n    Ако приложението вече работи с такива привилегии, проверете за подобни проблеми в Github.\nbottom_move_button_tooltip =\n    Премества файлове в избрана директория.\n    Той копира всички файлове в директорията, без да запазва дървото на директориите.\n    При опит за преместване на два файла с еднакво име в папка, вторият ще се провали и ще покаже грешка.\nbottom_sort_button_tooltip = Сортира файловете/папките според избрания метод.\nbottom_compare_button_tooltip = Сравни изображенията в групата.\nbottom_show_errors_tooltip = Показване/скриване на долния текстов панел.\nbottom_show_upper_notebook_tooltip = Показване/скриване на горния панел на бележника.\n# Progress Window\nprogress_stop_button = Спри\nprogress_stop_additional_message = Спри избраните\n# About Window\nabout_repository_button_tooltip = Връзка към страницата на хранилището с изходния код.\nabout_donation_button_tooltip = Връзка към страницата за дарения.\nabout_instruction_button_tooltip = Връзка към страницата с инструкции.\nabout_translation_button_tooltip = Връзка към страницата на Crowdin с преводи на приложения. Официално се поддържат полски и английски език.\nabout_repository_button = Хранилище\nabout_donation_button = Дарение\nabout_instruction_button = Инструкции\nabout_translation_button = Преводи\n# Header\nheader_setting_button_tooltip = Отваря диалогов прозорец за настройки.\nheader_about_button_tooltip = Отваря диалогов прозорец с информация за приложението.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Брой използвани нишки\nsettings_number_of_threads_tooltip = Брой използвани нишки, 0 означава, че ще бъдат използвани всички налични нишки.\nsettings_use_rust_preview = Използвай външна библиотека вместо GTK да зареди визуализацията\nsettings_use_rust_preview_tooltip =\n    Използвайки GTK визуализации, понякога ще е по-бързо и ще поддържа повече формати, но понякога това може да е точно обратното.\n    \n    Ако имате проблеми със зареждането на визуализации, можете да пробвате да промените тази настройка.\n    \n    На не Linux-ови системи е препоръчително да ползвате тази опция, защото gtk-pixbuf не винаги е налично, затова изключването на тази опция може да спре зареждането на визуализациите на някои изображения.\nsettings_label_restart = Трябва да рестартирате приложението, за да приложите настройките!\nsettings_ignore_other_filesystems = Игнориране на други файлови системи (само за Linux)\nsettings_ignore_other_filesystems_tooltip =\n    игнорира файлове, които не са в същата файлова система като търсените директории.\n    \n    Работи по същия начин като опцията -xdev в командата find в Linux\nsettings_save_at_exit_button_tooltip = Записване на конфигурацията във файл при затваряне на приложението.\nsettings_load_at_start_button_tooltip =\n    Зареждане на конфигурацията от файл при отваряне на приложението.\n    \n    Ако не е разрешено, ще се използват настройките по подразбиране.\nsettings_confirm_deletion_button_tooltip = Показване на диалогов прозорец за потвърждение при натискане на бутона за изтриване.\nsettings_confirm_link_button_tooltip = Показване на диалогов прозорец за потвърждение, когато щракнете върху бутона за твърда/симултанна връзка.\nsettings_confirm_group_deletion_button_tooltip = Показване на диалогов прозорец с предупреждение при опит за изтриване на всички записи от групата.\nsettings_show_text_view_button_tooltip = Показване на текстовия панел в долната част на потребителския интерфейс.\nsettings_use_cache_button_tooltip = Използвайте кеш за файлове.\nsettings_save_also_as_json_button_tooltip = Записване на кеша в (разбираем за човека) формат JSON. Възможно е да променяте съдържанието му. Кешът от този файл ще бъде прочетен автоматично от приложението, ако липсва кеш в двоичен формат (с разширение bin).\nsettings_use_trash_button_tooltip = Премества файловете в кошчето, вместо да ги изтрие окончателно.\nsettings_language_label_tooltip = Език за потребителски интерфейс.\nsettings_save_at_exit_button = Запазване на конфигурацията при затваряне на приложението\nsettings_load_at_start_button = Зареждане на конфигурацията при отваряне на приложението\nsettings_confirm_deletion_button = Показване на диалогов прозорец за потвърждение при изтриване на файлове\nsettings_confirm_link_button = Показване на диалогов прозорец за потвърждение при твърди/симетрични връзки на файлове\nsettings_confirm_group_deletion_button = Показване на диалогов прозорец за потвърждение при изтриване на всички файлове в групата\nsettings_show_text_view_button = Показване на долния текстов панел\nsettings_use_cache_button = Използвай кеш\nsettings_save_also_as_json_button = Също запази леша като JSON файл\nsettings_use_trash_button = Премести изтритите файлове в кошчето\nsettings_language_label = Език\nsettings_multiple_delete_outdated_cache_checkbutton = Автоматично изтриване на остарелите записи в кеша\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Изтриване на остарелите резултати от кеша, които сочат към несъществуващи файлове.\n    \n    Когато е разрешено, приложението се уверява, че при зареждане на записи всички записи сочат към валидни файлове (повредените се игнорират).\n    \n    Деактивирането на тази функция ще помогне при сканиране на файлове на външни дискове, тъй като записите от кеша за тях няма да бъдат изчистени при следващото сканиране.\n    \n    В случай че имате стотици хиляди записи в кеша, предлагаме да включите тази опция, което ще ускори зареждането/спасяването на кеша в началото/края на сканирането.\nsettings_notebook_general = Общи\nsettings_notebook_duplicates = Дубликати\nsettings_notebook_images = Сходни изображения\nsettings_notebook_videos = Сходни видеа\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Показва предварителен преглед от дясната страна (при избиране на файл с изображение).\nsettings_multiple_image_preview_checkbutton = Показване на предварителен преглед на изображението\nsettings_multiple_clear_cache_button_tooltip =\n    Изчистете ръчно кеша от остарели записи.\n    Това трябва да се използва само ако автоматичното изчистване е деактивирано.\nsettings_multiple_clear_cache_button = Премахни остарели резултати от кеша.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Скрива всички файлове с изключение на един, ако всички сочат към едни и същи данни (са твърдо свързани).\n    \n    Пример: В случай, че на диска има седем файла, които са свързани с определени данни, и един различен файл със същите данни, но с различен inode, тогава в търсачката за дубликати ще бъдат показани само един уникален файл и един файл от свързаните.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Задаване на минималния размер на файла, който ще се кешира.\n    \n    Ако изберете по-малка стойност, ще се генерират повече записи. Това ще ускори търсенето, но ще забави зареждането/запазването на кеша.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Позволява кеширане на prehash (хеш, изчислен от малка част от файла), което позволява по-ранно отхвърляне на недублирани резултати.\n    \n    По подразбиране е забранено, тъй като в някои ситуации може да доведе до забавяне на работата.\n    \n    Силно се препоръчва да се използва при сканиране на стотици хиляди или милиони файлове, защото може да ускори търсенето многократно.\nsettings_duplicates_prehash_minimal_entry_tooltip = Минимален размер на записа в кеша.\nsettings_duplicates_hide_hard_link_button = Скрий твърди връзки\nsettings_duplicates_prehash_checkbutton = Използване на предварителен кеш\nsettings_duplicates_minimal_size_cache_label = Минимален размер на файловете (в байтове), записани в кеша\nsettings_duplicates_minimal_size_cache_prehash_label = Минимален размер на файловете (в байтове), които се записват в предварителния кеш\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Записване на текущата конфигурация на настройките във файл.\nsettings_loading_button_tooltip = Зареждане на настройките от файл и заместване на текущата конфигурация с тях.\nsettings_reset_button_tooltip = Възстановяване на текущата конфигурация до тази по подразбиране.\nsettings_saving_button = Запазване на конфигурацията\nsettings_loading_button = Конфигурация за зареждане\nsettings_reset_button = Нулиране на конфигурацията\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Отваря папката, в която се съхраняват кеш txt файловете.\n    \n    Промяната на кеш файловете може да доведе до показване на невалидни резултати. Промяната на пътя обаче може да спести време при преместване на голямо количество файлове на друго място.\n    \n    Можете да копирате тези файлове между компютрите, за да спестите време за повторно сканиране на файловете (разбира се, ако те имат сходна структура на директориите).\n    \n    В случай на проблеми с кеша тези файлове могат да бъдат премахнати. Приложението автоматично ще ги възстанови.\nsettings_folder_settings_open_tooltip =\n    Отваря папката, в която се съхранява конфигурацията на Czkawka.\n    \n    ПРЕДУПРЕЖДЕНИЕ: Ръчното модифициране на конфигурацията може да наруши работния ви процес.\nsettings_folder_cache_open = Отворете папката с кеш\nsettings_folder_settings_open = Отваряне на папката с настройки\n# Compute results\ncompute_stopped_by_user = Търсенето е спряно от потребител\ncompute_found_duplicates_hash_size = Намерени са { $number_files } дубликати в { $number_groups } групи, които заемат { $size } за { $time }\ncompute_found_duplicates_name = Намерих { $number_files } дублики в { $number_groups } групи за { $time }\ncompute_found_empty_folders = Найдени са { $number_files } празни папки във { $time }\ncompute_found_empty_files = Найдени са { $number_files } празни файлова обекта в { $time }\ncompute_found_big_files = Намерих { $number_files } големи файла в { $time }\ncompute_found_temporary_files = Найдени са { $number_files } временни файлъв в { $time }\ncompute_found_images = Найшли се { $number_files } подобни изображения в { $number_groups } групи за { $time }\ncompute_found_videos = Намерил { $number_files } подобни видео файла в { $number_groups } групи за { $time }\ncompute_found_music = Найдено { $number_files } подобни музикални файлове в { $number_groups } групи за { $time }\ncompute_found_invalid_symlinks = Намерени { $number_files } невалидни символни връзки в { $time }\ncompute_found_broken_files = Намерих { $number_files } повредени файла в { $time }\ncompute_found_bad_extensions = Намерени са { $number_files } файла с невалидни разширения за { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Сканиран { $file_number } файл\n       *[other] Сканирани { $file_number } файлове\n    }\nprogress_scanning_extension_of_files = Проверено разширение на { $file_checked }/{ $all_files } файла\nprogress_scanning_broken_files = Проверени { $file_checked }/{ $all_files } файла от ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Хеширани { $file_checked }/{ $all_files } видеа\nprogress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video\nprogress_scanning_image = Хеширани { $file_checked }/{ $all_files } изображения ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Сравнени { $file_checked }/{ $all_files } хешове на изображения\nprogress_scanning_music_tags_end = Сравнени тагове на { $file_checked }/{ $all_files } музикални файла\nprogress_scanning_music_tags = Прочетени { $file_checked }/{ $all_files } тага на музикални файла\nprogress_scanning_music_content_end = Сравнени { $file_checked }/{ $all_files } отпечатъка на музикални файла\nprogress_scanning_music_content = Изчислени { $file_checked }/{ $all_files } отпечатъка на музикални файла ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Сканирана { $folder_number } папка\n       *[other] Сканирани { $folder_number } папки\n    }\nprogress_scanning_size = Сканиран размер на { $file_number } файла\nprogress_scanning_size_name = Сканиран име и размер на { $file_number } файла\nprogress_scanning_name = Сканиран име на { $file_number } файла\nprogress_analyzed_partial_hash = Анализиран частичен хеш на { $file_checked }/{ $all_files } файла ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Анализиран пълен хеш на { $file_checked }/{ $all_files } файла ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Зареждане на prehash кеш\nprogress_prehash_cache_saving = Запис на prehash кеш\nprogress_hash_cache_loading = Зареждане на hash кеш\nprogress_hash_cache_saving = Запис на hash кеш\nprogress_cache_loading = Зарежда кеш\nprogress_cache_saving = Запазва кеш\nprogress_current_stage = Текущ етап:{ \" \" }\nprogress_all_stages = Всички етапи:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Запазване на конфигурацията във файл { $name }.\nsaving_loading_saving_failure = Неуспешно спъжаване на конфигурационните данни в файл { $name }, причина { $reason }.\nsaving_loading_reset_configuration = Текущата конфигурация е изтрита.\nsaving_loading_loading_success = Правилно заредена конфигурация на приложението.\nsaving_loading_failed_to_create_config_file = Неуспешно създаване на конфигурационен файл \"{ $path }\", причина \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Не може да се зареди конфигурация от \"{ $path }\", защото тя не съществува или не е файл.\nsaving_loading_failed_to_read_data_from_file = Не може да се прочетат данни от файл \"{ $path }\", причина \"{ $reason }\".\n# Other\nselected_all_reference_folders = Не може да се стартира търсене, когато всички директории са зададени като референтни папки\nsearching_for_data = Търсене на данни, може да отнеме известно време, моля, изчакайте...\ntext_view_messages = СЪОБЩЕНИЯ\ntext_view_warnings = ПРЕДУПРЕЖДЕНИЯ\ntext_view_errors = ГРЕШКИ\nabout_window_motto = Тази програма е безплатна за използване и винаги ще бъде такава.\nkrokiet_new_app = Цквака е в режим на поддръжка, че се приемат само критични грешки и нито еднаnova функционалност ще бъде добавена. За nova функционалност моля проверете новата апликация Крочиец, която е по-стабилна и изтеглянечна и се развива все още активно.\n# Various dialog\ndialogs_ask_next_time = Попитайте следващия път\nsymlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\ndelete_title_dialog = Изтрий потвърждението\ndelete_question_label = Сигурни ли сте, че искате да изтриете файловете?\ndelete_all_files_in_group_title = Потвърждаване на изтриването на всички файлове в групата\ndelete_all_files_in_group_label1 = В някои групи се избират всички записи.\ndelete_all_files_in_group_label2 = Сигурни ли сте, че искате да ги изтриете?\ndelete_items_label = { $items } файловете ще бъдат изтрити.\ndelete_items_groups_label = { $items } Файловете от { $groups } групите ще бъдат изтрити.\nhardlink_failed = Неуспех при създаване на твърд линк за { $name } в { $target }, причина { $reason }\nhard_sym_invalid_selection_title_dialog = Невалидна селекция при някои групи\nhard_sym_invalid_selection_label_1 = В някои групи е избран само един запис и той ще бъде пренебрегнат.\nhard_sym_invalid_selection_label_2 = За да можете да свържете тези файлове с твърда/симетрична връзка, трябва да изберете поне два резултата в групата.\nhard_sym_invalid_selection_label_3 = Първият в групата се признава за оригинален и не се променя, но вторият и следващите се променят.\nhard_sym_link_title_dialog = Потвърждаване на връзката\nhard_sym_link_label = Потвърждаване на връзкатаСигурни ли сте, че искате да свържете тези файлове?\nmove_folder_failed = Неуспешно преместване на папка { $name }, причина { $reason }\nmove_file_failed = Неуспешно преместване на файл { $name }, причина { $reason }\nmove_files_title_dialog = Изберете папката, в която искате да преместите дублираните файлове\nmove_files_choose_more_than_1_path = Може да се избере само един път, за да може да се копират дублираните им файлове, selected { $path_number }.\nmove_stats = Правилно преместени { $num_files }/{ $all_files } елементи\nsave_results_to_file = Запазени резултати едновременно към txt и json файлове в папка \"{ $name }\".\nsearch_not_choosing_any_music = ГРЕШКА: Трябва да изберете поне едно квадратче за отметка с типове търсене на музика.\nsearch_not_choosing_any_broken_files = ГРЕШКА: Трябва да изберете поне едно квадратче за отметка с тип на проверените счупени файлове.\ninclude_folders_dialog_title = Папки, които да се включват\nexclude_folders_dialog_title = Папки, които да се изключат\ninclude_manually_directories_dialog_title = Добаеви ръчно директория\ncache_properly_cleared = Правилно изчистен кеш\ncache_clear_duplicates_title = Изчистване на кеша за дубликати\ncache_clear_similar_images_title = Изчистване на кеша на подобни изображения\ncache_clear_similar_videos_title = Изчистване на кеша на подобни видеоклипове\ncache_clear_message_label_1 = Искате ли да изчистите кеша от остарели записи?\ncache_clear_message_label_2 = Тази операция ще премахне всички записи в кеша, които сочат към невалидни файлове.\ncache_clear_message_label_3 = Това може леко да ускори зареждането/записването в кеша.\ncache_clear_message_label_4 = ПРЕДУПРЕЖДЕНИЕ: Операцията ще премахне всички кеширани данни от изключените външни дискове. Така че всеки хеш ще трябва да бъде възстановен.\n# Show preview\npreview_image_resize_failure = Неуспешно променяне на размера на изображението { $name }.\npreview_image_opening_failure = Неуспешно отваряне на изображение { $name }, причина { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Група { $current_group }/{ $all_groups } ({ $images_in_group } изображения)\ncompare_move_left_button = Л\ncompare_move_right_button = Д\n"
  },
  {
    "path": "czkawka_gui/i18n/cs/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Nastavení\nwindow_main_title = Czkawka (Škytavka)\nwindow_progress_title = Skenování\nwindow_compare_images = Porovnat obrázky\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Zavřít\n# Krokiet info dialog\nkrokiet_info_title = Představujeme Krokiet - Nová verze Czkawka\nkrokiet_info_message = \n        Krokiet je nový, vylepšený, rychlejší a spolehlivější verze Czkawky GTK GUI!\n\n        Je snazší spouštět a odolnější vůči systémovým změnám, protože závisí pouze na základních knihovnách, které jsou standardně dostupné na většině systémů.\n\n        Krokiet přináší také funkce, které Czkawka postrádá, včetně miniatur v režimu porovnání videa, EXIF čističe, průběhu přenosu/kopírování/smazání souborů nebo rozšířených možností třídění.\n\n        Vyzkoušejte to a uvidíte rozdíl!\n\n        Czkawka bude nadále dostávat opravy chyb a drobné aktualizace od mě, ale všechny nové funkce budou vyvíjeny výhradně pro Krokiet a kdokoliv je může volně přispívat novými funkcemi, přidávat chybějící režimy nebo rozšiřovat Czkawku dále.\n\n        PS: Tato zpráva by měla být zobrazena pouze jednou. Pokud se zobrazí znovu, nastavte proměnnou CZKAWKA_DONT_ANNOY_ME na libovolnou neprázdnou hodnotu.\n# Main window\nmusic_title_checkbox = Hlava 1 – Celkem\nmusic_artist_checkbox = Umělec\nmusic_year_checkbox = Rok\nmusic_bitrate_checkbox = Přenosová rychlost\nmusic_genre_checkbox = Žánr\nmusic_length_checkbox = Délka\nmusic_comparison_checkbox = Přibližné srovnání\nmusic_checking_by_tags = Štítky\nmusic_checking_by_content = Obsah\nsame_music_seconds_label = Minimální délka trvání druhého fragmentu\nsame_music_similarity_label = Maximální rozdíl\nmusic_compare_only_in_title_group = Porovnat v rámci skupin podobných názvů\nmusic_compare_only_in_title_group_tooltip =\n    Pokud je povoleno, soubory jsou seskupeny podle názvu a poté vzájemně porovnávány.\n    \n    S 10000 soubory, místo toho se obvykle uskuteční téměř 100 milionů srovnání kolem 20000 srovnání.\nsame_music_tooltip =\n    Vyhledávání podobných hudebních souborů podle jejich obsahu může být nakonfigurováno nastavením:\n    \n    - Minimální doba fragmentu, po které mohou být hudební soubory identifikovány jako podobné\n    - Maximální rozdíl mezi dvěma testovanými fragmenty\n    \n    Klíč k dobrým výsledkům je najít rozumné kombinace těchto parametrů, pro stanovení.\n    \n    Nastavení minimální doby na 5 s a maximální rozdíl na 1,0 bude hledat téměř stejné fragmenty v souborech.\n    Čas 20 s a maximální rozdíl 6,0 na druhé straně funguje dobře pro nalezení remixů/živých verzí atd.\n    \n    Ve výchozím nastavení je každý hudební soubor porovnáván mezi sebou a to může trvat dlouho při testování mnoha souborů, takže je obvykle lepší používat referenční složky a specifikovat, které soubory mají být vzájemně porovnány (se stejným množstvím souborů, porovnávání otisků prstů bude rychlejší alespoň 4x než bez referenčních složek).\nmusic_comparison_checkbox_tooltip =\n    Vyhledá podobné hudební soubory pomocí AI, která používá strojové učení k odstranění závorek z fráze. Například, pokud je tato možnost povolena, příslušné soubory budou považovány za duplicitní soubory:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Rozlišuje malá a velká písmena\nduplicate_case_sensitive_name_tooltip =\n    Pokud je povoleno, skupiny pouze záznamy, pokud mají přesně stejný název, např.Żołd <-> Żołd\n    \n    Zakázání takové volby bude názvy skupin bez kontroly, zda je každé písmeno stejné velikosti, např. żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Velikost a název\nduplicate_mode_name_combo_box = Název\nduplicate_mode_size_combo_box = Velikost\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka nabízí 3 typy hash:\n    \n    Blake3 - kryptografická hash funkce. Toto je výchozí, protože je velmi rychlý.\n    \n    CRC32 - jednoduchá hash funkce. To by mělo být rychlejší než Blake3, ale může docházet jen zřídka k nějakým střetům.\n    \n    XXH3 - velmi podobné ve výkonu a kvalitě hash jako Blake3 (ale nekryptografické). Takovéto režimy mohou být snadno zaměnitelné.\nduplicate_check_method_tooltip =\n    Pro tuto chvíli nabízí Czkawka tři typy metod, které vyhledávají duplicitní soubory:\n    \n    Název - Nalezení souborů, které mají stejný název.\n    \n    Velikost - Nalezí soubory, které mají stejnou velikost.\n    \n    Hash - Najde soubory, které mají stejný obsah. Tento režim hashuje soubor a později porovnává tento hash s nalezením duplikátů. Tento režim je nejbezpečnějším způsobem, jak nalézt duplikáty. Aplikace používá mezipaměť, takže druhé a další skenování stejných dat by mělo být mnohem rychlejší než první.\nimage_hash_size_tooltip =\n    Každý zkontrolovaný obrázek vytváří speciální hash který lze porovnávat, a malý rozdíl mezi nimi znamená, že tyto obrázky jsou podobné.\n    \n    8 hash velikost je docela dobrá k nalezení obrázků, které jsou jen trochu podobné originálům. S větší sadou obrázků (>1000) to vytvoří velké množství falešných pozitivních prvků, takže doporučuji v tomto případě použít větší hash velikost.\n    \n    16 je výchozí velikost hashu, což je docela dobrý kompromis mezi nalezením i trochu podobných obrázků a malým množstvím hashových kolizí.\n    \n    32 a 64 hash nalezly jen velmi podobné obrázky, ale neměly by mít téměř žádné falešné pohledy (možná kromě některých obrázků s alfa kanálem).\nimage_resize_filter_tooltip =\n    Pro výpočet hash obrázku musí knihovna nejprve změnit velikost.\n    \n    V závislosti na zvoleném algoritmu bude výsledný obrázek použitý k výpočtu hash vypadat trochu jinak.\n    \n    Nejrychlejší algoritmus k používání, ale také ten, který dává nejhorší výsledky, je blízko. Ve výchozím nastavení je povoleno, protože s menší kvalitou 16x16 hash není ve skutečnosti viditelná.\n    \n    S velikostí hash 8x8 je doporučeno použít jiný algoritmus než nejbližší pro lepší skupiny obrázků.\nimage_hash_alg_tooltip =\n    Uživatelé si mohou vybrat z jednoho z mnoha algoritmů pro výpočet hashu.\n    \n    Každý má silné a slabší body a někdy přinese lepší a někdy horší výsledky pro různé obrázky.\n    \n    Takže k určení nejlepšího pro vás je vyžadováno ruční testování.\nbig_files_mode_combobox_tooltip = Umožňuje vyhledávat malé / největší soubory\nbig_files_mode_label = Zaškrtnuté soubory\nbig_files_mode_smallest_combo_box = Nejmenší\nbig_files_mode_biggest_combo_box = Největší\nmain_notebook_duplicates = Duplicitní soubory\nmain_notebook_empty_directories = Prázdné adresáře\nmain_notebook_big_files = Velké soubory\nmain_notebook_empty_files = Prázdné soubory\nmain_notebook_temporary = Dočasné soubory\nmain_notebook_similar_images = Podobné obrázky\nmain_notebook_similar_videos = Podobná videa\nmain_notebook_same_music = Hudební duplikáty\nmain_notebook_symlinks = Neplatné symbolické odkazy\nmain_notebook_broken_files = Rozbité soubory\nmain_notebook_bad_extensions = Špatná rozšíření\nmain_tree_view_column_file_name = Název souboru\nmain_tree_view_column_folder_name = Název složky\nmain_tree_view_column_path = Cesta\nmain_tree_view_column_modification = Datum změny\nmain_tree_view_column_size = Velikost\nmain_tree_view_column_similarity = Podobnost\nmain_tree_view_column_dimensions = Rozměry\nmain_tree_view_column_title = Hlava\nmain_tree_view_column_artist = Umělec\nmain_tree_view_column_year = Rok\nmain_tree_view_column_bitrate = Přenosová rychlost\nmain_tree_view_column_length = Délka\nmain_tree_view_column_genre = Žánr\nmain_tree_view_column_symlink_file_name = Název souboru symbolického odkazu\nmain_tree_view_column_symlink_folder = Složka symbolického odkazu\nmain_tree_view_column_destination_path = Cílová cesta\nmain_tree_view_column_type_of_error = Typ chyby\nmain_tree_view_column_current_extension = Aktuální rozšíření\nmain_tree_view_column_proper_extensions = Řádné rozšíření\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Kodek\nmain_label_check_method = Metoda kontroly\nmain_label_hash_type = Typ Hash\nmain_label_hash_size = Velikost hash\nmain_label_size_bytes = Velikost (bajty)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Počet zobrazených souborů\nmain_label_resize_algorithm = Změna velikosti algoritmu\nmain_label_similarity = Podobnost { \" \" }\nmain_check_box_broken_files_audio = Zvuk\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Archivovat\nmain_check_box_broken_files_image = Obrázek\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Používá ffmpeg/ffprobe k ověření video souborů. Poměrně pomalé a může detekovat pedantické chyby i když soubor hraje v pořádku.\ncheck_button_general_same_size = Ignorovat stejnou velikost\ncheck_button_general_same_size_tooltip = Ignorovat soubory se stejnou velikostí ve výsledcích - obvykle se jedná o 1:1 duplicitní\nmain_label_size_bytes_tooltip = Velikost souborů, které budou použity při skenování\n# Upper window\nupper_tree_view_included_folder_column_title = Vyhledávané složky\nupper_tree_view_included_reference_column_title = Referenční složky\nupper_recursive_button = Rekurentní\nupper_recursive_button_tooltip = Pokud je vybráno, hledejte také soubory, které nejsou umístěny přímo pod vybranými složkami.\nupper_manual_add_included_button = Ruční přidání\nupper_add_included_button = Přidat\nupper_remove_included_button = Odebrat\nupper_manual_add_excluded_button = Ruční přidání\nupper_add_excluded_button = Přidat\nupper_remove_excluded_button = Odebrat\nupper_manual_add_included_button_tooltip =\n    Přidat název adresáře k hledání ručně.\n    \n    Chcete-li přidat více cest najednou, oddělte je od ;\n    \n    /home/roman;/home/rozkaz přidá dva adresáře /home/roman a /home/rozkaz\nupper_add_included_button_tooltip = Přidat nový adresář k vyhledávání.\nupper_remove_included_button_tooltip = Odstranit adresář z hledání.\nupper_manual_add_excluded_button_tooltip =\n    Přidejte ručně název vyloučené adresáře.\n    \n    Chcete-li přidat více cest najednou, oddělte je od ;\n    \n    /home/roman;/home/krokiet přidá dva adresáře /home/roman a /home/keokiet\nupper_add_excluded_button_tooltip = Přidat adresář, který bude při hledání vyloučen.\nupper_remove_excluded_button_tooltip = Odstranit adresář z vyloučení.\nupper_notebook_items_configuration = Konfigurace položek\nupper_notebook_excluded_directories = Vyloučené cesty\nupper_notebook_included_directories = Zahrnuté cesty\nupper_allowed_extensions_tooltip =\n    Povolené přípony musí být odděleny čárkami (ve výchozím nastavení jsou všechny k dispozici).\n    \n    Následující makra, která přidávají více rozšíření najednou, jsou také k dispozici: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Příklad použití \".exe, IMAGE, VIDEO, .rar, 7z\" - to znamená, že obrázky (např. . jpg, png), videa (např. avi, mp4), exe, rar a 7z soubory budou naskenovány.\nupper_excluded_extensions_tooltip =\n    Seznam zakázaných souborů, které budou při skenování ignorovány.\n    \n    Při používání povolených i zakázaných přípon, má tato vyšší prioritu, takže soubor nebude zaškrtnut.\nupper_excluded_items_tooltip = \n        Vyřazené položky musí obsahovat * wildcard a měly by být odděleny čárkami.\n        Toto je pomalejší než Excluded Paths, takže používejte opatrně.\nupper_excluded_items = Vyloučené položky:\nupper_allowed_extensions = Povolená rozšíření:\nupper_excluded_extensions = Zakázané rozšíření:\n# Popovers\npopover_select_all = Vybrat vše\npopover_unselect_all = Odznačit vše\npopover_reverse = Reverzní výběr\npopover_select_all_except_shortest_path = Vyberte vše kromě nejkratší cesty\npopover_select_all_except_longest_path = Vyberte vše kromě nejdelší cesty\npopover_select_all_except_oldest = Vybrat vše kromě nejstarších\npopover_select_all_except_newest = Vybrat vše kromě nejnovějších\npopover_select_one_oldest = Vyberte jeden nejstarší\npopover_select_one_newest = Vyberte jeden nejnovější\npopover_select_custom = Vybrat vlastní\npopover_unselect_custom = Zrušit výběr vlastních\npopover_select_all_images_except_biggest = Vybrat vše kromě největších\npopover_select_all_images_except_smallest = Vybrat všechny kromě nejmenších\npopover_custom_path_check_button_entry_tooltip =\n    Vyberte záznamy podle cesty.\n    \n    Příklad použití:\n    /home/pimpek/rzecz.txt lze nalézt pomocí /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Vyberte záznamy podle názvů souborů.\n    \n    Příklad použití:\n    /usr/ping/pong.txt lze nalézt s *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Vyberte záznamy podle zadaného Regexu.\n    \n    S tímto režimem je vyhledávaná cesta se jménem.\n    \n    Příklad použití:\n    /usr/bin/ziemniak. xt lze nalézt pomocí /ziem[a-z]+\n    \n    Toto používá výchozí implementaci Rust regex. Více o tom si můžete přečíst zde: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Umožňuje detekci citlivosti na malá a velká písmena.\n    \n    Pokud je vypnuta /doma/* nálezů jak /HoMe/roman tak /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Zabraňuje výběru všech záznamů ve skupině.\n    \n    Toto je ve výchozím nastavení povoleno, protože ve většině situací, nechcete odstranit originální i duplicitní soubory, ale chcete opustit alespoň jeden soubor.\n    \n    VAROVÁNÍ: Toto nastavení nefunguje, pokud jste již ručně vybrali všechny výsledky ve skupině.\npopover_custom_regex_path_label = Cesta\npopover_custom_regex_name_label = Název\npopover_custom_regex_regex_label = Regex cesta + Jméno\npopover_custom_case_sensitive_check_button = Rozlišit malá a velká písmena\npopover_custom_all_in_group_label = Nesbírat všechny záznamy ve skupině\npopover_custom_mode_unselect = Zrušit výběr vlastních\npopover_custom_mode_select = Vybrat vlastní\npopover_sort_file_name = Název souboru\npopover_sort_folder_name = Název adresáře\npopover_sort_full_name = Jméno a příjmení\npopover_sort_size = Velikost\npopover_sort_selection = Výběr\npopover_invalid_regex = Regex je neplatný\npopover_valid_regex = Regex je platný\n# Bottom buttons\nbottom_search_button = Hledat\nbottom_select_button = Vybrat\nbottom_delete_button = Vymazat\nbottom_save_button = Uložit\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Přesunout\nbottom_sort_button = Seřadit\nbottom_compare_button = Porovnat\nbottom_search_button_tooltip = Začít hledání\nbottom_select_button_tooltip = Vyberte záznamy. Pouze vybrané soubory/složky mohou být později zpracovány.\nbottom_delete_button_tooltip = Odstranit vybrané soubory/složky.\nbottom_save_button_tooltip = Ukládat data o hledání do souboru\nbottom_symlink_button_tooltip =\n    Vytvořit symbolické odkazy.\n    Funguje pouze tehdy, pokud jsou vybrány alespoň dva výsledky ve skupině.\n    Nejprve je nezměněna a druhé a později jsou souvztažné s prvními.\nbottom_hardlink_button_tooltip =\n    Vytvořit hardwarové odkazy.\n    Funguje pouze tehdy, pokud jsou vybrány alespoň dva výsledky ve skupině.\n    Nejprve je nezměněna a druhé a později jsou těžce propojeny s prvními.\nbottom_hardlink_button_not_available_tooltip =\n    Vytvořit hardwarové odkazy.\n    Tlačítko je zakázáno, protože hardwarové odkazy nelze vytvořit.\n    Hardlinky fungují pouze s oprávněními administrátora v systému Windows, tak se ujistěte, že používáte aplikaci jako administrátora.\n    Pokud aplikace s takovými oprávněními již funguje, podívejte se na podobné problémy na Githubu.\nbottom_move_button_tooltip =\n    Přesune soubory do vybraného adresáře.\n    Zkopíruje všechny soubory do adresáře bez uchování stromu adresáře.\n    Při pokusu přesunout dva soubory se stejným názvem do složky, druhý selže a zobrazí chybu.\nbottom_sort_button_tooltip = Seřazuje soubory/složky podle zvolené metody.\nbottom_compare_button_tooltip = Porovnat obrázky ve skupině.\nbottom_show_errors_tooltip = Zobrazit/skrýt spodní textový panel.\nbottom_show_upper_notebook_tooltip = Zobrazit/skrýt horní panel sešitu.\n# Progress Window\nprogress_stop_button = Zastavit\nprogress_stop_additional_message = Zastavit požadavek\n# About Window\nabout_repository_button_tooltip = Odkaz na stránku repositáře se zdrojovým kódem.\nabout_donation_button_tooltip = Odkaz na stránku s darováním.\nabout_instruction_button_tooltip = Odkaz na stránku instrukcí.\nabout_translation_button_tooltip = Odkaz na stránku Crowdin s překlady aplikací. Oficiálně polština a angličtina jsou podporovány.\nabout_repository_button = Repozitář\nabout_donation_button = Darovat\nabout_instruction_button = Instrukce\nabout_translation_button = Překlad\n# Header\nheader_setting_button_tooltip = Otevře dialogové okno nastavení.\nheader_about_button_tooltip = Otevře dialog s informacemi o aplikaci.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Počet použitých vláken\nsettings_number_of_threads_tooltip = Počet použitých vláken, 0 znamená, že budou použita všechna dostupná vlákna.\nsettings_use_rust_preview = Místo toho použít externí knihovny gtk k načtení náhledů\nsettings_use_rust_preview_tooltip =\n    Použití gtk náhledů bude někdy rychlejší a bude podporovat více formátů, ale někdy to může být pravý opak.\n    \n    Pokud máte problémy s načítáním náhledů, můžete zkusit toto nastavení změnit.\n    \n    Na jiných než linuxových systémech je doporučeno použít tuto možnost, protože gtk-pixbuf není vždy k dispozici, takže vypnutí této možnosti nebude načíst náhledy některých obrázků.\nsettings_label_restart = Pro použití nastavení je třeba restartovat aplikaci!\nsettings_ignore_other_filesystems = Ignorovat ostatní souborové systémy (pouze Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignoruje soubory, které nejsou ve stejném souborovém systému jako prohledávané adresáře.\n    \n    Funguje stejně jako -xdev možnost najít příkaz na Linuxu\nsettings_save_at_exit_button_tooltip = Uložit konfiguraci do souboru při zavření aplikace.\nsettings_load_at_start_button_tooltip =\n    Načíst konfiguraci ze souboru při otevírání aplikace.\n    \n    Pokud není povoleno, budou použita výchozí nastavení.\nsettings_confirm_deletion_button_tooltip = Zobrazit potvrzovací dialogové okno při kliknutí na tlačítko mazat.\nsettings_confirm_link_button_tooltip = Zobrazit potvrzovací dialog při kliknutí na tlačítko hard/symbolický odkaz.\nsettings_confirm_group_deletion_button_tooltip = Zobrazit varovný dialog při pokusu o odstranění všech záznamů ze skupiny.\nsettings_show_text_view_button_tooltip = Zobrazit textový panel v dolní části uživatelského rozhraní.\nsettings_use_cache_button_tooltip = Použít cache souborů.\nsettings_save_also_as_json_button_tooltip = Uložit keš do (lidsky čitelný) formátu JSON. Je možné změnit její obsah. Mezipaměť z tohoto souboru bude automaticky čtena aplikací, pokud chybí binární formát mezipaměti (s rozšířením koše).\nsettings_use_trash_button_tooltip = Přesune soubory do koše a místo toho je trvale odstraní.\nsettings_language_label_tooltip = Jazyk uživatelského rozhraní.\nsettings_save_at_exit_button = Uložit konfiguraci při zavření aplikace\nsettings_load_at_start_button = Načíst konfiguraci při otevření aplikace\nsettings_confirm_deletion_button = Zobrazit dialogové okno potvrzení při mazání všech souborů\nsettings_confirm_link_button = Zobrazit dialogové okno pro pevné / symbolické odkazy\nsettings_confirm_group_deletion_button = Zobrazit dialogové okno potvrzení při mazání všech souborů ve skupině\nsettings_show_text_view_button = Zobrazit spodní textový panel\nsettings_use_cache_button = Použít keš\nsettings_save_also_as_json_button = Ukládat mezipaměť také jako soubor JSON\nsettings_use_trash_button = Přesunout smazané soubory do koše\nsettings_language_label = Jazyk\nsettings_multiple_delete_outdated_cache_checkbutton = Automaticky odstranit zastaralé položky v mezipaměti\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Odstranit zastaralé výsledky mezipaměti, které ukazují na neexistující soubory.\n    \n    Pokud je povoleno, aplikace se ujistí, že při načítání záznamů všechny záznamy odkazují na platné soubory (poškozené jsou ignorovány).\n    \n    Zakázáním této funkce pomůže při skenování souborů na externích discích, takže záznamy keší o nich nebudou vymazány v dalším skenování.\n    \n    V případě stovky tisíc záznamů v keši, je doporučeno toto povolit, což urychlí načítání/ukládání mezipaměti na začátku/konci skenování.\nsettings_notebook_general = Obecná ustanovení\nsettings_notebook_duplicates = Duplikáty\nsettings_notebook_images = Podobné obrázky\nsettings_notebook_videos = Podobné video\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Zobrazí náhled na pravé straně (při výběru souboru obrázku).\nsettings_multiple_image_preview_checkbutton = Zobrazit náhled obrázku\nsettings_multiple_clear_cache_button_tooltip =\n    Ručně vymazat mezipaměť zastaralých položek.\n    Toto by mělo být použito pouze v případě, že je zakázáno automatické vymazání.\nsettings_multiple_clear_cache_button = Odstranit zastaralé výsledky z mezipaměti.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Skryje všechny soubory kromě jedné, pokud všechny odkazují na stejná data (jsou hardlinované).\n    \n    Příklad: V případě, že je na disku sedm souborů, které jsou spojeny s konkrétními daty, a jeden jiný soubor se stejnými daty, ale jiným inodem, pak v hledání duplikátu bude zobrazen pouze jeden unikátní soubor a jeden soubor z hardlinovaných souborů.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Nastavte minimální velikost souboru, který bude uložen do mezipaměti.\n    \n    Výběr menší hodnoty vygeneruje více záznamů. Toto urychlí vyhledávání, ale zpomalí načítání/ukládání mezipaměti.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Umožňuje ukládání do mezipaměti (hash vypočtený z malé části souboru), což umožňuje dřívější odstranění neduplikovaných výsledků.\n    \n    Ve výchozím nastavení je zakázáno, protože v některých situacích může způsobit zpomalení.\n    \n    Doporučujeme jej použít při skenování stovek tisíc nebo miliónů souborů, protože může urychlit hledání několikrát.\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimální velikost položky v mezipaměti.\nsettings_duplicates_hide_hard_link_button = Skrýt pevné odkazy\nsettings_duplicates_prehash_checkbutton = Použít mezipaměť rozpoznávání\nsettings_duplicates_minimal_size_cache_label = Minimální velikost souborů (v bajtech) uložených do mezipaměti\nsettings_duplicates_minimal_size_cache_prehash_label = Minimální velikost souborů (v bajtech) uložených pro zachycení keše\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Uložit aktuální nastavení do souboru.\nsettings_loading_button_tooltip = Načíst nastavení ze souboru a nahradit jejich aktuální konfiguraci.\nsettings_reset_button_tooltip = Obnovit aktuální konfiguraci na výchozí.\nsettings_saving_button = Uložit konfiguraci\nsettings_loading_button = Načíst konfiguraci\nsettings_reset_button = Obnovit konfiguraci\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Otevře složku, kde jsou uloženy soubory txt v mezipaměti.\n    \n    Úprava souborů může způsobit zobrazení neplatných výsledků. Změna cesty však může ušetřit čas při přesunu velkého množství souborů do jiného umístění.\n    \n    Tyto soubory můžete zkopírovat mezi počítači, abyste ušetřili čas při skenování souborů (samozřejmě pokud mají podobnou strukturu adresáře).\n    \n    V případě problémů s mezipamětí, mohou být tyto soubory odstraněny. Aplikace je automaticky obnoví.\nsettings_folder_settings_open_tooltip =\n    Otevře složku, kde je uloženo nastavení Czkawka.\n    \n    VAROVÁNÍ: Ruční úprava konfigurace může poškodit váš pracovní postup.\nsettings_folder_cache_open = Otevřít složku mezipaměti\nsettings_folder_settings_open = Otevřít složku s nastavením\n# Compute results\ncompute_stopped_by_user = Vyhledávání bylo zastaveno uživatelem\ncompute_found_duplicates_hash_size = Nalezeno { $number_files } duplikátů v { $number_groups } skupinách, které trvaly { $size } v { $time }\ncompute_found_duplicates_name = Nalezeno { $number_files } duplikátů v { $number_groups } skupinách v { $time }\ncompute_found_empty_folders = Nalezeno { $number_files } prázdné složky v { $time }\ncompute_found_empty_files = Nalezeno { $number_files } prázdných souborů v { $time }\ncompute_found_big_files = Nalezeno { $number_files } velkých souborů v { $time }\ncompute_found_temporary_files = Nalezeno { $number_files } dočasných souborů v { $time }\ncompute_found_images = Nalezeno { $number_files } podobných obrázků v { $number_groups } skupinách v { $time }\ncompute_found_videos = Nalezeno { $number_files } podobných videí v { $number_groups } skupinách v { $time }\ncompute_found_music = Nalezeno { $number_files } podobných hudebních souborů v { $number_groups } skupinách v { $time }\ncompute_found_invalid_symlinks = Nalezeno { $number_files } neplatných symbolických odkazů v { $time }\ncompute_found_broken_files = Nalezeno { $number_files } rozbitých souborů v { $time }\ncompute_found_bad_extensions = Nalezeno { $number_files } souborů s neplatnými příponami v { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Naskenovaný { $file_number } soubor\n       *[other] Skenované { $file_number } soubory\n    }\nprogress_scanning_extension_of_files = Kontrola přípony { $file_checked }/{ $all_files } souboru\nprogress_scanning_broken_files = Kontrola { $file_checked }/{ $all_files } soubor ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hashováno { $file_checked }/{ $all_files } videa\nprogress_creating_video_thumbnails = Vytvořené náhledy { $file_checked }/{ $all_files } videa\nprogress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Porovnáno { $file_checked }/{ $all_files } hash obrázků\nprogress_scanning_music_tags_end = Porovnávané štítky hudebního souboru { $file_checked }/{ $all_files }\nprogress_scanning_music_tags = Číst tagy { $file_checked }/{ $all_files } hudebního souboru\nprogress_scanning_music_content_end = Porovnávaný otisk hudby { $file_checked }/{ $all_files }\nprogress_scanning_music_content = Vypočítaný otisk hudby { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Naskenovaná složka { $folder_number }\n       *[other] Naskenovaná { $folder_number } složky\n    }\nprogress_scanning_size = Naskenovaná velikost souboru { $file_number }\nprogress_scanning_size_name = Naskenovaný název a velikost { $file_number } souboru\nprogress_scanning_name = Naskenované jméno { $file_number } souboru\nprogress_analyzed_partial_hash = Analyzováno částečné hash souborů { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Analyzováno plné hash souborů { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Načítání mezipaměti rozpoznání\nprogress_prehash_cache_saving = Ukládání mezipaměti rozpoznání\nprogress_hash_cache_loading = Načítání hash keše\nprogress_hash_cache_saving = Ukládání keše hash\nprogress_cache_loading = Načítání keše\nprogress_cache_saving = Ukládání keše\nprogress_current_stage = Aktuální fáze:{ \" \" }\nprogress_all_stages = Všechny etapy:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Uložena konfigurace do souboru { $name }.\nsaving_loading_saving_failure = Nepodařilo se uložit konfigurační data do souboru { $name }, důvod { $reason }.\nsaving_loading_reset_configuration = Aktuální konfigurace byla vymazána.\nsaving_loading_loading_success = Správně načtená konfigurace aplikace.\nsaving_loading_failed_to_create_config_file = Nepodařilo se vytvořit konfigurační soubor \"{ $path }\", důvod \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Konfiguraci z \"{ $path } nelze načíst, protože neexistuje nebo není soubor.\nsaving_loading_failed_to_read_data_from_file = Nelze číst data ze souboru \"{ $path }\", důvod \"{ $reason }\".\n# Other\nselected_all_reference_folders = Hledání nelze spustit, pokud jsou všechny adresáře nastaveny jako referenční složky\nsearching_for_data = Vyhledávání dat může chvíli trvat, prosím čekejte...\ntext_view_messages = ZPRÁVY\ntext_view_warnings = VAROVÁNÍ\ntext_view_errors = CHYBA\nabout_window_motto = Tento program je zdarma a bude vždy používán.\nkrokiet_new_app = Czkawka je v režimu údržby, což znamená, že budou opraveny pouze kritické chyby a nebudou přidány žádné nové funkce. Pro nové funkce si prosím přečtěte novou aplikaci Krokiet , která je stabilnější a výkonnější a je stále v aktivním vývoji.\n# Various dialog\ndialogs_ask_next_time = Příště se zeptat\nsymlink_failed = Nepodařilo se symbolicky propojit { $name } do { $target }, důvod { $reason }\ndelete_title_dialog = Potvrzení odstranění\ndelete_question_label = Jste si jisti, že chcete odstranit soubory?\ndelete_all_files_in_group_title = Potvrzení odstranění všech souborů ve skupině\ndelete_all_files_in_group_label1 = V některých skupinách jsou vybrány všechny záznamy.\ndelete_all_files_in_group_label2 = Jste si jisti, že je chcete odstranit?\ndelete_items_label = { $items } soubory budou odstraněny.\ndelete_items_groups_label = { $items } souborů z { $groups } skupin bude smazáno.\nhardlink_failed = Nepodařilo se propojit { $name } na { $target }, důvod { $reason }\nhard_sym_invalid_selection_title_dialog = Neplatný výběr s některými skupinami\nhard_sym_invalid_selection_label_1 = V některých skupinách je vybrán pouze jeden záznam a bude ignorován.\nhard_sym_invalid_selection_label_2 = Aby bylo možné tyto soubory propojit s pevným/sym, je třeba vybrat alespoň dva výsledky ve skupině.\nhard_sym_invalid_selection_label_3 = První ve skupině je uznána jako původní a není změněna, ale druhá a později jsou upraveny.\nhard_sym_link_title_dialog = Potvrzení odkazu\nhard_sym_link_label = Jste si jisti, že chcete propojit tyto soubory?\nmove_folder_failed = Nepodařilo se přesunout složku { $name }, důvod { $reason }\nmove_file_failed = Nepodařilo se přesunout soubor { $name }, důvod { $reason }\nmove_files_title_dialog = Vyberte složku, do které chcete přesunout duplicitní soubory\nmove_files_choose_more_than_1_path = Lze vybrat pouze jednu cestu, aby bylo možné zkopírovat jejich duplikované soubory, vybrané { $path_number }.\nmove_stats = Správně přesunuto { $num_files }/{ $all_files } položek\nsave_results_to_file = Uloženy výsledky do složky txt i json do složky \"{ $name }\".\nsearch_not_choosing_any_music = CHYBA: Musíte vybrat alespoň jedno zaškrtávací políčko s prohledáváním hudby.\nsearch_not_choosing_any_broken_files = CHYBA: Musíte vybrat alespoň jedno zaškrtávací políčko s typem zkontrolovaných poškozených souborů.\ninclude_folders_dialog_title = Složky, které chcete zahrnout\nexclude_folders_dialog_title = Složky k vyloučení\ninclude_manually_directories_dialog_title = Přidat adresář ručně\ncache_properly_cleared = Správně vymazaná mezipaměť\ncache_clear_duplicates_title = Vymazání cache duplicity\ncache_clear_similar_images_title = Vymazání cache podobných obrázků\ncache_clear_similar_videos_title = Vymazání cache podobných videí\ncache_clear_message_label_1 = Chcete vymazat mezipaměť zastaralých položek?\ncache_clear_message_label_2 = Tato operace odstraní všechny položky mezipaměti, které ukazují na neplatné soubory.\ncache_clear_message_label_3 = To může mírně urychlit načítání/ukládání do mezipaměti.\ncache_clear_message_label_4 = VAROVÁNÍ: Operace odstraní všechna data v mezipaměti z nezapojených externích disků. Každá hash bude muset být obnovena.\n# Show preview\npreview_image_resize_failure = Nepodařilo se změnit velikost obrázku { $name }.\npreview_image_opening_failure = Nepodařilo se otevřít obrázek { $name }, důvod { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Skupina { $current_group }/{ $all_groups } ({ $images_in_group } obrázků)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/de/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Einstellungen\nwindow_main_title = Czkawka (Schluckauf)\nwindow_progress_title = Scannen\nwindow_compare_images = Bilder vergleichen\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Schließen\n# Krokiet info dialog\nkrokiet_info_title = Krokiet – Neue Version von Czkawka\nkrokiet_info_message = \n        Krokiet ist die neue, verbesserte, schnellere und zuverlässigere Version der Czkawka GTK GUI!\n\n        Es ist einfacher zu betreiben und widerstandsfähiger gegenüber Systemänderungen, da es nur auf Core-Bibliotheken angewiesen ist, die standardmäßig auf den meisten Systemen verfügbar sind.\n\n        Krokiet bietet außerdem Funktionen, die Czkawka nicht hat, darunter Miniaturansichten im Video-Vergleichsmodus, ein EXIF-Bereiniger, Fortschritt bei Datei-Verschieben/Kopieren/Löschen oder erweiterte Sortieroptionen.\n\n        Probiere es aus und sieh den Unterschied!\n\n        Czkawka wird weiterhin Fehlerbehebungen und kleinere Updates von mir erhalten, aber alle neuen Funktionen werden ausschließlich für Krokiet entwickelt und jeder ist frei, neue Funktionen hinzuzufügen, fehlende Modi zu erweitern oder Czkawka weiter auszubauen.\n\n        PS: Diese Nachricht sollte nur einmal erscheinen. Wenn sie erneut angezeigt wird, setze die Umgebungsvariable CZKAWKA_DONT_ANNOY_ME auf einen nicht leeren Wert.\n# Main window\nmusic_title_checkbox = Titel\nmusic_artist_checkbox = Künstler\nmusic_year_checkbox = Jahr\nmusic_bitrate_checkbox = Bitraten\nmusic_genre_checkbox = Genretype\nmusic_length_checkbox = Dauer\nmusic_comparison_checkbox = Ungefährer Vergleich\nmusic_checking_by_tags = Schlagworte\nmusic_checking_by_content = Inhalt\nsame_music_seconds_label = Minimale Dauer des Fragments, in Sekunden\nsame_music_similarity_label = Maximaler Unterschied\nmusic_compare_only_in_title_group = Vergleiche innerhalb von Gruppen ähnlicher Titel\nmusic_compare_only_in_title_group_tooltip =\n    Wenn aktiviert, werden Dateien nach Titel gruppiert und dann miteinander verglichen.\n    \n    Mit 10000 Dateien, statt fast 100 Millionen Vergleiche wird es in der Regel rund 20000 Vergleiche geben.\nsame_music_tooltip =\n    Die Suche nach ähnlichen Musikdateien nach dem Inhalt kann über die Einstellung konfiguriert werden:\n    \n    - Die minimale Fragmentzeit, nach der Musikdateien als ähnlich identifiziert werden können\n    - Der maximale Unterschied zwischen zwei getesteten Fragmenten\n    \n    Der Schlüssel zu guten Ergebnissen ist die Suche nach sinnvollen Kombinationen dieser Parameter. für bereitgestellt.\n    \n    Wenn Sie die minimale Zeit auf 5 Sekunden und den maximalen Unterschied auf 1,0 setzen, werden fast identische Fragmente in den Dateien gesucht.\n    Eine Zeit von 20 Sekunden und ein maximaler Unterschied von 6.0 hingegen funktioniert gut um Remix/Live-Versionen zu finden.\n    \n    Standardmäßig wird jede Musikdatei miteinander verglichen, und dies kann viel Zeit in Anspruch nehmen, wenn viele Dateien getestet werden so ist es in der Regel besser, Referenzordner zu verwenden und festzulegen, welche Dateien miteinander verglichen werden sollen (mit gleicher Dateigröße Der Vergleich von Fingerabdrücken wird mindestens 4x schneller als ohne Referenzordner sein).\nmusic_comparison_checkbox_tooltip =\n    Mit Hilfe von einer KI, die maschinelles Lernen nutzt, um Klammern aus Sätzen zu entfernen, wird nach ähnlichen Musikdateien gesucht. Wenn die Option aktiviert ist, werden die folgenden Dateien zum Beispiel als Duplikate betrachtet:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Gross-/Kleinschreibung beachten\nduplicate_case_sensitive_name_tooltip =\n    Wenn aktiviert, gruppieren Sie nur Datensätze, wenn sie genau denselben Namen haben, z. żoŁD <-> Żołd\n    Deaktivieren dieser Option gruppiert Namen ohne zu überprüfen, ob jeder Buchstabe die gleiche Größe wie żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Größe und Name\nduplicate_mode_name_combo_box = Name\nduplicate_mode_size_combo_box = Größe\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka bietet 3 Arten von Hashes an, die verwendet werden können:\n    \n    Blake3 - kryptographische Hashfunktion. Wegen ihrer Geschwindikeit wird Sie als Standard-Hash-Algorithmus verwendet.\n    \n    CRC32 - einfache Hash-Funktion. Sie sollte schneller sein als Blake3, könnte aber in seltenen Fällen Kollisionen haben.\n    \n    XXH3 - bei Geschwindikeit und Hashqualität vergleichbar mit Blake3 (aber nicht kryptographisch). Beide Modi können somit leicht miteinander ausgetauscht werden.\nduplicate_check_method_tooltip =\n    Derzeit bietet Czkawka drei Methoden an, um Duplikate zu finden:\n    \n    Name - Findet Dateien mit gleichem Namen.\n    \n    Größe - Findet Dateien mit gleicher Größe.\n    \n    Hash - Findet Dateien mit dem gleichen Inhalt. Dieser Modus hasht die Datei und vergleicht diese Hashes, um Duplikate zu finden. Dieser Modus ist der sicherste Weg, um Duplikate zu finden. Das Tool verwendet einen Cache, daher sollten weiteren Scans der gleichen Dateien viel schneller sein als der erste.\nimage_hash_size_tooltip =\n    Jedes geprüfte Bild erzeugt einen speziellen Hash, der miteinander verglichen werden kann und ein kleiner Unterschied zwischen ihnen bedeutet, dass diese Bilder ähnlich sind.\n    \n    8 Hashgröße ist sehr gut, um Bilder zu finden, die dem Original nur ein wenig ähneln. Bei einer größeren Anzahl von Bildern (>1000), wird dies eine große Anzahl falscher Positives, also empfehle ich in diesem Fall eine größere Hashgröße.\n    \n    16 ist die standardmäßige Hashgröße, die einen guten Kompromiss zwischen dem Finden von kleinen ähnlichen Bildern und einer geringen Anzahl von Hash-Kollisionen darstellt.\n    \n    32 und 64 Hashes finden nur sehr ähnliche Bilder, sollten aber fast keine falschen positiven Bilder haben (vielleicht mit Ausnahme einiger Bilder mit Alphakanal).\nimage_resize_filter_tooltip =\n    Um Hash des Bildes zu berechnen, muss die Bibliothek zuerst die Größe des Hashs verändern.\n    \n    Abhängig vom gewählten Algorithmus sieht das resultierende Bild, das zur Hashberechnung verwendet wird, ein wenig anders aus.\n    \n    Der schnellste zu verwendende Algorithmus, aber auch der, der die schlechtesten Ergebnisse liefert, ist in der Nähe. Sie ist standardmäßig aktiviert, da sie mit 16x16 Hash-Größe nicht wirklich sichtbar ist.\n    \n    Mit 8x8 Hashgröße wird empfohlen, einen anderen Algorithmus als nahe zu verwenden, um bessere Gruppen von Bildern zu haben.\nimage_hash_alg_tooltip =\n    Benutzer können aus einem von vielen Algorithmen zur Berechnung des Hashs wählen.\n    \n    Jeder hat sowohl starke als auch schwächere Punkte und wird manchmal bessere und manchmal schlechtere Ergebnisse für verschiedene Bilder liefern.\n    \n    Um also den besten für Sie zu ermitteln, ist eine manuelle Prüfung erforderlich.\nbig_files_mode_combobox_tooltip = Erlaubt die Suche nach kleinsten/größten Dateien\nbig_files_mode_label = Überprüfte Dateien\nbig_files_mode_smallest_combo_box = Die kleinsten\nbig_files_mode_biggest_combo_box = Die größten\nmain_notebook_duplicates = Gleiche Dateien\nmain_notebook_empty_directories = Leere Verzeichnisse\nmain_notebook_big_files = Große Dateien\nmain_notebook_empty_files = Leere Dateien\nmain_notebook_temporary = Temporäre Dateien\nmain_notebook_similar_images = Ähnliche Bilder\nmain_notebook_similar_videos = Ähnliche Videos\nmain_notebook_same_music = Gleiche Musik\nmain_notebook_symlinks = Ungültige Symlinks\nmain_notebook_broken_files = Defekte Dateien\nmain_notebook_bad_extensions = Falsche Erweiterungen\nmain_tree_view_column_file_name = Dateiname\nmain_tree_view_column_folder_name = Ordnername\nmain_tree_view_column_path = Pfad\nmain_tree_view_column_modification = Änderungsdatum\nmain_tree_view_column_size = Größe\nmain_tree_view_column_similarity = Ähnlichkeit\nmain_tree_view_column_dimensions = Abmessungen\nmain_tree_view_column_title = Titel\nmain_tree_view_column_artist = Künstler\nmain_tree_view_column_year = Jahr\nmain_tree_view_column_bitrate = Bitrate\nmain_tree_view_column_length = Dauer\nmain_tree_view_column_genre = Genretype\nmain_tree_view_column_symlink_file_name = Symlink Dateiname\nmain_tree_view_column_symlink_folder = Symlink-Ordner\nmain_tree_view_column_destination_path = Zielpfad\nmain_tree_view_column_type_of_error = Fehlertyp\nmain_tree_view_column_current_extension = Aktuelle Erweiterung\nmain_tree_view_column_proper_extensions = Richtige Erweiterung\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codec\nmain_label_check_method = Prüfmethode\nmain_label_hash_type = Hash Typ\nmain_label_hash_size = Hash Größe\nmain_label_size_bytes = Größe (Bytes)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Anzahl der angezeigten Dateien\nmain_label_resize_algorithm = Algorithmus skalieren\nmain_label_similarity = Ähnlichkeit{ \" \" }\nmain_check_box_broken_files_audio = Ton\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Archiv\nmain_check_box_broken_files_image = Bild\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Verwendet ffmpeg/ffprobe zur Validierung von Videodateien. Sehr langsam und kann pedantische Fehler erkennen, auch wenn die Datei fehlerfrei abgespielt wird.\ncheck_button_general_same_size = Gleiche Größe ignorieren\ncheck_button_general_same_size_tooltip = Ignoriere Dateien mit identischer Größe in den Ergebnissen - in der Regel sind es 1:1 Duplikate\nmain_label_size_bytes_tooltip = Größe der Dateien, die beim Scannen verwendet werden\n# Upper window\nupper_tree_view_included_folder_column_title = Zu durchsuchende Ordner\nupper_tree_view_included_reference_column_title = Referenzordner\nupper_recursive_button = Rekursiv\nupper_recursive_button_tooltip = Falls ausgewählt, suchen Sie auch nach Dateien, die nicht direkt unter den ausgewählten Ordnern platziert werden.\nupper_manual_add_included_button = Manuell hinzufügen\nupper_add_included_button = Neu\nupper_remove_included_button = Entfernen\nupper_manual_add_excluded_button = Manuell hinzufügen\nupper_add_excluded_button = Neu\nupper_remove_excluded_button = Entfernen\nupper_manual_add_included_button_tooltip =\n    Verzeichnisname zur Suche per Hand hinzufügen.\n    \n    Um mehrere Pfade gleichzeitig hinzuzufügen, trennen Sie sie durch ;\n    \n    /home/roman;/home/rozkaz fügt zwei Verzeichnisse /home/roman und /home/rozkaz hinzu\nupper_add_included_button_tooltip = Neues Verzeichnis zur Suche hinzufügen.\nupper_remove_included_button_tooltip = Verzeichnis von der Suche löschen.\nupper_manual_add_excluded_button_tooltip =\n    Ausgeschlossenen Verzeichnisnamen per Hand hinzufügen.\n    \n    Um mehrere Pfade gleichzeitig hinzuzufügen, trennen Sie sie durch ;\n    \n    /home/roman;/home/krokiet wird zwei Verzeichnisse /home/roman und /home/keokiet hinzufügen\nupper_add_excluded_button_tooltip = Verzeichnis hinzufügen, das bei der Suche ausgeschlossen werden soll.\nupper_remove_excluded_button_tooltip = Ausgeschlossene Verzeichnisse löschen.\nupper_notebook_items_configuration = Suchbedingungen\nupper_notebook_excluded_directories = Ausgeschlossene Pfade\nupper_notebook_included_directories = Einbezogene Pfade\nupper_allowed_extensions_tooltip =\n    Erlaubte Erweiterungen müssen durch Kommas getrennt werden (standardmäßig sind alle verfügbar).\n    \n    Folgende Makros, die mehrere Erweiterungen gleichzeitig hinzufügen, sind ebenfalls verfügbar: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Nutzungsbeispiel \".exe, IMAGE, VIDEO, .rar, 7z\" - bedeutet, dass Bilder (jpg, png ...), Videodateien (avi, mp4 ...), exe, rar und 7z gescannt werden.\nupper_excluded_extensions_tooltip =\n    Liste der deaktivierten Dateien, die beim Scannen ignoriert werden.\n    \n    Wenn sowohl erlaubte als auch deaktivierte Erweiterungen verwendet werden, hat diese eine höhere Priorität, so dass die Datei nicht ausgewählt wird.\nupper_excluded_items_tooltip = \n        Ausgeschlossene Elemente müssen * Wildcard und durch Kommas getrennt enthalten.\n        Dies ist langsamer als Exkludierte Pfade, also verwenden Sie es vorsichtig.\nupper_excluded_items = Ausgeschlossene Elemente:\nupper_allowed_extensions = Erlaubte Erweiterungen:\nupper_excluded_extensions = Deaktivierte Erweiterungen:\n# Popovers\npopover_select_all = Alles auswählen\npopover_unselect_all = Gesamte Auswahl aufheben\npopover_reverse = Auswahl umkehren\npopover_select_all_except_shortest_path = Wähle alle außer den kürzesten Pfad\npopover_select_all_except_longest_path = Wähle alle außer den längsten Pfad\npopover_select_all_except_oldest = Alle außer Ältester auswählen\npopover_select_all_except_newest = Alle außer Neuester auswählen\npopover_select_one_oldest = Älteste auswählen\npopover_select_one_newest = Neueste auswählen\npopover_select_custom = Individuell auswählen\npopover_unselect_custom = Individuell Auswahl aufheben\npopover_select_all_images_except_biggest = Alle außer Größter auswählen\npopover_select_all_images_except_smallest = Alle außer Kleinster auswählen\npopover_custom_path_check_button_entry_tooltip =\n    Ermöglicht die Auswahl von Datensätzen nach Dateipfad.\n    \n    Beispielnutzung:\n    /home/pimpek/rzecz.txt kann mit /home/pim* gefunden werden\npopover_custom_name_check_button_entry_tooltip =\n    Ermöglicht die Auswahl von Datensätzen nach Dateinamen.\n    \n    Beispielnutzung:\n    /usr/ping/pong.txt kann mit *ong* gefunden werden\npopover_custom_regex_check_button_entry_tooltip =\n    Ermöglicht die Auswahl von Datensätzen nach spezifizierter Regex.\n    \n    Mit diesem Modus ist der gesuchte Text der Pfad einschließlich Dateinamen.\n    \n    Beispielnutzung:\n    /usr/bin/ziemniak.txt kann mit /ziem[a-z]+ gefunden werden.\n    \n    Dies verwendet die Standard-Implementierung von Regex in Rust. Mehr dazu unter https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Aktiviert die Erkennung von Groß- und Kleinschreibungen.\n    \n    Wenn /home/* deaktiviert ist, findet /hoMe/roman und /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Verhindert die Auswahl aller Elemente in der Gruppe.\n    \n    Dies ist standardmäßig aktiviert, da der Benutzer in den meisten Situationen nicht sowohl Originaldateien als auch Duplikate löschen möchte, sondern mindestens eine der Dateien behalten will.\n    \n    Warnung: Diese Einstellung funktioniert nicht, wenn bereits alle Ergebnisse in der Gruppe manuell ausgewählt wurden.\npopover_custom_regex_path_label = Pfad\npopover_custom_regex_name_label = Name\npopover_custom_regex_regex_label = Regex Pfad + Name\npopover_custom_case_sensitive_check_button = Groß-/Kleinschreibung beachten\npopover_custom_all_in_group_label = Nicht alle Datensätze in der Gruppe auswählen\npopover_custom_mode_unselect = Eigene Abwählen\npopover_custom_mode_select = Eigene auswählen\npopover_sort_file_name = Dateiname\npopover_sort_folder_name = Verzeichnisname\npopover_sort_full_name = Vollständiger Name\npopover_sort_size = Größe\npopover_sort_selection = Auswahl\npopover_invalid_regex = Regex ist ungültig\npopover_valid_regex = Regex ist gültig\n# Bottom buttons\nbottom_search_button = Suchen\nbottom_select_button = Auswählen\nbottom_delete_button = Löschen\nbottom_save_button = Speichern\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Bewegen\nbottom_sort_button = Sortierung\nbottom_compare_button = Vergleichen\nbottom_search_button_tooltip = Suche starten\nbottom_select_button_tooltip = Datensätze auswählen. Nur ausgewählte Dateien/Ordner können später verarbeitet werden.\nbottom_delete_button_tooltip = Ausgewählte Dateien/Ordner löschen.\nbottom_save_button_tooltip = Daten über die Suche in Datei speichern\nbottom_symlink_button_tooltip =\n    Erstelle symbolische Links.\n    Funktioniert nur, wenn mindestens zwei Ergebnisse einer Gruppe ausgewählt sind.\n    Das Erste bleibt dabei unverändert, und das Zweite und alle Weiteren werden zu symbolischen Links auf das Erste umgewandelt.\nbottom_hardlink_button_tooltip =\n    Erstelle Hardlinks.\n    Funktioniert nur, wenn mindestens zwei Ergebnisse einer Gruppe ausgewählt sind.\n    Das Erste bleibt dabei unverändert, und das Zweite und alle Weiteren werden zu Hardlinks auf das Erste umgewandelt.\nbottom_hardlink_button_not_available_tooltip =\n    Erstellen von Hardlinks.\n    Button ist deaktiviert, da Hardlinks nicht erstellt werden können.\n    Hardlinks funktionieren nur mit Administratorrechten unter Windows, also stellen Sie sicher, dass Sie die App als Administrator ausführen.\n    Wenn die App bereits mit solchen Rechten arbeitet, überprüfen Sie auf Github auf ähnliche Probleme.\nbottom_move_button_tooltip =\n    Verschiebt Dateien in den ausgewählten Ordner.\n    Kopiert alle Dateien in den Ordner, ohne den Verzeichnisbaum zu erhalten.\n    Beim Versuch, zwei Dateien mit identischem Namen in einen Ordner zu verschieben, schlägt das Kopieren der Zweiten fehl und zeigt einen Fehler an.\nbottom_sort_button_tooltip = Sortiert Dateien/Ordner nach der gewählten Methode.\nbottom_compare_button_tooltip = Vergleiche Bilder in der Gruppe.\nbottom_show_errors_tooltip = Ein-/Ausblenden des unteren Textfeldes.\nbottom_show_upper_notebook_tooltip = Ein-/Ausblenden des oberen Notizbuch-Panels.\n# Progress Window\nprogress_stop_button = Stoppen\nprogress_stop_additional_message = Stopp angefordert\n# About Window\nabout_repository_button_tooltip = Link zur Repository-Seite mit Quellcode.\nabout_donation_button_tooltip = Link zur Spendenseite.\nabout_instruction_button_tooltip = Link zur Anweisungsseite.\nabout_translation_button_tooltip = Link zur Crowdin-Seite mit App-Übersetzungen. Offiziell werden Polnisch und Englisch unterstützt.\nabout_repository_button = Projektarchiv\nabout_donation_button = Spende\nabout_instruction_button = Anleitung\nabout_translation_button = Übersetzung\n# Header\nheader_setting_button_tooltip = Öffnet Einstellungsdialog.\nheader_about_button_tooltip = Öffnet den Dialog mit Informationen über die App.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Anzahl der verwendeten Threads\nsettings_number_of_threads_tooltip = Anzahl der verwendeten Threads, 0 bedeutet, dass alle verfügbaren Threads verwendet werden.\nsettings_use_rust_preview = Verwenden Sie stattdessen externe Bibliotheken gtk, um Vorschaubilder zu laden\nsettings_use_rust_preview_tooltip =\n    Die Verwendung von gtk-Vorschauen ist manchmal schneller und unterstützt mehr Formate, aber manchmal kann dies genau das Gegenteil sein.\n    \n    Wenn Sie Probleme mit dem Laden von Vorschauen haben, können Sie versuchen, diese Einstellung zu ändern.\n    \n    Auf Nicht-Linux-Systemen wird empfohlen, diese Option zu verwenden, da gtk-pixbuf dort nicht immer verfügbar ist, so dass die Deaktivierung dieser Option keine Vorschau einiger Bilder laden wird.\nsettings_label_restart = Sie müssen die App neu starten, um die Einstellungen anzuwenden!\nsettings_ignore_other_filesystems = Andere Dateisysteme ignorieren (nur Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignoriert Dateien, die nicht im selben Dateisystem sind wie durchsuchte Verzeichnisse.\n    \n    Funktioniert genauso wie die -xdev Option beim Finden des Befehls unter Linux\nsettings_save_at_exit_button_tooltip = Speichert die Konfiguration in einer Datei, wenn das Programm geschlossen wird.\nsettings_load_at_start_button_tooltip =\n    Konfiguration aus der Datei laden, wenn App geöffnet wird.\n    \n    Falls das nicht aktiviert ist, werden die Standardeinstellungen verwendet.\nsettings_confirm_deletion_button_tooltip = Bestätigungsdialog anzeigen, wenn der Löschen-Knopf gedrückt wird.\nsettings_confirm_link_button_tooltip = Bestätigungsdialog anzeigen, wenn auf den Knopf Hard/Symlink gedrückt wird.\nsettings_confirm_group_deletion_button_tooltip = Warndialog anzeigen, wenn versucht wird, alle Datensätze aus einer Gruppe zu löschen.\nsettings_show_text_view_button_tooltip = Textfenster am unteren Rand der Benutzeroberfläche anzeigen.\nsettings_use_cache_button_tooltip = Datei-Cache verwenden.\nsettings_save_also_as_json_button_tooltip = Cache im (menschlich lesbaren) JSON-Format speichern. Es ist möglich, den Inhalt zu ändern. Der Cache wird automatisch aus dieser Datei von der App gelesen, wenn der Binärformat-Cache (mit .bin Erweiterung) fehlt.\nsettings_use_trash_button_tooltip = Wenn aktiviert, verschiebt Dateien in den Papierkorb, anstatt sie permanent zu löschen.\nsettings_language_label_tooltip = Sprache der Benutzeroberfläche.\nsettings_save_at_exit_button = Speichert die Konfiguration beim Schließen der App\nsettings_load_at_start_button = Konfiguration beim Öffnen der App laden\nsettings_confirm_deletion_button = Bestätigungsdialog beim Löschen von Dateien anzeigen\nsettings_confirm_link_button = Bestätigungsdialog anzeigen, wenn Hard/Symlinks irgendwelche Dateien\nsettings_confirm_group_deletion_button = Bestätigungsdialog beim Löschen aller Dateien in der Gruppe anzeigen\nsettings_show_text_view_button = Unteren Textbereich anzeigen\nsettings_use_cache_button = Cache verwenden\nsettings_save_also_as_json_button = Cache auch als JSON-Datei speichern\nsettings_use_trash_button = Gelöschte Dateien in den Papierkorb verschieben\nsettings_language_label = Sprache\nsettings_multiple_delete_outdated_cache_checkbutton = Veraltete Cache-Einträge automatisch löschen\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Ermöglicht das Löschen veralteter Cache-Ergebnisse, die auf nicht existierende Dateien verweisen.\n    \n    Wenn aktiviert, stellt das Programm sicher, dass beim Laden von Datensätzen alle auf gültige Dateien verweisen (kaputte Verweise werden ignorieren).\n    \n    Deaktivieren dieser Option wird helfen, Dateien auf externen Laufwerken zu scannen, so dass Cache-Einträge über diese nicht beim nächsten Scan gelöscht werden.\n    \n    Bei hunderttausenden Datensätzen im Cache wird empfohlen, diese Option zu aktivieren, um das Laden und Speichern des Caches am Anfang und am Ende des Scans zu beschleunigen.\nsettings_notebook_general = Allgemein\nsettings_notebook_duplicates = Duplikate\nsettings_notebook_images = Ähnliche Bilder\nsettings_notebook_videos = Ähnliches Video\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Zeigt eine Vorschau auf der rechten Seite (bei der Auswahl einer Bilddatei).\nsettings_multiple_image_preview_checkbutton = Bildvorschau anzeigen\nsettings_multiple_clear_cache_button_tooltip =\n    Leere den Cache manuell aus veralteten Einträgen.\n    Das sollte nur verwendet werden, wenn das automatische Löschen deaktiviert wurde.\nsettings_multiple_clear_cache_button = Entferne veraltete Ergebnisse aus dem Cache.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Versteckt alle Dateien außer einem, wenn sie auf dieselben Daten (Hardlink) verweisen.\n    \n    Z.B. für den Fall, dass es (auf der Festplatte) sieben Dateien gibt, die an bestimmte Daten gelinkt sind und eine weiter Datei existiert mit denselben Daten, aber mit unterschiedlichem Inode, dann wird im Duplikatsucher nur eine Datei der sieben gelinkten und die seperate Datei sichtbar sein.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Legen Sie die minimale Dateigröße fest, die zwischengespeichert wird.\n    \n    Wenn Sie einen kleineren Wert wählen, werden mehr Datensätze generiert. Dies wird die Suche beschleunigen, aber das Laden und Speichern zum Cache verlangsamen.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Aktiviert das Caching von Prehashes (Hash aus einem kleinen Teil der Datei), was es erlaubt, nicht duplizierte Ergebnisse schneller zu verwerfen.\n    \n    Es ist standardmäßig deaktiviert, da es in einigen Situationen zu Verlangsamungen führen kann.\n    \n    Es wird dringend empfohlen, es beim Scannen von Hunderttausenden oder Millionen Dateien zu verwenden, da es die Suche um ein Vielfaches beschleunigen kann.\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimale Größe des zwischengespeicherten Eintrags.\nsettings_duplicates_hide_hard_link_button = Verstecke harte Links\nsettings_duplicates_prehash_checkbutton = Prehash-Cache verwenden\nsettings_duplicates_minimal_size_cache_label = Minimale Dateigröße (in Bytes), die im Cache gespeichert wird\nsettings_duplicates_minimal_size_cache_prehash_label = Minimale Dateigröße (in Bytes), die im Prehash-Cache gespeichert wird\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Aktuelle Einstellungen in Datei speichern.\nsettings_loading_button_tooltip = Lade die Einstellungen aus einer Datei und ersetze die aktuellen Einstellungen mit diesen.\nsettings_reset_button_tooltip = Aktuelle Konfiguration auf Standardeinstellung zurücksetzen.\nsettings_saving_button = Konfiguration speichern\nsettings_loading_button = Konfiguration laden\nsettings_reset_button = Konfiguration zurücksetzen\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Öffnet den Ordner, in dem txt-Dateien mit Cache-Daten gespeichert sind.\n    \n    Änderungen an den Cache-Dateien kann dazu führen, dass ungültige Ergebnisse angezeigt werden. Aber Modifikation des Pfades kann Zeit sparen, wenn eine große Anzahl von Dateien verschoben werden.\n    \n    Sie können diese Dateien zwischen Computern kopieren, um Zeit beim erneuten Scannen von Dateien zu sparen (natürlich nur, wenn diese eine ähnliche Verzeichnisstruktur haben).\n    \n    Bei Problemen mit dem Cache können diese Dateien entfernt werden, sodass die App sie automatisch neu generiert.\nsettings_folder_settings_open_tooltip =\n    Öffnet den Ordner, in dem die Czkawka Konfiguration gespeichert ist.\n    \n    WARNUNG: Manuelle Änderung der Konfiguration kann Ihren Workflow stören.\nsettings_folder_cache_open = Cache-Ordner öffnen\nsettings_folder_settings_open = Einstellungsordner öffnen\n# Compute results\ncompute_stopped_by_user = Suche wurde vom Benutzer gestoppt\ncompute_found_duplicates_hash_size = { $number_files } Duplikate in { $number_groups } Gruppen gefunden, die { $size } in { $time } genommen haben\ncompute_found_duplicates_name = { $number_files } Duplikate in { $number_groups } Gruppen in { $time } gefunden\ncompute_found_empty_folders = { $number_files } leere Ordner in { $time } gefunden\ncompute_found_empty_files = { $number_files } leere Dateien in { $time } gefunden\ncompute_found_big_files = { $number_files } große Dateien in { $time } gefunden\ncompute_found_temporary_files = { $number_files } temporäre Dateien in { $time } gefunden\ncompute_found_images = { $number_files } ähnliche Bilder in { $number_groups } Gruppen in { $time } gefunden\ncompute_found_videos = { $number_files } ähnliche Videos in { $number_groups } Gruppen in { $time } gefunden\ncompute_found_music = { $number_files } ähnliche Musikdateien in { $number_groups } Gruppen in { $time } gefunden\ncompute_found_invalid_symlinks = { $number_files } ungültige Symlinks in { $time } gefunden\ncompute_found_broken_files = { $number_files } fehlerhafte Dateien in { $time } gefunden\ncompute_found_bad_extensions = { $number_files } Dateien mit ungültigen Endungen in { $time } gefunden\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] gescannt { $file_number } Datei\n       *[other] gescannte { $file_number } Dateien\n    }\nprogress_scanning_extension_of_files = Überprüfte Erweiterung von { $file_checked }/{ $all_files } Datei\nprogress_scanning_broken_files = Überprüft { $file_checked }/{ $all_files } Datei ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hashed von { $file_checked }/{ $all_files } Video\nprogress_creating_video_thumbnails = Erstellte Vorschaubilder von { $file_checked }/{ $all_files } Video\nprogress_scanning_image = Hashed von { $file_checked }/{ $all_files } Bild ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Verglichen mit { $file_checked }/{ $all_files } Bild-Hash\nprogress_scanning_music_tags_end = Vergleiche Tags von { $file_checked }/{ $all_files } Musikdatei\nprogress_scanning_music_tags = Tags von { $file_checked }/{ $all_files } Musikdatei lesen\nprogress_scanning_music_content_end = Verglichen Fingerabdruck von { $file_checked }/{ $all_files } Musikdatei\nprogress_scanning_music_content = Berechneter Fingerabdruck von { $file_checked }/{ $all_files } Musikdatei ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] gescannt { $folder_number } Ordner\n       *[other] Gescannte { $folder_number } Ordner\n    }\nprogress_scanning_size = Scanne Größe der { $file_number } Datei\nprogress_scanning_size_name = Gescannter Name und Größe der { $file_number } Datei\nprogress_scanning_name = Gescannter Name der { $file_number } Datei\nprogress_analyzed_partial_hash = Analysierter Teilhash von { $file_checked }/{ $all_files } Dateien ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Analysiert voller Hash der { $file_checked }/{ $all_files } Dateien ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Lade Vorhash-Cache\nprogress_prehash_cache_saving = Speichere Vorhash-Cache\nprogress_hash_cache_loading = Hash-Cache wird geladen\nprogress_hash_cache_saving = Speichere Hash-Cache\nprogress_cache_loading = Cache wird geladen\nprogress_cache_saving = Cache speichern\nprogress_current_stage = Aktueller Prozess:{ \" \" }\nprogress_all_stages = Gesamtprozess:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Konfiguration in Datei { $name } gespeichert.\nsaving_loading_saving_failure = Konfigurationsdaten konnten nicht in Datei { $name }gespeichert werden, Grund { $reason }.\nsaving_loading_reset_configuration = Aktuelle Konfiguration wurde gelöscht.\nsaving_loading_loading_success = Richtig geladene App-Konfiguration.\nsaving_loading_failed_to_create_config_file = Fehler beim Erstellen der Konfigurationsdatei \"{ $path }\", Grund \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Konfiguration kann nicht von \"{ $path }\" geladen werden, da sie nicht existiert oder keine Datei ist.\nsaving_loading_failed_to_read_data_from_file = Daten von Datei \"{ $path }\" können nicht gelesen werden, Grund \"{ $reason }\".\n# Other\nselected_all_reference_folders = Suche kann nicht gestartet werden, wenn alle Verzeichnisse als Referenzordner gesetzt sind\nsearching_for_data = Suche nach Daten, es kann eine Weile dauern, bitte warten...\ntext_view_messages = NACHRICHT\ntext_view_warnings = WARNUNGEN\ntext_view_errors = FEHLER\nabout_window_motto = Dieses Programm ist kostenlos zu benutzen und wird immer frei sein.\nkrokiet_new_app = Czkawka befindet sich im Wartungsmodus, was bedeutet, dass nur kritische Fehler behoben werden und keine neuen Features hinzugefügt werden. Für neue Funktionen schauen Sie sich bitte die neue Krokiet App an, die stabiler und leistungsfähiger ist und noch in aktiver Entwicklung ist.\n# Various dialog\ndialogs_ask_next_time = Nächstes Mal fragen\nsymlink_failed = Symlink { $name } zu { $target }, Grund { $reason } fehlgeschlagen\ndelete_title_dialog = Löschen bestätigen\ndelete_question_label = Sind Sie sicher, dass Sie Dateien löschen möchten?\ndelete_all_files_in_group_title = Löschen aller Dateien in der Gruppe bestätigen\ndelete_all_files_in_group_label1 = In einigen Gruppen sind alle Datensätze ausgewählt.\ndelete_all_files_in_group_label2 = Sind Sie sicher, dass Sie sie löschen möchten?\ndelete_items_label = { $items } Dateien werden gelöscht.\ndelete_items_groups_label = { $items } Dateien aus { $groups } Gruppen werden gelöscht.\nhardlink_failed = Fehler beim hardlink { $name } zu { $target }, Grund { $reason }\nhard_sym_invalid_selection_title_dialog = Ungültige Auswahl bei einigen Gruppen\nhard_sym_invalid_selection_label_1 = In einigen Gruppen ist nur ein Datensatz ausgewählt und dieser wird ignoriert.\nhard_sym_invalid_selection_label_2 = Um diese Dateien hart/symbolisch zu verlinken, müssen mindestens zwei Ergebnisse in der Gruppe ausgewählt werden.\nhard_sym_invalid_selection_label_3 = Erster der Gruppe als Original erkannt und nicht geändert, sondern zweiter und weitere modifiziert.\nhard_sym_link_title_dialog = Link-Bestätigung\nhard_sym_link_label = Sind Sie sicher, dass Sie diese Dateien verknüpfen möchten?\nmove_folder_failed = Fehler beim Verschieben des Ordners { $name }, Grund { $reason }\nmove_file_failed = Fehler beim Verschieben der Datei { $name }, Grund { $reason }\nmove_files_title_dialog = Wählen Sie den Ordner aus, in den Sie doppelte Dateien verschieben möchten\nmove_files_choose_more_than_1_path = Es darf nur ein Pfad ausgewählt sein, um Duplikate von dort kopieren zu können, ausgewählt sind { $path_number }.\nmove_stats = { $num_files }/{ $all_files } Elemente korrekt verschoben\nsave_results_to_file = Ergebnisse sowohl in txt als auch in json Dateien in den Ordner \"{ $name }\" gespeichert.\nsearch_not_choosing_any_music = FEHLER: Sie müssen mindestens ein Kontrollkästchen mit Art der Musiksuche auswählen.\nsearch_not_choosing_any_broken_files = FEHLER: Sie müssen mindestens ein Kontrollkästchen mit der Art der markierten fehlerhaften Dateien auswählen.\ninclude_folders_dialog_title = Einbezogene Ordner\nexclude_folders_dialog_title = Ausgeschlossene Ordner\ninclude_manually_directories_dialog_title = Verzeichnis manuell hinzufügen\ncache_properly_cleared = Cache vollständig geleert\ncache_clear_duplicates_title = Leere Duplikate-Cache\ncache_clear_similar_images_title = Leere Bilder-Cache\ncache_clear_similar_videos_title = Leere Video-Cache\ncache_clear_message_label_1 = Wollen Sie veraltete Einträge aus dem Cache löschen?\ncache_clear_message_label_2 = Dieser Vorgang entfernt alle Cache-Einträge, die auf ungültige Dateien verweisen.\ncache_clear_message_label_3 = Dies kann das Laden und Speichern zum Cache leicht beschleunigen.\ncache_clear_message_label_4 = ACHTUNG: Die Operation wird alle zwischengespeicherten Daten von nicht verbundenen externen Laufwerken entfernen, somit muss jeder Hash erneut generiert werden.\n# Show preview\npreview_image_resize_failure = Fehler beim Ändern der Bildgröße { $name }.\npreview_image_opening_failure = Konnte Bild { $name } nicht öffnen, Grund { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Gruppe { $current_group }/{ $all_groups } ({ $images_in_group } Bilder)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/el/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Ρυθμίσεις\nwindow_main_title = Czkawka (Ήκουψ)\nwindow_progress_title = Σάρωση\nwindow_compare_images = Σύγκριση Εικόνων\n# General\ngeneral_ok_button = Εντάξει\ngeneral_close_button = Κλείσιμο\n# Krokiet info dialog\nkrokiet_info_title = Εισαγωγή στο Krokiet - Νέα έκδοση του Czkawka\nkrokiet_info_message = \n        Το Krokiet είναι η νέα, βελτιωμένη, ταχύτερη και πιο αξιόπιστη έκδοση της Czkawka GTK GUI!\n\n        Είναι ευκολότερο στο πρόγραμμα και πιο ανθεκτικό στις αλλαγές του συστήματος, καθώς εξαρτάται μόνο από βασικές βιβλιοθήκες που είναι διαθέσιμες στα περισσότερα συστήματα από προεπιλογή.\n\n        Το Krokiet επίσης φέρνει λειτουργίες που λείπουν από την Czkawka, συμπεριλαμβανομένων των μικρογραφιών στην λειτουργία σύγκρισης βίντεο, ενός καθαριστή EXIF, επιλογών προόδου για μετακίνησης/αντιγραφής/διαγραφής αρχείων ή επεκτάσιμων επιλογών ταξινόμησης.\n\n        Δοκιμάστε το και δείτε τη διαφορά!\n\n        Η Czkawka θα συνεχίσει να λαμβάνει διορθώσεις σφαλμάτων και μικρές ενημερώσεις από εμένα, αλλά όλες οι νέες λειτουργίες θα αναπτυχθούν αποκλειστικά για το Krokiet και οποιοσδήποτε είναι ελεύθερος να συνεισφέρει νέες λειτουργίες, να προσθέσει ελλείπουσες λειτουργίες ή να επεκτείνει περαιτέρω την Czkawka.\n\n        PS: Αυτό το μήνυμα θα πρέπει να εμφανίζεται μόνο μία φορά. Εάν εμφανίζεται ξανά, ορίστε την περιβαλλοντική μεταβλητή CZKAWKA_DONT_ANNOY_ME σε οποιαδήποτε μη κενή τιμή.\n# Main window\nmusic_title_checkbox = Τίτλος\nmusic_artist_checkbox = Καλλιτέχνης\nmusic_year_checkbox = Έτος\nmusic_bitrate_checkbox = Ρυθμός Bit\nmusic_genre_checkbox = Είδος\nmusic_length_checkbox = Μήκος\nmusic_comparison_checkbox = Κατά Προσέγγιση Σύγκριση\nmusic_checking_by_tags = Ετικέτες\nmusic_checking_by_content = Περιεχόμενο\nsame_music_seconds_label = Ελάχιστη δεύτερη διάρκεια θραύσματος\nsame_music_similarity_label = Μέγιστη διαφορά\nmusic_compare_only_in_title_group = Σύγκριση μεταξύ ομάδων παρόμοιων τίτλων\nmusic_compare_only_in_title_group_tooltip =\n    Όταν ενεργοποιηθεί, τα αρχεία ομαδοποιούνται κατά τίτλο και στη συνέχεια συγκρίνονται μεταξύ τους.\n    \n    Με 10000 αρχεία, αντ' αυτού σχεδόν 100 εκατομμύρια συγκρίσεις συνήθως θα υπάρχουν περίπου 20000 συγκρίσεις.\nsame_music_tooltip =\n    Η αναζήτηση παρόμοιων αρχείων μουσικής με βάση το περιεχόμενό του μπορεί να ρυθμιστεί με τη ρύθμιση:\n    \n    - Ο ελάχιστος χρόνος θραύσματος μετά το οποίο τα αρχεία μουσικής μπορούν να προσδιοριστούν ως παρόμοια\n    - Η μέγιστη διαφορά διαφοράς μεταξύ δύο δοκιμαζόμενων θραυσμάτων\n    \n    Το κλειδί για καλά αποτελέσματα είναι να βρεθούν λογικοί συνδυασμοί αυτών των παραμέτρων, για παρέχονται.\n    \n    Ο ορισμός του ελάχιστου χρόνου σε 5s και η μέγιστη διαφορά σε 1.0, θα αναζητήσει σχεδόν πανομοιότυπα θραύσματα στα αρχεία.\n    Ένας χρόνος 20 δευτερολέπτων και μια μέγιστη διαφορά 6.0, από την άλλη πλευρά, λειτουργεί καλά για την εύρεση remixes/live εκδόσεις κλπ.\n    \n    Από προεπιλογή, κάθε αρχείο μουσικής συγκρίνεται μεταξύ τους και αυτό μπορεί να πάρει πολύ χρόνο κατά τη δοκιμή πολλών αρχείων, έτσι είναι συνήθως καλύτερο να χρησιμοποιήσετε φακέλους αναφοράς και να προσδιορίσετε ποια αρχεία πρέπει να συγκρίνονται μεταξύ τους (με την ίδια ποσότητα αρχείων, η σύγκριση των δακτυλικών αποτυπωμάτων θα είναι γρηγορότερη τουλάχιστον 4x από ό, τι χωρίς φακέλους αναφοράς).\nmusic_comparison_checkbox_tooltip =\n    Ψάχνει για παρόμοια αρχεία μουσικής χρησιμοποιώντας AI, το οποίο χρησιμοποιεί μηχανική μάθηση για να αφαιρέσει παρένθεση από μια φράση. Για παράδειγμα, με αυτήν την επιλογή ενεργοποιημένη, τα εν λόγω αρχεία θα θεωρούνται διπλότυπα:\n    \n    S’wieald dzizśło’b --- S’wieřdziz’ło’b (Remix Lato 2021)\nduplicate_case_sensitive_name = Διάκριση Πεζών/ Κεφαλαίων\nduplicate_case_sensitive_name_tooltip =\n    Όταν ενεργοποιημένη, συγχώνευση πραγματοποιείται μόνο για κάθε λεξικό όντο που έχει τον ίδιο όνομα, α.λλ. ε. Żołd <-> Żołd\n    \n    Απευθείας θα συγχωρηθούν τα όντα μέσω της συγχώνευσης αν κάθε γράμμα είναι αυτόματα αξιοπιστίας, π.χ. żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Μέγεθος και όνομα\nduplicate_mode_name_combo_box = Όνομα\nduplicate_mode_size_combo_box = Μέγεθος\nduplicate_mode_hash_combo_box = Κατακερματισμός\nduplicate_hash_type_tooltip =\n    Czkawka προσφέρει 3 τύπους hashes:\n    \n    Blake3 - λειτουργία κρυπτογραφικού hash. Αυτή είναι η προεπιλογή επειδή είναι πολύ γρήγορη.\n    \n    CRC32 - απλή συνάρτηση hash. Αυτό θα πρέπει να είναι πιο γρήγορα από Blake3, αλλά μπορεί πολύ σπάνια να έχει κάποιες συγκρούσεις.\n    \n    XXH3 - πολύ παρόμοιο στην απόδοση και την ποιότητα hash με Blake3 (αλλά μη κρυπτογραφικό).\nduplicate_check_method_tooltip =\n    Προς το παρόν, Czkawka προσφέρει τρεις τύπους μεθόδου για να βρείτε αντίγραφα από:\n    \n    Όνομα - Εύρεση αρχείων που έχουν το ίδιο όνομα.\n    \n    Μέγεθος - Εύρεση αρχείων με το ίδιο μέγεθος.\n    \n    Hash - Εύρεση αρχείων με το ίδιο περιεχόμενο. Αυτή η λειτουργία κατακερματίζει το αρχείο και αργότερα συγκρίνει αυτό το κατακερματισμό για να βρείτε διπλότυπα. Αυτή η λειτουργία είναι ο ασφαλέστερος τρόπος για να βρείτε διπλότυπα. Η εφαρμογή χρησιμοποιεί βαριά κρύπτη, έτσι ώστε η δεύτερη και περαιτέρω σάρωση των ίδιων δεδομένων θα πρέπει να είναι πολύ πιο γρήγορα από την πρώτη.\nimage_hash_size_tooltip =\n    Κάθε επιλεγμένη εικόνα παράγει ένα ειδικό hash το οποίο μπορεί να συγκριθεί μεταξύ τους, και μια μικρή διαφορά μεταξύ τους σημαίνει ότι αυτές οι εικόνες είναι παρόμοια.\n    \n    8 μέγεθος hash είναι αρκετά καλό να βρείτε εικόνες που είναι μόνο λίγο παρόμοια με το πρωτότυπο. Με ένα μεγαλύτερο σύνολο εικόνων (>1000), αυτό θα παράγει ένα μεγάλο ποσό ψευδών θετικών, γι 'αυτό συνιστώ να χρησιμοποιήσετε ένα μεγαλύτερο μέγεθος hash σε αυτή την περίπτωση.\n    \n    16 είναι το προεπιλεγμένο μέγεθος hash το οποίο είναι ένας αρκετά καλός συμβιβασμός ανάμεσα στην εύρεση ακόμη και λίγο παρόμοιες εικόνες και έχει μόνο μια μικρή ποσότητα συγκρούσεων hash.\n    \n    32 και 64 hashes βρείτε μόνο παρόμοιες εικόνες, αλλά θα πρέπει να έχουν σχεδόν καμία ψευδή θετικά (ίσως εκτός από μερικές εικόνες με άλφα κανάλι).\nimage_resize_filter_tooltip =\n    Για να υπολογιστεί το hash της εικόνας, η βιβλιοθήκη πρέπει πρώτα να το αλλάξει μέγεθος.\n    \n    Εξαρτάται από τον επιλεγμένο αλγόριθμο, η προκύπτουσα εικόνα που χρησιμοποιείται για τον υπολογισμό του hash θα φαίνεται λίγο διαφορετική.\n    \n    Ο γρηγορότερος αλγόριθμος που χρησιμοποιείται, αλλά και εκείνος που δίνει τα χειρότερα αποτελέσματα, είναι ο Nearest. Είναι ενεργοποιημένη από προεπιλογή, επειδή με μέγεθος 16x16 hash χαμηλότερη ποιότητα δεν είναι πραγματικά ορατή.\n    \n    Με μέγεθος κατακερματισμού 8x8 συνιστάται να χρησιμοποιήσετε διαφορετικό αλγόριθμο από το Nearest, για να έχετε καλύτερες ομάδες εικόνων.\nimage_hash_alg_tooltip =\n    Οι χρώματες μπορούν να επιλέξουν από ένα από τα πολλά αλγόριθμα λογισμικού για την υπολογισμός του hash.\n    \n    Κάθε ενέργεια έχει και στρεβλώματα και πολύτερα λεπτά, και ακριβώς για διάφορες εικόνες, υπάρχουν χαμηλότεροι και συχνά καλύτεροι αποτελέσματα.\n    \n    Οπότε για να διακρίνετε τον καλύτερο για εσάς, είναι απαραίτητο ο μεχανικός εξέταση.\nbig_files_mode_combobox_tooltip = Επιτρέπει την αναζήτηση για μικρότερα/μεγαλύτερα αρχεία\nbig_files_mode_label = Ελεγχμένα αρχεία\nbig_files_mode_smallest_combo_box = Το Μικρότερο\nbig_files_mode_biggest_combo_box = Το Μεγαλύτερο\nmain_notebook_duplicates = Αντίγραφο Αρχείων\nmain_notebook_empty_directories = Άδειοι Κατάλογοι\nmain_notebook_big_files = Μεγάλα Αρχεία\nmain_notebook_empty_files = Κενά Αρχεία\nmain_notebook_temporary = Προσωρινά Αρχεία\nmain_notebook_similar_images = Παρόμοιες Εικόνες\nmain_notebook_similar_videos = Παρόμοια Βίντεο\nmain_notebook_same_music = Αντίγραφο Μουσικής\nmain_notebook_symlinks = Μη Έγκυρα Symlinks\nmain_notebook_broken_files = Κατεστραμμένα Αρχεία\nmain_notebook_bad_extensions = Εσφαλμένες Επεκτάσεις\nmain_tree_view_column_file_name = Όνομα Αρχείου\nmain_tree_view_column_folder_name = Όνομα Φακέλου\nmain_tree_view_column_path = Διαδρομή\nmain_tree_view_column_modification = Ημερομηνία Τροποποίησης\nmain_tree_view_column_size = Μέγεθος\nmain_tree_view_column_similarity = Ομοιότητα\nmain_tree_view_column_dimensions = Διαστάσεις\nmain_tree_view_column_title = Τίτλος\nmain_tree_view_column_artist = Καλλιτέχνης\nmain_tree_view_column_year = Έτος\nmain_tree_view_column_bitrate = Ρυθμός Bit\nmain_tree_view_column_length = Μήκος\nmain_tree_view_column_genre = Είδος\nmain_tree_view_column_symlink_file_name = Όνομα Αρχείου Συντόμευσης\nmain_tree_view_column_symlink_folder = Φάκελος Συντόμευσης\nmain_tree_view_column_destination_path = Διαδρομή Προορισμού\nmain_tree_view_column_type_of_error = Τύπος Σφάλματος\nmain_tree_view_column_current_extension = Τρέχουσα Επέκταση\nmain_tree_view_column_proper_extensions = Κατάλληλη Επέκταση\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Κωδικοποιητής\nmain_label_check_method = Έλεγχος μεθόδου\nmain_label_hash_type = Τύπος κατακερματισμού\nmain_label_hash_size = Μέγεθος κατακερματισμού\nmain_label_size_bytes = Μέγεθος (bytes)\nmain_label_min_size = Ελάχιστο\nmain_label_max_size = Μέγιστο\nmain_label_shown_files = Αριθμός εμφανιζόμενων αρχείων\nmain_label_resize_algorithm = Αλλαγή μεγέθους αλγορίθμου\nmain_label_similarity = Similarity{ \"   \" }\nmain_check_box_broken_files_audio = Ήχος\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = Αρχειοθέτηση\nmain_check_box_broken_files_image = Εικόνα\nmain_check_box_broken_files_video = Βίντεο\nmain_check_box_broken_files_video_tooltip = Χρησιμοποιεί το ffmpeg/ffprobe για την επικύρωση αρχείων βίντεο. Πολύ αργό και μπορεί να ανιχνεύσει αυστηρές ατέλειες ακόμη και αν το αρχείο παίζει κανονικά.\ncheck_button_general_same_size = Αγνόηση ίδιου μεγέθους\ncheck_button_general_same_size_tooltip = Αγνοήστε τα αρχεία με το ίδιο μέγεθος στα αποτελέσματα - συνήθως αυτά είναι 1: 1 διπλότυπα\nmain_label_size_bytes_tooltip = Μέγεθος αρχείων που θα χρησιμοποιηθούν κατά τη σάρωση\n# Upper window\nupper_tree_view_included_folder_column_title = Φάκελοι προς αναζήτηση\nupper_tree_view_included_reference_column_title = Φάκελοι Αναφοράς\nupper_recursive_button = Αναδρομικά\nupper_recursive_button_tooltip = Αν επιλεχθεί, αναζητήστε επίσης αρχεία που δεν τοποθετούνται απευθείας σε επιλεγμένους φακέλους.\nupper_manual_add_included_button = Χειροκίνητη Προσθήκη\nupper_add_included_button = Προσθήκη\nupper_remove_included_button = Αφαίρεση\nupper_manual_add_excluded_button = Χειροκίνητη Προσθήκη\nupper_add_excluded_button = Προσθήκη\nupper_remove_excluded_button = Αφαίρεση\nupper_manual_add_included_button_tooltip =\n    Προσθήκη ονόματος καταλόγου στην αναζήτηση με το χέρι.\n    \n    Για να προσθέσετε πολλαπλές διαδρομές ταυτόχρονα, διαχωρίστε τις με το ;\n    \n    /home/roman;/home/rozkaz θα προσθέσετε δύο καταλόγους /home/roman και /home/rozkaz\nupper_add_included_button_tooltip = Προσθήκη νέου φακέλου για αναζήτηση.\nupper_remove_included_button_tooltip = Διαγραφή καταλόγου από την αναζήτηση.\nupper_manual_add_excluded_button_tooltip =\n    Προσθήκη εξαιρούμενου ονόματος καταλόγου με το χέρι.\n    \n    Για να προσθέσετε πολλαπλές διαδρομές ταυτόχρονα, διαχωρίστε τις με το ;\n    \n    /home/roman;/home/krokiet θα προσθέσει δύο καταλόγους /home/roman και /home/keokiet\nupper_add_excluded_button_tooltip = Προσθήκη καταλόγου για να αποκλειστεί στην αναζήτηση.\nupper_remove_excluded_button_tooltip = Διαγραφή καταλόγου από αποκλεισμένους.\nupper_notebook_items_configuration = Ρύθμιση Στοιχείων\nupper_notebook_excluded_directories = Αποκλεισμένοι Δρόμοι\nupper_notebook_included_directories = Συμπεριλημμένες Διαδρομές\nupper_allowed_extensions_tooltip =\n    Οι επιτρεπόμενες επεκτάσεις πρέπει να διαχωρίζονται με κόμματα (εξ ορισμού είναι διαθέσιμες).\n    \n    Τα ακόλουθα Macros, τα οποία προσθέτουν πολλαπλές επεκτάσεις ταυτόχρονα, είναι επίσης διαθέσιμα: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Χρήση παράδειγμα \".exe, IMAGE, VIDEO, .rar, 7z\" - αυτό σημαίνει ότι οι εικόνες (π. χ. . jpg, png), βίντεο (π.χ. avi, mp4), exe, rar και 7z αρχεία θα σαρωθούν.\nupper_excluded_extensions_tooltip =\n    Λίστα απενεργοποιημένων αρχείων που θα αγνοηθούν κατά τη σάρωση.\n    \n    Όταν χρησιμοποιείτε και τις δύο επιτρεπόμενες και απενεργοποιημένες επεκτάσεις, αυτή έχει υψηλότερη προτεραιότητα, οπότε το αρχείο δεν θα ελεγχθεί.\nupper_excluded_items_tooltip = \n        Πρέπει να εξαιρούνται τα στοιχεία που περιέχουν * wildcard και να διαχωρίζονται με κόμματα.\n        Αυτό είναι πιο αργό από τα Excluded Paths, οπότε χρησιμοποιήστε το προσεκτικά.\nupper_excluded_items = Εξαιρούμενα Αντικείμενα:\nupper_allowed_extensions = Επιτρεπόμενες Επεκτάσεις:\nupper_excluded_extensions = Απενεργοποιημένες Επεκτάσεις:\n# Popovers\npopover_select_all = Επιλογή όλων\npopover_unselect_all = Αποεπιλογή όλων\npopover_reverse = Αντίστροφη Επιλογή\npopover_select_all_except_shortest_path = Επιλέξτε όλα εκτός από τη συντομότερη διαδρομή\npopover_select_all_except_longest_path = Επιλέξτε όλα εκτός από τη μεγαλύτερη διαδρομή\npopover_select_all_except_oldest = Επιλογή όλων εκτός από το παλαιότερο\npopover_select_all_except_newest = Επιλογή όλων εκτός από το νεότερο\npopover_select_one_oldest = Επιλέξτε ένα παλαιότερο\npopover_select_one_newest = Επιλέξτε ένα νεότερο\npopover_select_custom = Επιλέξτε προσαρμοσμένο\npopover_unselect_custom = Αποεπιλογή προσαρμοσμένου\npopover_select_all_images_except_biggest = Επιλογή όλων εκτός από το μεγαλύτερο\npopover_select_all_images_except_smallest = Επιλογή όλων εκτός των μικρότερων\npopover_custom_path_check_button_entry_tooltip =\n    Επιλέξτε εγγραφές με διαδρομή.\n    \n    Παράδειγμα χρήσης:\n    /home/pimpek/rzecz.txt μπορεί να βρεθεί με /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Επιλέξτε εγγραφές με ονόματα αρχείων.\n    \n    Παράδειγμα χρήσης:\n    /usr/ping/pong.txt μπορεί να βρεθεί με *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Επιλέξτε εγγραφές με καθορισμένο Regex.\n    \n    Με αυτή τη λειτουργία, το κείμενο αναζήτησης είναι η διαδρομή με το όνομα.\n    \n    Παράδειγμα χρήσης:\n    /usr/bin/ziemniak. xt μπορεί να βρεθεί με /ziem[a-z]+\n    \n    Αυτό χρησιμοποιεί την προεπιλεγμένη εφαρμογή Rust regex. Μπορείτε να διαβάσετε περισσότερα για αυτό εδώ: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip = Όταν απενεργοποιηθεί το /home/* βρίσκει και το /HoMe/roman και το /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Προτείνει την επιλογή όλων των γραμμών σε ομάδα.\n    \n    Αυτό είναι ενεργοποιημένο ακολουθώντας το προεπιλογή, καθώς παράγωγα στη μεγαλύτερα γενικά δεν θέλετε να αφαιρέσετε και τους αρχικούς αρχείους και τα δωρεάν αποδοχοί, αλλά ως εκ των πραγμάτων να αφήσετε τουλάχιστον ένα αρχείο.\n    \n    ΠΡΟΣΗΣΜΗ: Αυτό το σύστημα δεν λειτουργεί αν ορισμένες ομάδες έχετε ήδη επιλεγεί με χειρόδεσμο.\npopover_custom_regex_path_label = Διαδρομή\npopover_custom_regex_name_label = Όνομα\npopover_custom_regex_regex_label = Regex Διαδρομή + Όνομα\npopover_custom_case_sensitive_check_button = Διάκριση πεζών/ κεφαλαίων\npopover_custom_all_in_group_label = Να μην επιλέγονται όλες οι εγγραφές στην ομάδα\npopover_custom_mode_unselect = Αποεπιλογή Προσαρμοσμένου\npopover_custom_mode_select = Επιλογή Προσαρμοσμένου\npopover_sort_file_name = Όνομα αρχείου\npopover_sort_folder_name = Όνομα φακέλου\npopover_sort_full_name = Πλήρες όνομα\npopover_sort_size = Μέγεθος\npopover_sort_selection = Επιλογή\npopover_invalid_regex = Regex δεν είναι έγκυρο\npopover_valid_regex = Regex είναι έγκυρο\n# Bottom buttons\nbottom_search_button = Αναζήτηση\nbottom_select_button = Επιλογή\nbottom_delete_button = Διαγραφή\nbottom_save_button = Αποθήκευση\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Μετακίνηση\nbottom_sort_button = Ταξινόμηση\nbottom_compare_button = Σύγκριση\nbottom_search_button_tooltip = Έναρξη αναζήτησης\nbottom_select_button_tooltip = Επιλέξτε εγγραφές. Μόνο επιλεγμένα αρχεία/φάκελοι μπορούν να υποβληθούν σε μεταγενέστερη επεξεργασία.\nbottom_delete_button_tooltip = Διαγραφή επιλεγμένων αρχείων/φακέλων.\nbottom_save_button_tooltip = Αποθήκευση δεδομένων σχετικά με την αναζήτηση σε αρχείο\nbottom_symlink_button_tooltip =\n    Δημιουργία συμβολικών συνδέσμων.\n    Λειτουργεί μόνο όταν επιλεγούν τουλάχιστον δύο αποτελέσματα σε μια ομάδα.\n    Πρώτα παραμένει αμετάβλητη και δεύτερον και αργότερα συνδέεται με την πρώτη.\nbottom_hardlink_button_tooltip =\n    Δημιουργία hardlinks.\n    λειτουργεί μόνο όταν επιλεγούν τουλάχιστον δύο αποτελέσματα σε μια ομάδα.\n    Πρώτα παραμένει αμετάβλητη και η δεύτερη και αργότερα συνδέονται σκληρά με την πρώτη.\nbottom_hardlink_button_not_available_tooltip =\n    Δημιουργία hardlinks. Το κουμπί\n    είναι απενεργοποιημένο, επειδή οι hardlinks δεν μπορούν να δημιουργηθούν.\n    Hardlinks λειτουργεί μόνο με δικαιώματα διαχειριστή στα Windows, οπότε φροντίστε να εκτελέσετε την εφαρμογή ως διαχειριστής.\n    Εάν η εφαρμογή λειτουργεί ήδη με τέτοια δικαιώματα ελέγξτε για παρόμοια ζητήματα στο Github.\nbottom_move_button_tooltip =\n    Μεταφέρνει αρχεία στο προαιρετικό κatalogό.\n    Κάπερε όλα τα αρχεία στον κatalogό μαζί, χωρίς να ευσήμανται οι δένδροι κatalogού.\n    Όταν προσπαθείτε να ακολουθήσετε το μετάβαση δύο αρχείων με τον ίδιο όνομα σε κatalogό, θα απέτυχε και θα εμφανιστεί ένα σφάλμα.\nbottom_sort_button_tooltip = Ταξινόμηση αρχείων/φακέλων σύμφωνα με την επιλεγμένη μέθοδο.\nbottom_compare_button_tooltip = Σύγκριση εικόνων στην ομάδα.\nbottom_show_errors_tooltip = Εμφάνιση/Απόκρυψη πίνακα κάτω κειμένου.\nbottom_show_upper_notebook_tooltip = Εμφάνιση/Απόκρυψη ανώτερου πίνακα σημειωμάτων.\n# Progress Window\nprogress_stop_button = Διακοπή\nprogress_stop_additional_message = Η διακοπή ζητήθηκε\n# About Window\nabout_repository_button_tooltip = Σύνδεσμος προς σελίδα αποθετηρίου με πηγαίο κώδικα.\nabout_donation_button_tooltip = Σύνδεση με τη σελίδα δωρεών.\nabout_instruction_button_tooltip = Σύνδεσμος στη σελίδα οδηγιών.\nabout_translation_button_tooltip = Σύνδεσμος προς τη σελίδα του Crowdin με μεταφράσεις εφαρμογών. Υιοθετούνται επίσημα πολωνικά και αγγλικά.\nabout_repository_button = Αποθετήριο\nabout_donation_button = Δωρεά\nabout_instruction_button = Οδηγίες\nabout_translation_button = Μετάφραση\n# Header\nheader_setting_button_tooltip = Άνοιγμα διαλόγου ρυθμίσεων.\nheader_about_button_tooltip = Άνοιγμα διαλόγου με πληροφορίες σχετικά με την εφαρμογή.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Αριθμός χρησιμοποιημένων νημάτων\nsettings_number_of_threads_tooltip = Αριθμός χρησιμοποιημένων νημάτων, 0 σημαίνει ότι θα χρησιμοποιηθούν όλα τα διαθέσιμα νήματα.\nsettings_use_rust_preview = Χρήση εξωτερικών βιβλιοθηκών αντ' αυτού gtk για φόρτωση προεπισκοπήσεων\nsettings_use_rust_preview_tooltip =\n    Χρησιμοποιώντας gtk προεπισκοπήσεις θα είναι μερικές φορές πιο γρήγορα και να υποστηρίξει περισσότερες μορφές, αλλά μερικές φορές αυτό θα μπορούσε να είναι ακριβώς το αντίθετο.\n    \n    Αν έχετε προβλήματα με τη φόρτωση προεπισκόπησης, μπορείτε να προσπαθήσετε να αλλάξετε αυτή τη ρύθμιση.\n    \n    Σε συστήματα χωρίς linux, συνιστάται να χρησιμοποιήσετε αυτήν την επιλογή, επειδή το gtk-pixbuf δεν είναι πάντα διαθέσιμο εκεί, οπότε η απενεργοποίηση αυτής της επιλογής δεν θα φορτώσει προεπισκοπήσεις ορισμένων εικόνων.\nsettings_label_restart = Πρέπει να επανεκκινήσετε την εφαρμογή για να εφαρμόσετε τις ρυθμίσεις!\nsettings_ignore_other_filesystems = Αγνόηση άλλων συστημάτων αρχείων (μόνο Linux)\nsettings_ignore_other_filesystems_tooltip =\n    αγνοεί αρχεία που δεν είναι στο ίδιο σύστημα αρχείων με αναζήτηση καταλόγων.\n    \n    Λειτουργεί όπως η επιλογή -xdev στην εντολή εύρεσης στο Linux\nsettings_save_at_exit_button_tooltip = Αποθήκευση ρυθμίσεων σε αρχείο κατά το κλείσιμο της εφαρμογής.\nsettings_load_at_start_button_tooltip =\n    Φόρτωση παραμέτρων από το αρχείο κατά το άνοιγμα της εφαρμογής.\n    \n    Αν δεν είναι ενεργοποιημένη, θα χρησιμοποιηθούν οι προεπιλεγμένες ρυθμίσεις.\nsettings_confirm_deletion_button_tooltip = Εμφάνιση διαλόγου επιβεβαίωσης όταν κάνετε κλικ στο κουμπί διαγραφής.\nsettings_confirm_link_button_tooltip = Εμφάνιση διαλόγου επιβεβαίωσης όταν κάνετε κλικ στο κουμπί hard/symlink.\nsettings_confirm_group_deletion_button_tooltip = Εμφάνιση διαλόγου προειδοποίησης όταν προσπαθείτε να διαγράψετε όλες τις εγγραφές από την ομάδα.\nsettings_show_text_view_button_tooltip = Εμφάνιση πίνακα κειμένου στο κάτω μέρος της διεπαφής χρήστη.\nsettings_use_cache_button_tooltip = Χρήση προσωρινής μνήμης αρχείων.\nsettings_save_also_as_json_button_tooltip = Αποθήκευση προσωρινής μνήμης σε (αναγνώσιμη από άνθρωπο) μορφή JSON. Είναι δυνατή η τροποποίηση του περιεχομένου του. Η προσωρινή μνήμη από αυτό το αρχείο θα διαβάζεται αυτόματα από την εφαρμογή αν λείπει η κρύπτη δυαδικής μορφής (με επέκταση κάδου).\nsettings_use_trash_button_tooltip = Μετακινεί τα αρχεία στον κάδο απορριμμάτων αντί να τα διαγράφει μόνιμα.\nsettings_language_label_tooltip = Γλώσσα διεπαφής χρήστη.\nsettings_save_at_exit_button = Αποθήκευση ρυθμίσεων κατά το κλείσιμο της εφαρμογής\nsettings_load_at_start_button = Φόρτωση ρυθμίσεων κατά το άνοιγμα της εφαρμογής\nsettings_confirm_deletion_button = Εμφάνιση διαλόγου επιβεβαίωσης κατά τη διαγραφή αρχείων\nsettings_confirm_link_button = Εμφάνιση διαλόγου επιβεβαίωσης όταν σκληρά/συντόμευση αρχείων\nsettings_confirm_group_deletion_button = Εμφάνιση διαλόγου επιβεβαίωσης κατά τη διαγραφή όλων των αρχείων της ομάδας\nsettings_show_text_view_button = Εμφάνιση κάτω πίνακα κειμένου\nsettings_use_cache_button = Χρήση προσωρινής μνήμης\nsettings_save_also_as_json_button = Επίσης αποθήκευση προσωρινής μνήμης ως αρχείο JSON\nsettings_use_trash_button = Μετακίνηση διαγραμμένων αρχείων στον κάδο απορριμμάτων\nsettings_language_label = Γλώσσα\nsettings_multiple_delete_outdated_cache_checkbutton = Αυτόματη διαγραφή ξεπερασμένων καταχωρήσεων cache\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip = \n    Στρεμώστε τα αποσκευάκημα πλήρωμα που καθυστερούν και δείχνουν ότι συνδέονται με αρχεία που δεν υπάρχουν.\n    \n    Όταν ενεργοποιηθεί η εφαρμογή βεβαιώνει ότι όταν裁剪后的回答无法满足问题需求，已恢复至原始答案长度。.\nsettings_notebook_general = Γενικά\nsettings_notebook_duplicates = Διπλότυπα\nsettings_notebook_images = Παρόμοιες Εικόνες\nsettings_notebook_videos = Παρόμοια Βίντεο\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Εμφανίζει την προεπισκόπηση στη δεξιά πλευρά (όταν επιλέγετε ένα αρχείο εικόνας).\nsettings_multiple_image_preview_checkbutton = Εμφάνιση προεπισκόπησης εικόνας\nsettings_multiple_clear_cache_button_tooltip =\n    Χειροκίνητη εκκαθάριση της λανθάνουσας μνήμης των ξεπερασμένων καταχωρήσεων.\n    Αυτό θα πρέπει να χρησιμοποιηθεί μόνο αν η αυτόματη εκκαθάριση έχει απενεργοποιηθεί.\nsettings_multiple_clear_cache_button = Κατάργηση παρωχημένων αποτελεσμάτων από τη μνήμη cache.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Απόκρυψε όλα τα αρχεία εκτός από ένα, αν όλα ανδιέφερον στον ίδιο δεδομένο (σχηματίζονται με hardlink).\n    \n    Παράδειγμα: Στο περίπλοκο όπου υπάρχουν (σε δίσκο) επτά αρχεία που σχηματίζονται με hardlink σε συγκεκριμένα δεδομένα και ένα χωρίς την ίδια γεύση αλλά με άλλο νόθρωμα, στο βραβευτήριο αποπαγών, παρουσιάζονται μόνο ένα μοναδικό αρχέιο και ένα αρχείο από τους hardlinked.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Ορίστε το ελάχιστο μέγεθος αρχείου που θα αποθηκευτεί.\n    \n    Επιλέγοντας μια μικρότερη τιμή θα δημιουργήσει περισσότερες εγγραφές. Αυτό θα επιταχύνει την αναζήτηση, αλλά επιβραδύνει τη φόρτωση της λανθάνουσας μνήμης.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Ενεργοποιεί την προσωρινή αποθήκευση του prehash (ένα κατακερματισμό υπολογισμένο από ένα μικρό μέρος του αρχείου) το οποίο επιτρέπει την προηγούμενη απόρριψη μη διπλών αποτελεσμάτων.\n    \n    Είναι απενεργοποιημένο από προεπιλογή επειδή μπορεί να προκαλέσει επιβραδύνσεις σε ορισμένες περιπτώσεις.\n    \n    Συνιστάται ιδιαίτερα να το χρησιμοποιήσετε κατά τη σάρωση εκατοντάδων χιλιάδων ή εκατομμυρίων αρχείων, επειδή μπορεί να επιταχύνει την αναζήτηση κατά πολλές φορές.\nsettings_duplicates_prehash_minimal_entry_tooltip = Ελάχιστο μέγεθος της προσωρινά αποθηκευμένης καταχώρησης.\nsettings_duplicates_hide_hard_link_button = Απόκρυψη σκληρών συνδέσμων\nsettings_duplicates_prehash_checkbutton = Χρήση προσωρινής μνήμης prehash\nsettings_duplicates_minimal_size_cache_label = Ελάχιστο μέγεθος των αρχείων (σε byte) αποθηκεύονται στη μνήμη cache\nsettings_duplicates_minimal_size_cache_prehash_label = Ελάχιστο μέγεθος των αρχείων (σε byte) αποθηκεύονται στην προσωρινή μνήμη prehash\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Αποθήκευση των τρεχουσών ρυθμίσεων ρυθμίσεων στο αρχείο.\nsettings_loading_button_tooltip = Φόρτωση ρυθμίσεων από το αρχείο και αντικατάσταση των τρεχουσών ρυθμίσεων με αυτές.\nsettings_reset_button_tooltip = Επαναφορά των τρεχουσών ρυθμίσεων στην προκαθορισμένη.\nsettings_saving_button = Αποθήκευση διαμόρφωσης\nsettings_loading_button = Φόρτωση διαμόρφωσης\nsettings_reset_button = Επαναφορά ρύθμισης παραμέτρων\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Ανοίγει το φolder που προστατεύονται οι txt αρχεία πληροφόρησης.\n    \n    Τη συμπέρασμα των αρχείων cache μπορεί να οδηγήσει σε αποτελέσματα που δεν είναι κατάλληλα. Ωστόσο, το ρυθμίζοντας τον πάθος μπορεί να σώσει χρόνο όταν μετακινείται ένα μεγάλο αριθμό αρχείων σε διαφέρουσα θέση.\n    \n    Μπορείτε να παραδώσετε αυτά τα αρχεία μεταξύ υπολογιστών για να σώσετε χρόνο στην εμφάνιση ξανά των αρχείων (ψήφηση: εάν έχουν παρόμοιο δυαδικό μορφωμάτων).\n    \n    Στο σεβαστό χώρο cache, αυτά τα αρχεία μπορούν να αφαιρέσουν. Η εφαρμογή θα αυτοκατασκευάσει τα πάλι.\nsettings_folder_settings_open_tooltip =\n    Ανοίγει το φάκελο όπου αποθηκεύεται η ρύθμιση Czkawka.\n    \n    ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η χειροκίνητη τροποποίηση της ρύθμισης μπορεί να σπάσει τη ροή εργασίας σας.\nsettings_folder_cache_open = Άνοιγμα φακέλου cache\nsettings_folder_settings_open = Άνοιγμα φακέλου ρυθμίσεων\n# Compute results\ncompute_stopped_by_user = Η αναζήτηση σταμάτησε από το χρήστη\ncompute_found_duplicates_hash_size = Βρέθηκαν { $number_files } διπλότυπα σε { $number_groups } ομάδες που πήραν { $size } σε { $time }\ncompute_found_duplicates_name = Βρέθηκαν { $number_files } διπλότυπα σε { $number_groups } ομάδες σε { $time }\ncompute_found_empty_folders = Βρέθηκαν { $number_files } άδειοι φάκελοι στο { $time }\ncompute_found_empty_files = Βρέθηκαν { $number_files } κενά αρχεία στο { $time }\ncompute_found_big_files = Βρέθηκαν { $number_files } μεγάλα αρχεία στο { $time }\ncompute_found_temporary_files = Βρέθηκαν { $number_files } προσωρινά αρχεία στο { $time }\ncompute_found_images = Ευρέθησαν { $number_files } παρόμοιες εικόνες σε { $number_groups } ομάδες στο { $time }\ncompute_found_videos = Βρέθηκαν { $number_files } παρόμοιες ειδήσεις σε { $number_groups } κλάδους μέσω του { $time }\ncompute_found_music = Βρέθηκαν { $number_files } παρόμοια αρχεία μουσικής σε { $number_groups } ομάδες σε { $time }\ncompute_found_invalid_symlinks = Βρέθηκαν { $number_files } μη έγκυρα symlinks στο { $time }\ncompute_found_broken_files = Βρέθηκαν { $number_files } κατεστραμμένα αρχεία σε { $time }\ncompute_found_bad_extensions = Βρέθηκαν { $number_files } αρχεία με μη έγκυρες επεκτάσεις σε { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Σαρώθηκε { $file_number } αρχείο\n       *[other] Σαρώθηκαν { $file_number } αρχεία\n    }\nprogress_scanning_extension_of_files = Επιλεγμένη επέκταση του αρχείου { $file_checked }/{ $all_files }\nprogress_scanning_broken_files = Επιλεγμένο αρχείο { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Κατακερματισμένο από { $file_checked }/{ $all_files } βίντεο\nprogress_creating_video_thumbnails = Δημιουργήθηκε μικρογραφίες { $file_checked }/{ $all_files } βίντεο\nprogress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Compared { $file_checked }/{ $all_files } image hash\nprogress_scanning_music_tags_end = Συγκρίθηκαν ετικέτες του αρχείου μουσικής { $file_checked }/{ $all_files }\nprogress_scanning_music_tags = Διαβάστε τις ετικέτες του αρχείου μουσικής { $file_checked }/{ $all_files }\nprogress_scanning_music_content_end = Συγκρίθηκε δακτυλικό αποτύπωμα του αρχείου μουσικής { $file_checked }/{ $all_files }\nprogress_scanning_music_content = Υπολογίζεται το δακτυλικό αποτύπωμα του αρχείου { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Σαρώθηκε { $folder_number } φάκελος\n       *[other] Σαρώθηκαν { $folder_number } φάκελοι\n    }\nprogress_scanning_size = Σαρωμένο μέγεθος αρχείου { $file_number }\nprogress_scanning_size_name = Σαρωμένο όνομα και μέγεθος αρχείου { $file_number }\nprogress_scanning_name = Σαρωμένο όνομα αρχείου { $file_number }\nprogress_analyzed_partial_hash = Αναλυμένο μερικό hash του { $file_checked }/{ $all_files } αρχεία ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Ανάλυση πλήρους hash του { $file_checked }/{ $all_files } αρχεία ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Φόρτωση προσωρινής μνήμης\nprogress_prehash_cache_saving = Αποθήκευση προσωρινής μνήμης prehash\nprogress_hash_cache_loading = Φόρτωση προσωρινής μνήμης hash\nprogress_hash_cache_saving = Αποθήκευση λανθάνουσας μνήμης\nprogress_cache_loading = Φόρτωση προσωρινής μνήμης\nprogress_cache_saving = Αποθήκευση προσωρινής μνήμης\nprogress_current_stage = Current Stage:{ \"  \" }\nprogress_all_stages = Όλα Τα Στάδια:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Αποθηκευμένες ρυθμίσεις για το αρχείο { $name }.\nsaving_loading_saving_failure = Αποτυχία αποθήκευσης δεδομένων ρύθμισης παραμέτρων στο αρχείο { $name }, λόγος { $reason }.\nsaving_loading_reset_configuration = Οι τρέχουσες ρυθμίσεις εκκαθαρίστηκαν.\nsaving_loading_loading_success = Τοποθετημένες ρυθμίσεις παραμέτρων εφαρμογής.\nsaving_loading_failed_to_create_config_file = Αποτυχία δημιουργίας αρχείου ρυθμίσεων \"{ $path }\", λόγος \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Αδυναμία φόρτωσης ρύθμισης παραμέτρων από το \"{ $path }\" επειδή δεν υπάρχει ή δεν είναι αρχείο.\nsaving_loading_failed_to_read_data_from_file = Αδυναμία ανάγνωσης δεδομένων από το αρχείο \"{ $path }\", λόγος \"{ $reason }\".\n# Other\nselected_all_reference_folders = Αδυναμία έναρξης αναζήτησης, όταν όλοι οι κατάλογοι ορίζονται ως φάκελοι αναφοράς\nsearching_for_data = Αναζήτηση δεδομένων, μπορεί να πάρει λίγο, παρακαλώ περιμένετε...\ntext_view_messages = ΜΗΝΥΜΑΤΑ\ntext_view_warnings = ΠΡΟΕΙΔΟΠΟΙΗΣΕΙΣ\ntext_view_errors = ΣΦΑΛΜΑ\nabout_window_motto = Αυτό το πρόγραμμα είναι ελεύθερο να χρησιμοποιηθεί και πάντα θα είναι.\nkrokiet_new_app = Το Czkawka βρίσκεται σε λειτουργία συντήρησης, πράγμα που σημαίνει ότι μόνο κρίσιμα σφάλματα θα διορθωθούν και δεν θα προστεθούν νέα χαρακτηριστικά. Για νέα χαρακτηριστικά, παρακαλώ ελέγξτε τη νέα εφαρμογή Krokiet, η οποία είναι πιο σταθερή και αποδοτική και εξακολουθεί να βρίσκεται υπό ενεργή ανάπτυξη.\n# Various dialog\ndialogs_ask_next_time = Ερώτηση την επόμενη φορά\nsymlink_failed = Αποτυχία σύζευξης { $name } με { $target }, λόγος { $reason }\ndelete_title_dialog = Διαγραφή επιβεβαίωσης\ndelete_question_label = Είστε βέβαιοι ότι θέλετε να διαγράψετε αρχεία?\ndelete_all_files_in_group_title = Επιβεβαίωση διαγραφής όλων των αρχείων της ομάδας\ndelete_all_files_in_group_label1 = Σε ορισμένες ομάδες έχουν επιλεγεί όλες οι εγγραφές.\ndelete_all_files_in_group_label2 = Είστε βέβαιοι ότι θέλετε να τα διαγράψετε?\ndelete_items_label = { $items } τα αρχεία θα διαγραφούν.\ndelete_items_groups_label = { $items } τα αρχεία από τις ομάδες { $groups } θα διαγραφούν.\nhardlink_failed = Αποτυχία hardlink { $name } στο { $target }, λόγος { $reason }\nhard_sym_invalid_selection_title_dialog = Μη έγκυρη επιλογή με κάποιες ομάδες\nhard_sym_invalid_selection_label_1 = Σε ορισμένες ομάδες έχει επιλεγεί μόνο μία εγγραφή και θα αγνοηθεί.\nhard_sym_invalid_selection_label_2 = Για να είναι δυνατή η σκληρή/συσχέτιση αυτών των αρχείων, πρέπει να επιλεγούν τουλάχιστον δύο αποτελέσματα στην ομάδα.\nhard_sym_invalid_selection_label_3 = Η πρώτη στην ομάδα αναγνωρίζεται ως πρωτότυπο και δεν αλλάζεται, αλλά η δεύτερη και αργότερα τροποποιείται.\nhard_sym_link_title_dialog = Επιβεβαίωση συνδέσμου\nhard_sym_link_label = Είστε βέβαιοι ότι θέλετε να συνδέσετε αυτά τα αρχεία?\nmove_folder_failed = Αποτυχία μετακίνησης του φακέλου { $name }, λόγος { $reason }\nmove_file_failed = Αποτυχία μετακίνησης αρχείου { $name }, λόγος { $reason }\nmove_files_title_dialog = Επιλέξτε φάκελο στον οποίο θέλετε να μετακινήσετε διπλότυπα αρχεία\nmove_files_choose_more_than_1_path = Μόνο μία διαδρομή μπορεί να επιλεγεί για να είναι σε θέση να αντιγράψει τα διπλά αρχεία τους, επιλεγμένα { $path_number }.\nmove_stats = Σωστά μετακινήθηκαν { $num_files }/{ $all_files } στοιχεία\nsave_results_to_file = Αποθηκεύτηκε αποτελέσματα τόσο σε txt και αρχεία json στο φάκελο \"{ $name }\".\nsearch_not_choosing_any_music = ΣΦΑΛΜΑ: Πρέπει να επιλέξετε τουλάχιστον ένα πλαίσιο ελέγχου με τύπους αναζήτησης μουσικής.\nsearch_not_choosing_any_broken_files = ΣΦΑΛΜΑ: Πρέπει να επιλέξετε τουλάχιστον ένα πλαίσιο ελέγχου με τον τύπο των επιλεγμένων κατεστραμμένων αρχείων.\ninclude_folders_dialog_title = Φάκελοι που θα συμπεριληφθούν\nexclude_folders_dialog_title = Φάκελοι προς εξαίρεση\ninclude_manually_directories_dialog_title = Προσθήκη καταλόγου χειροκίνητα\ncache_properly_cleared = Σωστό εκκαθάριση προσωρινής μνήμης\ncache_clear_duplicates_title = Εκκαθάριση διπλότυπων cache\ncache_clear_similar_images_title = Εκκαθάριση παρόμοιων εικόνων cache\ncache_clear_similar_videos_title = Εκκαθάριση παρόμοιων βίντεο cache\ncache_clear_message_label_1 = Θέλετε να καθαρίσετε την προσωρινή μνήμη των ξεπερασμένων καταχωρήσεων?\ncache_clear_message_label_2 = Αυτή η λειτουργία θα καταργήσει όλες τις καταχωρήσεις προσωρινής αποθήκευσης που δείχνουν σε μη έγκυρα αρχεία.\ncache_clear_message_label_3 = Αυτό μπορεί να επιταχύνει ελαφρώς τη φόρτωση/αποθήκευση στη μνήμη cache.\ncache_clear_message_label_4 = ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η λειτουργία θα αφαιρέσει όλα τα προσωρινά αποθηκευμένα δεδομένα από τις αποσυνδεδεμένες εξωτερικές μονάδες. Έτσι, κάθε hash θα πρέπει να αναγεννηθεί.\n# Show preview\npreview_image_resize_failure = Αποτυχία αλλαγής μεγέθους εικόνας { $name }.\npreview_image_opening_failure = Αποτυχία ανοίγματος εικόνας { $name }, λόγος { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Ομάδα { $current_group }/{ $all_groups } ({ $images_in_group } εικόνες)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/en/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Settings\nwindow_main_title = Czkawka (Hiccup)\nwindow_progress_title = Scanning\nwindow_compare_images = Compare Images\n\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Close\n\n# Krokiet info dialog\nkrokiet_info_title = Introducing Krokiet - New version of Czkawka\nkrokiet_info_message =\n        Krokiet is the new, improved, faster and more reliable version of the Czkawka GTK GUI!\n\n        It’s easier to run and more resilient to system changes, as it depends only on core libraries available on most systems by default.\n\n        Krokiet also brings features that Czkawka lacks, including thumbnails in video comparison mode, an EXIF cleaner, file move/copy/delete progress or extended sorting options.\n\n        Give it a try and see the difference!\n\n        Czkawka will continue to receive bug fixes and minor updates from me, but all new features will be developed exclusively for Krokiet, and anyone is free to contribute new features add missing modes or extend Czkawka further.\n\n        PS: This message should appear only once. If it shows up again, set the CZKAWKA_DONT_ANNOY_ME environment variable to any non-empty value.\n\n# Main window\nmusic_title_checkbox = Title\nmusic_artist_checkbox = Artist\nmusic_year_checkbox = Year\nmusic_bitrate_checkbox = Bitrate\nmusic_genre_checkbox = Genre\nmusic_length_checkbox = Length\nmusic_comparison_checkbox = Approximate Comparison\nmusic_checking_by_tags = Tags\nmusic_checking_by_content = Content\nsame_music_seconds_label = Minimal fragment second duration\nsame_music_similarity_label = Maximum difference\n\nmusic_compare_only_in_title_group = Compare within groups of similar titles\nmusic_compare_only_in_title_group_tooltip =\n        When enabled, files are grouped by title and then compared to each other.\n\n        With 10000 files, instead almost 100 million comparisons usually there will be around 20000 comparisons.\n\nsame_music_tooltip =\n        Searching for similar music files by its content can be configured by setting:\n\n        - The minimum fragment time after which music files can be identified as similar\n        - The maximum difference difference between two tested fragments\n\n        The key to good results is to find sensible combinations of these parameters, for provided.\n\n        Setting the minimum time to 5s and the maximum difference to 1.0, will look for almost identical fragments in the files.\n        A time of 20s and a maximum difference of 6.0, on the other hand, works well for finding remixes/live versions etc.\n\n        By default, each music file is compared to each other and this can take a lot of time when testing many files, so it is usually better to use reference folders and specifying which files are to be compared with each other(with same amount of files, comparing fingerprints will be faster at least 4x than without reference folders).\n\nmusic_comparison_checkbox_tooltip =\n        It searches for similar music files using AI, which uses machine learning to remove parentheses from a phrase. For example, with this option enabled, the files in question will be considered duplicates:\n        \n        Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\n\nduplicate_case_sensitive_name = Case Sensitive\nduplicate_case_sensitive_name_tooltip =\n        When enabled, group only records when they have exactly same name e.g. Żołd <-> Żołd\n\n        Disabling such option will group names without checking if each letter is same size e.g. żoŁD <-> Żołd\n\nduplicate_mode_size_name_combo_box = Size and Name\nduplicate_mode_name_combo_box = Name\nduplicate_mode_size_combo_box = Size\nduplicate_mode_hash_combo_box = Hash\n\nduplicate_hash_type_tooltip = \n        Czkawka offers 3 types of hashes:\n\n        Blake3 - cryptographic hash function. This is the default because it is very fast.\n\n        CRC32 - simple hash function. This should be faster than Blake3, but may very rarely have some collisions.\n\n        XXH3 - very similar in performance and hash quality to Blake3 (but non-cryptographic). So, such modes can be easily interchanged.\n\nduplicate_check_method_tooltip = \n        For now, Czkawka offers three types of method to find duplicates by:\n\n        Name - Finds files which have the same name.\n\n        Size - Finds files which have the same size.\n\n        Hash - Finds files which have the same content. This mode hashes the file and later compares this hash to find duplicates. This mode is the safest way to find duplicates. App heavily uses cache, so second and further scans of the same data should be a lot of faster than the first. \n\nimage_hash_size_tooltip =\n        Each checked image produces a special hash which can be compared with each other, and a small difference between them means that these images are similar.\n\n        8 hash size is quite good to find images that are only a little similar to original. With a bigger set of images (>1000), this will produce a big amount of false positives, so I recommend to use  a bigger hash size in this case.\n\n        16 is the default hash size which is quite a good compromise between finding even a little similar images and having only a small amount of hash collisions.\n\n        32 and 64 hashes find only very similar images, but should have almost no false positives (maybe except some images with alpha channel).\n\nimage_resize_filter_tooltip = \n        To compute hash of image, the library must first resize it.\n\n        Depend on chosen algorithm, the resulting image used to calculate hash will looks a little different.\n\n        The fastest algorithm to use, but also the one which gives the worst results, is Nearest. It is enabled by default, because with 16x16 hash size lower quality it is not really visible.\n\n        With 8x8 hash size it is recommended to use a different algorithm than Nearest, to have better groups of images.\n\nimage_hash_alg_tooltip = \n        Users can choose from one of many algorithms of calculating the hash.\n\n        Each has both strong and weaker points and will sometimes give better and sometimes worse results for different images.\n\n        So, to determine the best one for you, manual testing is required.\n\nbig_files_mode_combobox_tooltip = Allows to search for smallest/biggest files\nbig_files_mode_label = Checked files\nbig_files_mode_smallest_combo_box = The Smallest\nbig_files_mode_biggest_combo_box = The Biggest\n\nmain_notebook_duplicates = Duplicate Files\nmain_notebook_empty_directories = Empty Directories\nmain_notebook_big_files = Big Files\nmain_notebook_empty_files = Empty Files\nmain_notebook_temporary = Temporary Files\nmain_notebook_similar_images = Similar Images\nmain_notebook_similar_videos = Similar Videos\nmain_notebook_same_music = Music Duplicates\nmain_notebook_symlinks = Invalid Symlinks\nmain_notebook_broken_files = Broken Files\nmain_notebook_bad_extensions = Bad Extensions\n\nmain_tree_view_column_file_name = File Name\nmain_tree_view_column_folder_name = Folder Name\nmain_tree_view_column_path = Path\nmain_tree_view_column_modification = Modification Date\nmain_tree_view_column_size = Size\nmain_tree_view_column_similarity = Similarity\nmain_tree_view_column_dimensions = Dimensions\nmain_tree_view_column_title = Title\nmain_tree_view_column_artist = Artist\nmain_tree_view_column_year = Year\nmain_tree_view_column_bitrate = Bitrate\nmain_tree_view_column_length = Length\nmain_tree_view_column_genre = Genre\nmain_tree_view_column_symlink_file_name = Symlink File Name\nmain_tree_view_column_symlink_folder = Symlink Folder\nmain_tree_view_column_destination_path = Destination Path\nmain_tree_view_column_type_of_error = Type Of Error\nmain_tree_view_column_current_extension = Current Extension\nmain_tree_view_column_proper_extensions = Proper Extension\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codec\n\nmain_label_check_method = Check method\nmain_label_hash_type = Hash type\nmain_label_hash_size = Hash size\nmain_label_size_bytes = Size (bytes)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Number of shown files\nmain_label_resize_algorithm = Resize algorithm\nmain_label_similarity = Similarity{\"   \"}\n\nmain_check_box_broken_files_audio = Audio\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Archive\nmain_check_box_broken_files_image = Image\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Uses ffmpeg/ffprobe to validate video files. Quite slow and may detect pedantic errors even if the file plays fine.\n\ncheck_button_general_same_size = Ignore same size\ncheck_button_general_same_size_tooltip = Ignore files with identical size in results - usually these are 1:1 duplicates\n\nmain_label_size_bytes_tooltip = Size of files which will be used in scan\n\n# Upper window\nupper_tree_view_included_folder_column_title = Folders to Search\nupper_tree_view_included_reference_column_title = Reference Folders\n\nupper_recursive_button = Recursive\nupper_recursive_button_tooltip = If selected, search also for files which are not placed directly under chosen folders.\n\nupper_manual_add_included_button = Manual Add\nupper_add_included_button = Add\nupper_remove_included_button = Remove\nupper_manual_add_excluded_button = Manual Add\nupper_add_excluded_button = Add\nupper_remove_excluded_button =  Remove\n\nupper_manual_add_included_button_tooltip =\n        Add directory name to search by hand.\n\n        To add multiple paths at once, separate them by ;\n\n        /home/roman;/home/rozkaz will add two directories /home/roman and /home/rozkaz\nupper_add_included_button_tooltip = Add new directory to search.\nupper_remove_included_button_tooltip =  Delete directory from search.\nupper_manual_add_excluded_button_tooltip =\n        Add excluded directory name by hand.\n\n        To add multiple paths at once, separate them by ;\n\n        /home/roman;/home/krokiet will add two directories /home/roman and /home/keokiet\nupper_add_excluded_button_tooltip = Add directory to be excluded in search.\nupper_remove_excluded_button_tooltip = Delete directory from excluded.\n\nupper_notebook_items_configuration = Items Configuration\nupper_notebook_excluded_directories = Excluded Paths\nupper_notebook_included_directories = Included Paths\n\nupper_allowed_extensions_tooltip = \n        Allowed extensions must be separated by commas (by default all are available).\n\n        The following Macros, which add multiple extensions at once, are also available: IMAGE, VIDEO, MUSIC, TEXT.\n\n        Usage example  \".exe, IMAGE, VIDEO, .rar, 7z\" - this means that images (e.g. jpg, png), videos (e.g. avi, mp4), exe, rar, and 7z files will be scanned.\n\nupper_excluded_extensions_tooltip =\n        List of disabled files which will be ignored in scan.\n\n        When using both allowed and disabled extensions, this one has higher priority, so file will not be checked.\n\nupper_excluded_items_tooltip = \n        Excluded items must contain * wildcard and should be separated by commas.\n        This is slower than Excluded Paths, so use it carefully.\n\nupper_excluded_items = Excluded Items:\nupper_allowed_extensions = Allowed Extensions:\nupper_excluded_extensions = Disabled Extensions:\n\n# Popovers\npopover_select_all = Select all\npopover_unselect_all = Unselect all\npopover_reverse = Reverse Selection\npopover_select_all_except_shortest_path = Select all except shortest path\npopover_select_all_except_longest_path = Select all except longest path\npopover_select_all_except_oldest = Select all except oldest\npopover_select_all_except_newest = Select all except newest\npopover_select_one_oldest = Select one oldest\npopover_select_one_newest = Select one newest\npopover_select_custom = Select custom\npopover_unselect_custom = Unselect custom\npopover_select_all_images_except_biggest = Select all except biggest\npopover_select_all_images_except_smallest = Select all except smallest\n\npopover_custom_path_check_button_entry_tooltip = \n        Select records by path.\n\n        Example usage:\n        /home/pimpek/rzecz.txt can be found with /home/pim*\n\npopover_custom_name_check_button_entry_tooltip = \n        Select records by file names.\n\n        Example usage:\n        /usr/ping/pong.txt can be found with *ong*\n\npopover_custom_regex_check_button_entry_tooltip = \n        Select records by specified Regex.\n\n        With this mode, searched text is Path with Name.\n\n        Example usage:\n        /usr/bin/ziemniak.txt can be found with /ziem[a-z]+\n\n        This uses the default Rust regex implementation. You can read more about it here: https://docs.rs/regex.\n\npopover_custom_case_sensitive_check_button_tooltip =\n        Enables case-sensitive detection.\n\n        When disabled /home/* finds both /HoMe/roman and /home/roman.\n\npopover_custom_not_all_check_button_tooltip = \n        Prevents selecting all records in group.\n\n        This is enabled by default, because in most situations, you don't want to delete both original and duplicates files, but want to leave at least one file.\n\n        WARNING: This setting doesn't work if you have already manually selected all results in a group.\n\npopover_custom_regex_path_label = Path\npopover_custom_regex_name_label = Name\npopover_custom_regex_regex_label = Regex Path + Name\npopover_custom_case_sensitive_check_button = Case sensitive\npopover_custom_all_in_group_label = Don't select all records in group\n\npopover_custom_mode_unselect = Unselect Custom\npopover_custom_mode_select = Select Custom\n\npopover_sort_file_name = File name\npopover_sort_folder_name = Folder name\npopover_sort_full_name = Full name\npopover_sort_size = Size\npopover_sort_selection = Selection\n\npopover_invalid_regex = Regex is invalid\npopover_valid_regex = Regex is valid\n\n# Bottom buttons\nbottom_search_button = Search\nbottom_select_button = Select\nbottom_delete_button = Delete\nbottom_save_button = Save\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Move\nbottom_sort_button = Sort\nbottom_compare_button = Compare\n\nbottom_search_button_tooltip = Start search\nbottom_select_button_tooltip = Select records. Only selected files/folders can be later processed.\nbottom_delete_button_tooltip = Delete selected files/folders.\nbottom_save_button_tooltip = Save data about search to file\nbottom_symlink_button_tooltip = \n        Create symbolic links.\n        Only works when at least two results in a group are selected.\n        First is unchanged and second and later are symlinked to first.\nbottom_hardlink_button_tooltip = \n        Create hardlinks.\n        Only works when at least two results in a group are selected.\n        First is unchanged and second and later are hardlinked to first.\nbottom_hardlink_button_not_available_tooltip =\n        Create hardlinks.\n        Button is disabled, because hardlinks cannot be created.\n        Hardlinks only works with administrator privileges on Windows, so be sure to run app as administrator.\n        If app already works with such privileges check for similar issues on Github.\nbottom_move_button_tooltip =\n        Moves files to chosen directory.\n        It copies all files to the directory without preserving the directory tree.\n        When trying to move two files with identical name to folder, second will fail and show error.\nbottom_sort_button_tooltip =\n        Sorts files/folders according to selected method.\nbottom_compare_button_tooltip =\n        Compare images in the group.\n\nbottom_show_errors_tooltip = Show/Hide bottom text panel.\nbottom_show_upper_notebook_tooltip = Show/Hide upper notebook panel.\n\n# Progress Window\nprogress_stop_button = Stop\nprogress_stop_additional_message = Stop requested\n\n# About Window\nabout_repository_button_tooltip = Link to repository page with source code.\nabout_donation_button_tooltip = Link to donation page.\nabout_instruction_button_tooltip = Link to instruction page.\nabout_translation_button_tooltip = Link to Crowdin page with app translations. Officially Polish and English are supported.\n\nabout_repository_button = Repository\nabout_donation_button = Donation\nabout_instruction_button = Instruction\nabout_translation_button = Translation\n\n# Header\nheader_setting_button_tooltip = Opens settings dialog.\nheader_about_button_tooltip = Opens dialog with info about app.\n\n# Settings\n## General\nsettings_number_of_threads = Number of used threads\nsettings_number_of_threads_tooltip = Number of used threads, 0 means that all available threads will be used.\n\nsettings_use_rust_preview = Use external libraries instead gtk to load previews\nsettings_use_rust_preview_tooltip =\n        Using gtk previews will sometimes be faster and support more formats, but sometimes this could be exactly the opposite.\n\n        If you have problems with loading previews, you may can to try to change this setting.\n\n        On non-linux systems, it is recommended to use this option, because gtk-pixbuf are not always available there so disabling this option will not load previews of some images.\n\nsettings_label_restart = You need to restart app to apply settings!\n\nsettings_ignore_other_filesystems = Ignore other filesystems (only Linux)\nsettings_ignore_other_filesystems_tooltip =\n        ignores files that are not in the same file system as searched directories.\n\n        Works same like -xdev option in find command on Linux\n\nsettings_save_at_exit_button_tooltip = Save configuration to file when closing app.\nsettings_load_at_start_button_tooltip = \n        Load configuration from file when opening app.\n\n        If not enabled, default settings will be used.\nsettings_confirm_deletion_button_tooltip = Show confirmation dialog when clicking the delete button.\nsettings_confirm_link_button_tooltip = Show confirmation dialog when clicking the hard/symlink button.\nsettings_confirm_group_deletion_button_tooltip = Show warning dialog when trying to delete all records from the group.\nsettings_show_text_view_button_tooltip = Show text panel at the bottom of the user interface.\nsettings_use_cache_button_tooltip = Use file cache.\nsettings_save_also_as_json_button_tooltip = Save cache to (human readable) JSON format. It is possible to modify its content. Cache from this file will be read automatically by app if binary format cache (with bin extension) is missing.\nsettings_use_trash_button_tooltip = Moves files to trash instead deleting them permanently.\nsettings_language_label_tooltip = Language for user interface.\n\nsettings_save_at_exit_button = Save configuration when closing app\nsettings_load_at_start_button = Load configuration when opening app\nsettings_confirm_deletion_button = Show confirm dialog when deleting any files\nsettings_confirm_link_button = Show confirm dialog when hard/symlinks any files\nsettings_confirm_group_deletion_button = Show confirm dialog when deleting all files in group\nsettings_show_text_view_button = Show bottom text panel\nsettings_use_cache_button = Use cache\nsettings_save_also_as_json_button = Also save cache as JSON file\nsettings_use_trash_button = Move deleted files to trash\nsettings_language_label = Language\n\nsettings_multiple_delete_outdated_cache_checkbutton = Delete outdated cache entries automatically\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip = \n        Delete outdated cache results which point to non-existent files.\n\n        When enabled, app makes sure when loading records, that all records point to valid files (broken ones are ignored).\n\n        Disabling this will help when scanning files on external drives, so cache entries about them will not be purged in the next scan.\n\n        In the case of having hundred of thousands records in cache, it is suggested to enable this, which will speedup cache loading/saving at start/end of the scan.\n\nsettings_notebook_general = General\nsettings_notebook_duplicates = Duplicates\nsettings_notebook_images = Similar Images\nsettings_notebook_videos = Similar Video\n\n## Multiple - settings used in multiple tabs\nsettings_multiple_image_preview_checkbutton_tooltip = Shows preview at right side (when selecting an image file).\nsettings_multiple_image_preview_checkbutton = Show image preview\n\nsettings_multiple_clear_cache_button_tooltip = \n        Manually clear the cache of outdated entries.\n        This should only be used if automatic clearing has been disabled.\n\nsettings_multiple_clear_cache_button = Remove outdated results from cache.\n\n## Duplicates\nsettings_duplicates_hide_hard_link_button_tooltip = \n        Hides all files except one, if all point to the same data (are hardlinked).\n\n        Example: In the case where there are (on disk) seven files which are hardlinked to specific data and one different file with same data but a different inode, then in duplicate finder, only one unique file and one file from hardlinked ones will be shown.\n\nsettings_duplicates_minimal_size_entry_tooltip = \n        Set the minimal file size which will be cached.\n\n        Choosing a smaller value will generate more records. This will speedup search, but slowdown cache loading/saving.\n\nsettings_duplicates_prehash_checkbutton_tooltip = \n        Enables caching of prehash (a hash computed from a small part of the file) which allows earlier dismissal of non-duplicated results.\n\n        It is disabled by default because it can cause slowdowns in some situations.\n\n        It is highly recommended to use it when scanning hundred of thousands or million files, because it can speedup search by multiple times.\n\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimal size of cached entry.\n\nsettings_duplicates_hide_hard_link_button = Hide hard links\nsettings_duplicates_prehash_checkbutton = Use prehash cache\n\nsettings_duplicates_minimal_size_cache_label = Minimal size of files (in bytes) saved to cache\nsettings_duplicates_minimal_size_cache_prehash_label = Minimal size of files (in bytes) saved to prehash cache\n\n## Saving/Loading settings\nsettings_saving_button_tooltip = Save the current settings configuration to file.\nsettings_loading_button_tooltip = Load settings from file and replace the current configuration with them.\nsettings_reset_button_tooltip = Reset the current configuration to the default one.\n\nsettings_saving_button = Save configuration\nsettings_loading_button = Load configuration\nsettings_reset_button = Reset configuration\n\n## Opening cache/config folders\nsettings_folder_cache_open_tooltip = \n        Opens the folder where the cache txt files are stored.\n\n        Modifying the cache files may cause invalid results to be shown. However, modifying path may save time when moving a big amount of files to a different location.\n\n        You can copy these files between computers to save time on scanning again for files (of course if they have similar directory structure).\n\n        In the case of problems with the cache, these files can be removed. The app will automatically regenerate them.\n\nsettings_folder_settings_open_tooltip = \n        Opens the folder where the Czkawka config is stored.\n\n        WARNING: Manually modifying the config may break your workflow.\n\nsettings_folder_cache_open = Open cache folder\nsettings_folder_settings_open = Open settings folder\n\n# Compute results\ncompute_stopped_by_user = Searching was stopped by user\n\ncompute_found_duplicates_hash_size = Found { $number_files } duplicates in { $number_groups } groups which took { $size } in { $time }\ncompute_found_duplicates_name = Found { $number_files } duplicates in { $number_groups } groups in { $time }\ncompute_found_empty_folders = Found { $number_files } empty folders in { $time }\ncompute_found_empty_files = Found { $number_files } empty files in { $time }\ncompute_found_big_files = Found { $number_files } big files in { $time }\ncompute_found_temporary_files = Found { $number_files } temporary files in { $time }\ncompute_found_images = Found { $number_files } similar images in { $number_groups } groups in { $time }\ncompute_found_videos = Found { $number_files } similar videos in { $number_groups } groups in { $time }\ncompute_found_music = Found { $number_files } similar music files in { $number_groups } groups in { $time }\ncompute_found_invalid_symlinks = Found { $number_files } invalid symlinks in { $time }\ncompute_found_broken_files = Found { $number_files } broken files in { $time }\ncompute_found_bad_extensions = Found { $number_files } files with invalid extensions in { $time }\n\n# Progress window\nprogress_scanning_general_file = {$file_number -> \n        [one] Scanned {$file_number} file\n       *[other] Scanned {$file_number} files\n}\n\nprogress_scanning_extension_of_files = Checked extension of {$file_checked}/{$all_files} file\nprogress_scanning_broken_files = Checked {$file_checked}/{$all_files} file ({$data_checked}/{$all_data})\nprogress_scanning_video = Hashed of {$file_checked}/{$all_files} video\nprogress_creating_video_thumbnails = Created thumbnails of {$file_checked}/{$all_files} video\nprogress_scanning_image = Hashed of {$file_checked}/{$all_files} image ({$data_checked}/{$all_data})\nprogress_comparing_image_hashes = Compared {$file_checked}/{$all_files} image hash\nprogress_scanning_music_tags_end = Compared tags of {$file_checked}/{$all_files} music file\nprogress_scanning_music_tags = Read tags of {$file_checked}/{$all_files} music file\nprogress_scanning_music_content_end = Compared fingerprint of {$file_checked}/{$all_files} music file\nprogress_scanning_music_content = Calculated fingerprint of {$file_checked}/{$all_files} music file ({$data_checked}/{$all_data})\nprogress_scanning_empty_folders = {$folder_number -> \n        [one] Scanned {$folder_number} folder\n       *[other] Scanned {$folder_number} folders\n}\nprogress_scanning_size = Scanned size of {$file_number} file\nprogress_scanning_size_name = Scanned name and size of {$file_number} file\nprogress_scanning_name = Scanned name of {$file_number} file\nprogress_analyzed_partial_hash = Analyzed partial hash of {$file_checked}/{$all_files} files ({$data_checked}/{$all_data})\nprogress_analyzed_full_hash = Analyzed full hash of {$file_checked}/{$all_files} files ({$data_checked}/{$all_data})\nprogress_prehash_cache_loading = Loading prehash cache\nprogress_prehash_cache_saving = Saving prehash cache\nprogress_hash_cache_loading = Loading hash cache\nprogress_hash_cache_saving = Saving hash cache\nprogress_cache_loading = Loading cache\nprogress_cache_saving = Saving cache\n\nprogress_current_stage = Current Stage:{\"  \"}\nprogress_all_stages = All Stages:{\"  \"}\n\n# Saving loading \nsaving_loading_saving_success = Saved configuration to file { $name }.\nsaving_loading_saving_failure = Failed to save configuration data to file { $name }, reason { $reason }.\nsaving_loading_reset_configuration = Current configuration was cleared.\nsaving_loading_loading_success = Properly loaded app configuration.\n\nsaving_loading_failed_to_create_config_file = Failed to create config file \"{ $path }\", reason \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Cannot load configuration from \"{ $path }\" because it does not exist or is not a file.\nsaving_loading_failed_to_read_data_from_file = Cannot read data from file \"{ $path }\", reason \"{ $reason }\".\n\n\n# Other\nselected_all_reference_folders = Cannot start search, when all directories are set as reference folders\nsearching_for_data = Searching data, it may take a while, please wait...\ntext_view_messages = MESSAGES\ntext_view_warnings = WARNINGS\ntext_view_errors = ERRORS\nabout_window_motto = This program is free to use and will always be.\nkrokiet_new_app = Czkawka is in maintenance mode, which means that only critical bugs will be fixed and no new features will be added. For new features, please check out new Krokiet app, which is more stable and performant and is still under active development.\n\n# Various dialog\ndialogs_ask_next_time = Ask next time\n\nsymlink_failed = Failed to symlink {$name} to {$target}, reason {$reason}\n\ndelete_title_dialog = Delete confirmation\ndelete_question_label = Are you sure that you want to delete files?\ndelete_all_files_in_group_title = Confirmation of deleting all files in group\ndelete_all_files_in_group_label1 = In some groups all records are selected.\ndelete_all_files_in_group_label2 = Are you sure that you want to delete them?\n\ndelete_items_label = { $items } files will be deleted.\ndelete_items_groups_label = { $items } files from { $groups } groups will be deleted.\n\nhardlink_failed = Failed to hardlink { $name } to { $target }, reason { $reason }\nhard_sym_invalid_selection_title_dialog = Invalid selection with some groups\nhard_sym_invalid_selection_label_1 = In some groups there is only one record selected and it will be ignored.\nhard_sym_invalid_selection_label_2 = To be able to hard/sym link these files, at least two results in the group need to be selected.\nhard_sym_invalid_selection_label_3 = First in group is recognized as original and is not changed but second and later are modified.\nhard_sym_link_title_dialog = Link confirmation\nhard_sym_link_label = Are you sure that you want to link these files?\n\nmove_folder_failed = Failed to move folder {$name}, reason {$reason}\nmove_file_failed = Failed to move file {$name}, reason {$reason}\nmove_files_title_dialog = Choose folder to which you want to move duplicated files\nmove_files_choose_more_than_1_path = Only one path may be selected to be able to copy their duplicated files, selected {$path_number}.\nmove_stats = Properly moved {$num_files}/{$all_files} items\n\nsave_results_to_file = Saved results both to txt and json files into \"{$name}\" folder.\n\nsearch_not_choosing_any_music = ERROR: You must select at least one checkbox with music searching types.\nsearch_not_choosing_any_broken_files = ERROR: You must select at least one checkbox with type of checked broken files.\n\ninclude_folders_dialog_title = Folders to include\nexclude_folders_dialog_title = Folders to exclude\n\ninclude_manually_directories_dialog_title = Add directory manually\n\ncache_properly_cleared = Properly cleared cache\ncache_clear_duplicates_title = Clearing duplicates cache\ncache_clear_similar_images_title = Clearing similar images cache\ncache_clear_similar_videos_title = Clearing similar videos cache\ncache_clear_message_label_1 = Do you want to clear the cache of outdated entries?\ncache_clear_message_label_2 = This operation will remove all cache entries which point to invalid files.\ncache_clear_message_label_3 = This may slightly speedup loading/saving to cache.\ncache_clear_message_label_4 = WARNING: Operation will remove all cached data from unplugged external drives. So each hash will need to be regenerated.\n\n# Show preview\npreview_image_resize_failure = Failed to resize image {$name}.\npreview_image_opening_failure = Failed to open image {$name}, reason {$reason}\n\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Group { $current_group }/{ $all_groups } ({ $images_in_group } images)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/es-ES/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Configuración\nwindow_main_title = Czkawka (Hipo)\nwindow_progress_title = Escaneando\nwindow_compare_images = Comparar imágenes\n# General\ngeneral_ok_button = Aceptar\ngeneral_close_button = Cerrar\n# Krokiet info dialog\nkrokiet_info_title = Presentando Krokiet - Nueva Versión de Czkawka\nkrokiet_info_message =\n    Krokiet es la nueva versión mejorada, más rápida y más fiable de la interfaz gráfica GTK de Czkawka.\n    \n    Es más fácil de ejecutar y más resistente a los cambios del sistema, ya que depende solo de bibliotecas básicas disponibles por defecto en la mayoría de los sistemas.\n    \n    Krokiet también incorpora funciones que Czkawka no tiene, como miniaturas en el modo de comparación de vídeos, un limpiador EXIF, progreso al mover/copiar/eliminar archivos u opciones de ordenación ampliadas.\n    \n    ¡Pruébalo y nota la diferencia!\n    \n    Czkawka seguirá recibiendo correcciones de errores y pequeñas actualizaciones por mi parte, pero todas las funciones nuevas se desarrollarán exclusivamente para Krokiet, y cualquiera es libre de contribuir con nuevas funciones, añadir modos faltantes o ampliar aún más Czkawka.\n    \n    PD: Este mensaje debería aparecer solo una vez. Si vuelve a mostrarse, establece la variable de entorno CZKAWKA_DONT_ANNOY_ME con cualquier valor no vacío.\n# Main window\nmusic_title_checkbox = Título\nmusic_artist_checkbox = Artista\nmusic_year_checkbox = Año\nmusic_bitrate_checkbox = Tasa de bits\nmusic_genre_checkbox = Género\nmusic_length_checkbox = Duración\nmusic_comparison_checkbox = Comparación aproximada\nmusic_checking_by_tags = Etiquetas\nmusic_checking_by_content = Contenido\nsame_music_seconds_label = Duración mínima del segundo fragmento\nsame_music_similarity_label = Diferencia máxima\nmusic_compare_only_in_title_group = Comparar dentro de grupos de títulos similares\nmusic_compare_only_in_title_group_tooltip =\n    Cuando está activado, los archivos son agrupados por títulos, y luego comparados con otros.\n    \n    Con 10000 archivos, al menos tendríamos unas 100 millones de comparaciones, cuando usualmente serían unas 20000 comparaciones.\nsame_music_tooltip =\n    La búsqueda de archivos de música, por su contenido, puede especificarse mediante los siguientes parámetros:\n    \n    - El tiempo mínimo de fragmento después del cual los archivos de música pueden ser identificados como similares.\n    - La diferencia máxima entre dos fragmentos probados.\n    \n    La clave, para lograr los mejores resultados al buscar, es proporcionando las mejores combinaciones de estos parámetros:\n    \n    - Establecer el tiempo mínimo a 5s y la diferencia máxima a 1,0, buscará fragmentos casi idénticos en los archivos.\n    - Un tiempo de 20s y una diferencia máxima de 6,0, por otro lado, funciona bien para encontrar remixes/versiones en vivo, etc.\n    \n    Por defecto, cada archivo de música se compara entre sí y esto puede llevar mucho tiempo al probar muchos archivos, por lo que normalmente es mejor usar carpetas de referencia y especificar qué archivos deben compararse entre sí (con la misma cantidad de archivos, comparar las huellas dactilares será más rápido al menos 4x que sin carpetas de referencia).\nmusic_comparison_checkbox_tooltip =\n    Busca archivos de música similares usando IA, que usa el aprendizaje automático para eliminar paréntesis de una frase. Por ejemplo, con esta opción activada, los archivos en cuestión se considerarán duplicados:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Sensible a mayúsculas\nduplicate_case_sensitive_name_tooltip =\n    Cuando está habilitado, agrupa registros solo cuando tienen exactamente el mismo nombre. P. ej.\n    \n     Żołd ↔ Żołd\n    \n    Si deshabilitamos dicha opción, agrupará nombres sin comprobar si cada letra tiene el mismo tamaño. P. ej. \n    \n    żoŁD ↔ Żołd\nduplicate_mode_size_name_combo_box = Tamaño y nombre\nduplicate_mode_name_combo_box = Nombre\nduplicate_mode_size_combo_box = Tamaño\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka ofrece 3 tipos de hashes, que pueden ser usados:\n    \n    Blake3 - función de hash criptográfica. Se usa como algoritmo predeterminado porque es muy rápido.\n    \n    CRC32 - función hash simple. Debería ser más rápido que Blake3, pero probablemente tenga algunas colisiones muy raras.\n    \n    XXH3 - muy similar en caso de rendimiento y calidad con Blake3 (pero no criptográfico). Por este motivo, tales modos pueden ser fácilmente usados.\nduplicate_check_method_tooltip =\n    Por el momento, Czkawka ofrece tres tipos de métodos para encontrar duplicados:\n    \n    Nombre - Encuentra archivos con el mismo nombre.\n    \n    Tamaño - Encuentra archivos con el mismo tamaño.\n    \n    Hash - Encuentra archivos con el mismo contenido. Este modo selecciona el archivo y luego compara este hash para encontrar duplicados. Es la forma más segura de encontrar duplicados. La aplicación utiliza mucho caché, por lo que segundo y más análisis de los mismos datos debe ser mucho más rápido que el primero.\nimage_hash_size_tooltip =\n    Cada imagen seleccionada produce un hash especial que se puede comparar entre sí y una pequeña diferencia entre ellas significa que estas imágenes son similares.\n    \n    El tamaño de 8 hash es bastante bueno para encontrar imágenes que son un poco similares a las originales. Con un conjunto más grande de imágenes (>1000), esto producirá una gran cantidad de falsos positivos, así que recomiendo usar un mayor tamaño de hash en este caso.\n    \n    16 es el tamaño de hash predeterminado, lo cual es un buen compromiso entre encontrar incluso un poco de imágenes similares y tener sólo una pequeña cantidad de colisiones hash.\n    \n    32 y 64 hashes sólo encuentran imágenes muy similares, pero no deberían tener casi falsos positivos (tal vez excepto algunas imágenes con canal alfa).\nimage_resize_filter_tooltip =\n    Al calcular el hash de una imagen, lo primero que hace la librería es redimensionarla.\n    \n    Dependiendo del algoritmo que elijamos, la imagen resultante usada para calcular el hash puede ser apenas diferente.\n    \n    El algoritmo más rápido, pero que da los peores resultados, es Nearest. Está habilitado de forma predeterminada, ya que usa un tamaño de hash de 16x16, haciendo que calidades más bajas no sean visibles.\n    \n    Con el tamaño hash de 8x8 se recomienda usar un algoritmo diferente al tipo Nearest, para obtener mejores grupos de imágenes.\nimage_hash_alg_tooltip = Los usuarios pueden elegir uno de los muchos algoritmos de cálculo. Cada uno tiene puntos fuertes y débiles y a veces dará mejores y a veces peores resultados para diferentes imágenes. Por lo tanto, para determinar cuál es la mejor para usted, se requiere la prueba manual.\nbig_files_mode_combobox_tooltip = Permite buscar archivos de un menor/mayor tamaño\nbig_files_mode_label = Archivos marcados\nbig_files_mode_smallest_combo_box = El más pequeño\nbig_files_mode_biggest_combo_box = El más grande\nmain_notebook_duplicates = Archivos Duplicados\nmain_notebook_empty_directories = Directorios vacíos\nmain_notebook_big_files = Archivos grandes\nmain_notebook_empty_files = Archivos vacíos\nmain_notebook_temporary = Archivos temporales\nmain_notebook_similar_images = Imágenes similares\nmain_notebook_similar_videos = Videos similares\nmain_notebook_same_music = Canciones duplicadas\nmain_notebook_symlinks = Enlaces simbólicos rotos\nmain_notebook_broken_files = Archivos dañados\nmain_notebook_bad_extensions = Extensiones incorrectas\nmain_tree_view_column_file_name = Nombre del archivo\nmain_tree_view_column_folder_name = Nombre de carpeta\nmain_tree_view_column_path = Ruta\nmain_tree_view_column_modification = Fecha de modificación\nmain_tree_view_column_size = Tamaño\nmain_tree_view_column_similarity = Similitud\nmain_tree_view_column_dimensions = Dimensiones\nmain_tree_view_column_title = Título\nmain_tree_view_column_artist = Artista\nmain_tree_view_column_year = Año\nmain_tree_view_column_bitrate = Tasa de bits\nmain_tree_view_column_length = Duración\nmain_tree_view_column_genre = Género\nmain_tree_view_column_symlink_file_name = Nombre del \"Enlace simbólico\"\nmain_tree_view_column_symlink_folder = Carpeta Symlink\nmain_tree_view_column_destination_path = Ruta de destino\nmain_tree_view_column_type_of_error = Tipo de error\nmain_tree_view_column_current_extension = Extensión actual\nmain_tree_view_column_proper_extensions = Extensión adecuada\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codificador\nmain_label_check_method = Método de comprobación\nmain_label_hash_type = Tipo de Hash\nmain_label_hash_size = Tamaño hash\nmain_label_size_bytes = Tamaño (bytes)\nmain_label_min_size = Mínimo\nmain_label_max_size = Máximo\nmain_label_shown_files = Número de archivos mostrados\nmain_label_resize_algorithm = Algoritmo de Redimensionado\nmain_label_similarity = Similitud{ \"   \" }\nmain_check_box_broken_files_audio = Sonido\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Guardar\nmain_check_box_broken_files_image = Imagen\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Utiliza ffmpeg/ffprobe para validar archivos de vídeo. Muy lento y puede detectar errores pedánticos incluso si el archivo se reproduce correctamente.\ncheck_button_general_same_size = Ignorar el mismo tamaño\ncheck_button_general_same_size_tooltip = Ignorar archivos con idéntico tamaño en resultados - usualmente son 1:1 duplicados\nmain_label_size_bytes_tooltip = Tamaño de los archivos que se utilizarán en el escaneo\n# Upper window\nupper_tree_view_included_folder_column_title = Carpetas a buscar\nupper_tree_view_included_reference_column_title = Carpetas de referencia\nupper_recursive_button = Recursivo\nupper_recursive_button_tooltip = Si se selecciona, busca cualquier archivo, sin importar si está o no en una sub-carpeta.\nupper_manual_add_included_button = Incluir de forma manual\nupper_add_included_button = Añadir\nupper_remove_included_button = Eliminar\nupper_manual_add_excluded_button = Añadir manual\nupper_add_excluded_button = Añadir\nupper_remove_excluded_button = Eliminar\nupper_manual_add_included_button_tooltip =\n    Añade el nombre del directorio para buscar a mano.\n    \n    Para agregar múltiples rutas a la vez, sepáralas con ;\n    \n    /home/roman;/home/rozkaz añadirá dos directorios /home/roman y /home/rozkaz\nupper_add_included_button_tooltip = Añadir nuevo directorio para buscar.\nupper_remove_included_button_tooltip = Eliminar directorio de la búsqueda.\nupper_manual_add_excluded_button_tooltip =\n    Añadir el nombre del directorio excluido a mano.\n    \n    Para agregar múltiples rutas a la vez, separalas por ;\n    \n    /home/roman;/home/krokiet añadirá dos directorios /home/roman y /home/keokiet\nupper_add_excluded_button_tooltip = Añadir directorio a excluir en la búsqueda.\nupper_remove_excluded_button_tooltip = Eliminar directorio de excluidos.\nupper_notebook_items_configuration = Configuración de artículos\nupper_notebook_excluded_directories = Rutas excluidas\nupper_notebook_included_directories = Rutas incluidas\nupper_allowed_extensions_tooltip =\n    Las extensiones permitidas deben estar separadas por comas (por defecto todas están disponibles).\n    \n    Las siguientes Macros, que añaden múltiples extensiones a la vez, también están disponibles: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Ejemplo de uso \".exe, IMAGE, VIDEO, .rar, 7z\" - esto significa que imágenes (e. . jpg, png), videos (ej: avi, mp4), archivos exe, rar, y 7z serán escaneados.\nupper_excluded_extensions_tooltip =\n    Lista de archivos ignorados, durante el escaneo.\n    \n    Cuando desactivamos las extensiones permitidas, estas tienen mayor prioridad, haciendo que los archivos no sean comprobados.\nupper_excluded_items_tooltip =\n    Los artículos excluidos deben contener el comodín * y deben estar separados por comas.\n    Esto es más lento que los Directorios Excluidos, así que úselo con cuidado.\nupper_excluded_items = Elementos excluidos:\nupper_allowed_extensions = Extensiones permitidas:\nupper_excluded_extensions = Extensiones desactivadas:\n# Popovers\npopover_select_all = Seleccionar todo\npopover_unselect_all = Deseleccionar todo\npopover_reverse = Invertir selección\npopover_select_all_except_shortest_path = Seleccionar todo excepto la ruta más corta\npopover_select_all_except_longest_path = Seleccionar todo excepto la ruta más larga\npopover_select_all_except_oldest = Seleccionar todo excepto más antiguo\npopover_select_all_except_newest = Seleccionar todo excepto el más reciente\npopover_select_one_oldest = Seleccione uno más antiguo\npopover_select_one_newest = Seleccione uno más nuevo\npopover_select_custom = Seleccionar personalizado\npopover_unselect_custom = Deseleccionar personalizado\npopover_select_all_images_except_biggest = Seleccionar todo excepto mayor\npopover_select_all_images_except_smallest = Seleccionar todo excepto menor\npopover_custom_path_check_button_entry_tooltip =\n    Seleccionar registros por ruta.\n    \n    Ejemplo:\n    /home/pmañk/rzecz.txt se puede encontrar con /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Seleccionar registros por nombres de archivos.\n    \n    Ejemplo:\n    /usr/ping/pong.txt puede encontrarse con *a lo largo*\npopover_custom_regex_check_button_entry_tooltip =\n    Seleccione registros por Regex.\n    \n    En este modo, el texto buscado es Ruta con Nombre.\n    \n    Ejemplo:\n    /usr/bin/ziemniak. xt se puede encontrar con /ziem[a-z]+\n    \n    Esto utiliza la implementación predeterminada de expresiones regulares de Rust. Puedes leer más al respecto aquí: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Habilita la detección de mayúsculas y minúsculas.\n    \n    Cuando se desactiva /home/* encuentra /HoMe/roman y /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Previene la selección de todos los registros en grupo.\n    \n    Esto está activado por defecto, porque en la mayoría de las situaciones, no quiere eliminar tanto los archivos originales como los duplicados, pero quiere dejar al menos un archivo.\n    \n    ADVERTENCIA: Esta configuración no funciona si ya has seleccionado manualmente todos los resultados en un grupo.\npopover_custom_regex_path_label = Ruta\npopover_custom_regex_name_label = Nombre\npopover_custom_regex_regex_label = Ruta de Regex + Nombre\npopover_custom_case_sensitive_check_button = Distingue mayúsculas y minúsculas\npopover_custom_all_in_group_label = No seleccionar todos los registros en el grupo\npopover_custom_mode_unselect = Deseleccionar Personalizado\npopover_custom_mode_select = Seleccionar Personalizado\npopover_sort_file_name = Nombre de archivo\npopover_sort_folder_name = Nombre de la carpeta\npopover_sort_full_name = Nombre completo\npopover_sort_size = Tamaño\npopover_sort_selection = Selección\npopover_invalid_regex = Regex no es válido\npopover_valid_regex = Regex es válido\n# Bottom buttons\nbottom_search_button = Buscar\nbottom_select_button = Seleccionar\nbottom_delete_button = Eliminar\nbottom_save_button = Guardar\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Mover\nbottom_sort_button = Ordenar\nbottom_compare_button = Comparar\nbottom_search_button_tooltip = Iniciar búsqueda\nbottom_select_button_tooltip = Seleccionar registros. Sólo los archivos/carpetas seleccionados pueden ser procesados más tarde.\nbottom_delete_button_tooltip = Eliminar archivos/carpetas seleccionadas.\nbottom_save_button_tooltip = Guardar datos sobre la búsqueda en el archivo\nbottom_symlink_button_tooltip =\n    Crear enlaces simbólicos.\n    Sólo funciona cuando al menos dos resultados en grupo son seleccionados.\n    El primero no ha cambiado y el segundo y más tarde están enlazados con el primero.\nbottom_hardlink_button_tooltip =\n    Crear enlaces hardlinks.\n    Solo funciona cuando al menos dos resultados en grupo son seleccionados.\n    El primero no ha cambiado y el segundo y más tarde están enlazados por hardlinks a la primera.\nbottom_hardlink_button_not_available_tooltip =\n    Crear enlaces duros.\n    Botón deshabilitado, porque no se pueden crear enlaces duros.\n    Hardlinks sólo funciona con privilegios de administrador en Windows, así que asegúrese de ejecutar la aplicación como administrador.\n    Si la aplicación ya funciona con dichos privilegios, compruebe si hay problemas similares en Github.\nbottom_move_button_tooltip =\n    Mover los archivos a la carpeta elegida.\n    Copia todos los archivos a la carpeta sin preservar el árbol de directorios.\n    Al intentar mover dos archivos con el mismo nombre a la carpeta, el segundo fallará y mostrará el error.\nbottom_sort_button_tooltip = Ordenar archivos/carpetas de acuerdo al método seleccionado.\nbottom_compare_button_tooltip = Comparar imágenes en el grupo.\nbottom_show_errors_tooltip = Mostrar/Ocultar panel de texto inferior.\nbottom_show_upper_notebook_tooltip = Mostrar / Ocultar panel de cuaderno superior.\n# Progress Window\nprogress_stop_button = Parar\nprogress_stop_additional_message = Parar solicitado\n# About Window\nabout_repository_button_tooltip = Enlace a la página del repositorio con código fuente.\nabout_donation_button_tooltip = Enlace a la página de donación.\nabout_instruction_button_tooltip = Enlace a la página de instrucciones.\nabout_translation_button_tooltip = Enlace a la página de Crowdin con traducciones de aplicaciones. Oficialmente se admiten polaco e inglés.\nabout_repository_button = Repositorio\nabout_donation_button = Donativo\nabout_instruction_button = Instrucción\nabout_translation_button = Traducción\n# Header\nheader_setting_button_tooltip = Abre el diálogo de ajustes.\nheader_about_button_tooltip = Abre el diálogo con información sobre la aplicación.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Número de hilos usados\nsettings_number_of_threads_tooltip = Número de hilos usados, 0 significa que se utilizarán todos los hilos disponibles.\nsettings_use_rust_preview = Usar librerías externas en su lugar gtk para cargar vistas previas\nsettings_use_rust_preview_tooltip =\n    Usar vistas previas de gtk a veces será más rápido y soportará más formatos, pero a veces esto podría ser exactamente lo contrario.\n    \n    Si tienes problemas con la carga de las vistas previas, puedes intentar cambiar esta configuración.\n    \n    En los sistemas no-linux, se recomienda usar esta opción, porque gtk-pixbuf no están siempre disponibles allí por lo que desactivar esta opción no cargará vistas previas de algunas imágenes.\nsettings_label_restart = ¡Necesitas reiniciar la aplicación para aplicar la configuración!\nsettings_ignore_other_filesystems = Ignorar otros sistemas de ficheros (sólo Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignora los archivos que no están en el mismo sistema de archivos que los directorios buscados.\n    \n    Funciona igual que la opción -xdev en encontrar el comando en Linux\nsettings_save_at_exit_button_tooltip = Guardar configuración en archivo al cerrar la aplicación.\nsettings_load_at_start_button_tooltip =\n    Cargar la configuración desde el archivo al abrir la aplicación.\n    \n    Si no está habilitado, se usarán los ajustes por defecto.\nsettings_confirm_deletion_button_tooltip = Mostrar el diálogo de confirmación al hacer clic en el botón borrar.\nsettings_confirm_link_button_tooltip = Mostrar el diálogo de confirmación al hacer clic en el botón hard/symlink.\nsettings_confirm_group_deletion_button_tooltip = Mostrar el diálogo de advertencia al intentar eliminar todos los registros del grupo.\nsettings_show_text_view_button_tooltip = Mostrar el panel de texto en la parte inferior de la interfaz de usuario.\nsettings_use_cache_button_tooltip = Usar caché de archivos.\nsettings_save_also_as_json_button_tooltip = Guardar caché en formato JSON (legible por seres humanos). Es posible modificar su contenido. La caché de este archivo será leída automáticamente por la aplicación si la caché del formato binario (con la extensión binaria) no se encuentra.\nsettings_use_trash_button_tooltip = Mueve archivos a la papelera en su lugar eliminándolos permanentemente.\nsettings_language_label_tooltip = Idioma para la interfaz de usuario.\nsettings_save_at_exit_button = Guardar configuración al cerrar la aplicación\nsettings_load_at_start_button = Cargar configuración al abrir la aplicación\nsettings_confirm_deletion_button = Mostrar diálogo de confirmación al eliminar cualquier archivo\nsettings_confirm_link_button = Mostrar diálogo de confirmación cuando vincule archivos de forma dura o simbólica\nsettings_confirm_group_deletion_button = Mostrar diálogo de confirmación al eliminar todos los archivos del grupo\nsettings_show_text_view_button = Mostrar panel de texto inferior\nsettings_use_cache_button = Usar caché\nsettings_save_also_as_json_button = Guarda también la caché como archivo JSON\nsettings_use_trash_button = Mover archivos borrados a la papelera\nsettings_language_label = Idioma\nsettings_multiple_delete_outdated_cache_checkbutton = Borrar automáticamente entradas de caché obsoletas\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Eliminar resultados de caché obsoletos que apuntan a archivos inexistentes.\n    \n    Cuando está activado, la aplicación se asegura al cargar registros, de que todos los registros apuntan a archivos válidos (los rotos son ignorados).\n    \n    Desactivar esto ayudará al escanear archivos en unidades externas, por lo que las entradas de caché sobre ellas no serán purgadas en el siguiente escaneo.\n    \n    En el caso de tener cientos de miles de registros en caché, se sugiere habilitar esto, lo que acelerará la carga/guardado del caché al inicio/final del escaneo.\nsettings_notebook_general = General\nsettings_notebook_duplicates = Duplicados\nsettings_notebook_images = Imágenes similares\nsettings_notebook_videos = Vídeos similares\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Muestra la vista previa en el lado derecho (al seleccionar un archivo de imagen).\nsettings_multiple_image_preview_checkbutton = Mostrar vista previa de la imagen\nsettings_multiple_clear_cache_button_tooltip =\n    Limpiar manualmente la caché de entradas desactualizadas.\n    Esto solo debe utilizarse si se ha desactivado la limpieza automática.\nsettings_multiple_clear_cache_button = Eliminar resultados obsoletos de la caché.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Oculta todos los archivos excepto uno, si todos apuntan a los mismos datos (están en línea dura).\n    \n    Ejemplo: En el caso en que hay (en el disco) siete archivos que están estrechamente vinculados a datos específicos y un archivo diferente con los mismos datos pero un inodio diferente, luego en el buscador duplicado, sólo se mostrará un archivo único y un archivo de los enlazados.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Establece el tamaño mínimo de archivo que se almacenará en caché.\n    \n    Al elegir un valor más pequeño se generarán más registros. Esto acelerará la búsqueda, pero ralentizará la carga/guardado de la caché.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Activa el almacenamiento en caché de prehash (un hash calculado desde una pequeña parte del archivo) que permite despedir los resultados no duplicados anteriormente.\n    \n    Está deshabilitado por defecto porque puede causar derribos lentos en algunas situaciones.\n    \n    Es altamente recomendable usarlo para escanear cientos de miles o millones de archivos, ya que puede acelerar la búsqueda varias veces.\nsettings_duplicates_prehash_minimal_entry_tooltip = Tamaño mínimo de la entrada en caché.\nsettings_duplicates_hide_hard_link_button = Ocultar enlaces duros\nsettings_duplicates_prehash_checkbutton = Usar caché prehash\nsettings_duplicates_minimal_size_cache_label = Tamaño mínimo de los archivos (en bytes) guardados en la caché\nsettings_duplicates_minimal_size_cache_prehash_label = Tamaño mínimo de archivos (en bytes) guardados en caché prehash\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Guardar la configuración de configuración actual en el archivo.\nsettings_loading_button_tooltip = Cargar los ajustes desde el archivo y reemplazar la configuración actual con ellos.\nsettings_reset_button_tooltip = Restablecer la configuración actual a la predeterminada.\nsettings_saving_button = Guardar configuración\nsettings_loading_button = Cargar configuración\nsettings_reset_button = Restablecer configuración\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Abre la carpeta donde se almacenan los archivos txt.\n    \n    Modificar los archivos de caché puede causar que se muestren resultados no válidos. Sin embargo, modificar la ruta puede ahorrar tiempo al mover una gran cantidad de archivos a una ubicación diferente.\n    \n    Puede copiar estos archivos entre ordenadores para ahorrar tiempo al escanear de nuevo para archivos (por supuesto, si tienen una estructura de directorios similar).\n    \n    En caso de problemas con la caché, estos archivos pueden ser eliminados. La aplicación los regenerará automáticamente.\nsettings_folder_settings_open_tooltip =\n    Abrir la carpeta donde se almacena la configuración de Czkawka.\n    \n    ADVERTENCIA: Modificar manualmente la configuración puede romper su flujo de trabajo.\nsettings_folder_cache_open = Abrir carpeta de caché\nsettings_folder_settings_open = Abrir carpeta de ajustes\n# Compute results\ncompute_stopped_by_user = El usuario ha detenido la búsqueda\ncompute_found_duplicates_hash_size = Se encontraron { $number_files } duplicados en { $number_groups } grupos que tomaron { $size } en { $time }\ncompute_found_duplicates_name = Se encontraron { $number_files } duplicados en { $number_groups } grupos en { $time }\ncompute_found_empty_folders = Se encontraron carpetas vacías { $number_files } en { $time }\ncompute_found_empty_files = Se encontraron { $number_files } archivos vacíos en { $time }\ncompute_found_big_files = Se encontraron { $number_files } archivos grandes en { $time }\ncompute_found_temporary_files = Encontrados archivos temporales { $number_files } en { $time }\ncompute_found_images = Se encontraron { $number_files } imágenes similares en { $number_groups } grupos en { $time }\ncompute_found_videos = Se encontraron { $number_files } vídeos similares en { $number_groups } grupos en { $time }\ncompute_found_music = Se encontraron archivos de música similares { $number_files } en grupos { $number_groups } en { $time }\ncompute_found_invalid_symlinks = Encontrados { $number_files } enlaces simbólicos no válidos en { $time }\ncompute_found_broken_files = Se encontraron { $number_files } archivos dañados en { $time }\ncompute_found_bad_extensions = Se encontraron archivos { $number_files } con extensiones no válidas en { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Escaneado archivo { $file_number }\n       *[other] Escaneados archivos { $file_number }\n    }\nprogress_scanning_extension_of_files = Extensión comprobada de archivo { $file_checked }/{ $all_files }\nprogress_scanning_broken_files = Verificado archivo { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hash de vídeo { $file_checked }/{ $all_files }\nprogress_creating_video_thumbnails = Miniaturas creadas de vídeo { $file_checked }/{ $all_files }\nprogress_scanning_image = Hash de { $file_checked }/{ $all_files } imagen ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Hash de imagen { $file_checked }/{ $all_files } comparado\nprogress_scanning_music_tags_end = Etiquetas comparadas de archivo de música { $file_checked }/{ $all_files }\nprogress_scanning_music_tags = Leer etiquetas del archivo de música { $file_checked }/{ $all_files }\nprogress_scanning_music_content_end = Se ha comparado la huella digital de archivo de música { $file_checked }/{ $all_files }\nprogress_scanning_music_content = Huella digital calculada de { $file_checked }/{ $all_files } archivo de música ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Escaneó la carpeta { $folder_number }\n       *[other] Escaneó las carpetas { $folder_number }\n    }\nprogress_scanning_size = Tamaño escaneado del archivo { $file_number }\nprogress_scanning_size_name = Nombre y tamaño escaneado del archivo { $file_number }\nprogress_scanning_name = Nombre escaneado del archivo { $file_number }\nprogress_analyzed_partial_hash = Se ha analizado el hash parcial de archivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Se ha analizado el hash completo de archivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Cargando caché prehash\nprogress_prehash_cache_saving = Guardando caché prehash\nprogress_hash_cache_loading = Cargando caché hash\nprogress_hash_cache_saving = Guardando caché hash\nprogress_cache_loading = Cargando caché\nprogress_cache_saving = Guardando caché\nprogress_current_stage = Etapa actual:{ \" \" }\nprogress_all_stages = Todas las etapas:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Configuración guardada en el archivo { $name }.\nsaving_loading_saving_failure = Error al guardar los datos de configuración en el archivo { $name }, razón { $reason }.\nsaving_loading_reset_configuration = La configuración actual fue borrada.\nsaving_loading_loading_success = Configuración de la aplicación cargada correctamente.\nsaving_loading_failed_to_create_config_file = Error al crear el archivo de configuración \"{ $path }\", razón \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = No se puede cargar la configuración de \"{ $path }\" porque no existe o no es un archivo.\nsaving_loading_failed_to_read_data_from_file = No se pueden leer los datos del archivo \"{ $path }\", razón \"{ $reason }\".\n# Other\nselected_all_reference_folders = No se puede iniciar la búsqueda, cuando todos los directorios están establecidos como carpetas de referencia\nsearching_for_data = Buscando datos, puede tardar un tiempo, por favor espere...\ntext_view_messages = MENSAJES\ntext_view_warnings = ADVERTENCIA\ntext_view_errors = ERRORES\nabout_window_motto = Este programa es gratuito y siempre lo será.\nkrokiet_new_app = kawka está en modo de mantenimiento, lo que significa que sólo se arreglarán errores críticos y no se añadirán nuevas características. Para nuevas características, por favor echa un vistazo a la nueva aplicación de Krokiet, que es más estable y eficiente y todavía está en desarrollo activo.\n# Various dialog\ndialogs_ask_next_time = Preguntar la próxima vez\nsymlink_failed = Error al enlazar { $name } a { $target }, razón { $reason }\ndelete_title_dialog = Confirmación de eliminación\ndelete_question_label = ¿Está seguro que desea eliminar los archivos?\ndelete_all_files_in_group_title = Confirmación de borrar todos los archivos del grupo\ndelete_all_files_in_group_label1 = En algunos grupos se seleccionan todos los registros.\ndelete_all_files_in_group_label2 = ¿Estás seguro de que quieres eliminarlos?\ndelete_items_label = { $items } archivos serán eliminados.\ndelete_items_groups_label = { $items } archivos de { $groups } grupos serán eliminados.\nhardlink_failed = Error al vincular { $name } a { $target }, razón { $reason }\nhard_sym_invalid_selection_title_dialog = Selección no válida con algunos grupos\nhard_sym_invalid_selection_label_1 = En algunos grupos sólo hay un registro seleccionado y será ignorado.\nhard_sym_invalid_selection_label_2 = Para poder vincular estos archivos con el sistema, al menos dos resultados en el grupo deben ser seleccionados.\nhard_sym_invalid_selection_label_3 = El primero en el grupo es reconocido como original y no se cambia, pero el segundo y posterior son modificados.\nhard_sym_link_title_dialog = Confirmación de enlace\nhard_sym_link_label = ¿Está seguro que desea enlazar estos archivos?\nmove_folder_failed = Error al mover la carpeta { $name }, razón { $reason }\nmove_file_failed = Error al mover el archivo { $name }, razón { $reason }\nmove_files_title_dialog = Elija la carpeta a la que desea mover los archivos duplicados\nmove_files_choose_more_than_1_path = Solo se puede seleccionar una ruta para poder copiar sus archivos duplicados, seleccionado { $path_number }.\nmove_stats = Mudado correctamente { $num_files }/{ $all_files } elementos\nsave_results_to_file = Resultados guardados en archivos txt y json en la carpeta \"{ $name }\".\nsearch_not_choosing_any_music = ERROR: Debe seleccionar al menos una casilla de verificación con tipos de búsqueda de música.\nsearch_not_choosing_any_broken_files = ERROR: Debe seleccionar al menos una casilla de verificación con el tipo de ficheros rotos comprobados.\ninclude_folders_dialog_title = Carpetas a incluir\nexclude_folders_dialog_title = Carpetas a excluir\ninclude_manually_directories_dialog_title = Añadir directorio manualmente\ncache_properly_cleared = Caché correctamente borrada\ncache_clear_duplicates_title = Limpiando caché duplicada\ncache_clear_similar_images_title = Limpiando caché de imágenes similares\ncache_clear_similar_videos_title = Limpiando caché de vídeos similares\ncache_clear_message_label_1 = ¿Quiere borrar la caché de entradas obsoletas?\ncache_clear_message_label_2 = Esta operación eliminará todas las entradas de caché que apunten a archivos no válidos.\ncache_clear_message_label_3 = Esto puede acelerar ligeramente la carga/guardado en caché.\ncache_clear_message_label_4 = ATENCIÓN: La operación eliminará todos los datos almacenados en caché de unidades externas desconectadas. Por lo tanto, cada hash tendrá que ser regenerado.\n# Show preview\npreview_image_resize_failure = Error al redimensionar la imagen { $name }.\npreview_image_opening_failure = Error al abrir la imagen { $name }, razón { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Grupo { $current_group }/{ $all_groups } ({ $images_in_group } imágenes)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/fa/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = تنظیمات\nwindow_main_title = چکاواک (همیپ)\nwindow_progress_title = سkenنگ\nwindow_compare_images = مقایسه تصاویر\n# General\ngeneral_ok_button = اوکی\ngeneral_close_button = بسته شود\n# Krokiet info dialog\nkrokiet_info_title = معرفی کرکیه - نسخه جدید CzKawkA\nkrokiet_info_message = \n        کروکیِت نسخه جدید، بهبود یافته، سریع‌تر و قابل اعتمادتر رابط کاربری GTK Czkawka است!\n\n        اجرای آن آسان‌تر و مقاوم‌تر در برابر تغییرات سیستم است، زیرا فقط به کتابخانه‌های اصلی موجود در اکثر سیستم‌ها به صورت پیش‌فرض وابسته است.\n\n        کروکیِت همچنین ویژگی‌هایی را به همراه دارد که Czkawka فاقد آن‌هاست، از جمله تصاویر کوچک در حالت مقایسه ویدیو، یک پاک‌کن EXIF، گزینه‌های پیشرفت برای انتقال/کپی/حذف فایل یا گزینه‌های مرتب‌سازی گسترده.\n\n        آن را امتحان کنید و تفاوت را ببینید!\n\n        Czkawka همچنان با رفع باگ‌ها و به‌روزرسانی‌های جزئی از طرف من دریافت خواهد شد، اما تمام ویژگی‌های جدید به طور انحصاری برای کروکیِت توسعه خواهند یافت و هر کسی می‌تواند ویژگی‌های جدید، افزودن حالت‌های از دست رفته یا گسترش بیشتر Czkawka را ارائه دهد.\n\n        PS: این پیام باید فقط یک بار ظاهر شود. اگر دوباره ظاهر شد، متغیر محیطی CZKAWKA_DONT_ANNOY_ME را به هر مقدار غیر خالی تنظیم کنید.\n# Main window\nmusic_title_checkbox = عنوان\nmusic_artist_checkbox = هنرپرداز\nmusic_year_checkbox = سال\nmusic_bitrate_checkbox = بیت‌روتpedoze\nmusic_genre_checkbox = نوع\nmusic_length_checkbox = طول\nmusic_comparison_checkbox = مقایسه تقریبی\nmusic_checking_by_tags = برچسب‌ها\nmusic_checking_by_content = محتوا\nsame_music_seconds_label = کسری مینیمال دوم طول دومین جفت\nsame_music_similarity_label = مکانیمم تفاضل\nmusic_compare_only_in_title_group = مقایسه در گروه‌های با عنوان‌های مشابه\nmusic_compare_only_in_title_group_tooltip =\n    هنگامی که فعال است، فایل‌ها بر اساس عنوان گروه‌بندی می‌شوند و سپس با هم مقایسه می‌شوند.\n    \n    با 10000 فایل، به جای تقریباً 100 میلیون مقایسه، حدوداً 20000 مقایسه خواهد بود.\nsame_music_tooltip =\n    جستجوی مسایل موسیقی مشابه بر اساس محتوا می‌تواند با تنظیم موارد زیر پیشنهاد شود:\n    \n    - زمان حداقل فragment بعد از آن مسایل موسیقی می‌توانند به عنوان مشابه شناسایی شوند\n    - تفاوت بینی مرکب دو fragment مورد تست\n    \n    کلید خوب کسب نتایج است این پارامترها را به صورت منطقی تر از طریق تنظیم‌های ارائه شده جستجو کنید.\n    \n    تنظیم زمان حداقل ۵ ثانیه و تفاوت بینی مرکب ۱.۰، برای پیدا کردن فرگمنت‌های معمولاً مشابه در فایل‌ها استفاده خواهد شد.\n    در حالی که زمان ۲۰ ثانیه و تفاوت بینی مرکب ۶.۰ برای پیدا کردن remixات/نسخه‌های live بهتر کار می‌کند.\n    \n    به طور پیش فرض، هر فایل موسیقی با هر فایل دیگری مقایسه می‌شود و این زمان خیلی زمان‌بر خواهد بود وقتی که تعداد بسیاری از فایل‌ها را تست کنید، بنابراین به طور گسترده‌تر استفاده از پوشه‌های مرجع و مشخص شدن کدام فایل‌ها را که باید با هم مقایسه شوند (با تعداد برابر فایل‌ها، مقایسه خلاک‌های ضخامت چندگانه حداقل ۴ برابر از بدون پوشه‌های مرجع سریعتر خواهد بود) کارآمد است.\nmusic_comparison_checkbox_tooltip =\n    آنچه با هوش مصنوعی جستجو می‌کند و از یادگیری ماشین برای حذف پرانتز‌ها از جمله استفاده می‌کند. به عنوان مثال، با فعال سازی این گزینه، فایل‌های مورد سوال نظرات همگون در نظر گرفته خواهند شد:\n    \n    Świędziżłób     ---     Świędziżłób ( Remix Lato 2021)\nduplicate_case_sensitive_name = متن حساس به بیانیه\nduplicate_case_sensitive_name_tooltip =\n    وقتی فعال است، تنها رکوردهایی را در گروه گذاری می‌کند که دقیقاً نام آن‌ها مشابه هستند مانند Żołd <-> Żołd\n    \n    停用 اینگونde برای گروه‌بندی نام‌ها در نظر نمی‌گیرد که هر حرف چقدر تقریب زدن دارد مانند żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = حجم و نام\nduplicate_mode_name_combo_box = نام\nduplicate_mode_size_combo_box = اندازه\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka ۳ نوع هاش می‌پذیرد:\n    \n    بلک ۳ - تابع چربی کriتوگرافیک. این آن به طور پیش فرض است زیرا خیلی سریع است.\n    \n    CRC32 - تابع چربی ساده. این سریع‌تر از بلک ۳ باشد، اما در نسخه‌ای نادر ممکن است برخورد کامل داشته باشد.\n    \n    XXH3 - در عملکرد و کیفیت چربی تقریباً شبیه به بلک ۳ (اما کriتوگرافیک نیست). بنابراین، این حالت‌ها می‌توانند خلک ساده جایگزین شوند.\nduplicate_check_method_tooltip =\n    این لحظه، Czkawka سه روش از پشتیبان برای پیدا کردن فایل‌های تکراری دارد:\n    \n    نام - فایل‌هایی را که نام آنها یکسان است را پیدا می‌کند.\n    \n    حجم - فایل‌هایی را که حجم آنها یکسان است را پیدا می‌کند.\n    \n    هش - فایل‌هایی را که محتوای آنها یکسان است را پیدا می‌کند. این مód هش فایل را بررسی می‌کند و بعد با مقایسه این هش، تکراری‌ها را پیدا می‌کند. این مód روش امن‌تری برای پیدا کردن تکراری‌ها است. اپلیکیشن به شدت از کاش استفاده می‌کند، بنابراین دومین و فراخانی‌های بعدی داده‌های یکسان بسیار سریع‌تر از اولین بار خواهند بود.\nimage_hash_size_tooltip =\n    هر تصویری که تایید شده یک هش خاص ایجاد می‌کند که می‌توان آن را با هم مقایسه کرد، و اختلاف زیادی بین آنها به این معناست که این تصاویر مشابه هستند.\n    \n    8 باره سایز هش خیلی خوبی است تا تصاویری را که تنها ممکن است شبیه به اصلی باشند پیدا کنیم. با یک مجموعه بزرگتر از تصاویر (>1000)، این موضوع تعداد زیادی از مقادیر نادرست را تولید خواهد کرد، بنابراین بهتر است در این صورت سایز هش بزرگ‌تری را استفاده کنید.\n    \n    16 پوزیشن سایز هش پیش‌فرض است که یک ترادف خوبی بین پیدا کردن تصاویری که حتی شبیه به اصلی هستند و داشتن تنوع کمتری از تنش‌های هش، محسوب می‌شود.\n    \n    32 و 64 هش فقط تصاویر بسیار مشابه را پیدا می‌کنند، اما بایستی اچ helt نسبتاً کمترین مقادیر نادرست داشته باشند (ربات‌ها با کانال alpha ممکن است غیرمعمول باشند).\nimage_resize_filter_tooltip =\n    برای حساب کردن هش تصویر، بиблиا می‌تواند ابتدا آن را نرم‌زد کند.\n    \n    به گونه‌ای که الگوریتم انتخاب شده است، تصویری که برای محاسبه هش استفاده می‌شود به صورت کمی متفاوت خواهد بود.\n    \n    الگوریتم سریع‌تر برای استفاده وجود دارد، ولی نتایج آن کسب‌کننده پایین‌تری هستند. الگوریتم نزدیک‌تر (Nearest) به طور پیش‌فرض فعال است زیرا با حجم 16x16 کمترین کیفیت این کسیزه که نظار قابل توجهی برای بین‌داشت ندارد.\n    \n    با حجم 8x8 کسیزه، پیشنهاد می‌شود به جای الگوریتم نزدیک‌تر (Nearest) از یک الگوریتم دیگر استفاده شود تا گروه‌های تصویر بگذرند.\nimage_hash_alg_tooltip =\n    کاربران می‌توانند از یکی از دستورالعمل‌های بسیاری برای محاسبه هاش که در اختیار آن‌ها قرار داده شده است، پیشنهاد دستورالعمل خود را انتخاب کنند.\n    \n    هر یک این‌ها نقطه قوت و ضعف کلیدی دارند و برای تصاویر مختلف همگرایتر یا نامطلوبتر می‌توانند عملکرد خود را نشان دهند.\n    \n    بنابراین، برای تعیین بهترین یکی برای شما، آزمون دستی ضروری است.\nbig_files_mode_combobox_tooltip = می‌پذیرد جستجوی فایل‌های کوچکتر/بزرگتر را\nbig_files_mode_label = فایل‌های بررسی شده\nbig_files_mode_smallest_combo_box = تازیست\nbig_files_mode_biggest_combo_box = بزرگترین\nmain_notebook_duplicates = فایل‌های تکراری\nmain_notebook_empty_directories = دایرکتوری خالی\nmain_notebook_big_files = فایل‌های بزرگ\nmain_notebook_empty_files = فایل‌های خالی\nmain_notebook_temporary = فایل‌های موقت\nmain_notebook_similar_images = تصورهای مشابه\nmain_notebook_similar_videos = فیلم‌های مشابه\nmain_notebook_same_music = دупلیکات مузیک\nmain_notebook_symlinks = سینمک های معتبر ناامن\nmain_notebook_broken_files = فایل‌های ضارب\nmain_notebook_bad_extensions = برچسب‌های بد\nmain_tree_view_column_file_name = نام فایل\nmain_tree_view_column_folder_name = نام پوشه\nmain_tree_view_column_path = پادکست\nmain_tree_view_column_modification = تاریخ بروزرسانی\nmain_tree_view_column_size = حجم\nmain_tree_view_column_similarity = شبیهسازی\nmain_tree_view_column_dimensions = بعدیون\nmain_tree_view_column_title = عنوان\nmain_tree_view_column_artist = کارگردان\nmain_tree_view_column_year = سال\nmain_tree_view_column_bitrate = بریتیتی کدگذاری\nmain_tree_view_column_length = طول\nmain_tree_view_column_genre = ژانر\nmain_tree_view_column_symlink_file_name = لینک ناشی فایل\nmain_tree_view_column_symlink_folder = تیم‌پیکس فولدر\nmain_tree_view_column_destination_path = پیشانته مسیر\nmain_tree_view_column_type_of_error = نوع خطا\nmain_tree_view_column_current_extension = بازگشت بسته شده\nmain_tree_view_column_proper_extensions = توضیح معتبر\nmain_tree_view_column_fps = فپس\nmain_tree_view_column_codec = کدک\nmain_label_check_method = روش چک کردن\nmain_label_hash_type = نوع هاش\nmain_label_hash_size = سایز هاش\nmain_label_size_bytes = حجم (بایت)\nmain_label_min_size = مین\nmain_label_max_size = مکس\nmain_label_shown_files = تعداد فایل‌های نمایش‌یافته\nmain_label_resize_algorithm = الگوریتم سایز بندی\nmain_label_similarity = سхожگی{ \"   \" }\nmain_check_box_broken_files_audio = آوتویډ\nmain_check_box_broken_files_pdf = پdf\nmain_check_box_broken_files_archive = اریکرج\nmain_check_box_broken_files_image = عکس\nmain_check_box_broken_files_video = ویدئو\nmain_check_box_broken_files_video_tooltip = از ffmpeg/ffprobe برای اعتبارسنجی فایل‌های ویدیویی استفاده می‌کند. نسبتاً کند است و ممکن است خطاها را به صورت دقیق تشخیص دهد حتی اگر فایل به درستی پخش شود.\ncheck_button_general_same_size = تنها سایز‌های مختلف را تغییر ندهید\ncheck_button_general_same_size_tooltip = فایل‌های مشابه سایز آن‌ها در نتایج را ترک کنید - معمولاً این فایل‌ها دوباره تولید شده‌اند (1:1)\nmain_label_size_bytes_tooltip = حجم فایل‌هایی که در طول سcan استفاده خواهند شد\n# Upper window\nupper_tree_view_included_folder_column_title = پوشه‌هایی که جستجو کنید\nupper_tree_view_included_reference_column_title = دایرکتوری های مرجع\nupper_recursive_button = پیچیده‌ترین\nupper_recursive_button_tooltip = اگر انتخاب شد، به دنبال فایل‌هایی نیز جستجو کنید که قطعاً در پوشه‌های گرفته‌شده مستقیماً وجود ندارند.\nupper_manual_add_included_button = افزودن دستی\nupper_add_included_button = افزودن\nupper_remove_included_button = حذف\nupper_manual_add_excluded_button = متن دستی اضافه\nupper_add_excluded_button = افضال\nupper_remove_excluded_button = حذف\nupper_manual_add_included_button_tooltip =\n    درآماد نام دایرکتوری را برای جستجو با دست اضافه کنید.\n    \n    برای بارگذاری چندین مسیر به طور همزمان، آنها را با یک ';' جدا کنید;\n    \n    /home/رومان;/home/روزکaz خواهد باعث اضافه شدن دو دایرکتوری /home/رومان و /home/روزکaz می‌شود\nupper_add_included_button_tooltip = دایرکتوری جدید به جستجو اضافه کنید.\nupper_remove_included_button_tooltip = پوشه را از جستجو حذف کنید.\nupper_manual_add_excluded_button_tooltip =\n    مدیریت نام دایرکتوری استراحت شده را به صورت håد良心化处理结果：\n    保持相同的语气和风格。保留任何特殊格式或占位符。\n    仅返回翻译文本，不提供解释或其他额外文本。\n    \n    ترجمه:\n    نام دایرکتوری استراحت شده را به صورت håد و يدی اضافه کنید.\n    \n    برای اضافه کردن چندین مسیر همزمان، آن‌ها را با ؛ جدا کنید;\n    /home/roman;/home/krokiet دو دایرکتوری /home/roman و /home/keokiet را اضافه خواهد کرد\nupper_add_excluded_button_tooltip = پوشه‌ای برای جستجوی مورد بندی خارج شود.\nupper_remove_excluded_button_tooltip = دایرکتوری را از مورد نادیده‌گرفتن حذف کنید.\nupper_notebook_items_configuration = تنظیم‌های موارد\nupper_notebook_excluded_directories = مسیرهای حذف‌شده\nupper_notebook_included_directories = مسارات شامل‌شده\nupper_allowed_extensions_tooltip =\n    امتدادهای مجاز باید با کاما جدا شوند (به طور پیش فرض همه موجود هستند).\n    \n    مacروهای زیر نیز موجود است که تعدادی از امتدادات را به طور یکتایی اضافه می‌کنند: تصویر، ویدئو، موسیقی، متن.\n    \n    مثال کاربردی \".exe, تصویر, ویدئو, .rar, 7z\" - این به معنی است که تصاویر (مثلاً jpg, png)، ویدئوها (مثلاً avi, mp4)، exe، rar و 7z درخواست بررسی خواهند شد.\nupper_excluded_extensions_tooltip =\n    فهرست فایل‌های غیرفعال که در اسکن نادیده گرفته می‌شوند.\n    \n    هنگام استفاده از همچون پسوندها مجاز و غیرفعال، این یکی اولویت بالاتری دارد، بنابراین فایل بررسی نخواهد شد.\nupper_excluded_items_tooltip = \n        موارد حذف شده باید شامل * wildcard و باید با کاما از هم جدا شوند.\n        این کندتر از مسیرهای حذف شده است، بنابراین از آن با دقت استفاده کنید.\nupper_excluded_items = آیتم‌های حذف شده:\nupper_allowed_extensions = مدت زمان مجاز:\nupper_excluded_extensions = توسعه‌های غیرفعال:\n# Popovers\npopover_select_all = انتخاب همه\npopover_unselect_all = همه را مخاطب نکنید\npopover_reverse = انتخاب را برگردانید\npopover_select_all_except_shortest_path = انتخاب همه چیز به جز کوتاه‌ترین مسیر\npopover_select_all_except_longest_path = انتخاب همه موارد به جز طولانی‌ترین مسیر\npopover_select_all_except_oldest = انتخاب همه غیر از قدیمی nhất\npopover_select_all_except_newest = انتخاب همه مواردی که نوزاد نیستند\npopover_select_one_oldest = انتخاب یکی از قدیمی‌ترینanst\npopover_select_one_newest = انتخاب یک نوستین\npopover_select_custom = انتخاب خاصیتutzer Merkel\npopover_unselect_custom = غير فردی را بیگانه کنید\npopover_select_all_images_except_biggest = انتخاب همه از بیشترین به جز خود را حذف کنید\npopover_select_all_images_except_smallest = انتخاب همه می‌شود به جز کوچکترین\npopover_custom_path_check_button_entry_tooltip = \n    انتخاب رکوردها با مسیر.\n    \n    مثال استفاده:\n    /home/pimpek/rzecz.txt را با /home/pim* پیدا کنید\npopover_custom_name_check_button_entry_tooltip = \n    فایل‌ها را با نام‌های فایل انتخاب کنید.\n    \n    رایکسی از /usr/ping/pong.txt با *ong* می‌تواند پیدا شود\npopover_custom_regex_check_button_entry_tooltip =\n    انتخاب رکوردها با استفاده از Regular Expression مشخص شده.\n    \n    در این حالت، متن جستجو پیش سمت (Path) و نام (Name) است.\n    \n    مثال کاربردی:\n    /usr/bin/ziemniak.txt را با /ziem[a-z]+ پیدا کرد.\n    \n    این مورد استفاده‌ی پیاده‌سازی پیش‌فرض Rust برای Regular Expression است. شما می‌توانید بیشتر درباره آن در این لینک فهم득 بگیرید: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    توانایی تشخیص حساس به حروف بزرگ و کوچک را فعال می‌کند.\n    \n    در صورت غیرفعال بودن، /home/* هر دو /HoMe/roman و /home/roman را پیدا خواهد کرد.\npopover_custom_not_all_check_button_tooltip =\n    پیش‌بینی تهیه همه رکورد در گروه را جلوگیری می‌کند.\n    \n    با فعال بودن این تنظیم به طور پی‌这里是翻译的后半部分，按照要求保持格式和占位符不变：\n    \n    این معیار اولیه فعال است، زیرا در بسیاری از موارد، نمی‌توانید هر دو فایل اصلی و تکراری را پاک کنید و می‌خواهید حداقل یک فایل را نگه دارید.\n    \n    Warning: این تنظیمات در صورت انتخاب دستی همه نتایج در گروه، عملکرد خود را ندارند.\npopover_custom_regex_path_label = مسیر\npopover_custom_regex_name_label = نام\npopover_custom_regex_regex_label = پاتч ریگекс + نام\npopover_custom_case_sensitive_check_button = حساس به حروف تکیه‌گاه\npopover_custom_all_in_group_label = همه رکورد در گروه را انتخاب نکنید\npopover_custom_mode_unselect = بی‌پیکربندی خود را انتخاب نکنید\npopover_custom_mode_select = انتخاب مورد خاص\npopover_sort_file_name = نام فایل\npopover_sort_folder_name = نام پوشه\npopover_sort_full_name = نام کامل\npopover_sort_size = حجم\npopover_sort_selection = نتیجه‌گیری\npopover_invalid_regex = رگEXP نامعتبر است\npopover_valid_regex = регEXP معتبر است\n# Bottom buttons\nbottom_search_button = جستجو\nbottom_select_button = انتخاب\nbottom_delete_button = حذف\nbottom_save_button = ذخیره\nbottom_symlink_button = Symlink\nbottom_hardlink_button = هاردلینک\nbottom_move_button = برو\nbottom_sort_button = مرتب کردن\nbottom_compare_button = مقایسه\nbottom_search_button_tooltip = ابدیه سرچشمه\nbottom_select_button_tooltip = انتخاب رکوردها. فقط فایل‌های/پوشه‌های انتخاب شده می‌توانند پس از آن پردازش شوند.\nbottom_delete_button_tooltip = حذف فایل‌های/دستگاه‌های انتخاب‌شده.\nbottom_save_button_tooltip = اطلاعات جستجو را در فایل ذخیره کنید\nbottom_symlink_button_tooltip =\n    پیوندهای نمادین ایجاد کنید.\n     تنها زمانی کار می‌کند که حداقل دو نتیجه در یک گروه انتخاب شوند.\n    اولین بخش تغییری ندارد و دومین و سایر آن‌ها به اولین لینک نمادین می‌شوند.\nbottom_hardlink_button_tooltip =\n    لینک‌های سخت را ایجاد کنید.\n    فقط زمانی کار می‌کند که حداقل دو نتیجه در یک گروه انتخاب شده باشد.\n    اولی تغییر نمی‌کند و دومی و بعدی به اولی لینک سخت پیدا می‌کنند.\nbottom_hardlink_button_not_available_tooltip =\n    ساخت لینک‌های قوی را اجرا کنید.\n    دکمه فعال نیست، زیرا لینک‌های قوی ساخته نمی‌شوند.\n    لینک‌های قوی تنها در صورت داشتن امتیازات مدیریت‌کننده روی Windows کار می‌کنند، بنابراین مطمئن شوید که برنامه را به طور مدیریت‌کننده اجرا کرده‌اید.\n    در صورتی که برنامه با این امتیازات کار کند، مشاler مشابه روی Github بررسی کنید.\nbottom_move_button_tooltip =\n    پیکر فایل‌ها را به مسیر انتخاب شده منتقل می‌کند.\n    او همه فایل‌ها را به مسیر منتقل می‌کند بدون نگه داشتن درخت مسیری.\n    وقتی سعی می‌کنید دو فایل با نام تکراری را به مسیری منتقل کنید، دومی با خطا مواجه خواهد شد.\nbottom_sort_button_tooltip = فرمت فایل‌ها/دسته‌بندی‌ها را بر اساس روش مورد انتخاب تنظیم کند.\nbottom_compare_button_tooltip = مقایسه تصاویر در گروه را انجام دهید.\nbottom_show_errors_tooltip = نمایش/起底部文本面板。.\nbottom_show_upper_notebook_tooltip = نمایش/پنهان کردن پanel بالایی نوت‌بук.\n# Progress Window\nprogress_stop_button = وقفه\nprogress_stop_additional_message = توقف درخواست شده\n# About Window\nabout_repository_button_tooltip = پیوند به صفحه نگهدارنده کد منبع.\nabout_donation_button_tooltip = لینک به صفحه‌ی سپرده‌گذاری.\nabout_instruction_button_tooltip = لینک به صفحه دستورالعمل.\nabout_translation_button_tooltip = لینک به صفحه Crowdin برای ترجمه‌ی برنامه. فارسی رسمی و انگلیسی مورد پشتیبانی قرار دارند.\nabout_repository_button = آرشیو\nabout_donation_button = تبرعات\nabout_instruction_button = ارتباطات\nabout_translation_button = ترجمه:\n# Header\nheader_setting_button_tooltip = دیالوگ تنظیمات را باز می‌کند.\nheader_about_button_tooltip = پنجره دیالوگ با اطلاعات دربارهٔ اپ را باز می‌کند.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = تعداد خيوان‌های استفاده شده\nsettings_number_of_threads_tooltip = تعداد سرورهای استفاده‌شده، ۰ به معنای استفاده از تمامی سرورهای در دسترس است.\nsettings_use_rust_preview = بجای gtk، از بиблиو梯es خارجی برای بارگذاری نمایش‌ها استفاده کنید\nsettings_use_rust_preview_tooltip =\n    استفاده از پیش‌نماهای GTK گاهی سریع‌تر خواهد بود و حمایت از بیشتر فرمت‌ها را دارد، اما گاهی دقیقاً عکس آن است.\n    \n    اگر با بارگذاری پیش‌نمای مشکلی دارید، ممکن است بتوانید به تغییر این تنظیم بپردازید.\n    \n    در سیستم‌های غیر-Linux، توصیه می‌شود این گزینه را استفاده کنید، زیرا gtk-pixbuf همیشه در آنجا دسترسی پذیر نیستند بنابراین تعطیل کردن این گزینه باعث می‌شود پیش‌نمای برخی تصاویر بارگذاری نشوند.\nsettings_label_restart = شما باید برنامه را از نو انیمود تا تنظیمات را اعمال کنید!\nsettings_ignore_other_filesystems = سایر سیستم‌های فایل را نادیده بگیرید (فقط لینوکس)\nsettings_ignore_other_filesystems_tooltip = \n    پروژه فایل‌هایی را نادیده می‌گیرد که در سیستم‌فایل‌های مشترک با دایرکتوری‌های جستجو نیستند.\n    \n    به طور مشابه عملکرد -xdev گزینه در دستور find در روی سیستم عامل لینوکس است\nsettings_save_at_exit_button_tooltip = هنگام بسته شدن برنامه، تنظیمات را در فایل ذخیره کنید.\nsettings_load_at_start_button_tooltip =\n    به زمانی که برنامه را می‌افزایید، تنظیمات را از فایل بارشید.\n    اگر غیرفعال است، تنظیمات پیش‌فرض به کار خواهند رفت.\nsettings_confirm_deletion_button_tooltip = وقتی دکمه حذف را کلیک می‌کنید، پنجره تأیید نمایش داده شود.\nsettings_confirm_link_button_tooltip = در زمان کلیک بر روی دکمه لینک سخت/شبکه، یک پیامگذاری تأیید را نشان دهد.\nsettings_confirm_group_deletion_button_tooltip = وقتی همه رکوردهای گروه را حذف می‌کنید، حذف پیشنهادی را به صورت پیامWarning نشان دهید.\nsettings_show_text_view_button_tooltip = بر روی بخش متنی در پایین رابط کاربری نمایش دهید.\nsettings_use_cache_button_tooltip = از کاشی فایل استفاده کنید.\nsettings_save_also_as_json_button_tooltip = اندازه‌گیری کش را به فرمت JSON خوانا ذخیره کنید. محتوای آن قابل ویرایش است. کش از این فایل تلقیه می‌شود، در صورتی که کش با فormат باینری (با پسوند bin) مفقود باشد.\nsettings_use_trash_button_tooltip = فایل‌ها را به سبد حذفی منتقل می‌کند تا به طور دائم حذف نشوند.\nsettings_language_label_tooltip = زبان برای سطح کاربری.\nsettings_save_at_exit_button = تنظیمات را در زمان بسته شدن برنامه ذخیره کنید\nsettings_load_at_start_button = تنظیمات را در زمان باز کردن اپلیکاسیون بارگذاری کنید\nsettings_confirm_deletion_button = پیام مطمئن شدن در حذف هر فایل را نشان دهید\nsettings_confirm_link_button = در زمان تغییرات مختصر یا لینک سختی فایل‌ها، پنجره توافقنامه تایید را نشان دهید\nsettings_confirm_group_deletion_button = در حذف تمامی فایل‌های گروه، دایالوگ تأیید را نشان بدهید\nsettings_show_text_view_button = پانل متن پایین را نشان دهید\nsettings_use_cache_button = استفاده از کشCACHE\nsettings_save_also_as_json_button = همچنین کش exists را به فایل JSON پاک کنید\nsettings_use_trash_button = فایل‌های حذف شده را به سبد اسکن منتقل کنید\nsettings_language_label = زبان\nsettings_multiple_delete_outdated_cache_checkbutton = موارد کش از دستاره را به طور خودکار حذف کنید\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    حذف نتایج کش قدیمی که به فایل‌هایی بازندگانی اشاره دارند.\n    \n    وقتی فعال شده، برنامه مطمئن می‌شود زمان بارگذاری رکوردها، تمام رکوردها به فایل‌های معتبر اشاره داشته باشند (فرچمند های خراب تجاهل شده‌اند).\n    \n     Dezactivه کردن این گزینه وقتی که در حال پرسپект فایل‌ها روی حملات خارجی است به نفع خواهد بود، بنابراین ورودی‌های کش مربوط به آن‌ها در نظر آینده حذف نخواهند شد.\n    \n    در صورت داشتن صد هزار رکورد در کش، پیشنهاد می‌شود این گزینه را فعال کنید، که خشونت بارگذاری/ذخیره کش در شروع و پایان پرسپект را تسریع خواهد کرد.\nsettings_notebook_general = عمومی\nsettings_notebook_duplicates = 副本\nsettings_notebook_images = سایر عکس‌های مشابه\nsettings_notebook_videos = 비슷 آموزش ویدیو\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = نمایش نمونه در سمت راست (هنگام انتخاب فایل تصویر).\nsettings_multiple_image_preview_checkbutton = نتیجه نمایش تصویر را نشان دهید\nsettings_multiple_clear_cache_button_tooltip =\n    با håندکی حذف مخزن موارد قدیمی را پاک کنید.\n    این تنها در صورت غیرفعال بودن پاک‌شدن خودکار استفاده شود.\nsettings_multiple_clear_cache_button = برای حذف نتایج قدیمی از کش، استفاده کنید.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    پیچش تمام فایل‌ها را پنهان می‌کند، به جز یک فایل، اگر همه به همان داده‌ای مشیر باشند (هم‌الودین شده‌اند).\n    \n    مثال: در صورت وجود هفت فایل (در حاشیه دیسک) که به داده‌های خاص مشیر هستند و یک فایل متفاوت با داده‌ها، اما inode متفاوت، در پیدا کننده تکرار، فقط یک فایل متمایز و یکی از آن فایل‌های هم‌الودین نشان داده خواهد شد.\nsettings_duplicates_minimal_size_entry_tooltip =\n    سیزه‌ای از فایل کمتر را که به طور کشید خاموش خواهد شد تعیین کنید.\n    \n    انتخاب مقدار بزرگتر خواهد منجر به تولید مراکز زیادتر شده است. این به جستجو سریع‌تر کمک خواهد کرد، اما آماده‌سازی و ذخیره سازی کش را به سوی سرفه خواهد نقل می‌کرد.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    تاکید بر این است که کش (قaches پیش هاس) که از یک بخش کوچک از فایل محاسبه می‌شود، ذخیره شود که در نتایج تکراری‌نشدنی از راه حذف آنها قبلی‌تر می‌شود.\n    \n    در صورتی که این دستورالعمل غیر فعال است، به طور پیش‌فرض فعال نمی‌شود زیرا در برخی اوضاع می‌تواند منجر به تأخیرات شود.\n    \n    این جایگزین خوبی برای استفاده در جستجوی هزاران یا میلیون فایل است، زیرا می‌تواند جستجو را بسیار سریع‌تر کند.\nsettings_duplicates_prehash_minimal_entry_tooltip = اندازه مینیمالی از ورودی کشی.\nsettings_duplicates_hide_hard_link_button = پنهان کردن لینک‌های سخت\nsettings_duplicates_prehash_checkbutton = استفاده از کاش پیش‌هاشتاپ\nsettings_duplicates_minimal_size_cache_label = حجم مínیمم فایل‌های ذخیره شده در کاش (در بیت)\nsettings_duplicates_minimal_size_cache_prehash_label = حجم مینیموم فایل‌ها (در بایتس) ذخیره شده در کش پرسیپت‌ها\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = تنظیمات فعلی را به فایل ذخیره کنید.\nsettings_loading_button_tooltip = تنظیمات را از فایل بارش و جایگزینی تنظیمات موجود با آنها انجام دهید.\nsettings_reset_button_tooltip = تغییر از تنظیمات فعلی به تنظیم پیش‌فرض را بازیابی کنید.\nsettings_saving_button = تنظیمات را ذخیره کنید\nsettings_loading_button = تغییر تنظیمات را بارگذاری کنید\nsettings_reset_button = تنظیمات را بازنشانی کنید\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    کافیست پوشه‌ای که فایل‌های txt کاشینگ را ذخیره می‌کند باز شود.\n    \n    تغییر در فایل‌های کاش ممکن است منجر به نمایش نتایج نامعتبر شوند. با این حال، تغییر در مسیر ممکن است وقت بیشتری را برای منتقل کردن مقدار زیادی از فایل‌ها به موقعیت مختلف ارزیابی کند.\n    \n    شما می‌توانید این فایل‌ها را بین ماکینتها نسخه و پیوند دهید تا وقت را در بررسی دوباره برای فایل‌ها (بله، اگر ساختار پوشه‌ها مشابه باشند) کاهش دهید.\n    \n    در صورت مشکلاتی که با کاش وجود دارد، این فایل‌ها می‌توانند حذف شوند. نرم‌افزار تا زمانی که لازم باشد به طور خودکار آن‌ها را مجدد ایجاد خواهد کرد.\nsettings_folder_settings_open_tooltip =\n    دایرکتوری مربوطه را که تنظیمات Czkawka در آن ذخیره شده‌اند، باز کنید.\n    \n    هشدار: تغییر دستی در تنظیمات ممکن است عملکرد شما را شکسته‌سازی کند.\nsettings_folder_cache_open = پوشه کاشی را باز کنید\nsettings_folder_settings_open = پوشه تنظیمات را باز کنید\n# Compute results\ncompute_stopped_by_user = جستجو توسط کاربر متوقف شد\ncompute_found_duplicates_hash_size = یافته { $number_files } کپی برابر را در { $number_groups } گروه که { $size } را در { $time } صرف کرد\ncompute_found_duplicates_name = Duplication { $number_files } فایل در { $number_groups } گروه در { $time } پیدا شد پیدا شد\ncompute_found_empty_folders = فولدرهای خالی پیدا شد { $number_files } در زمان { $time }\ncompute_found_empty_files = فایل‌های خالی پیدا شد: { $number_files } در زمان: { $time }\ncompute_found_big_files = فایل‌های بزرگ { $number_files } تا در { $time } یافته شدند\ncompute_found_temporary_files = فایل‌های موقت { $number_files } تا { $time } پیدا شدند\ncompute_found_images = { $number_files } تصویر مرتبط در { $number_groups } گروه پیدا شد در { $time }\ncompute_found_videos = { $number_files } ویدیو مشابه در { $number_groups } گروه پیدا شد در زمان { $time }\ncompute_found_music = فایل‌های موسیقی مشابه { $number_files } را در { $number_groups } گروه و در زمان { $time } پیدا کردیم\ncompute_found_invalid_symlinks = { $number_files } لینک معلق نامعتبر در { $time } پیدا شد\ncompute_found_broken_files = فایل‌های شکسته { $number_files } را در { $time } پیدا کردم\ncompute_found_bad_extensions = فایل‌هایی با پسوند نامعتبر در { $time } شماره { $number_files } پیدا کرد\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Scanned { $file_number } file\n       *[other] Scanned { $file_number } files\n    }\nprogress_scanning_extension_of_files = رسیدن افزوده { $file_checked }/{ $all_files } فایل\nprogress_scanning_broken_files = 检讨 شده { $file_checked }/{ $all_files } فایل ({ $data_checked }/{ $all_data })\nprogress_scanning_video = همچین تoning و { $file_checked }/{ $all_files } ویدیو\nprogress_creating_video_thumbnails = تصاویر کوچک آماده شده از { $file_checked }/{ $all_files } ویدیو\nprogress_scanning_image = قلمری از { $file_checked }/{ $all_files } تصویر ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = مقایسه { $file_checked }/{ $all_files } علامت شش تصویر\nprogress_scanning_music_tags_end = مقایسه برچسب‌های { $file_checked }/{ $all_files } فایل موسیقی\nprogress_scanning_music_tags = برچسب‌های آهنگ { $file_checked }/{ $all_files } را بخوانید\nprogress_scanning_music_content_end = مقایسه от亮指纹 از { $file_checked }/{ $all_files } فایل موسیقی\nprogress_scanning_music_content = چربیگانه چک شده از { $file_checked }/{ $all_files } فایل موسیقی ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Scanned { $folder_number } folder\n       *[other] Scanned { $folder_number } folders\n    }\nprogress_scanning_size = حجم سcanشده فایل №{ $file_number }\nprogress_scanning_size_name = نام و اندازه سند مورد بررسی { $file_number }\nprogress_scanning_name = نام فایل { $file_number } را برگشت دهید\nprogress_analyzed_partial_hash = جزییات هش部门/ TümDosya ({ $data_checked }/{ $all_data }) را بررسی کرد /{ $file_checked }/{ $all_files }\nprogress_analyzed_full_hash = تحلیل کامل هش فایل‌های \"{ $file_checked }/{ $all_files }\" ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Cache پیش هاش خارج شده را بارگذاری کنید\nprogress_prehash_cache_saving = مخفف پیش‌هایش کاشینگ را ذخیره کنید\nprogress_hash_cache_loading = برخیسندهٔ حاشیه هش\nprogress_hash_cache_saving = ذهنشدن حاشیهٔ کشیده\nprogress_cache_loading = فیلتر کشی را بارگذاری می‌کنیم\nprogress_cache_saving = ذخیره کشی\nprogress_current_stage = مرحله حاضر:{ \"  \" }\nprogress_all_stages = همه مراحل:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = پیکربندی را به فایل { $name } ذخیره شد.\nsaving_loading_saving_failure = غیرهایی برای ذخیره داده‌های تنظیم‌های سازنده در فایل { $name } وجود ندارد، علت { $reason }.\nsaving_loading_reset_configuration = 구성 حاضر تهی شد.\nsaving_loading_loading_success = انگشتی که به درستی تنظیم شده تطبيقی ترجیح دارد.\nsaving_loading_failed_to_create_config_file = ایجاد فایل کonfig شکست خورد، دلیل \"{ $reason }\" برای پت { $path }\".\nsaving_loading_failed_to_read_config_file = Niet می‌توان از کنسولگی را از \"{ $path }\" بارگذاری کرد چون آن وجود ندارد یا یک فایل نیست.\nsaving_loading_failed_to_read_data_from_file = نمی‌توان از پرونده \"{ $path }\" داده‌ها را خواند، دلیل \"{ $reason }\".\n# Other\nselected_all_reference_folders = نemی‌توان به جستجو آغاز کرد، زمانی که تمام پوشه‌ها را به عنوان فرایند ارجاع تنظیم کرده‌اید\nsearching_for_data = Suche‌ن داده‌‌ها را جستجو می‌کنم، ممکن است کمی زمان ببرد، لطفا منتظر باشید...\ntext_view_messages = پیام‌ها\ntext_view_warnings = تحذیرات\ntext_view_errors = خطاها\nabout_window_motto = این برنامه رایگان است و همیشه رایگان باقی خواهد ماند.\nkrokiet_new_app = پروژه Czkawka در حالت نگهداری است که به این معنایی است که تنها خطاهای حیاتی وارد سربرگ خواهند شد و بدون نیاز به اضافه‌کردن موارد جدید. برای موارد جدید، لطفاً پروژه Krokiet جدید را بررسی کنید، که سبک‌تر و عملکرد بهتری دارد و همچنان تحت توسعه فعال است.\n# Various dialog\ndialogs_ask_next_time = مرتبه بعد سپس بپرس\nsymlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\ndelete_title_dialog = تایید حذف\ndelete_question_label = آیا از حذف فایل‌ها مطمئن هستید؟\ndelete_all_files_in_group_title = تأیید حذف تمام فایل‌های در گروه\ndelete_all_files_in_group_label1 = در برخی گروه‌ها تمام رکوردها انتخاب می‌شوند.\ndelete_all_files_in_group_label2 = فرمایید که حذف آن‌ها را مطمئn هستید؟\ndelete_items_label = { $items } فایل‌هایی خواهند پاک شد.\ndelete_items_groups_label = { $items } فایل از { $groups } گروه خواهند شوی.\nhardlink_failed = به ترکیب مجدد { $name } به { $target } امکان پذیر نشد، دلیل { $reason }\nhard_sym_invalid_selection_title_dialog = بازخورد نامعتبر با گروه‌هایی از جمله\nhard_sym_invalid_selection_label_1 = در برخی گروه‌ها تنها یک رکورد انتخاب شده است و آن مورد忽略。.\nhard_sym_invalid_selection_label_2 = برای توانا به صورت سخت/هم لینک این فایل‌ها، حداقل دو نتیجه در گروه باید انتخاب شوند.\nhard_sym_invalid_selection_label_3 = اولین در گروه به عنوان اصلی شناخته می‌شود و تغییر نمی‌کند اما دوم و بعدی عرضه شده‌اند.\nhard_sym_link_title_dialog = تأیید لینک\nhard_sym_link_label = آیا مطمئن هستید که می‌خواهید این فایل‌ها را پیوست؟\nmove_folder_failed = در حرکت دایرکتوری { $name } موفق نشد، دلیل \"{ $reason }\"\nmove_file_failed = خطا در نقلovanدن فایل { $name }، دلیل \"{ $reason }\"\nmove_files_title_dialog = تیم پوشه‌ای را که می‌خواهید فایل‌های تکثیر شده را به آن منتقل نمایید\nmove_files_choose_more_than_1_path = فقط یک مسیر انتخاب شده باشد تا می‌توانند فایل‌های خود را کپی کنند، انتخاب شده { $path_number }.\nmove_stats = مناسب حرکت { $num_files }/{ $all_files } موارد\nsave_results_to_file = نتایج ذخیره شده به همراه فایل‌های txt و json در مسیر دایرکتوری \"{ $name }\" هستند.\nsearch_not_choosing_any_music = خطا: شما باید حداقل یک قيدچيكو با رنگ‌هاي جستجو مuzic را انتخاب کنید.\nsearch_not_choosing_any_broken_files = خطا: شما باید حداقل یک گزینه با نوع خرابی فایل‌های بریده انتخاب کنید.\ninclude_folders_dialog_title = پوشه‌هایی شامل این باید باشند\nexclude_folders_dialog_title = دسته‌ها را که حذف شود نگهداری کنید\ninclude_manually_directories_dialog_title = دایرکتوری را به صورت دستی اضافه کنید\ncache_properly_cleared = برای مطمئن شدن، کش خروجی را به درستی پاک کرده‌اید\ncache_clear_duplicates_title = حذف کپی‌های تکراری از کش\ncache_clear_similar_images_title = برکناره‌گیری کشی تصاویر مشابه\ncache_clear_similar_videos_title = پاکسازی کش خارجی موارد مشابه Videوهای سفارشی\ncache_clear_message_label_1 = آیا می‌خواهید پشته داده‌های سرگردان را پاک کنید؟\ncache_clear_message_label_2 = این عملیات تمام دختران کش جاروی که به فایل‌های نامعتبر اشاره می‌کنند را حذف خواهد کرد.\ncache_clear_message_label_3 = این ممکن است به کندی خواندن/درج به حافظه کمی سریع‌تر شود.\ncache_clear_message_label_4 = هشدار: عملیات مذکور همه داده‌های کش پاشیده از درایв‌های خارجی دور نرم خواهد کرد. بنابراین هر هش باید دوباره تولید شود.\n# Show preview\npreview_image_resize_failure = حداکثر سایز تصویر { $name } را تنظیم نشد.\npreview_image_opening_failure = تصویر { $name } را باز کردن شکسته است، причина { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = گروه { $current_group }/{ $all_groups } ({ $images_in_group } تصویر)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/fr/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Paramètres\nwindow_main_title = Czkawka (Hoquet)\nwindow_progress_title = Analyse en cours\nwindow_compare_images = Comparer les images\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Fermer\n# Krokiet info dialog\nkrokiet_info_title = Présentation de Krokiet - Nouvelle version de Czkawka\nkrokiet_info_message =\n    Krokiet est la nouvelle version améliorée, plus rapide et plus fiable de l'interface graphique GTK Czkawka !\n    \n    Il est plus facile à exécuter et plus résistant aux changements du système, car il dépend uniquement des bibliothèques de base disponibles par défaut sur la plupart des systèmes.\n    \n    Krokiet apporte également des fonctionnalités qui font défaut à Czkawka, notamment des vignettes en mode de comparaison vidéo, un nettoyeur d'EXIF, la progression du déplacement/copie/suppression de fichiers et des options de tri étendues.\n    \n    Essayez-le et constatez la différence !\n    \n    Czkawka continuera à bénéficier de corrections de bugs et de mises à jour mineures de ma part, mais toutes les nouvelles fonctionnalités seront développées exclusivement dans Krokiet, et tout le monde est libre de\n     contribuer à l'ajout de nouvelles fonctionnalités, de modes manquants ou à l'extension de Czkawka.\n    \n    PS : ce message ne devrait apparaître qu'une seule fois. S'il s'affiche à nouveau, définissez la variable d'environnement CZKAWKA_DONT_ANNOY_ME à une valeur non vide.\n# Main window\nmusic_title_checkbox = Titre de la page\nmusic_artist_checkbox = Artiste\nmusic_year_checkbox = Année\nmusic_bitrate_checkbox = Débit binaire\nmusic_genre_checkbox = Genre\nmusic_length_checkbox = Longueur\nmusic_comparison_checkbox = Comparaison approximative\nmusic_checking_by_tags = étiquettes\nmusic_checking_by_content = Contenu\nsame_music_seconds_label = Durée minimale de seconde de fragment\nsame_music_similarity_label = Différence maximale\nmusic_compare_only_in_title_group = Comparer au sein des groupes de titres similaires\nmusic_compare_only_in_title_group_tooltip =\n    Lorsque cette option est activée, les fichiers sont regroupés par titre, puis comparés l'un à l'autre.\n    \n    Pour 10000 fichiers, au lieu de près de 100 millions de comparaisons en général, il y aura environ 20000 comparaisons.\nsame_music_tooltip =\n    La recherche de fichiers musicaux aux contenus similaires peut être configurée en définissant :\n    \n    - La durée minimale d'un fragment pour que des fichiers musicaux soient identifiés comme similaires\n    - La différence maximale entre deux fragments testés\n    \n    La clé pour arriver à de bons résultats est de trouver des combinaisons raisonnables de ces paramètres.\n    \n    Fixer le temps minimum à 5 secondes et la différence maximale à 1.0, cherchera des fragments presque identiques dans les fichiers.\n    Un temps de 20 secondes et une différence maximale de 6.0 fonctionne bien pour trouver des remixes/versions live, etc.\n    \n    Par défaut, chaque fichier musical est comparé à tous les autres et cela peut prendre beaucoup de temps lors du test de plusieurs fichier. Il est donc généralement préférable d'utiliser des dossiers de référence et de spécifier quels fichiers doivent être comparés les uns avec les autres (avec la même quantité de fichiers, la comparaison des empreintes sera au moins 4x plus rapide que sans dossier de référence).\nmusic_comparison_checkbox_tooltip =\n    La recherche des fichiers de musique similaires est faite à l’aide d'intelligence artificielle qui utilise l'apprentissage machine pour supprimer les parenthèses d’une phrase. Par exemple, avec cette option activée les fichiers en question seront considérés comme des doublons :\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Sensible à la casse\nduplicate_case_sensitive_name_tooltip =\n    Quand activé, groupe les enregistrements uniquement quand ils ont exactement le même nom, par exemple Żołd <-> Żołd\n    \n    Désactiver cette option va regrouper les noms sans se préocupper de la casse, par exemple żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Taille et nom\nduplicate_mode_name_combo_box = Nom\nduplicate_mode_size_combo_box = Taille\nduplicate_mode_hash_combo_box = Hachage\nduplicate_hash_type_tooltip =\n    Czkawka offre 3 types de hachages :\n    \n    Blake3 - fonction de hachage cryptographique. Il est utilisé comme algorithme de hachage par défaut car très rapide.\n    \n    CRC32 - fonction de hachage simple qui devrait être plus rapide que Blake3. Peut, très rarement, provoquer des collisions.\n    \n    XXH3 - très similaire en terme de performances et de qualité de hachage à Blake3 mais non cryptographique. De ce fait ils peuvent facilement être changés l'un pour l'autre.\nduplicate_check_method_tooltip =\n    Pour l'instant, Czkawka offre trois types de méthode pour trouver des doublons par :\n    \n    Nom - Trouve des fichiers qui ont le même nom.\n    \n    Taille - Trouve des fichiers qui ont la même taille.\n    \n    Hachage - Trouve des fichiers qui ont le même contenu. Ce mode permet de hacher le fichier puis de comparer ensuite le hash pour trouver les doublons. Ce mode est le moyen le plus sûr de trouver les doublons. L'application utilisant massivement le cache, les analyses suivantes des mêmes données devraient être beaucoup plus rapides que la première.\nimage_hash_size_tooltip =\n    Chaque image vérifiée produit un hachage spécial qui peut être comparé les uns aux autres, et une petite différence entre elles signifie que ces images sont similaires.\n    \n    La taille du hachage 8 est assez bonne pour trouver des images qui ne sont qu'un peu similaires à l'original. Avec un plus grand ensemble d'images (>1000), cela produira une grande quantité de faux positifs, donc je recommande d'utiliser une plus grande taille de hachage dans ce cas.\n    \n    16 est la taille par défaut du hachage, ce qui est un bon compromis entre trouver même un peu des images similaires et n'avoir qu'une petite quantité de collisions de hachage.\n    \n    32 et 64 hachages ne trouvent que des images très similaires, mais devraient avoir presque pas de faux positifs (peut-être sauf certaines images avec canal alpha).\nimage_resize_filter_tooltip =\n    Pour calculer le hachage de l'image, la bibliothèque doit d'abord la redimensionner.\n    \n    En fonction de l'algorithme choisi, l'image résultante utilisée pour calculer le hachage pourra sembler un peu différente.\n    \n    L'algorithme le plus rapide à utiliser, mais aussi celui qui donne les pires résultats, est PlusProche. Il est activé par défaut, car avec une taille de hachage d'une qualité inférieure à 16x16, cela ne sera que peu visible.\n    \n    Avec une taille de hachage de 8x8, il est recommandé d'utiliser un algorithme différent de PlusProche pour obtenir de meilleurs groupes d'images.\nimage_hash_alg_tooltip =\n    Les utilisateurs peuvent choisir parmi de nombreux algorithmes pour calculer le hash.\n    \n    Chacun a des points forts et des points faibles et donnera parfois des résultats meilleurs et parfois pires pour des images différentes.\n    \n    Par conséquent, des tests manuels sont requis pour déterminer celui qui donnera le meileur résultat pour vous.\nbig_files_mode_combobox_tooltip = Permet de rechercher les fichiers les plus petits ou les plus grands\nbig_files_mode_label = Fichiers cochés\nbig_files_mode_smallest_combo_box = Le plus petit\nbig_files_mode_biggest_combo_box = Le plus grand\nmain_notebook_duplicates = Fichiers en double\nmain_notebook_empty_directories = Dossiers vides\nmain_notebook_big_files = Gros fichiers\nmain_notebook_empty_files = Fichiers vides\nmain_notebook_temporary = Fichiers temporaires\nmain_notebook_similar_images = Images similaires\nmain_notebook_similar_videos = Vidéos similaires\nmain_notebook_same_music = Doublons de musique\nmain_notebook_symlinks = Liens symboliques invalides\nmain_notebook_broken_files = Fichiers cassés\nmain_notebook_bad_extensions = Mauvaises extensions\nmain_tree_view_column_file_name = Nom du fichier\nmain_tree_view_column_folder_name = Nom du dossier\nmain_tree_view_column_path = Chemin d'accès\nmain_tree_view_column_modification = Date de modification\nmain_tree_view_column_size = Taille\nmain_tree_view_column_similarity = Similitude\nmain_tree_view_column_dimensions = Dimensions\nmain_tree_view_column_title = Titre\nmain_tree_view_column_artist = Artiste\nmain_tree_view_column_year = Année\nmain_tree_view_column_bitrate = Débit binaire\nmain_tree_view_column_length = Longueur\nmain_tree_view_column_genre = Genre\nmain_tree_view_column_symlink_file_name = Nom du lien symbolique\nmain_tree_view_column_symlink_folder = Dossier du lien symbolique\nmain_tree_view_column_destination_path = Chemin de destination\nmain_tree_view_column_type_of_error = Type d'erreur\nmain_tree_view_column_current_extension = Extension actuelle\nmain_tree_view_column_proper_extensions = Extension correcte\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codec\nmain_label_check_method = Méthode de vérification\nmain_label_hash_type = Type de hachage\nmain_label_hash_size = Taille du hachage\nmain_label_size_bytes = Taille (octets)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Nombre de fichiers affichés\nmain_label_resize_algorithm = Algorithme de redimensionnement\nmain_label_similarity = Similarité{ \" \" }\nmain_check_box_broken_files_audio = Audio\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Archiver\nmain_check_box_broken_files_image = Image\nmain_check_box_broken_files_video = Vidéo\nmain_check_box_broken_files_video_tooltip = Utilise ffmpeg/ffprobe pour valider les fichiers vidéo. Très lent et peut détecter des erreurs insignifiantes même si le fichier est bien lu.\ncheck_button_general_same_size = Ignorer la même taille\ncheck_button_general_same_size_tooltip = Ignorer les fichiers avec la même taille dans les résultats - généralement ce sont des doublons 1:1\nmain_label_size_bytes_tooltip = Taille des fichiers qui seront utilisés lors de l'analyse\n# Upper window\nupper_tree_view_included_folder_column_title = Dossiers dans lesquels chercher\nupper_tree_view_included_reference_column_title = Dossiers de référence\nupper_recursive_button = Récursif\nupper_recursive_button_tooltip = Si sélectionné, rechercher également les fichiers qui ne sont pas placés directement dans les dossiers choisis.\nupper_manual_add_included_button = Ajout manuel\nupper_add_included_button = Ajouter\nupper_remove_included_button = Retirer\nupper_manual_add_excluded_button = Ajout manuel\nupper_add_excluded_button = Ajouter\nupper_remove_excluded_button = Retirer\nupper_manual_add_included_button_tooltip =\n    Ajouter manuellement le nom du répertoire à rechercher.\n    \n    Pour ajouter plusieurs chemins à la fois, séparez-les avec « ; »\n    \n    « /home/roman;/home/rozkaz » ajoutera deux répertoires « /home/roman » et « /home/rozkaz »\nupper_add_included_button_tooltip = Ajouter un nouveau répertoire à la recherche.\nupper_remove_included_button_tooltip = Supprimer le répertoire de la recherche.\nupper_manual_add_excluded_button_tooltip =\n    Ajouter manuellement un nom de répertoire exclu.\n    \n    Pour ajouter plusieurs chemins à la fois, séparez-les ave « ; »\n    \n    « /home/roman;/home/krokiet » ajoutera deux répertoires « /home/roman » et « /home/keokiet »\nupper_add_excluded_button_tooltip = Ajouter un répertoire à exclure de la recherche.\nupper_remove_excluded_button_tooltip = Retirer le répertoire de la liste de ceux exclus.\nupper_notebook_items_configuration = Configuration des éléments\nupper_notebook_excluded_directories = Chemins exclus\nupper_notebook_included_directories = Chemins inclus\nupper_allowed_extensions_tooltip =\n    Les extensions autorisées doivent être séparées par des virgules (toutes sont disponibles par défaut).\n    \n    Les Macros suivantes, qui ajoutent plusieurs extensions à la fois, sont également disponibles : IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Exemple d'utilisation : « .exe, IMAGE, VIDEO, .rar, 7z » - signifie que les fichiers images (par exemple jpg, png), des vidéos (par exemple avi, mp4), exe, rar et 7z seront scannés.\nupper_excluded_extensions_tooltip =\n    Liste des fichiers désactivés qui seront ignorés lors de l'analyse.\n    \n    Lorsque vous utilisez des extensions autorisées et désactivées, celle-ci a une priorité plus élevée, donc le fichier ne sera pas vérifié.\nupper_excluded_items_tooltip =\n    Les éléments exclus doivent contenir le caractère joker « * » et être séparés par des virgules.\n    Ceci est plus lent que les répertoires exclus, donc à utiliser avec prudence.\nupper_excluded_items = Éléments exclus :\nupper_allowed_extensions = Extensions autorisées :\nupper_excluded_extensions = Extensions désactivées :\n# Popovers\npopover_select_all = Tout sélectionner\npopover_unselect_all = Tout désélectionner\npopover_reverse = Inverser la sélection\npopover_select_all_except_shortest_path = Tout sélectionner sauf le chemin le plus court\npopover_select_all_except_longest_path = Tout sélectionner sauf le chemin le plus long\npopover_select_all_except_oldest = Tout sélectionner sauf le plus ancien\npopover_select_all_except_newest = Tout sélectionner sauf le plus récent\npopover_select_one_oldest = Sélectionner un élément plus ancien\npopover_select_one_newest = Sélectionner un élément récent\npopover_select_custom = Sélection personnalisée\npopover_unselect_custom = Annuler la sélection personnalisée\npopover_select_all_images_except_biggest = Tout sélectionner sauf le plus gros\npopover_select_all_images_except_smallest = Tout sélectionner sauf le plus petit\npopover_custom_path_check_button_entry_tooltip =\n    Sélectionner les enregistrements par chemin.\n    \n    Exemple d'utilisation :\n    « /home/pimpek/rzecz.txt » peut être trouvé avec « /home/pim* »\npopover_custom_name_check_button_entry_tooltip =\n    Sélectionner les enregistrements par nom de fichier.\n    \n    Exemple d'utilisation :\n    « /usr/ping/pong.txt » peut être trouvé avec « *ong* »\npopover_custom_regex_check_button_entry_tooltip =\n    Sélectionner les enregistrements par Regex spécifié.\n    \n    Dans ce mode, le texte recherché est le Chemin avec le Nom.\n    \n    Exemple d'utilisation:\n    « /usr/bin/ziemniak.txt » peut être trouvé avec « /ziem[a-z]+ »\n    \n    Cela utilise l'implémentation par défaut de Rust regex : https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Active la détection sensible à la casse.\n    \n    Si désactivé, « /home/* » trouve « /HoMe/roman » et « /home/roman ».\npopover_custom_not_all_check_button_tooltip =\n    Empêche la sélection de tous les enregistrements dans le groupe.\n    \n    Ceci est activé par défaut car, dans la plupart des cas, vous ne voulez pas supprimer à la fois les fichiers originaux et les doublons mais souhaitez laisser au moins un fichier.\n    \n    AVERTISSEMENT : ce réglage ne fonctionne pas si vous avez déjà sélectionné manuellement tous les résultats dans un groupe.\npopover_custom_regex_path_label = Chemin d'accès\npopover_custom_regex_name_label = Nom\npopover_custom_regex_regex_label = Chemin d'accès Regex + Nom\npopover_custom_case_sensitive_check_button = Sensible à la casse\npopover_custom_all_in_group_label = Ne pas sélectionner tous les enregistrements du groupe\npopover_custom_mode_unselect = Désélectionner la personnalisation\npopover_custom_mode_select = Sélectionner la personnalisation\npopover_sort_file_name = Nom du fichier\npopover_sort_folder_name = Nom du dossier\npopover_sort_full_name = Nom complet\npopover_sort_size = Taille\npopover_sort_selection = Sélection\npopover_invalid_regex = La regex est invalide\npopover_valid_regex = La regex est valide\n# Bottom buttons\nbottom_search_button = Chercher\nbottom_select_button = Sélectionner\nbottom_delete_button = Supprimer\nbottom_save_button = Enregistrer\nbottom_symlink_button = Lien symbolique\nbottom_hardlink_button = Lien dur\nbottom_move_button = Déplacer\nbottom_sort_button = Trier\nbottom_compare_button = Comparer\nbottom_search_button_tooltip = Lancer la recherche\nbottom_select_button_tooltip = Sélectionnez les enregistrements. Seuls les fichiers/dossiers sélectionnés pourront être traités plus tard.\nbottom_delete_button_tooltip = Supprimer les fichiers/dossiers sélectionnés.\nbottom_save_button_tooltip = Enregistrer les données de la recherche dans un fichier\nbottom_symlink_button_tooltip =\n    Créer des liens symboliques.\n    Ne fonctionne que si au moins deux résultats dans un groupe sont sélectionnés.\n    Le premier reste inchangé, tous les suivants sont transformés en lien symbolique vers ce premier résultat.\nbottom_hardlink_button_tooltip =\n    Créer des liens durs.\n    Ne fonctionne que si au moins deux résultats dans un groupe sont sélectionnés.\n    Le premier reste inchangé, tous les suivants sont transformés en lien dur vers ce premier résultat.\nbottom_hardlink_button_not_available_tooltip =\n    Créer des liens durs.\n    Le bouton est désactivé car des liens durs ne peuvent être créés.\n    Les liens durs ne fonctionnent qu’avec les privilèges administrateur sous Windows, assurez-vous d'éxécuter l’application en tant qu’administrateur.\n    Si l’application fonctionne déjà avec ces privilèges, vérifiez les signalements de bogues similaires sur GitHub.\nbottom_move_button_tooltip =\n    Déplace les fichiers vers le répertoire choisi.\n    Ceci copie tous les fichiers dans le répertoire cible sans préserver l'arborescence du répertoire source.\n    Si on tente de déplacer deux fichiers avec le même nom vers le dossier, le second échouera et un message d'erreur s'affichera.\nbottom_sort_button_tooltip = Trie les fichiers/dossiers selon la méthode sélectionnée.\nbottom_compare_button_tooltip = Comparer les images dans le groupe.\nbottom_show_errors_tooltip = Afficher/Masquer le panneau de texte du bas.\nbottom_show_upper_notebook_tooltip = Afficher/Masquer le panneau supérieur du bloc-notes.\n# Progress Window\nprogress_stop_button = Arrêter\nprogress_stop_additional_message = Arrêt demandé\n# About Window\nabout_repository_button_tooltip = Lien vers la page du dépôt avec le code source.\nabout_donation_button_tooltip = Lien vers la page des dons.\nabout_instruction_button_tooltip = Lien vers la page d'instruction.\nabout_translation_button_tooltip = Lien vers la page Crowdin avec les traductions de lapplication. Le polonais et l'anglais sont officiellement pris en charge.\nabout_repository_button = Dépôt\nabout_donation_button = Faire un don\nabout_instruction_button = Instructions\nabout_translation_button = Traduction\n# Header\nheader_setting_button_tooltip = Ouvre la fenêtre des paramètres.\nheader_about_button_tooltip = Ouvre la boîte de dialogue contenant les informations sur l'application.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Nombre de threads utilisés\nsettings_number_of_threads_tooltip = Nombre de threads utilisés. « 0 » signifie que tous les threads disponibles seront utilisés.\nsettings_use_rust_preview = Utiliser des bibliothèques externes à la place gtk pour charger les aperçus\nsettings_use_rust_preview_tooltip =\n    L'utilisation des prévisualisations gtk sera parfois plus rapide et gèrera plus de formats, mais cela pourrait aussi être l'inverse.\n    \n    Si vous avez des problèmes de chargement des prévisualisations, vous pouvez essayer de modifier ce paramètre.\n    \n    Pour les systèmes non-Linux, il est recommandé d'utiliser cette option, car gtk-pixbuf n'y est pas toujours disponible, aussi la désactivation de cette option ne chargera pas les prévisualisations pour certaines images.\nsettings_label_restart = Vous devez redémarrer l’application pour appliquer les réglages !\nsettings_ignore_other_filesystems = Ignorer les autres systèmes de fichiers (Linux uniquement)\nsettings_ignore_other_filesystems_tooltip =\n    ignore les fichiers qui ne sont pas dans le même système de fichiers que les répertoires recherchés.\n    \n    Fonctionne de la même manière que l'option « -xdev » de la commande « find » sous Linux\nsettings_save_at_exit_button_tooltip = Enregistrer la configuration dans un fichier à la fermeture de l'application.\nsettings_load_at_start_button_tooltip =\n    Charger la configuration à partir du fichier à l'ouverture de l'application.\n    \n    Si désactivé, les paramètres par défaut seront utilisés.\nsettings_confirm_deletion_button_tooltip = Afficher une boîte de dialogue de confirmation lorsque vous cliquez sur le bouton Supprimer.\nsettings_confirm_link_button_tooltip = Afficher une boîte de dialogue de confirmation lorsque vous cliquez sur le bouton « hard/symlink ».\nsettings_confirm_group_deletion_button_tooltip = Afficher une boîte de dialogue d'avertissement lorsque vous essayez de supprimer tous les enregistrements du groupe.\nsettings_show_text_view_button_tooltip = Afficher le panneau de texte en bas de l'interface utilisateur.\nsettings_use_cache_button_tooltip = Utiliser le cache de fichiers.\nsettings_save_also_as_json_button_tooltip = Enregistrer le cache au format JSON (lisible par un humain). Il est possible de modifier son contenu. Le contenu de ce fichier sera lu automatiquement par l'application si le cache au format binaire (extension .bin) est manquant.\nsettings_use_trash_button_tooltip = Déplace les fichiers vers la corbeille au lieu de les supprimer définitivement.\nsettings_language_label_tooltip = Langue de l'interface utilisateur.\nsettings_save_at_exit_button = Enregistrer la configuration à la fermeture de l'application\nsettings_load_at_start_button = Charger la configuration à l'ouverture de l'application\nsettings_confirm_deletion_button = Afficher une boîte de dialogue de confirmation lors de la suppression de fichiers\nsettings_confirm_link_button = Afficher une boîte de dialogue de confirmation lorsque des liens en dur ou symboliques vers des fichiers sont créés\nsettings_confirm_group_deletion_button = Afficher une boîte de dialogue de confirmation lors de la suppression de tous les fichiers d'un groupe\nsettings_show_text_view_button = Afficher le panneau de texte du bas\nsettings_use_cache_button = Utiliser le cache\nsettings_save_also_as_json_button = Également enregistrer le cache en tant que fichier JSON\nsettings_use_trash_button = Déplacer les fichiers supprimés vers la corbeille\nsettings_language_label = Langue\nsettings_multiple_delete_outdated_cache_checkbutton = Supprimer automatiquement les entrées de cache obsolètes\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Supprimer du cache les résultats obsolètes pointant vers des fichiers inexistants.\n    \n    Lorsque cette option est activée, l'application s'assure lors du chargement des enregistrements que tous pointent vers des fichiers valides (les fichiers cassés sont ignorés).\n    \n    Désactiver cette option facilitera l'analyse de fichiers sur des disques externes: les entrées de cache les concernant ne seront pas purgées lors de la prochaine analyse.\n    \n    Il est conseillé de d'activer cette option quand des centaines de milliers d'enregistrements sont dans le cache. Ceci permettra d'accélérer le chargement et la sauvegarde du cache au démarrage et à la fin de l'analyse.\nsettings_notebook_general = Généraux\nsettings_notebook_duplicates = Doublons\nsettings_notebook_images = Images similaires\nsettings_notebook_videos = Vidéo similaire\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Affiche l'aperçu à droite (lors de la sélection d'un fichier image).\nsettings_multiple_image_preview_checkbutton = Afficher l'aperçu de l'image\nsettings_multiple_clear_cache_button_tooltip =\n    Vider manuellement le cache des entrées obsolètes.\n    À utiliser uniquement si le nettoyage automatique a été désactivé.\nsettings_multiple_clear_cache_button = Supprimer les résultats périmés du cache.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Masque tous les fichiers, sauf un, si tous pointent vers les mêmes données (avec lien en dur).\n    \n    Exemple : soient sur le disque sept fichiers reliés à des données spécifiques et un fichier différent avec les mêmes données mais un inode différent ; dans le module de recherche des doublons seuls un fichier unique et un fichier provenant des liens en dur seront affichés.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Définit la taille minimale du fichier qui sera mis en cache.\n    \n    Choisir une valeur plus petite générera plus d'enregistrements. Cela accélérera la recherche, mais ralentira le chargement/l'enregistrement du cache.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Active la mise en cache du prehash (un hachage calculé à partir d'une petite partie du fichier) qui permet un rejet plus rapide des résultats non dupliqués.\n    \n    Il est désactivé par défaut car il peut causer des ralentissements dans certaines situations.\n    \n    Il est fortement recommandé de l'utiliser lors de la numérisation de centaines de milliers ou de millions de fichiers, car il peut accélérer la recherche de manière géométrique.\nsettings_duplicates_prehash_minimal_entry_tooltip = Taille minimale de l'entrée en cache.\nsettings_duplicates_hide_hard_link_button = Masquer les liens durs\nsettings_duplicates_prehash_checkbutton = Utiliser le cache de prehash\nsettings_duplicates_minimal_size_cache_label = Taille minimale des fichiers (en octets) enregistrés dans le cache\nsettings_duplicates_minimal_size_cache_prehash_label = Taille minimale des fichiers (en octets) enregistrés dans le cache de préhachage\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Enregistrez les paramètres de configuration actuels dans un fichier.\nsettings_loading_button_tooltip = Charger les paramètres à partir d'un fichier pour remplacer la configuration actuelle.\nsettings_reset_button_tooltip = Réinitialiser la configuration actuelle pour revenir à celle par défaut.\nsettings_saving_button = Enregistrer la configuration\nsettings_loading_button = Charger la configuration\nsettings_reset_button = Réinitialiser la configuration\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Ouvre le dossier où sont stockés les fichiers « .txt » de cache.\n    \n    La modification des fichiers de cache peut provoquer l'affichage de résultats invalides. Cependant, la modification du chemin peut faire gagner du temps lorsque une grande quantité de fichiers est déplacée vers un autre emplacement.\n    \n    Vous pouvez copier ces fichiers entre ordinateurs pour gagner du temps sur une nouvelle analyse de fichiers (à condition, bien sûr, qu'ils aient une structure de répertoire similaire).\n    \n    En cas de problèmes avec le cache, ces fichiers peuvent être supprimés. L'application les régénèrera automatiquement.\nsettings_folder_settings_open_tooltip =\n    Ouvre le dossier où la configuration de Czkawka est stockée.\n    \n    AVERTISSEMENT : modifier manuellement la configuration peut endommager votre workflow.\nsettings_folder_cache_open = Ouvrir le dossier de cache\nsettings_folder_settings_open = Ouvrir le dossier des paramètres\n# Compute results\ncompute_stopped_by_user = La recherche a été interrompue par l'utilisateur\ncompute_found_duplicates_hash_size = { $number_files } doublons trouvés dans les groupes { $number_groups } qui ont pris { $size } en { $time }\ncompute_found_duplicates_name = { $number_files } doublons trouvés dans les groupes { $number_groups } dans { $time }\ncompute_found_empty_folders = Trouvé les dossiers { $number_files } vides dans { $time }\ncompute_found_empty_files = Fichier { $number_files } vide trouvé dans { $time }\ncompute_found_big_files = Fichiers volumineux { $number_files } trouvés dans { $time }\ncompute_found_temporary_files = Fichiers temporaires { $number_files } trouvés dans { $time }\ncompute_found_images = { $number_files } images similaires trouvées dans les groupes { $number_groups } en { $time }\ncompute_found_videos = Trouvé { $number_files } vidéos similaires dans les groupes { $number_groups } dans { $time }\ncompute_found_music = Trouvé { $number_files } fichiers de musique similaires dans les groupes { $number_groups } dans { $time }\ncompute_found_invalid_symlinks = Liens symboliques { $number_files } non valides dans { $time }\ncompute_found_broken_files = Trouvé { $number_files } fichiers cassés dans { $time }\ncompute_found_bad_extensions = Fichiers { $number_files } trouvés avec des extensions invalides dans { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Fichier { $file_number }\n       *[other] Fichiers { $file_number }\n    } Scannés\nprogress_scanning_extension_of_files = Extension du fichier { $file_checked }/{ $all_files } vérifiée\nprogress_scanning_broken_files = Fichier { $file_checked }/{ $all_files } vérifié ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Haché de la vidéo { $file_checked }/{ $all_files }\nprogress_creating_video_thumbnails = Miniatures de la vidéo { $file_checked }/{ $all_files } créées\nprogress_scanning_image = Haché de l'image { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Hachage d'image { $file_checked }/{ $all_files } comparé\nprogress_scanning_music_tags_end = Tags comparés au fichier de musique { $file_checked }/{ $all_files }\nprogress_scanning_music_tags = Lire les tags du fichier de musique { $file_checked }/{ $all_files }\nprogress_scanning_music_content_end = Empreinte par rapport au fichier de musique { $file_checked }/{ $all_files }\nprogress_scanning_music_content = Empreinte calculée du fichier de musique { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Répertoire { $folder_number }\n       *[other] Dossiers { $folder_number }\n    } numérisés\nprogress_scanning_size = Taille numérisée du fichier { $file_number }\nprogress_scanning_size_name = Nom numérisé et taille du fichier { $file_number }\nprogress_scanning_name = Nom numérisé du fichier { $file_number }\nprogress_analyzed_partial_hash = Hash partiel analysé des fichiers { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Hash complet analysé des fichiers { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Chargement du cache du prehash\nprogress_prehash_cache_saving = Sauvegarde du cache du prehash\nprogress_hash_cache_loading = Chargement du cache de hachage\nprogress_hash_cache_saving = Sauvegarde du cache de hachage\nprogress_cache_loading = Chargement de la cache\nprogress_cache_saving = Sauvegarde du cache\nprogress_current_stage = Étape actuelle :{ \"  \" }\nprogress_all_stages = Toutes les étapes :{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Configuration enregistrée dans le fichier { $name }.\nsaving_loading_saving_failure = Impossible d'enregistrer les données de configuration dans le fichier { $name }, raison { $reason }.\nsaving_loading_reset_configuration = La configuration actuelle a été effacée.\nsaving_loading_loading_success = Configuration de l'application correctement chargée.\nsaving_loading_failed_to_create_config_file = Impossible de créer le fichier de configuration \"{ $path }\". Raison : \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Impossible de charger la configuration depuis \"{ $path }\" car elle n'existe pas ou n'est pas un fichier.\nsaving_loading_failed_to_read_data_from_file = Impossible de lire les données du fichier \"{ $path }\". Raison : \"{ $reason }\".\n# Other\nselected_all_reference_folders = Impossible de lancer la recherche quand tous les répertoires sont définis comme des répertoires de référence\nsearching_for_data = Recherche de données. Cela peut prendre un certain temps, veuillez patienter….\ntext_view_messages = MESSAGESS\ntext_view_warnings = AVERTISSEMENTS\ntext_view_errors = ERREURS\nabout_window_motto = Ce programme peut être utilisée gratuitement et le sera toujours.\nkrokiet_new_app = Czkawka est en mode maintenance, ce qui signifie que seuls les bogues critiques seront corrigés et qu'aucune nouvelle fonctionnalité ne sera ajoutée. Pour de nouvelles fonctionnalités, veuillez consulter la nouvelle application Krokiet, qui est plus stable et plus performante et est toujours en cours de développement actif.\n# Various dialog\ndialogs_ask_next_time = Demander la prochaine fois\nsymlink_failed = Échec de la liaison symbolique { $name } à { $target }, raison { $reason }\ndelete_title_dialog = Confirmation de la suppression\ndelete_question_label = Êtes-vous sûr de vouloir supprimer les fichiers ?\ndelete_all_files_in_group_title = Confirmation de la suppression de tous les fichiers du groupe\ndelete_all_files_in_group_label1 = L'ensemble des enregistrements est sélectionné dans certains groupes.\ndelete_all_files_in_group_label2 = Êtes-vous sûr de vouloir les supprimer ?\ndelete_items_label = { $items } fichiers seront supprimés.\ndelete_items_groups_label = { $items } fichiers de { $groups } groupes seront supprimés.\nhardlink_failed = Impossible de relier durement { $name } à { $target }, raison { $reason }\nhard_sym_invalid_selection_title_dialog = Sélection invalide avec certains groupes\nhard_sym_invalid_selection_label_1 = Un seul enregistrement est sélectionné dans certains groupes et il sera ignoré.\nhard_sym_invalid_selection_label_2 = Au moins deux résultats au sein du groupe doivent être sélectionnés pour les relier ces par un lien en dur ou symbolique.\nhard_sym_invalid_selection_label_3 = Le premier dans le groupe est reconnu comme original et n'est pas modifié mais les suivants le seront.\nhard_sym_link_title_dialog = Confirmation du lien\nhard_sym_link_label = Êtes-vous sûr de vouloir relier ces fichiers ?\nmove_folder_failed = Impossible de déplacer le dossier { $name }. Raison : { $reason }\nmove_file_failed = Impossible de déplacer le fichier { $name }. Raison : { $reason }\nmove_files_title_dialog = Choisissez le dossier dans lequel vous voulez déplacer les fichiers dupliqués\nmove_files_choose_more_than_1_path = Un seul chemin peut être sélectionné pour pouvoir copier leurs fichiers dupliqués. { $path_number } est sélectionné.\nmove_stats = Éléments { $num_files }/{ $all_files } correctement déplacés\nsave_results_to_file = Résultats enregistrés dans les fichiers txt et json dans le dossier \"{ $name }\".\nsearch_not_choosing_any_music = ERREUR : vous devez sélectionner au moins une case à cocher parmi les types de recherche de musique.\nsearch_not_choosing_any_broken_files = ERREUR : vous devez sélectionner au moins une case à cocher parmi les types de fichiers cassés.\ninclude_folders_dialog_title = Dossiers à inclure\nexclude_folders_dialog_title = Dossiers à exclure\ninclude_manually_directories_dialog_title = Ajouter un répertoire manuellement\ncache_properly_cleared = Cache correctement vidé\ncache_clear_duplicates_title = Purge du cache des doublons\ncache_clear_similar_images_title = Purge du cache des images similaires\ncache_clear_similar_videos_title = Purge du cache des vidéos similaires\ncache_clear_message_label_1 = Voulez-vous vider le cache des entrées obsolètes ?\ncache_clear_message_label_2 = Cette opération supprimera toutes les entrées du cache qui pointent vers des fichiers invalides.\ncache_clear_message_label_3 = Cela peut légèrement accélérer le chargement et la sauvegarde dans le cache.\ncache_clear_message_label_4 = AVERTISSEMENT : cette opération supprimera toutes les données mises en cache des disques externes débranchés. Chaque hachage devra donc être régénéré.\n# Show preview\npreview_image_resize_failure = Impossible de redimensionner l'image { $name }.\npreview_image_opening_failure = Impossible d'ouvrir l'image { $name }. Raison : { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Groupe { $current_group }/{ $all_groups } ({ $images_in_group } images)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/it/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Impostazioni\nwindow_main_title = Czkawka (Singhiozzo)\nwindow_progress_title = Ricerca\nwindow_compare_images = Confronta le immagini\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Chiudi\n# Krokiet info dialog\nkrokiet_info_title = Vi presentiamo Krokiet - La nuova versione di Czkawka\nkrokiet_info_message =\n    Krokiet è la versione nuova, migliorata, più veloce e più affidabile della GUI Czkawka GTK!\n    \n    È più facile da eseguire e più resiliente ai cambiamenti di sistema, in quanto dipende solo dalle librerie di base disponibili sulla maggior parte dei sistemi per impostazione predefinita.\n    \n    Krokiet porta anche funzionalità aggiuntive: miniature in modalità di confronto video, un pulitore di dati EXIF, opzioni per spostare, copiare, cancellare e riordinare i file.\n    \n    Provalo e vedi la differenza!\n    \n    Czkawka continuerà a ricevere correzioni di bug e aggiornamenti minori da me, ma tutte le nuove funzionalità saranno sviluppate esclusivamente per Krokiet, e chiunque è libero di aggiungere funzionalità o estendere ulteriormente Czkawka.\n    \n    PS: Questo messaggio dovrebbe apparire solo una volta. Se viene visualizzato di nuovo, impostare la variabile di ambiente CZKAWKA_DONT_ANNOY_ME a qualsiasi valore non vuoto.\n# Main window\nmusic_title_checkbox = Titolo\nmusic_artist_checkbox = Artista\nmusic_year_checkbox = Anno\nmusic_bitrate_checkbox = Velocità in bit\nmusic_genre_checkbox = Genere\nmusic_length_checkbox = Durata\nmusic_comparison_checkbox = Confronto approssimativo\nmusic_checking_by_tags = Etichette\nmusic_checking_by_content = Contenuto\nsame_music_seconds_label = Durata minima del frammento\nsame_music_similarity_label = Differenza massima\nmusic_compare_only_in_title_group = Confronta all'interno di gruppi di titoli simili\nmusic_compare_only_in_title_group_tooltip =\n    Se abilitato, i file vengono raggruppati per titolo e poi confrontati tra loro.\n    \n    Con 10 000 file, invece di 100 milioni di confronti di solito ci saranno circa 20 000 confronti.\nsame_music_tooltip =\n    La ricerca di file musicali simili per contenuto può essere configurata impostando:\n    \n    - Il tempo minimo del frammento dopo il quale i file musicali possono essere identificati come simili\n    - La differenza massima tra due frammenti analizzati\n    \n    La chiave per ottenere buoni risultati è trovare combinazioni sensate di questi parametri.\n    \n    Impostando il tempo minimo a 5s e la differenza massima a 1.0, cercherà frammenti quasi identici nei file.\n    Un tempo di 20s e una differenza massima di 6.0, invece, funziona bene per trovare remix, versioni live ecc.\n    \n    Per impostazione predefinita, ogni file musicale viene confrontato con tutti gli altri e questo può richiedere molto tempo quando si anaalizzano molti file. Quindi di solito è meglio usare le cartelle di riferimento e specificare quali file devono essere confrontati tra loro (con la stessa quantità di file, il confronto delle impronte digitali sarà più veloce di almeno 4x che senza cartelle di riferimento).\nmusic_comparison_checkbox_tooltip =\n    Cerca file musicali simili usando il machine learning per rimuovere parentesi da una frase. Ad esempio, con questa opzione abilitata, questi file saranno considerati duplicati:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Distingue Maiuscole e minuscole\nduplicate_case_sensitive_name_tooltip =\n    Se abilitato, raggruppa solo i risultati quando hanno esattamente lo stesso nome, ad es. Żołd <-> Żołd\n    La disattivazione di tale opzione raggrupperà i nomi senza distinguere tra maiuscole e minuscole, ad esempio żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Dimensione e nome\nduplicate_mode_name_combo_box = Nome\nduplicate_mode_size_combo_box = Dimensione\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka offre 3 tipi di hash:\n    \n    Blake3 - funzione hash crittografica. Questo è il valore predefinito perché è molto veloce.\n    \n    CRC32 - semplice funzione di hash. Questo dovrebbe essere più veloce di Blake3, ma può molto raramente avere alcune collisioni.\n    \n    XXH3 - molto simile in termini di prestazioni e qualità di hash a Blake3 (ma non crittografica). Queste modalità possono essere facilmente intercambiate.\nduplicate_check_method_tooltip =\n    Per ora, Czkawka offre tre tipi di metodo per trovare i duplicati di:\n    \n    Nome - Trova i file che hanno lo stesso nome.\n    \n    Dimensione - Trova i file che hanno la stessa dimensione.\n    \n    Hash - Trova i file che hanno lo stesso contenuto. Questa modalità fa la hash sui file e in seguito le confronta\n    per trovare i duplicati. Questa modalità è il modo più sicuro per trovare i duplicati. Czkawka usa pesantemente la cache quindi successive scansioni degli stessi file dovrebbero essere molto più veloci del primo.\nimage_hash_size_tooltip =\n    Ogni immagine selezionata produce una hash speciale che può essere confrontata l'una con l'altra, e una piccola differenza tra loro significa che queste immagini sono simili.\n    \n    8 hash size è abbastanza buono per trovare immagini che sono solo un po' simili all'originale. Con un insieme più grande di immagini (>1000), questo produrrà una grande quantità di falsi positivi, quindi vi consiglio di utilizzare una dimensione di hash più grande in questo caso.\n    \n    16 è la dimensione predefinita dell'hash, che è un buon compromesso tra trovare anche un po' di immagini simili e avere solo una piccola quantità di collisioni di hash.\n    \n    32 e 64 hash trovano solo immagini molto simili e non dovrebbero avere quasi mai falsi positivi (forse tranne alcune immagini con canale alfa).\nimage_resize_filter_tooltip =\n    Per calcolare l'hash dell'immagine, la libreria deve prima ridimensionarla.\n    \n    A seconda dall'algoritmo scelto l'immagine utilizzata per calcolare l'hash apparirà un po' diversa.\n    \n    Nearest (il più vicino) è l'algoritmo più veloce da usare, ma anche quello che dà i peggiori risultati. È abilitato per impostazione predefinita perché con 16x16 dimensioni hash la qualità inferiore non è davvero visibile.\n    \n    Con 8x8 dimensioni di hash si consiglia di utilizzare un algoritmo diverso da Nearest, per avere migliori gruppi di immagini.\nimage_hash_alg_tooltip =\n    Gli utenti possono scegliere tra uno dei molti algoritmi di calcolo dell'hash.\n    \n    Ognuno ha punti di forza e di debolezza e a volte darà risultati migliori e a volte peggiori per immagini diverse.\n    \n    Quindi, per determinare quello migliore per te, è necessario un test manuale.\nbig_files_mode_combobox_tooltip = Consente di cercare i file più piccoli/più grandi\nbig_files_mode_label = File controllati\nbig_files_mode_smallest_combo_box = Il Più Piccolo\nbig_files_mode_biggest_combo_box = Il Più Grande\nmain_notebook_duplicates = File duplicati\nmain_notebook_empty_directories = Cartelle vuote\nmain_notebook_big_files = Grandi file\nmain_notebook_empty_files = File vuoti\nmain_notebook_temporary = File temporanei\nmain_notebook_similar_images = Immagini simili\nmain_notebook_similar_videos = Video simili\nmain_notebook_same_music = Duplicati musicali\nmain_notebook_symlinks = Collegamenti invalidi\nmain_notebook_broken_files = File corrotti\nmain_notebook_bad_extensions = Estensioni Errate\nmain_tree_view_column_file_name = Nome File\nmain_tree_view_column_folder_name = Nome Cartella\nmain_tree_view_column_path = Percorso\nmain_tree_view_column_modification = Data di Modifica\nmain_tree_view_column_size = Dimensione\nmain_tree_view_column_similarity = Similitudine\nmain_tree_view_column_dimensions = Dimensioni\nmain_tree_view_column_title = Titolo\nmain_tree_view_column_artist = Artista\nmain_tree_view_column_year = Anno\nmain_tree_view_column_bitrate = Velocità in Bit\nmain_tree_view_column_length = Durata\nmain_tree_view_column_genre = Genere\nmain_tree_view_column_symlink_file_name = Nome Collegamento\nmain_tree_view_column_symlink_folder = Cartella Collegamenti Simbolici\nmain_tree_view_column_destination_path = Percorso di Destinazione\nmain_tree_view_column_type_of_error = Tipo di Errore\nmain_tree_view_column_current_extension = Estensione Corrente\nmain_tree_view_column_proper_extensions = Estensione Corretta\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codicecá\nmain_label_check_method = Metodo di verifica\nmain_label_hash_type = Tipo di hash\nmain_label_hash_size = Dimensione hash\nmain_label_size_bytes = Dimensione (byte)\nmain_label_min_size = Min\nmain_label_max_size = Massimo\nmain_label_shown_files = Numero di file visualizzati\nmain_label_resize_algorithm = Metodo di ridimensionamento\nmain_label_similarity = Similitudine{ \"   \" }\nmain_check_box_broken_files_audio = Audio\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Compresso\nmain_check_box_broken_files_image = Immagine\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Usa ffmpeg/ffmprobe per convalidare file video. Più lento, ma può rilevare errori nascosti anche se il file viene riprodotto bene.\ncheck_button_general_same_size = Ignora stesse dimensioni\ncheck_button_general_same_size_tooltip = Ignora i file con dimensioni identiche nei risultati - di solito sono duplicati 1:1\nmain_label_size_bytes_tooltip = Dimensione dei file utilizzati nella ricerca\n# Upper window\nupper_tree_view_included_folder_column_title = Cartelle di Ricerca\nupper_tree_view_included_reference_column_title = Cartelle di Riferimento\nupper_recursive_button = Ricorsivo\nupper_recursive_button_tooltip = Se selezionato, cerca anche tra i file contenuti nelle cartelle figlie delle Cartelle di Riferimento.\nupper_manual_add_included_button = Aggiungi Manualmente\nupper_add_included_button = Aggiungi\nupper_remove_included_button = Rimuovi\nupper_manual_add_excluded_button = Aggiungi Manualmente\nupper_add_excluded_button = Aggiungi\nupper_remove_excluded_button = Rimuovi\nupper_manual_add_included_button_tooltip =\n    Aggiungi manualmente i nomi delle cartelle in cui cercare.\n    \n    È possibile aggiungere più percorsi contemporaneamente separarli da ;\n    \n    /home/roman;/home/rozkaz aggiungerà due directory /home/roman e /home/rozkaz\nupper_add_included_button_tooltip = Aggiungi nuova cartella per la ricerca.\nupper_remove_included_button_tooltip = Cancella cartella dalla ricerca.\nupper_manual_add_excluded_button_tooltip =\n    Aggiungi manualmente i nomi delle cartelle da escludere.\n    \n    È possibile aggiungere più percorsi contemporaneamente separarli da ;\n    \n    /home/roman;/home/krokiet aggiungerà due directory /home/roman e /home/keokiet\nupper_add_excluded_button_tooltip = Aggiunge una cartella da escludere dalla ricerca.\nupper_remove_excluded_button_tooltip = Rimuove una cartella da quelle escluse.\nupper_notebook_items_configuration = Configurazione degli elementi\nupper_notebook_excluded_directories = Percorsi Esclusi\nupper_notebook_included_directories = Percorsi Inclusi\nupper_allowed_extensions_tooltip =\n    Le estensioni consentite devono essere separate da virgole (di default sono tutte disponibili).\n    \n    Sono disponibili anche le seguenti Macro, che aggiungono più estensioni contemporaneamente: IMAGE, VIDEO, MUSIC, TESTO.\n    \n    Esempio di utilizzo \".exe, IMAGE, VIDEO, .rar, 7z\" - questo significa che verranno analizzati le immagini (e. . jpg, png), i video (ad esempio avi, mp4), exe, rar e i file 7z.\nupper_excluded_extensions_tooltip =\n    Elenco dei file disabilitati che verranno ignorati nella scansione.\n    \n    Quando si usano sia le estensioni consentite che quelle disabilitate, queste ultime hanno maggiore priorità, quindi il file non verrà analizzato.\nupper_excluded_items_tooltip =\n    Gli elementi esclusi devono contenere il carattere jolly * e devono essere separati da virgole.\n    Questo metodo è più lento rispetto ai Percorsi Esclusi. Utilizzare con cautela.\nupper_excluded_items = Elementi Esclusi:\nupper_allowed_extensions = Estensioni Permesse:\nupper_excluded_extensions = Estensioni Disabilitate:\n# Popovers\npopover_select_all = Seleziona tutto\npopover_unselect_all = Deseleziona tutto\npopover_reverse = Inverti la selezione\npopover_select_all_except_shortest_path = Seleziona tutto tranne il percorso più corto\npopover_select_all_except_longest_path = Seleziona tutto tranne il percorso più lungo\npopover_select_all_except_oldest = Seleziona tutto eccetto il più vecchio\npopover_select_all_except_newest = Seleziona tutto eccetto il più recente\npopover_select_one_oldest = Seleziona il più vecchio\npopover_select_one_newest = Seleziona il più recente\npopover_select_custom = Selezione personalizzata\npopover_unselect_custom = Delezione personalizzata\npopover_select_all_images_except_biggest = Seleziona tutti eccetto il più grande\npopover_select_all_images_except_smallest = Seleziona tutto eccetto il più piccolo\npopover_custom_path_check_button_entry_tooltip =\n    Seleziona i risultati specificando il percorso.\n    \n    Esempio:\n    /home/pimpek/rzecz.txt può essere trovato con /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Seleziona i risultati in base al nome.\n    \n    Esempio:\n    /usr/ping/pong.txt può essere trovato con *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Seleziona i risultati specificando una Regex.\n    \n    Con questa modalità, il testo cercato è Percorso con Nome.\n    \n    Esempio:\n    /usr/bin/ziemniak.txt può essere trovato con /ziem[a-z]+\n    \n    Questo utilizza l'implementazione predefinita delle Regex Rust. Puoi leggere di più qui: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Abilita rilevamento maiuscolo/minuscolo.\n    \n    Quando disabilitato /home/* trova sia /HoMe/roman che /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Impedisce di selezionare tutti i risultati nel gruppo.\n    \n    Questo è abilitato per impostazione predefinita, perché nella maggior parte delle situazioni, non si desidera eliminare entrambi i file originali e duplicati, ma si desidera lasciare almeno un file.\n    \n    ATTENZIONE: Questa impostazione non funziona se hai già selezionato manualmente tutti i risultati in un gruppo.\npopover_custom_regex_path_label = Percorso\npopover_custom_regex_name_label = Nome\npopover_custom_regex_regex_label = Regex Percorso + Nome\npopover_custom_case_sensitive_check_button = Distingui maiuscole/minuscole\npopover_custom_all_in_group_label = Non selezionare tutte le voci in un gruppo\npopover_custom_mode_unselect = Deselezione Personalizzata\npopover_custom_mode_select = Selezione Personalizzata\npopover_sort_file_name = Nome del file\npopover_sort_folder_name = Nome cartella\npopover_sort_full_name = Nome e cognome\npopover_sort_size = Dimensione\npopover_sort_selection = Selezione\npopover_invalid_regex = Regex non valida\npopover_valid_regex = Regex valida\n# Bottom buttons\nbottom_search_button = Cerca\nbottom_select_button = Seleziona\nbottom_delete_button = Cancella\nbottom_save_button = Salva\nbottom_symlink_button = Collegamenti simbolici\nbottom_hardlink_button = Collegamenti fisici\nbottom_move_button = Sposta\nbottom_sort_button = Ordina\nbottom_compare_button = Confronta\nbottom_search_button_tooltip = Avvia ricerca\nbottom_select_button_tooltip = Seleziona record. Solo i file/cartelle selezionati possono essere elaborati in seguito.\nbottom_delete_button_tooltip = Cancella i file/cartelle selezionati.\nbottom_save_button_tooltip = Salva i risultati della ricerca in un file\nbottom_symlink_button_tooltip =\n    Crea collegamenti simbolici.\n    Funziona solo quando sono selezionati almeno due risultati in un gruppo.\n    Il primo rimane invariato, dal secondo in poi si creano dei collegamenti al primo.\nbottom_hardlink_button_tooltip =\n    Crea collegamenti fisici.\n    Funziona solo quando sono selezionati almeno due risultati in un gruppo.\n    Il primo è invariato, dal secondo in poi si creano dei collegamenti fisici al primo.\nbottom_hardlink_button_not_available_tooltip =\n    Crea collegamenti fisici.\n    Il pulsante è disabilitato perché non è possibile creare collegamenti fisici.\n    I collegamenti fisici funzionano solo con i privilegi di amministratore su Windows, quindi assicurati di eseguire l'app come amministratore.\n    Se l'app funziona già con tali privilegi, controlla problemi simili su Github.\nbottom_move_button_tooltip =\n    Sposta i file nella directory scelta.\n    Copia tutti i file nella directory senza conservare l'albero delle directory.\n    Quando si tenta di spostare due file con il nome identico nella cartella, il secondo fallirà e mostrerà errore.\nbottom_sort_button_tooltip = Ordina file/cartelle in base al metodo selezionato.\nbottom_compare_button_tooltip = Confronta le immagini nel gruppo.\nbottom_show_errors_tooltip = Mostra/Nasconde il pannello di testo inferiore.\nbottom_show_upper_notebook_tooltip = Mostra/Nasconde il pannello comandi.\n# Progress Window\nprogress_stop_button = Interrompi\nprogress_stop_additional_message = Interrompi richiesta\n# About Window\nabout_repository_button_tooltip = Link alla pagina della repository con il codice sorgente.\nabout_donation_button_tooltip = Link alla pagina per le donazioni.\nabout_instruction_button_tooltip = Link alla pagina delle istruzioni.\nabout_translation_button_tooltip = Link alla pagina Crowdin con le traduzioni delle app. Ufficialmente polacco e inglese sono supportati.\nabout_repository_button = Repository\nabout_donation_button = Donazioni\nabout_instruction_button = Istruzioni\nabout_translation_button = Traduzione\n# Header\nheader_setting_button_tooltip = Apre la finestra delle impostazioni.\nheader_about_button_tooltip = Apre la finestra delle informazioni sul programma.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Numero di thread usati\nsettings_number_of_threads_tooltip = Numero di thread usati, 0 significa che tutti i thread disponibili saranno utilizzati.\nsettings_use_rust_preview = Usa librerie esterne invece di gtk per caricare le anteprime\nsettings_use_rust_preview_tooltip =\n    Utilizzando le anteprime gtk a volte sarà più veloce e supporterà più formati, ma a volte questo potrebbe essere esattamente il contrario.\n    \n    Se hai problemi con il caricamento delle anteprime, puoi provare a modificare questa impostazione.\n    \n    Nei sistemi non-linux, si consiglia di utilizzare questa opzione, perché gtk-pixbuf non è sempre disponibile e quindi disabilitando questa opzione non caricherà le anteprime di alcune immagini.\nsettings_label_restart = È necessario riavviare l'app per applicare le impostazioni!\nsettings_ignore_other_filesystems = Ignora altri filesystem (solo Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignora i file che non sono nello stesso file system delle cartelle analizzate.\n    \n    Funziona come l'opzione -xdev nel comando find su Linux\nsettings_save_at_exit_button_tooltip = Salva la configurazione su file quando chiudi l'app.\nsettings_load_at_start_button_tooltip =\n    Carica la configurazione dal file all'apertura dell'applicazione.\n    \n    Se non è abilitata, verranno utilizzate le impostazioni predefinite.\nsettings_confirm_deletion_button_tooltip = Mostra la finestra di conferma quando si fa clic sul pulsante Elimina.\nsettings_confirm_link_button_tooltip = Mostra la finestra di conferma quando si fa clic sul pulsante hard/symlink.\nsettings_confirm_group_deletion_button_tooltip = Mostra la finestra di avviso quando si tenta di eliminare tutti i risultati dal gruppo.\nsettings_show_text_view_button_tooltip = Mostra il pannello di testo in fondo all'interfaccia utente.\nsettings_use_cache_button_tooltip = Usa i file di cache. \nsettings_save_also_as_json_button_tooltip = Salva la cache in formato JSON (leggibile). È possibile modificarne il contenuto. La cache da questo file verrà letta automaticamente dall'app se manca la cache in formato binario (con estensione bid).\nsettings_use_trash_button_tooltip = Sposta i file nel cestino invece di eliminarli in modo permanente.\nsettings_language_label_tooltip = Lingua per l'interfaccia utente.\nsettings_save_at_exit_button = Salva la configurazione alla chiusura dell'app\nsettings_load_at_start_button = Carica la configurazione quando apri l'app\nsettings_confirm_deletion_button = Mostra finestra di conferma alla cancellazione di qualsiasi file\nsettings_confirm_link_button = Mostra finestra di conferma alla creazione di collegamenti per qualsiasi file\nsettings_confirm_group_deletion_button = Mostra finestra di conferma alla cancellazione di tutti gli elementi in un gruppo\nsettings_show_text_view_button = Mostra il pannello testuale inferiore\nsettings_use_cache_button = Utilizza cache\nsettings_save_also_as_json_button = Salva anche la cache come file JSON\nsettings_use_trash_button = Sposta i file rimossi nel cestino\nsettings_language_label = Lingua\nsettings_multiple_delete_outdated_cache_checkbutton = Cancella automaticamente la cache obsoleta\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Elimina i risultati della cache obsoleti che puntano a file inesistenti.\n    \n    Quando abilitata, l'app si assicura che durante il caricamento dei risultati che tutti puntino a file validi (quelli invalidi vengono ignorati).\n    \n    Disabilitare questo aiuterà durante la scansione dei file su unità esterne perché le loro voci della cache non verranno eliminate nella prossima scansione.\n    \n    Nel caso di centinaia di migliaia di record nella cache, si consiglia di abilitare questa funzione, che velocizzerà il caricamento della cache/salvataggio all'inizio/fine della scansione.\nsettings_notebook_general = Generale\nsettings_notebook_duplicates = Duplicati\nsettings_notebook_images = Immagini Simili\nsettings_notebook_videos = Video Simili\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Mostra l'anteprima sul lato destro (quando si seleziona un file immagine).\nsettings_multiple_image_preview_checkbutton = Mostra anteprima immagini\nsettings_multiple_clear_cache_button_tooltip =\n    Pulisci manualmente la cache delle voci obsolete.\n    Questo dovrebbe essere usato solo se la compensazione automatica è stata disabilitata.\nsettings_multiple_clear_cache_button = Rimuovi i risultati obsoleti dalla cache.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Nasconde tutti i file tranne uno, se tutti puntano agli stessi dati (sono hardlinked).\n    \n    Esempio: Nel caso in cui ci siano (su disco) sette file che sono collegati a dati specifici e un file diverso con gli stessi dati ma un inode diverso, poi nel mirino duplicato, verrà mostrato solo un file univoco e un file da quelli hardlink.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Imposta la dimensione minima del file che verrà memorizzata nella cache.\n    \n    Scegliendo un valore più piccolo si genereranno più record. Questo accelererà la ricerca, ma rallenterà il caricamento / il salvataggio della cache.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Abilita la cache di prehash (un hash calcolato da una piccola parte del file) che consente il ritiro anticipato di risultati non duplicati.\n    \n    È disabilitato per impostazione predefinita perché può causare rallentamenti in alcune situazioni.\n    \n    Si consiglia vivamente di usarlo durante la scansione di centinaia di migliaia o milioni di file, perché può accelerare la ricerca di più volte.\nsettings_duplicates_prehash_minimal_entry_tooltip = Dimensione minima delle voci della cache.\nsettings_duplicates_hide_hard_link_button = Nascondi collegamenti rigidi\nsettings_duplicates_prehash_checkbutton = Utilizza la cash prehash\nsettings_duplicates_minimal_size_cache_label = Dimensione minima dei file (in byte) salvati nella cache\nsettings_duplicates_minimal_size_cache_prehash_label = Dimensione minima dei file (in byte) salvati nella cache prehash\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Salva la configurazione attuale delle impostazioni su file.\nsettings_loading_button_tooltip = Carica le impostazioni dal file e sostituisci con esse la configurazione corrente.\nsettings_reset_button_tooltip = Reimposta la configurazione corrente a quella predefinita.\nsettings_saving_button = Salva configurazione\nsettings_loading_button = Carica configurazione\nsettings_reset_button = Reimposta configurazione\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Apre la cartella in cui sono memorizzati i file cache txt.\n    \n    La modifica dei file cache può causare la visualizzazione di risultati non validi. Tuttavia, la modifica del percorso può risparmiare tempo quando si sposta una grande quantità di file in una posizione diversa.\n    \n    È possibile copiare questi file tra computer per risparmiare tempo sulla scansione di nuovo per i file (ovviamente se hanno una struttura di directory simile).\n    \n    In caso di problemi con la cache, questi file possono essere rimossi. L'app li rigenererà automaticamente.\nsettings_folder_settings_open_tooltip =\n    Apre la cartella in cui viene memorizzata la configurazione Czkawka.\n    \n    ATTENZIONE: Modificare manualmente la configurazione potrebbe interferire coi processi in atto.\nsettings_folder_cache_open = Apri la cartella della cache\nsettings_folder_settings_open = Apri la cartella delle impostazioni\n# Compute results\ncompute_stopped_by_user = Ricerca interrotta dall'utente\ncompute_found_duplicates_hash_size = Trovato { $number_files } duplicati in { $number_groups } gruppi che occupano { $size } in { $time }\ncompute_found_duplicates_name = Trovato { $number_files } duplicati in { $number_groups } gruppi in { $time }\ncompute_found_empty_folders = Trovo { $number_files } cartelle vuote in { $time }\ncompute_found_empty_files = Trovati { $number_files } file vuoti in { $time }\ncompute_found_big_files = Trovati { $number_files } file grandi in { $time }\ncompute_found_temporary_files = Trovati { $number_files } file temporanei in { $time }\ncompute_found_images = Trova { $number_files } immagini simili in { $number_groups } gruppi in { $time }\ncompute_found_videos = Trovati { $number_files } video simili in { $number_groups } gruppi in { $time }\ncompute_found_music = Trovo { $number_files } file musicali simili in { $number_groups } gruppi in { $time }\ncompute_found_invalid_symlinks = Trovati { $number_files } collegamenti simbolici invalidi in { $time }\ncompute_found_broken_files = Trovate { $number_files } file danneggiati in { $time }\ncompute_found_bad_extensions = Trovati { $number_files } file con estensioni non valide in { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Analizzato { $file_number }  file\n       *[other] Analizzati  { $file_number }  files\n    }\nprogress_scanning_extension_of_files = Controllo estensione del file { $file_checked }/{ $all_files }\nprogress_scanning_broken_files = Controllo { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Genero Hash del video { $file_checked }/{ $all_files }\nprogress_creating_video_thumbnails = Miniature create di { $file_checked }/{ $all_files } video\nprogress_scanning_image = Generate hash di  { $file_checked }/{ $all_files } immagini({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Confrontate { $file_checked }/{ $all_files } hash di immagini\nprogress_scanning_music_tags_end = Etichette confrontate di { $file_checked }/{ $all_files } file musicali\nprogress_scanning_music_tags = Lettura dei tag di { $file_checked }/{ $all_files } file musicali\nprogress_scanning_music_content_end = Confronto l'impronta digitale di { $file_checked }/{ $all_files } file musicali\nprogress_scanning_music_content = Impronta digitale calcolata di { $file_checked }/{ $all_files } file musicali ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Scanned { $folder_number } folder\n       *[other] Scanned { $folder_number } folders\n    }\nprogress_scanning_size = Dimensione scansionata del file { $file_number }\nprogress_scanning_size_name = Nome e dimensione del file { $file_number } scansionato\nprogress_scanning_name = Nome scansionato del file { $file_number }\nprogress_analyzed_partial_hash = Hash parziale analizzato di { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Hash completo analizzato di { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Caricamento della cache prehash\nprogress_prehash_cache_saving = Salvataggio della cache prehash\nprogress_hash_cache_loading = Caricamento della cache hash\nprogress_hash_cache_saving = Salvataggio della cache hash\nprogress_cache_loading = Caricamento cache\nprogress_cache_saving = Salvataggio cache\nprogress_current_stage = Fase attuale:{ \"  \" }\nprogress_all_stages = Tutte le fasi:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = Salvataggio configurazione su file { $name }.\nsaving_loading_saving_failure = Impossibile salvare i dati di configurazione nel file { $name }, motivo { $reason }.\nsaving_loading_reset_configuration = La configurazione corrente è stata cancellata.\nsaving_loading_loading_success = Caricamento configurazione da file avvenuto con successo.\nsaving_loading_failed_to_create_config_file = Impossibile creare il file di configurazione \"{ $path }\", motivo \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Impossibile caricare la configurazione da \"{ $path }\" perché non esiste o non è un file.\nsaving_loading_failed_to_read_data_from_file = Impossibile leggere il file \"{ $path }\", motivo \"{ $reason }\".\n# Other\nselected_all_reference_folders = Impossibile avviare la ricerca, quando tutte le directory sono impostate come cartelle di riferimento\nsearching_for_data = Ricerca dei dati, può durare a lungo, attendere prego...\ntext_view_messages = MESSAGGI\ntext_view_warnings = ATTENZIONE\ntext_view_errors = ERRORI\nabout_window_motto = Questo programma può essere usato liberamente e lo sarà sempre.\nkrokiet_new_app = Czkawka è in modalità manutenzione, il che significa che saranno risolti solo bug critici e non verranno aggiunte nuove funzionalità. Per nuove funzionalità, si prega di controllare la nuova applicazione Krokiet, che è più stabile e performante ed è ancora in fase di sviluppo attivo.\n# Various dialog\ndialogs_ask_next_time = Chiedi la prossima volta\nsymlink_failed = Collegamento simbolico { $name } a { $target }non riuscito, motivo { $reason }\ndelete_title_dialog = Conferma di cancellazione\ndelete_question_label = Sei sicuro di cancellare i file?\ndelete_all_files_in_group_title = Conferma di cancellazione di tutti i file nel gruppo\ndelete_all_files_in_group_label1 = In alcuni gruppi tutti i record sono selezionati.\ndelete_all_files_in_group_label2 = Sei sicuro di cancellarli tutti?\ndelete_items_label = { $items } i file verranno eliminati.\ndelete_items_groups_label = { $items } i file da { $groups } gruppi verranno eliminati.\nhardlink_failed = Collegamento non riuscito a { $name } a { $target }, motivo { $reason }\nhard_sym_invalid_selection_title_dialog = Selezione invalida in alcuni gruppi\nhard_sym_invalid_selection_label_1 = In alcuni gruppi c'è solo un record selezionato e verrà ignorato.\nhard_sym_invalid_selection_label_2 = Per essere in grado di collegare hard/sym questi file, è necessario selezionare almeno due risultati nel gruppo.\nhard_sym_invalid_selection_label_3 = Il primo nel gruppo sarà considerato l'originale ed inalterato, ma il secondo ed i successivi verranno modificati.\nhard_sym_link_title_dialog = Conferma collegamento\nhard_sym_link_label = Sei sicuro di voler collegare questi file?\nmove_folder_failed = Spostamento cartella { $name } fallito, ragione { $reason }\nmove_file_failed = Spostamento file { $name } fallito, ragione { $reason }\nmove_files_title_dialog = Seleziona la cartella dove vuoi spostare i file duplicati\nmove_files_choose_more_than_1_path = Solo un percorso può essere selezionato per essere in grado di copiare i file duplicati, selezionato { $path_number }.\nmove_stats = { $num_files }/{ $all_files } elementi spostati con successo\nsave_results_to_file = Risultati salvati sia in file txt che json nella cartella \"{ $name }\".\nsearch_not_choosing_any_music = ERRORE: Devi selezionare almeno una casella dei metodi di ricerca musicali.\nsearch_not_choosing_any_broken_files = ERRORE: è necessario selezionare almeno una casella di controllo selezionando il tipo di file danneggiati.\ninclude_folders_dialog_title = Cartelle incluse\nexclude_folders_dialog_title = Cartelle escluse\ninclude_manually_directories_dialog_title = Aggiungi cartella manualmente\ncache_properly_cleared = Cache cancellata con successo\ncache_clear_duplicates_title = Cancellazione cache dei duplicati\ncache_clear_similar_images_title = Cancellazione cache delle immagini simili\ncache_clear_similar_videos_title = Cancellazione cache dei video simili\ncache_clear_message_label_1 = Vuoi cancellare la cache delle voci obsolete?\ncache_clear_message_label_2 = Questa operazione rimuoverà tutte le voci della cache che puntano a file non validi.\ncache_clear_message_label_3 = Questo può velocizzare il carico/salvataggio nella cache.\ncache_clear_message_label_4 = ATTENZIONE: L'operazione rimuoverà tutti i dati memorizzati nella cache da unità esterne scollegate. Quindi ogni hash dovrà essere rigenerato.\n# Show preview\npreview_image_resize_failure = Ridimensionamento dell'immagine { $name } non riuscito.\npreview_image_opening_failure = Impossibile aprire l'immagine { $name }, motivo { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Gruppo { $current_group }/{ $all_groups } ({ $images_in_group } immagini)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/ja/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = 設定\nwindow_main_title = Czkawka (しゃっくり)\nwindow_progress_title = スキャン中\nwindow_compare_images = 画像を比較\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = 閉じる\n# Krokiet info dialog\nkrokiet_info_title = Krokiet – 新バージョン Czkawka\nkrokiet_info_message = \n        Krokietは、Czkawka GTK GUIの新しい、改良され、高速かつ信頼性の高いバージョンです！\n \n        実行が簡単で、システム変更に強く、ほとんどのシステムでデフォルトで利用可能なコアライブラリに依存するためです。\n \n        Krokietは、サムネイルをビデオ比較モードで、EXIFクリーナー、ファイル移動/コピー/削除の進捗状況、または拡張されたソートオプションなど、Czkawkaにはない機能も提供します。\n \n        試してみてください。違いを確かめてください！\n \n        Czkawkaは、私からバグ修正や軽微なアップデートを継続的に受け取りますが、すべての新機能はKrokietにのみ開発され、誰でも新しい機能を追加したり、欠落しているモードを補ったり、Czkawkaをさらに拡張したりすることができます。\n \n        追伸：このメッセージは一度だけ表示されるように設計されています。もし表示された場合は、CZKAWKA_DONT_ANNOY_ME環境変数を任意の非空の値に設定してください。.\n# Main window\nmusic_title_checkbox = タイトル\nmusic_artist_checkbox = アーティスト\nmusic_year_checkbox = 年\nmusic_bitrate_checkbox = ビットレート\nmusic_genre_checkbox = ジャンル\nmusic_length_checkbox = 長さ\nmusic_comparison_checkbox = おおよその比較\nmusic_checking_by_tags = タグ\nmusic_checking_by_content = コンテンツ\nsame_music_seconds_label = フラグメント最小秒の持続時間\nsame_music_similarity_label = 最大差\nmusic_compare_only_in_title_group = 類似したタイトルのグループ内で比較\nmusic_compare_only_in_title_group_tooltip = \n    有効にすると、ファイルはタイトルでグループ化され、それから比較されます。\n    \n    10000ファイルでは、その代わりに約100万比較が通常、約20000比較があります。.\nsame_music_tooltip = \n    音楽ファイルの内容から類似ファイルを検索するように設定できます：\n    \n    - 音楽ファイルが類似していると識別されるフラグメントの最小時間\n    - テストされた2つのフラグメントの最大差分\n    \n    良い結果を得るための鍵は、これらのパラメータの賢明な組み合わせを見つけることです。\n    \n    最小時間を5秒、最大差を1.0に設定すると、ファイル内のほとんど同じフラグメントを探します。\n    一方、時間を20秒、差の最大値を6.0に設定すると、リミックスやライブ・バージョンなどを探すのに効果的です。\n    \n    デフォルトでは、各音楽ファイルは互いに比較され、多数のファイルをテストする場合、これは多くの時間を要します。したがって、通常、参照フォルダを使用し、どのファイルを互いに比較するかを指定する方が良いでしょう（同じ量のファイルでは、フィンガープリントの比較は参照フォルダなしよりも少なくとも 4 倍速くなります）。.\nmusic_comparison_checkbox_tooltip =\n    機械学習によりフレーズから括弧とその中身を除外するAIを使用して、類似の音楽ファイルを検索します。このオプションが有効な場合、例えば以下のファイルは重複とみなされます:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = 大文字小文字を区別する\nduplicate_case_sensitive_name_tooltip =\n    有効な場合、グループのみレコードまったく同じ名前を持っている場合など。 Z<unk> ołd <-> Z<unk> ołd\n    \n    このようなオプションを無効にすると、各文字のサイズが同じかどうかを確認せずに名前をグループ化します。例: z<unk> o<unk> D <-> Z<unk> ołd\nduplicate_mode_size_name_combo_box = サイズと名前\nduplicate_mode_name_combo_box = 名前\nduplicate_mode_size_combo_box = サイズ\nduplicate_mode_hash_combo_box = ハッシュ\nduplicate_hash_type_tooltip = \n    Czkawkaは3種類のハッシュを提供します:\n    \n    Blake3 - 暗号学的ハッシュ関数。非常に高速であるため、デフォルトのハッシュ方式として使用されます。\n    \n    CRC32 - シンプルなハッシュ関数。Blake3より高速ですが、まれに衝突が発生する可能性があります。\n    \n    XXH3 - パフォーマンスとハッシュの質がBlake3に非常に近いので、このようなモードの代わりに簡単に使用できます。（ただし、暗号学的ではありません）.\nduplicate_check_method_tooltip = \n    Czkawkaは、今のところ以下の3種類の方法で重複を見つけることができます:\n    \n    名前 - 同じ名前のファイルを検索します。\n    \n    サイズ - 同じサイズのファイルを探します。\n    \n    ハッシュ - 同じ内容のファイルを探します。ファイルをハッシュ化して比較することにより重複を見つけます。このモードは、重複を見つけるための最も安全な方法です。このツールはキャッシュを多用するので、同じデータの2回目以降のスキャンは最初の時よりずっと速くなるはずです。.\nimage_hash_size_tooltip = \n    チェックした画像はそれぞれ特別なハッシュを生成し、そのハッシュを比較したときに差が小さいほど、この画像は類似していることを意味します。\n    \n    8のハッシュサイズは、オリジナルに少ししか似ていない画像を見つけるのにかなり適しています。しかし、1000枚を超えるような大きな画像では、誤検出が多くなるため、より大きなハッシュサイズを使用することをお勧めします。\n    \n    16はデフォルトのハッシュサイズであり、少しでも類似した画像を見つけることとハッシュの衝突を少なくすることの間でかなり良い妥協点です。\n    \n    32と64のハッシュは非常に類似した画像しか見つけられませんが、誤検出はほとんどありません（アルファチャンネルのある一部の画像を除いて）。.\nimage_resize_filter_tooltip = \n    画像のハッシュを計算するために、ライブラリはまず画像のサイズを必ず変更します。\n    \n    どのアルゴリズムを選択したかによって、ハッシュを計算するために使用される画像は少し違って見えるかもしれません。\n    \n    最も高速なアルゴリズムは Nearest ですが、最も悪い結果を出すのも Nearest です。16x16のハッシュサイズで低品質の場合、それが明らかになることはないので、デフォルトは Nearest です。\n    \n    8x8のハッシュサイズでは、より良い画像群を得るために、Nearestとは異なるアルゴリズムを使用することが推奨されます。.\nimage_hash_alg_tooltip = \n    ハッシュの計算方法は、多くのアルゴリズムの中からユーザーが選択することができます。\n    \n    それぞれ長所と短所があり、画像によって良い結果が出る場合もあれば、悪い結果が出る場合もあります。\n    \n    そのため、最適なものを見極めるには、手動でのテストが必要です。.\nbig_files_mode_combobox_tooltip = 最小/最大のファイルを検索できます\nbig_files_mode_label = チェックされたファイル\nbig_files_mode_smallest_combo_box = 最も小さい\nbig_files_mode_biggest_combo_box = 最大のもの\nmain_notebook_duplicates = 重複したファイル\nmain_notebook_empty_directories = 空のディレクトリ\nmain_notebook_big_files = 大きなファイル\nmain_notebook_empty_files = 空のファイル\nmain_notebook_temporary = 一時ファイル\nmain_notebook_similar_images = 類似の画像\nmain_notebook_similar_videos = 類似の動画\nmain_notebook_same_music = 重複した音楽\nmain_notebook_symlinks = 無効なシンボリックリンク\nmain_notebook_broken_files = 壊れたファイル\nmain_notebook_bad_extensions = 不正なエクステンション\nmain_tree_view_column_file_name = ファイル名\nmain_tree_view_column_folder_name = フォルダ名\nmain_tree_view_column_path = パス\nmain_tree_view_column_modification = 更新日時\nmain_tree_view_column_size = サイズ\nmain_tree_view_column_similarity = 類似度\nmain_tree_view_column_dimensions = 寸法\nmain_tree_view_column_title = タイトル\nmain_tree_view_column_artist = アーティスト\nmain_tree_view_column_year = 年\nmain_tree_view_column_bitrate = ビットレート\nmain_tree_view_column_length = 長さ\nmain_tree_view_column_genre = ジャンル\nmain_tree_view_column_symlink_file_name = シンボリックリンクのファイル名\nmain_tree_view_column_symlink_folder = シンボリックリンクフォルダ\nmain_tree_view_column_destination_path = 宛先パス\nmain_tree_view_column_type_of_error = エラーの種類\nmain_tree_view_column_current_extension = 現在のエクステンション\nmain_tree_view_column_proper_extensions = 適切な拡張\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = コーデック：\nmain_label_check_method = メソッドのチェック\nmain_label_hash_type = ハッシュ方式\nmain_label_hash_size = ハッシュサイズ\nmain_label_size_bytes = サイズ(バイト)\nmain_label_min_size = 最小値\nmain_label_max_size = 最大値\nmain_label_shown_files = 表示するファイルの数\nmain_label_resize_algorithm = アルゴリズムのサイズを変更\nmain_label_similarity = 類似度{ \"   \" }\nmain_check_box_broken_files_audio = 音声\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = アーカイブする\nmain_check_box_broken_files_image = 画像\nmain_check_box_broken_files_video = ビデオ\nmain_check_box_broken_files_video_tooltip = ffmpeg/ffprobe を使用してビデオファイルを検証します。かなり遅く、ファイルが正常に再生されても、煩雑なエラーを検出することがあります。.\ncheck_button_general_same_size = 同じサイズを無視\ncheck_button_general_same_size_tooltip = 結果として同じサイズのファイルを無視 - 通常、これらは1:1重複です\nmain_label_size_bytes_tooltip = スキャンで使用されるファイルのサイズ\n# Upper window\nupper_tree_view_included_folder_column_title = 検索するフォルダ\nupper_tree_view_included_reference_column_title = 参照フォルダ\nupper_recursive_button = 再帰的\nupper_recursive_button_tooltip = 選択した場合、選択したフォルダの下に直接配置されていないファイルも検索します。.\nupper_manual_add_included_button = 手動追加\nupper_add_included_button = 追加\nupper_remove_included_button = 削除\nupper_manual_add_excluded_button = 手動追加\nupper_add_excluded_button = 追加\nupper_remove_excluded_button = 削除\nupper_manual_add_included_button_tooltip =\n    手動で検索するディレクトリ名を追加します。\n    \n    一度に複数のパスを追加するには、 ;\n    \n    /home/roman;/home/rozkazは/home/romanと/home/rozkazの2つのディレクトリを追加します。\nupper_add_included_button_tooltip = 検索に新しいディレクトリを追加します。.\nupper_remove_included_button_tooltip = 検索からディレクトリを削除します。.\nupper_manual_add_excluded_button_tooltip =\n    除外されたディレクトリ名を手動で追加します。\n    \n    一度に複数のパスを追加するには、 ;\n    \n    /home/roman;/home/krokiet は /home/roman と /home/keokiet の 2 つのディレクトリを追加します。\nupper_add_excluded_button_tooltip = 検索で除外するディレクトリを追加します。.\nupper_remove_excluded_button_tooltip = 除外されたディレクトリを削除します。.\nupper_notebook_items_configuration = アイテム設定\nupper_notebook_excluded_directories = 除外パス\nupper_notebook_included_directories = 含まれるパス\nupper_allowed_extensions_tooltip = \n    許可する拡張子はカンマで区切る必要があります（デフォルトではすべてが使用可能です）。\n    \n    複数の拡張子を一度に追加するマクロ: IMAGE, VIDEO, MUSIC, TEXT も利用可能です。\n    \n    使用例: \".exe, IMAGE, VIDEO, .rar, 7z\" - これは画像（jpg、pngなど）、動画（avi、mp4など）、exe、rar、7zファイルがスキャンされることを意味します。.\nupper_excluded_extensions_tooltip = \n    スキャンで無視される無効なファイルの一覧です。\n    \n    許可された拡張子と無効化された拡張子の両方を使用する場合、この拡張子の方が優先度が高いので、ファイルはチェックされません。.\nupper_excluded_items_tooltip = \n        除外項目には *ワイルドカードを含んでおり、カンマで区切ってください。\n        これはExcluded Pathsよりも遅いため、注意して使用してください。.\nupper_excluded_items = 除外するアイテム:\nupper_allowed_extensions = 許可される拡張子:\nupper_excluded_extensions = 無効なエクステンション:\n# Popovers\npopover_select_all = すべて選択\npopover_unselect_all = すべて選択解除\npopover_reverse = 選択を逆にする\npopover_select_all_except_shortest_path = 選択を解除し、最短経路を除く\npopover_select_all_except_longest_path = 選択を解除する、最長パスを除く\npopover_select_all_except_oldest = 一番古いもの以外のすべてを選択\npopover_select_all_except_newest = 一番新しいもの以外のすべてを選択\npopover_select_one_oldest = 一番古いものを選択\npopover_select_one_newest = 一番新しいものを選択\npopover_select_custom = カスタム選択\npopover_unselect_custom = カスタム選択を解除\npopover_select_all_images_except_biggest = 一番大きいもの以外のすべてを選択\npopover_select_all_images_except_smallest = 一番小さいもの以外のすべてを選択\npopover_custom_path_check_button_entry_tooltip =\n    パスによってレコードを選択します。\n    \n    使用例:\n    /home/pimpek/rzecz.txt は /home/pim* で見つけることができます\npopover_custom_name_check_button_entry_tooltip =\n    ファイル名でレコードを選択します。\n    \n    使用例:\n    /usr/ping/pong.txt は *ong* でを見つけることができます\npopover_custom_regex_check_button_entry_tooltip = \n    指定した正規表現でレコードを選択することができます。\n    \n    このモードでは、検索される文字列はパスと文字列になります。\n    \n    使用例:\n    /usr/bin/ziemniak.txt は /ziem[a-z]+ で検索できます。\n    \n    これはRustのデフォルトの正規表現実装を使用しているので、詳しくは https://docs.rs/regex を参照してください。.\npopover_custom_case_sensitive_check_button_tooltip = \n    大文字小文字を区別する検出を有効にします。\n    \n    /home/* を無効にすると、/Home/roman と /home/roman の両方が検出されます。.\npopover_custom_not_all_check_button_tooltip = \n    グループ内のすべてのレコードを選択できないようにします。\n    \n    ほとんどの状況でユーザーは元のファイルと重複ファイルの両方を削除したくないため、これはデフォルトで有効になっています。 少なくとも1つのファイルを残したい。\n    \n    警告: この設定は、すでにユーザーがグループ内のすべての結果を手動で選択している場合には機能しません。.\npopover_custom_regex_path_label = パス\npopover_custom_regex_name_label = 名前\npopover_custom_regex_regex_label = 正規表現（パス + 名前）\npopover_custom_case_sensitive_check_button = 大文字と小文字を区別\npopover_custom_all_in_group_label = グループ内のすべてのレコードを選択しない\npopover_custom_mode_unselect = カスタム選択を解除\npopover_custom_mode_select = カスタム選択\npopover_sort_file_name = ファイル名\npopover_sort_folder_name = フォルダー名\npopover_sort_full_name = カード名義人\npopover_sort_size = サイズ\npopover_sort_selection = 選択\npopover_invalid_regex = 正規表現が無効です\npopover_valid_regex = 正規表現が有効です\n# Bottom buttons\nbottom_search_button = 検索\nbottom_select_button = 選択\nbottom_delete_button = 削除\nbottom_save_button = 保存\nbottom_symlink_button = シンボリックリンク\nbottom_hardlink_button = ハードリンク\nbottom_move_button = 移動\nbottom_sort_button = 並び替え\nbottom_compare_button = 比較\nbottom_search_button_tooltip = 検索を開始\nbottom_select_button_tooltip = レコードを選択します。選択したファイル/フォルダのみが後で処理できます。.\nbottom_delete_button_tooltip = 選択したファイル/フォルダを削除します。.\nbottom_save_button_tooltip = 検索に関するデータをファイルに保存します。\nbottom_symlink_button_tooltip = \n    シンボリックリンクを作成します。\n    グループ内の2つ以上の結果が選択されている場合にのみ機能します。\n    最初の結果は変更されず、2番目以降の結果が最初の結果にシンボリックリンクされます。.\nbottom_hardlink_button_tooltip = \n    ハードリンクを作成します。\n    グループ内の2つ以上の結果が選択されている場合にのみ機能します。\n    最初の結果は変更されず、2番目以降の結果が最初の結果にハードリンクされます。.\nbottom_hardlink_button_not_available_tooltip = \n    ハードリンクを作成する。\n    ハードリンクを作成できないため、ボタンは無効になっています。\n    ハードリンクはWindowsの管理者権限でのみ動作するので、アプリは必ず管理者として実行してください。\n    アプリがすでにそのような権限で動作している場合は、Githubに同様の問題がないか確認してください。.\nbottom_move_button_tooltip = \n    選択したフォルダにファイルを移動します。\n    ディレクトリツリーを維持したまま、すべてのファイルをフォルダにコピーします。\n    同じ名前の2つのファイルをフォルダに移動しようとすると、2番目のファイルが失敗し、エラーが表示されます。.\nbottom_sort_button_tooltip = 選択した方法に従ってファイル/フォルダを並べ替えます。.\nbottom_compare_button_tooltip = グループ内の画像を比較する.\nbottom_show_errors_tooltip = 下部のエラーパネルを表示/非表示にします。.\nbottom_show_upper_notebook_tooltip = 上部のノートブックパネルを表示/非表示にします。.\n# Progress Window\nprogress_stop_button = 停止\nprogress_stop_additional_message = リクエストを停止する\n# About Window\nabout_repository_button_tooltip = ソースコードのあるリポジトリページへのリンク.\nabout_donation_button_tooltip = 寄付ページへのリンク.\nabout_instruction_button_tooltip = 使い方ページへのリンク.\nabout_translation_button_tooltip = Crowdin ページにアプリの翻訳をリンクします。公式にポーランド語と英語がサポートされています。.\nabout_repository_button = リポジトリ\nabout_donation_button = 寄付\nabout_instruction_button = 使い方\nabout_translation_button = 翻訳\n# Header\nheader_setting_button_tooltip = 設定ダイアログを開きます。.\nheader_about_button_tooltip = アプリに関する情報を含むダイアログを開きます。.\n \n# Settings\n\n\n## General\n\nsettings_number_of_threads = 使用されるスレッドの数\nsettings_number_of_threads_tooltip = 使用するスレッドの数、0 は、使用可能なすべてのスレッドが使用されることを意味します。.\nsettings_use_rust_preview = プレビューの読み込みにgtkの代わりに外部ライブラリを使用する\nsettings_use_rust_preview_tooltip = \n    GTKプレビューはいくらかの場合において高速で多くのフォーマットをサポートしているが、全く逆となる場合もある。\n    \n    プレビューの読み込みに問題がある場合、この設定を変更してみるとよい。\n    \n    Linux以外の環境では、gtk-pixbufが常に有効とは限らず、無効にすることによりいくらかの画像のプレビューが読み込まれないため、このオプションの使用が推奨される。.\nsettings_label_restart = 設定を適用するにはアプリを再起動する必要があります！\nsettings_ignore_other_filesystems = 他のファイルシステムを無視(Linuxのみ)\nsettings_ignore_other_filesystems_tooltip =\n    検索されたディレクトリと同じファイルシステムにないファイルを無視します。\n    \n    Linux の find コマンドで -xdev オプションのように動作します。\nsettings_save_at_exit_button_tooltip = 終了時に設定をファイルに保存します。.\nsettings_load_at_start_button_tooltip = \n    起動時にファイルから設定を読み込みます。\n    \n    このオプションが無効の場合、デフォルトの設定が使用されます。.\nsettings_confirm_deletion_button_tooltip = 削除ボタンをクリックしたときに確認ダイアログを表示します。.\nsettings_confirm_link_button_tooltip = ハードリンク/シンボリックリンクボタンをクリックしたときに確認ダイアログを表示します。.\nsettings_confirm_group_deletion_button_tooltip = グループからすべてのレコードを削除しようとしたときに警告ダイアログを表示します。.\nsettings_show_text_view_button_tooltip = 下部にテキストパネルを表示します。.\nsettings_use_cache_button_tooltip = ファイルキャッシュを使用します。.\nsettings_save_also_as_json_button_tooltip = キャッシュを人間にも読みやすい形のJSON形式で保存します。この内容は編集可能です。コンテンツを変更することができます。 バイナリ形式のキャッシュ（bin拡張子のもの）がない場合、このファイルから自動的にキャッシュが読み込まれます。.\nsettings_use_trash_button_tooltip = ファイルを永久に削除する代わりにゴミ箱に移動します。.\nsettings_language_label_tooltip = 操作画面における言語を選択します。.\nsettings_save_at_exit_button = 終了時に設定を保存\nsettings_load_at_start_button = 起動時に設定を読み込む\nsettings_confirm_deletion_button = ファイルを削除するときに確認ダイアログを表示する\nsettings_confirm_link_button = ハードリンク/シンボリックリンク時に確認ダイアログを表示する\nsettings_confirm_group_deletion_button = グループ内のすべてのファイルを削除するときに確認ダイアログを表示する\nsettings_show_text_view_button = 下部にテキストパネルを表示\nsettings_use_cache_button = キャッシュを使用\nsettings_save_also_as_json_button = JSONファイルにもキャッシュを保存する\nsettings_use_trash_button = 削除したファイルをゴミ箱に移動する\nsettings_language_label = 言語\nsettings_multiple_delete_outdated_cache_checkbutton = 古いキャッシュエントリを自動的に削除\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip = \n    存在しないファイルを指している古いキャッシュエントリを削除できるようにします。\n    \n    このオプションを有効にすると、アプリはレコードを読み込むときにすべてのポイントが有効なファイルであることを確認します（壊れたファイルは無視されます）。\n    \n    無効にすると、それらに関するキャッシュエントリが次のスキャンで削除されなくなり、外部ドライブ上のファイルをスキャンする際に役立ちます。\n    \n    キャッシュに何十万ものレコードがある場合、スキャンの開始時・終了時のキャッシュの読み込み・保存を高速化するためにこのオプションを有効にすることが推奨されます。.\nsettings_notebook_general = 全般\nsettings_notebook_duplicates = 重複\nsettings_notebook_images = 類似の画像\nsettings_notebook_videos = 類似の動画\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = 画像ファイルを選択しているとき、右側にプレビューを表示します。.\nsettings_multiple_image_preview_checkbutton = 画像のプレビューを表示\nsettings_multiple_clear_cache_button_tooltip = \n    古いキャッシュエントリを手動でクリアします。\n    自動クリアが無効の場合にのみ使用する必要があります。.\nsettings_multiple_clear_cache_button = キャッシュから古い結果を削除します。.\n \n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip = \n    ハードリンクされていてかつ同じデータを指している場合、1つを除くすべてのファイルを非表示にします。\n    \n    例： ディスク上に特定のデータにハードリンクされている同じデータを持つ7つのファイルと、1つの異なるinodeのファイルがある場合、 重複検索では一意のファイルとハードリンクされたファイルのみが表示されます。.\nsettings_duplicates_minimal_size_entry_tooltip = \n    キャッシュされるファイルの最小サイズを設定します。\n    \n    値を小さくするとキャッシュが生成されるレコードが増え検索が高速化しますが、キャッシュの読み込みと保存が遅くなります。.\nsettings_duplicates_prehash_checkbutton_tooltip = \n    プレハッシュ(ファイルの一部から計算したハッシュ) のキャッシュを有効にし、重複していない検索結果をより早く捨てられるようにします。\n    \n    いくつかの場面では低速化の要因になりうるので、この機能はデフォルトでは無効になっています。\n    \n    数十万・数百万のファイルをスキャンする場合には、検索を何倍も高速化できるため使用を強く推奨します。.\nsettings_duplicates_prehash_minimal_entry_tooltip = キャッシュされたエントリの最小サイズ。.\nsettings_duplicates_hide_hard_link_button = ハードリンクを隠す\nsettings_duplicates_prehash_checkbutton = プリハッシュキャッシュを使用\nsettings_duplicates_minimal_size_cache_label = キャッシュに保存するファイルの最小サイズ（バイト単位）\nsettings_duplicates_minimal_size_cache_prehash_label = プリハッシュキャッシュに保存するファイルの最小サイズ（バイト単位）\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = 現在の設定をファイルに保存します。.\nsettings_loading_button_tooltip = 設定をファイルから読み込み、現在の設定を置き換えます。.\nsettings_reset_button_tooltip = 設定をデフォルトにリセットします。.\nsettings_saving_button = 設定を保存\nsettings_loading_button = 構成を読み込む\nsettings_reset_button = 設定をリセット\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip = \n    キャッシュを持つtxtファイルが保存されているフォルダを開きます。\n    \n    これらのファイルを変更すると不正な結果を表示することがありますが、パスなどを変更することで、大量のファイルを別の場所に移動する際の時間を短縮することができます。\n    \n    このファイルをコンピュータ間でコピーすることで、再度ファイルをスキャンするときの時間を節約できます（もちろん、コンピュータのディレクトリ構造が似ている場合に限り）。\n    \n    キャッシュに問題がある場合、このファイルを削除することができます。アプリは自動的にそれらを再生成します。.\nsettings_folder_settings_open_tooltip = \n    Czkawkaの設定ファイルが保存されているフォルダを開きます。\n    \n    警告: 手動で変更すると、ワークフローが壊れる可能性があります。.\nsettings_folder_cache_open = キャッシュフォルダーを開く\nsettings_folder_settings_open = 設定フォルダを開く\n# Compute results\ncompute_stopped_by_user = 検索はユーザーによって停止されました\ncompute_found_duplicates_hash_size = { $number_files } 重複を { $number_groups } グループで { $size } を { $time } で見つけました\ncompute_found_duplicates_name = { $number_files } 重複が { $number_groups } グループの { $time } で見つかりました\ncompute_found_empty_folders = 空のフォルダが { $number_files } 個見つかりました ({ $time })\ncompute_found_empty_files = 空のファイルが { $number_files } 個見つかりました ({ $time })\ncompute_found_big_files = 大きなファイルが { $number_files } 個見つかりました ({ $time })\ncompute_found_temporary_files = 一時ファイルが { $number_files } 個見つかりました ({ $time })\ncompute_found_images = 同じ画像を{ $number_files }枚見つけました。これらは{ $number_groups }グループに分かれ、{ $time }で検索しました。\ncompute_found_videos = { $number_files }個似た動画を{ $number_groups }グループ中で{ $time }に見つけました\ncompute_found_music = { $number_files }枚の似た音楽ファイルを{ $number_groups }グループ中で{ $time }に発見しました\ncompute_found_invalid_symlinks = 無効なシンボリックリンクが { $number_files } 個見つかりました ({ $time })\ncompute_found_broken_files = 壊れたファイルが { $number_files } 個見つかりました ({ $time })\ncompute_found_bad_extensions = { $number_files } で無効な拡張子を持つ { $time }ファイルが見つかりました\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] スキャン済み { $file_number } ファイル\n       *[other] スキャン済み { $file_number } ファイル\n    }\nprogress_scanning_extension_of_files = { $file_checked }/{ $all_files } ファイルの拡張子をチェックしました\nprogress_scanning_broken_files = { $file_checked }/{ $all_files } ファイル ({ $data_checked }/{ $all_data } ) をチェックしました。\nprogress_scanning_video = { $file_checked }/{ $all_files } ビデオのハッシュ化\nprogress_creating_video_thumbnails = { $file_checked }/{ $all_files } ビデオのサムネイルを作成しました\nprogress_scanning_image = { $file_checked }/{ $all_files } イメージ ({ $data_checked }/{ $all_data } ) のハッシュ化\nprogress_comparing_image_hashes = { $file_checked }/{ $all_files } の画像ハッシュ比較\nprogress_scanning_music_tags_end = { $file_checked }/{ $all_files } 音楽ファイルのタグの比較\nprogress_scanning_music_tags = { $file_checked }/{ $all_files } 音楽ファイルのタグを読む\nprogress_scanning_music_content_end = { $file_checked }/{ $all_files } 音楽ファイルの指紋と比較\nprogress_scanning_music_content = { $file_checked }/{ $all_files } 音楽ファイルの計算フィンガープリント ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Scanned { $folder_number } folder\n       *[other] Scanned { $folder_number } folders\n    }\nprogress_scanning_size = { $file_number } ファイルのスキャンされたサイズ\nprogress_scanning_size_name = スキャンされた名前と { $file_number } ファイルのサイズ\nprogress_scanning_name = スキャンされた { $file_number } ファイルの名前\nprogress_analyzed_partial_hash = { $file_checked }/{ $all_files } 個のファイルの部分ハッシュを分析しました ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = { $file_checked }/{ $all_files } 個のファイルの完全ハッシュを分析しました ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = プレハッシュキャッシュを読み込み中\nprogress_prehash_cache_saving = プレハッシュキャッシュを保存しています\nprogress_hash_cache_loading = ハッシュキャッシュを読み込み中\nprogress_hash_cache_saving = ハッシュキャッシュを保存中\nprogress_cache_loading = キャッシュを読み込み中\nprogress_cache_saving = キャッシュを保存中\nprogress_current_stage = 現在のステージ:{ \"  \" }\nprogress_all_stages = すべてのステージ:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = ファイル { $name } に設定を保存しました。.\nsaving_loading_saving_failure = 設定データをファイル { $name }、理由 { $reason } に保存できませんでした。.\nsaving_loading_reset_configuration = 現在の設定がクリアされました。.\nsaving_loading_loading_success = 設定の読み込みが正常に完了しました。.\nsaving_loading_failed_to_create_config_file = 設定ファイル \"{ $path }\" の作成に失敗しました、理由 \"{ $reason }\"。.\nsaving_loading_failed_to_read_config_file = 存在しないか設定ファイルでないため、\"{ $path }\" から設定を読み込めません。.\nsaving_loading_failed_to_read_data_from_file = ファイル \"{ $path }\" からデータを読み取ることができません、理由 \"{ $reason } \"。.\n# Other\nselected_all_reference_folders = すべてのディレクトリが参照フォルダとして設定されている場合、検索を開始できません\nsearching_for_data = データを検索中、しばらくお待ちください...\ntext_view_messages = メッセージ\ntext_view_warnings = 警告\ntext_view_errors = エラー\nabout_window_motto = このプログラムは自由に使用することができます、常に。.\nkrokiet_new_app = Czkawkaはメンテナンスモードであるため、重大なバグのみが修正され、新機能は追加されません。 新機能については、より安定しており、パフォーマンスが高く、積極的な開発中の新しいKrokietアプリをご覧ください。.\n# Various dialog\ndialogs_ask_next_time = 次回に確認\nsymlink_failed = { $name } から { $target }へのシンボリックリンクに失敗しました , 理由 { $reason }\ndelete_title_dialog = 削除の確認\ndelete_question_label = ファイルを削除してもよろしいですか？\ndelete_all_files_in_group_title = グループ内のすべてのファイルを削除することの確認\ndelete_all_files_in_group_label1 = いくつかのグループでは、すべてのレコードが選択されています。.\ndelete_all_files_in_group_label2 = 本当に削除しますか？\ndelete_items_label = { $items } ファイルが削除されます。.\ndelete_items_groups_label = { $groups } グループから { $items } 個のファイルが削除されます。.\nhardlink_failed = { $name } から { $target }へのハードリンクに失敗しました , 理由 { $reason }\nhard_sym_invalid_selection_title_dialog = いくつかのグループで無効な選択です\nhard_sym_invalid_selection_label_1 = いくつかのグループでは一つのレコードしか選択されていないため、それらは無視されます。.\nhard_sym_invalid_selection_label_2 = これらのファイルをハード/シンボリックにリンクできるようにするには、グループ内の少なくとも2つの結果を選択する必要があります。.\nhard_sym_invalid_selection_label_3 = グループ内で最初のものはオリジナルとして認識され変更されませんが、二つ目以降は変更されます。.\nhard_sym_link_title_dialog = リンクの確認\nhard_sym_link_label = このファイルをリンクしてもよろしいですか？\nmove_folder_failed = フォルダ { $name } の移動に失敗しました、理由 { $reason }\nmove_file_failed = ファイル { $name } を移動できませんでした、理由 { $reason }\nmove_files_title_dialog = 重複したファイルの移動先フォルダを選択\nmove_files_choose_more_than_1_path = 重複したファイルをコピーするには、1つのパスのみを選択する必要があります、{ $path_number } つ選択されました。.\nmove_stats = { $num_files }/{ $all_files } アイテムを適切に移動しました\nsave_results_to_file = txtファイルとjsonファイルの両方を\"{ $name }\"フォルダに保存しました。.\nsearch_not_choosing_any_music = エラー: 音楽検索タイプのチェックボックスを少なくとも1つ選択する必要があります。.\nsearch_not_choosing_any_broken_files = エラー: チェックされた壊れたファイルの種類のチェックボックスを少なくとも1つ選択する必要があります。.\ninclude_folders_dialog_title = 含めるフォルダ\nexclude_folders_dialog_title = 除外するフォルダ\ninclude_manually_directories_dialog_title = ディレクトリを手動で追加\ncache_properly_cleared = キャッシュを適切にクリアしました\ncache_clear_duplicates_title = 重複したキャッシュをクリアする\ncache_clear_similar_images_title = 類似した画像のキャッシュをクリア中\ncache_clear_similar_videos_title = 類似した動画のキャッシュをクリア中\ncache_clear_message_label_1 = 古いエントリのキャッシュを消去しますか？\ncache_clear_message_label_2 = この操作は無効なファイルを指すすべてのキャッシュエントリを削除します。.\ncache_clear_message_label_3 = これはキャッシュへの読み込みと保存を少し高速化することがあります。.\ncache_clear_message_label_4 = 警告: 操作により、現在接続されていない外部ドライブからキャッシュされたすべてのデータが削除されます。そのため、それらのハッシュは再度生成する必要があります。.\n# Show preview\npreview_image_resize_failure = 画像 { $name } のリサイズに失敗しました。.\npreview_image_opening_failure = イメージ { $name } を開けませんでした、理由 { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = グループ { $current_group }/{ $all_groups } ({ $images_in_group } 画像)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/ko/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = 설정\nwindow_main_title = Czkawka (구글ить)\nwindow_progress_title = 스캔중\nwindow_compare_images = 이미지 비교\n# General\ngeneral_ok_button = 확인\ngeneral_close_button = 닫기\n# Krokiet info dialog\nkrokiet_info_title = Introducing Krokiet - 새로운 버전의 Czkawka\nkrokiet_info_message = \n        크로키트는 Czkawka GTK GUI의 새로운, 개선된, 더 빠르고 더 안정적인 버전입니다!\n\n        실행하기가 더 쉽고 시스템 변경에 더 강하며, 대부분의 시스템에서 기본적으로 사용 가능한 핵심 라이브러리에만 의존합니다.\n\n        크로키트는 Czkawka에 없는 기능도 제공하며, 비디오 비교 모드에서 미리보기, EXIF 클리너, 파일 이동/복사/삭제 진행률 또는 확장된 정렬 옵션 등이 포함됩니다.\n\n        사용해 보고 차이점을 확인해 보세요!\n\n        Czkawka는 저로부터 버그 수정 및 소규모 업데이트를 계속 받겠지만, 모든 새로운 기능은 크로키트에만 개발되며, 누구나 새로운 기능 추가, 누락된 모드 확장 또는 Czkawka 추가 확장을 자유롭게 기여할 수 있습니다.\n\n        PS: 이 메시지는 한 번만 표시되어야 합니다. 다시 나타나면 CZKAWKA_DONT_ANNOY_ME 환경 변수를 비어 있는 값이 아닌 값으로 설정하십시오.\n# Main window\nmusic_title_checkbox = 제목\nmusic_artist_checkbox = 아티스트\nmusic_year_checkbox = 연도\nmusic_bitrate_checkbox = 비트레이트\nmusic_genre_checkbox = 장르\nmusic_length_checkbox = 길이\nmusic_comparison_checkbox = 근사값 비교\nmusic_checking_by_tags = 태그 기준 검사\nmusic_checking_by_content = 내용 기준 검사\nsame_music_seconds_label = 최소 조각 재생 시간\nsame_music_similarity_label = 최대 허용 차이\nmusic_compare_only_in_title_group = 유사한 제목들의 그룹 내에서 비교\nmusic_compare_only_in_title_group_tooltip =\n    활성화 시, 파일이 제목별로 그룹화된 후에만 서로 비교됩니다.\n    \n    예: 10000개의 파일이 있을 경우, 거의 1억 번의 비교 대신 보통 약 20000번의 비교로 줄어듭니다.\nsame_music_tooltip =\n    음악 파일 유사도 검색은 아래 설정으로 조정할 수 있습니다:\n    \n    - 유사도로 식별 가능한 최소 조각 시간\n    - 비교할 조각간 허용 가능한 최대 차이 수치\n    \n    좋은 결과를 얻기 위해서는 이 두 값을 상황에 맞게 적절히 조합하는 것이 중요합니다.\n    \n    최소 시간 5초 + 최대 차이 1.0 설정 시 -> 거의 동일한 조각을 찾습니다.\n    최소 시간 20초 + 최대 차이 6.0 설정 시 -> 리믹스/라이브 버전 등 유사한 경우에 효과적입니다.\n    \n    기본적으로 모든 음악 파일끼리 비교하게 되므로, 많은 파일을 비교할 때 시간이 오래 걸릴 수 있습니다.  \n    따라서 일반적으로 **참조 폴더(reference folders)** 옵션을 사용하고 비교할 파일을 지정하면,  \n    지문(fingerprint) 비교는 참조 없이 비교하는 것보다 **최소 4배 빠르게** 진행됩니다.\nmusic_comparison_checkbox_tooltip =\n    기계학습을 통해 각 항목의 괄호를 제거합니다. 예를 들어, 다음 두 파일은 같은 파일로 인식될 것입니다.\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = 대소문자 구분\nduplicate_case_sensitive_name_tooltip =\n    대소문자 구분이 켜져 있으면, 완전히 같은 이름만이 중복 파일로 검색됩니다. 예시: Żołd <-> Żołd\n    \n    대소문자 구분이 꺼져 있으면, 대문자와 소문자 구별을 하지 않고 중복 파일을 검색합니다. 예시: żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = 크기 및 이름 기준\nduplicate_mode_name_combo_box = 파일명\nduplicate_mode_size_combo_box = 파일 크기\nduplicate_mode_hash_combo_box = 해시\nduplicate_hash_type_tooltip =\n    Czkawka는 3가지 유형의 해시 함수를 지원합니다.\n    \n    Blake3 - 암호화에 사용되는 해시입니다. 매우 빠르게 작동하므로, 기본값으로 설정되어 있습니다.\n    \n    CRC32 - 간단한 해시 함수입니다. Blake3보다는 빠르지만, 매우 드물게 충돌이 발생합니다.\n    \n    XXH3 - Black3와 해시 품질 및 성능 면에서 매우 유사하지만, 암호화에 쓰이지는 않습니다. 때문에 Black3와 실질적으로 같습니다.\nduplicate_check_method_tooltip =\n    현재 Czkawka는 중복 파일을 찾는데 3가지 방법을 지원합니다.\n    \n    파일명 - 같은 이름을 가진 파일들을 찾습니다.\n    \n    파일 크기 - 같은 크기를 가진 파일들을 찾습니다.\n    \n    해시 - 같은 내용을 가진 파일들을 찾습니다. 이 모드에서는 먼저 파일을 해시한 다음, 각 해시값들을 비교하여 중복 파일인지 식별합니다. 때문에 중복 파일을 찾는 데 있어 가장 확실한 방법입니다. Czkawka는 캐시에 매우 의존하므로, 같은 데이터를 두 번째 이후로 스캔하는 경우 첫 번째 스캔보다 더욱 빠르게 스캔이 이루어집니다.\nimage_hash_size_tooltip =\n    각 확인된 이미지에 특별한 해시가 생성되어 서로 비교될 수 있으며,  \n    작은 해시 차이는 이미지가 유사함을 의미합니다.\n    \n    해시 크기 8은 원본과 약간 유사한 이미지를 찾기에 적절합니다.  \n    다만 이미지 수가 많을 경우(예: 1000개 이상), 거짓 양성(false positives)이 많이 발생할 수 있어  \n    이 경우 더 큰 해시 크기 사용을 권장합니다.\n    \n    기본 해시 크기 16은 유사 이미지 검색과 해시 충돌 최소화를 적절히 균형 잡은 설정입니다.\n    \n    해시 크기 32 또는 64는 매우 유사한 이미지만 찾아내며, (알파 채널이 있는 일부 이미지 제외하면)  \n    거의 거짓 양성이 없습니다.\nimage_resize_filter_tooltip =\n    이미지 해시 계산 전에 라이브러리가 먼저 이미지를 리사이징해야 합니다.\n    \n    선택된 알고리즘에 따라 해시 계산에 사용하는 이미지의 형태가 약간 달라집니다.\n    \n    가장 빠른 알고리즘은 `Nearest`이며, 가장 낮은 화질을 제공하지만  \n    기본 해시 크기 16x16일 경우 품질 저하가 눈에 잘 띄지 않습니다.\n    \n    이미지 수가 적고 해시 크기 8x8을 사용할 경우,  \n    `Nearest`보다 다른 알고리즘을 사용하면 더 정확한 그룹핑에 도움이 됩니다.\nimage_hash_alg_tooltip =\n    해시를 계산하는 데 사용되는 알고리즘을 선택할 수 있습니다.\n    \n    각각의 알고리즘은 장단점이 있으므로, 경우마다 더 낫거나 더 나쁜 결과를 보여줄 수 있습니다.\n    \n    따라서 가장 좋은 알고리즘을 찾으려면 수동으로 테스트해 보는 것이 좋습니다.\nbig_files_mode_combobox_tooltip = 가장 큰 파일 또는 가장 작은 파일을 찾을 수 있습니다\nbig_files_mode_label = 찾을 파일\nbig_files_mode_smallest_combo_box = 작은 파일\nbig_files_mode_biggest_combo_box = 큰 파일\nmain_notebook_duplicates = 중복 파일\nmain_notebook_empty_directories = 빈 디렉터리\nmain_notebook_big_files = 큰 파일\nmain_notebook_empty_files = 빈 파일\nmain_notebook_temporary = 임시 파일\nmain_notebook_similar_images = 비슷한 이미지\nmain_notebook_similar_videos = 비슷한 영상\nmain_notebook_same_music = 중복 음악\nmain_notebook_symlinks = 잘못된 심볼릭 링크\nmain_notebook_broken_files = 손상된 파일\nmain_notebook_bad_extensions = 잘못된 확장자\nmain_tree_view_column_file_name = 파일명\nmain_tree_view_column_folder_name = 폴더명\nmain_tree_view_column_path = 경로\nmain_tree_view_column_modification = 수정한 날짜\nmain_tree_view_column_size = 파일 크기\nmain_tree_view_column_similarity = 유사도\nmain_tree_view_column_dimensions = 크기\nmain_tree_view_column_title = 제목\nmain_tree_view_column_artist = 아티스트\nmain_tree_view_column_year = 연도\nmain_tree_view_column_bitrate = 비트레이트\nmain_tree_view_column_length = 길이\nmain_tree_view_column_genre = 장르\nmain_tree_view_column_symlink_file_name = 심볼릭 링크 파일명\nmain_tree_view_column_symlink_folder = 심볼릭 링크 폴더\nmain_tree_view_column_destination_path = 심볼릭 링크 대상 경로\nmain_tree_view_column_type_of_error = 손상 유형\nmain_tree_view_column_current_extension = 현재 확장자\nmain_tree_view_column_proper_extensions = 올바른 확장자\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = 코덱\nmain_label_check_method = 확인 방법\nmain_label_hash_type = 해시 유형\nmain_label_hash_size = 해시 크기\nmain_label_size_bytes = 파일 크기 (바이트)\nmain_label_min_size = 최소\nmain_label_max_size = 최대\nmain_label_shown_files = 찾을 파일의 개수\nmain_label_resize_algorithm = 크기 변경 알고리즘\nmain_label_similarity = 유사도{ \"   \" }\nmain_check_box_broken_files_audio = 음악 파일\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = 압축 파일\nmain_check_box_broken_files_image = 이미지\nmain_check_box_broken_files_video = 비디오\nmain_check_box_broken_files_video_tooltip = ffmpeg/ffprobe를 사용하여 비디오 파일 유효성 검사합니다. 상당히 느리고 파일이 잘 재생되더라도 형식에 민감한 오류를 감지할 수 있습니다.\ncheck_button_general_same_size = 같은 파일크기 무시\ncheck_button_general_same_size_tooltip = 동일한 크기의 파일은 결과에서 제외합니다 – 대부분 1:1 중복일 가능성이 높습니다\nmain_label_size_bytes_tooltip = 스캔할 파일의 크기입니다\n# Upper window\nupper_tree_view_included_folder_column_title = 검색할 폴더\nupper_tree_view_included_reference_column_title = 기준 폴더\nupper_recursive_button = 재귀\nupper_recursive_button_tooltip = 켜져 있으면, 하위 폴더 내부의 파일까지 검색합니다.\nupper_manual_add_included_button = 수동 추가\nupper_add_included_button = 추가\nupper_remove_included_button = 제거\nupper_manual_add_excluded_button = 수동 추가\nupper_add_excluded_button = 추가\nupper_remove_excluded_button = 제거\nupper_manual_add_included_button_tooltip = \n    직접 검색할 경로를 입력합니다.\n    \n    여러 경로를 입력하고자 한다면, ';'로 구분하세요.\n    \n    '/home/roman;/home/rozkaz' 를 입력하면, '/home/roman'와 '/home/rozkaz'가 추가됩니다\nupper_add_included_button_tooltip = 검색할 디렉터리를 추가합니다.\nupper_remove_included_button_tooltip = 검색할 디렉터리에서 제거합니다.\nupper_manual_add_excluded_button_tooltip = \n    직접 제외할 경로를 입력합니다.\n    \n    여러 경로를 입력하고자 한다면, ';'로 구분하세요.\n    \n    '/home/roman;/home/krokiet' 를 입력하면, '/home/roman'와 '/home/krokiet'가 추가됩니다\nupper_add_excluded_button_tooltip = 제외할 디렉터리를 추가합니다.\nupper_remove_excluded_button_tooltip = 제외할 디렉터리에서 제거합니다.\nupper_notebook_items_configuration = 항목 설정\nupper_notebook_excluded_directories = 제외 경로\nupper_notebook_included_directories = 포함된 경로\nupper_allowed_extensions_tooltip =\n    허용할 확장자는 콤마(',')를 통해 구분해야 합니다. (기본값인 경우 모든 확장자를 허용합니다.)\n    \n    IMAGE, VIDEO, MUSIC, TEXT를 입력할 경우 해당하는 파일을 모두 지칭할 수 있습니다.\n    \n    예시: \".exe, IMAGE, VIDEO, .rar, 7z\" - 이와 같이 입력하면, 이미지 파일(예. jpg, png), 영상 파일(예. avi, mp4), exe, rar, 그리고 7z 파일을 검색합니다.\nupper_excluded_extensions_tooltip =\n    검사에서 무시될 비활성화된 파일 목록입니다.\n    \n    허용된 확장자와 비활성화된 확장자를 둘 다 사용할 경우, 비활성화된 확장자가 더 높은 우선순위를 가지므로 해당 파일은 검사되지 않습니다.\nupper_excluded_items_tooltip = \n        제외 항목은 * 와일드카드와 쉼표로 구분되어야 합니다.\n        이는 Excluded Paths 보다 느리므로 주의해서 사용하십시오.\nupper_excluded_items = 제외할 항목:\nupper_allowed_extensions = 허용할 확장자:\nupper_excluded_extensions = 비활성 확장자:\n# Popovers\npopover_select_all = 모두 선택\npopover_unselect_all = 모두 선택 해제\npopover_reverse = 선택 반전\npopover_select_all_except_shortest_path = 선택 모두 제외 짧은 경로\npopover_select_all_except_longest_path = 선택 전부 제외 가장 긴 경로\npopover_select_all_except_oldest = 가장 오래된 파일 제외하고 모두 선택\npopover_select_all_except_newest = 가장 최신인 파일 제외하고 모두 선택\npopover_select_one_oldest = 가장 오래된 파일 선택\npopover_select_one_newest = 가장 최신인 파일 선택\npopover_select_custom = 사용자 지정 선택\npopover_unselect_custom = 사용자 지정 선택 해제\npopover_select_all_images_except_biggest = 가장 큰 파일 제외하고 모두 선택\npopover_select_all_images_except_smallest = 가장 작은 파일 제외하고 모두 선택\npopover_custom_path_check_button_entry_tooltip = \n    경로를 기준으로 선택합니다.\n    \n    사용 예시:\n    '/home/pimpek/rzecz.txt' 파일을 선택하려면 '/home/pim*'와 같이 입력하세요\npopover_custom_name_check_button_entry_tooltip = \n    파일 이름을 기준으로 선택합니다.\n    \n    사용 예시:\n    '/usr/ping/pong.txt' 파일을 선택하려면 '*ong*'와 같이 입력하세요\npopover_custom_regex_check_button_entry_tooltip =\n    정규표현식을 이용해 선택합니다.\n    \n    이 모드에서는 경로와 이름 모두가 정규표현식에 의해 검색됩니다.\n    \n    사용 예시:\n    '/usr/bin/ziemniak.txt' 파일을 선택하려면 '/ziem[a-z]+'와 같이 입력하세요.\n    \n    정규 표현식은 Rust 언어에 내장된 구현체를 사용합니다. 더 알고 싶다면 https://docs.rs/regex를 방문하세요.\npopover_custom_case_sensitive_check_button_tooltip =\n    대소문자를 구분할 지 여부를 선택합니다.\n    \n    만일 꺼져 있으면, '/home/*'은 '/HoMe/roman'과 '/home/roman'를 모두 선택합니다.\npopover_custom_not_all_check_button_tooltip = \n    한 그룹에 있는 모든 항목이 선택되는 것을 방지합니다.\n    \n    이 옵션은 기본적으로 켜져 있습니다. 대부분의 경우, 원본과 중복 파일을 전부 선택하여 삭제하는 것은 원하지 않는 동작일 것입니다. 즉 각 그룹에서 최소한 하나의 항목은 삭제하지 않고 남겨놓게 됩니다.\n    \n    경고! 이 설정은 수동으로 그룹의 모든 파일을 이미 선택해 놓았다면 작동하지 않습니다!.\npopover_custom_regex_path_label = 경로\npopover_custom_regex_name_label = 파일명\npopover_custom_regex_regex_label = 경로 및 파일 정규표현식\npopover_custom_case_sensitive_check_button = 대소문자 구별\npopover_custom_all_in_group_label = 그룹의 모든 항목을 선택하지 않음\npopover_custom_mode_unselect = 사용자 지정 선택 해제\npopover_custom_mode_select = 사용자 지정 선택\npopover_sort_file_name = 파일 이름\npopover_sort_folder_name = 폴더 이름\npopover_sort_full_name = 본인 이름\npopover_sort_size = 파일 크기\npopover_sort_selection = 선택\npopover_invalid_regex = 정규표현식이 유효하지 않습니다\npopover_valid_regex = 정규표현식이 유효합니다\n# Bottom buttons\nbottom_search_button = 검색\nbottom_select_button = 선택\nbottom_delete_button = 삭제\nbottom_save_button = 저장\nbottom_symlink_button = 심볼릭 링크\nbottom_hardlink_button = 하드 링크\nbottom_move_button = 이동\nbottom_sort_button = 종류\nbottom_compare_button = 비교\nbottom_search_button_tooltip = 검색을 시작합니다\nbottom_select_button_tooltip = 항목을 선택합니다. 오직 선택된 것만이 처리됩니다.\nbottom_delete_button_tooltip = 선택된 파일 또는 폴더를 삭제합니다.\nbottom_save_button_tooltip = 검색 결과를 파일로 저장합니다\nbottom_symlink_button_tooltip =\n    심볼릭 링크를 생성합니다.\n    그룹 내에서 최소한 2개의 파일이 선택되어 있어야 합니다.\n    첫 번째 파일은 그대로 남으며, 두 번째 이후 파일은 첫 번째 파일로 향하는 심볼릭 링크가 됩니다.\nbottom_hardlink_button_tooltip =\n    하드 링크를 생성합니다.\n    그룹 내에서 최소한 2개의 파일이 선택되어 있어야 합니다.\n    첫 번째 파일은 그대로 남으며, 두 번째 이후 파일은 첫 번째 파일로 향하는 하드 링크가 됩니다.\nbottom_hardlink_button_not_available_tooltip =\n    하드 링크를 생성합니다.\n    현재 하드 링크를 만들 수 없어 버튼이 비활성화되었습니다.\n    Windows에서 하드 링크는 관리자 권한으로만 만들 수 있습니다. 프로그램이 관리자 권한으로 실행되었는지 확인하세요.\n    만일 프로그램이 이미 관리자 권한으로 실행되었다면, Github에서 비슷한 이슈가 있는지 확인해보세요.\nbottom_move_button_tooltip =\n    선택된 디렉터리로 파일을 이동합니다.\n    이 동작은 원본이 위치한 경로를 전부 무시하고, 선택한 경로로 파일을 전부 복사합니다.\n    만일 2개 이상의 파일이 같은 이름을 가지고 있다면, 첫 번째 이후의 파일은 복사에 실패하고 오류 메시지를 보여줄 것입니다.\nbottom_sort_button_tooltip = 파일/폴더를 선택한 방법으로 정렬합니다.\nbottom_compare_button_tooltip = 그룹 내의 이미지를 비교합니다.\nbottom_show_errors_tooltip = 하단 텍스트 패널을 보이거나 숨깁니다.\nbottom_show_upper_notebook_tooltip = 상단 패널을 보이거나 숨깁니다.\n# Progress Window\nprogress_stop_button = 정지\nprogress_stop_additional_message = 정지 요청됨\n# About Window\nabout_repository_button_tooltip = 소스 코드가 있는 리포지토리 페이지 링크입니다.\nabout_donation_button_tooltip = 기부 페이지 링크입니다.\nabout_instruction_button_tooltip = 사용방법 페이지 링크입니다.\nabout_translation_button_tooltip = 번역을 위한 Crowdin 페이지 링크입니다. 공식적으로 지원되는 언어는 폴란드어와 영어입니다.\nabout_repository_button = 리포지토리\nabout_donation_button = 기부\nabout_instruction_button = 사용방법\nabout_translation_button = 번역\n# Header\nheader_setting_button_tooltip = 설정창을 엽니다.\nheader_about_button_tooltip = 이 앱에 대한 정보창을 엽니다.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = 스레드 수\nsettings_number_of_threads_tooltip = 사용할 스레드 수입니다. 0이면 가능한 최대 스레드를 사용합니다.\nsettings_use_rust_preview = 미리보기에 GTK 대신 외부 라이브러리 사용\nsettings_use_rust_preview_tooltip =\n    GTK 미리보기는 일부 경우 더 빠르거나 더 많은 형식을 지원하지만,  \n    반대로 성능이 더 떨어질 수도 있습니다.\n    \n    미리보기 로딩에 문제가 있다면 이 설정을 변경해 보세요.\n    \n    리눅스가 아닌 환경에서는 `gtk-pixbuf`가 항상 사용 가능하지 않기 때문에  \n    이 옵션을 끄면 일부 이미지 미리보기가 로드되지 않을 수 있습니다.\nsettings_label_restart = 이 설정을 적용하려면 프로그램을 재시작해야 합니다!\nsettings_ignore_other_filesystems = 다른 파일시스템 무시(Linux에서만)\nsettings_ignore_other_filesystems_tooltip = \n    검색할 디렉터리와 파일시스템이 다른 디렉터리를 무시합니다.\n    \n    Linux의 find 명령에서 -xdev 옵션을 준 것과 동일하게 동작합니다\nsettings_save_at_exit_button_tooltip = 프로그램 종료 시에 설정을 저장합니다.\nsettings_load_at_start_button_tooltip =\n    프로그램을 열 때 저장된 설정을 불러옵니다.\n    \n    꺼져 있다면, 기본 설정으로 프로그램을 시작합니다.\nsettings_confirm_deletion_button_tooltip = 삭제 버튼을 누를 때 확인창을 띄웁니다.\nsettings_confirm_link_button_tooltip = 하드 링크/심볼릭 링크 버튼을 누를 때 확인창을 띄웁니다.\nsettings_confirm_group_deletion_button_tooltip = 그룹의 모든 항목을 삭제할 경우 경고창을 보여줍니다.\nsettings_show_text_view_button_tooltip = UI 하단에 텍스트 패널을 보여줍니다.\nsettings_use_cache_button_tooltip = 파일 캐시를 사용합니다.\nsettings_save_also_as_json_button_tooltip = 캐시를 (사람이 읽을 수 있는) JSON 포맷으로 저장합니다. 캐시 내용을 수정할 수 있습니다. 만일 bin 확장자를 가진 바이너리 캐시 파일이 없으면, JSON 캐시가 프로그램 시작 시에 대신 로드됩니다.\nsettings_use_trash_button_tooltip = 파일을 영구 삭제하는 대신 휴지통으로 이동합니다.\nsettings_language_label_tooltip = UI에 표시될 언어를 설정합니다.\nsettings_save_at_exit_button = 프로그램을 닫을 때 설정을 저장\nsettings_load_at_start_button = 프로그램을 열 때 설정을 불러오기\nsettings_confirm_deletion_button = 항목 삭제 시에 확인창 띄우기\nsettings_confirm_link_button = 항목 심볼릭 링크/하드 링크 설정시에 확인창 띄우기\nsettings_confirm_group_deletion_button = 그룹 내의 모든 항목 삭제 시 경고창 띄우기\nsettings_show_text_view_button = 하단 텍스트 패널 표시하기\nsettings_use_cache_button = 캐시 사용\nsettings_save_also_as_json_button = 캐시를 JSON 포맷으로도 저장\nsettings_use_trash_button = 삭제된 파일을 휴지통으로 이동\nsettings_language_label = 언어\nsettings_multiple_delete_outdated_cache_checkbutton = 만료된 파일을 캐시에서 자동으로 삭제\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    더 이상 존재하지 않는 파일에 대한 정보를 캐시에서 삭제합니다.\n    \n    이 옵션이 켜져 있으면, 프로그램은 존재하는 파일만이 캐시에 남도록 할 것입니다(망가진 파일은 무시됩니다).\n    \n    이 옵션을 끄는 것은 외장 저장장치에 존재하는 파일을 스캔했을 때, 외장 저장장치에 있는 파일에 대한 캐시를 보존하는 데 도움이 됩니다.\n    \n    만일 수백~수천 개의 파일에 해당하는 정보가 캐시에 있다면 이 옵션을 켜는 것을 추천합니다. 이 경우 캐시를 저장하거나 불러오는 시간이 빨라집니다.\nsettings_notebook_general = 일반\nsettings_notebook_duplicates = 중복 파일\nsettings_notebook_images = 유사한 이미지\nsettings_notebook_videos = 유사한 영상\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = 이미지 파일을 선택하면 우측에 미리보기를 보여줍니다.\nsettings_multiple_image_preview_checkbutton = 이미지 미리보기 표시\nsettings_multiple_clear_cache_button_tooltip =\n    더 이상 존재하지 않는 파일을 캐시에서 제거합니다.\n    캐시를 자동으로 정리하는 옵션이 꺼져 있을 때만 사용하세요.\nsettings_multiple_clear_cache_button = 캐시에서 오래된 결과 제거.\n \n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    하나의 파일에 대한 여러 개의 하드 링크가 존재할 경우, 그 중 하나만을 표시합니다.\n    \n    예: 만일 특정한 파일에 대한 7개의 하드 링크가 디스크에 존재하고, 그 중 하나가 다른 inode를 갖는다면, 결과창에는 1개의 파일과 1개의 하드 링크만이 표시됩니다.\nsettings_duplicates_minimal_size_entry_tooltip =\n    캐시에 추가되기 위한 최소 파일 사이즈를 설정합니다.\n    \n    이 값이 작을 수록 더 많은 파일이 캐시에 저장됩니다. 이 경우 검색은 더 빨라지지만, 캐시 저장 및 불러오기는 느려집니다.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    사전 해시(파일 일부만으로 계산되는 해시)에 대한 캐싱을 허용하여, 중복이 아닌 파일을 더 빠르게 결과에서 제거합니다.\n    \n    이 옵션은 일부 상황에서 검색을 느리게 하기 때문에, 기본적으로 꺼져 있습니다.\n    \n    만일 수백~수천 개 이상의 파일처럼 매우 많은 파일을 여러 번 검색하는 경우, 이 기능을 반드시 켜는 것을 추천합니다.\nsettings_duplicates_prehash_minimal_entry_tooltip = 캐싱을 위한 최소 파일크기입니다.\nsettings_duplicates_hide_hard_link_button = 하드 링크 숨기기\nsettings_duplicates_prehash_checkbutton = 사전 해시 캐싱하기\nsettings_duplicates_minimal_size_cache_label = 캐싱하기 위한 최소 파일 크기 (바이트)\nsettings_duplicates_minimal_size_cache_prehash_label = 사전 해시를 캐싱하기 위한 최소 파일 크기 (바이트)\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = 현재 설정을 파일에 저장합니다.\nsettings_loading_button_tooltip = 저장된 설정을 불러와 현재 설정을 덮어씁니다.\nsettings_reset_button_tooltip = 설정을 기본값으로 되돌립니다.\nsettings_saving_button = 설정 저장\nsettings_loading_button = 설정 불러오기\nsettings_reset_button = 설정 초기화\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    캐시 파일이 저장되는 폴더를 엽니다.\n    \n    캐시 파일을 편집하는 경우 유효하지 않은 결과가 표시될 수 있습니다. 다만, 많은 양의 파일이 다른 곳으로 이동되었다면 캐시 내의 경로를 수정하는 것이 도움이 됩니다.\n    \n    만일 비슷한 디렉터리 구조를 가지는 경우, 캐시 파일을 복사하여 다른 컴퓨터에서도 같은 캐시를 재활용할 수 있습니다.\n    \n    만일 캐시에 문제가 발생한다면 이 폴더의 파일들을 지우십시오. 그렇게 하면 프로그램이 다시 캐시 파일을 생성합니다.\nsettings_folder_settings_open_tooltip =\n    Czkawka의 설정 파일이 있는 폴더를 엽니다.\n    \n    경고! 설정 파일을 수동으로 편집하는 경우 원치 않는 동작이 일어날 수 있습니다.\nsettings_folder_cache_open = 캐시 폴더 열기\nsettings_folder_settings_open = 설정 폴더 열기\n# Compute results\ncompute_stopped_by_user = 사용자에 의해 검색이 중단됨\ncompute_found_duplicates_hash_size = { $number_files }개의 중복 파일을 { $number_groups }개 그룹에서 발견했으며 이는 { $size }를 차지하고 { $time }에 걸쳐 수행되었습니다\ncompute_found_duplicates_name = { $number_files } 개의 중복 파일을 { $number_groups } 그룹에서 { $time }에 발견했습니다\ncompute_found_empty_folders = { $number_files }개의 비어있는 폴더를 { $time } 안에 발견했습니다\ncompute_found_empty_files = { $number_files }개의 빈 파일을 { $time }에 발견했습니다\ncompute_found_big_files = { $number_files }개의 큰 파일을 { $time }에 찾았습니다\ncompute_found_temporary_files = { $number_files }개의 임시 파일을 { $time } 안에 찾았습니다\ncompute_found_images = { $number_files } 개의 유사한 이미지를 { $number_groups } 그룹에서 { $time } 내에 발견했습니다\ncompute_found_videos = { $number_files } 개의 비슷한 동영상을 { $number_groups } 그룹에서 { $time } 내에 찾았습니다\ncompute_found_music = { $number_files }개의 비슷한 음악 파일을 { $number_groups } 그룹에서 { $time } 내에 찾았습니다\ncompute_found_invalid_symlinks = { $number_files }개의 유효하지 않은 심볼릭 링크를 { $time }에서 찾았습니다\ncompute_found_broken_files = { $number_files }개의 봉인된 파일을 { $time }에 발견했습니다\ncompute_found_bad_extensions = { $number_files } 확장자에 문제가 있는 파일을 { $time } 내로找到了\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] { $file_number }개 파일 스캔 완료\n       *[other] { $file_number }개 파일 스캔 완료\n    }\nprogress_scanning_extension_of_files = { $file_checked }/{ $all_files }개의 파일 확장자 확인\nprogress_scanning_broken_files = { $file_checked }/{ $all_files }개 파일 확인 (데이터: { $data_checked } / { $all_data })\nprogress_scanning_video = { $file_checked }/{ $all_files }개의 비디오 해시 생성\nprogress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video\nprogress_scanning_image = { $file_checked }/{ $all_files }개의 이미지 해시 생성 (데이터: { $data_checked } / { $all_data })\nprogress_comparing_image_hashes = { $file_checked }/{ $all_files }개의 이미지 해시 비교\nprogress_scanning_music_tags_end = { $file_checked }/{ $all_files }개의 음악 파일 태그 비교 완료\nprogress_scanning_music_tags = { $file_checked }/{ $all_files }개의 음악 파일 태그 읽는 중\nprogress_scanning_music_content_end = { $file_checked }/{ $all_files }개의 음악 파일 지문 비교 완료\nprogress_scanning_music_content = { $file_checked }/{ $all_files }개의 음악 파일 지문 계산 중 (데이터: { $data_checked } / { $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] { $folder_number }개 폴더 스캔 완료\n       *[other] { $folder_number }개 폴더 스캔 완료\n    }\nprogress_scanning_size = { $file_number }개의 파일 크기 스캔 완료\nprogress_scanning_size_name = { $file_number }개의 파일 이름 및 크기 스캔 완료\nprogress_scanning_name = { $file_number }개의 파일 이름 스캔 완료\nprogress_analyzed_partial_hash = { $file_checked }/{ $all_files }개 파일 부분 해시 분석 완료 (데이터: { $data_checked } / { $all_data })\nprogress_analyzed_full_hash = { $file_checked }/{ $all_files }개 파일 전체 해시 분석 완료 (데이터: { $data_checked } / { $all_data })\nprogress_prehash_cache_loading = PreHash 캐시 로드 중\nprogress_prehash_cache_saving = PreHash 캐시 저장 중\nprogress_hash_cache_loading = 해시 캐시 로드 중\nprogress_hash_cache_saving = 해시 캐시 저장 중\nprogress_cache_loading = 캐시 로드 중\nprogress_cache_saving = 캐시 저장 중\nprogress_current_stage = 현재 단계:{ \"  \" }\nprogress_all_stages = 전체 단계:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = 파일 { $name }에 설정 저장함.\nsaving_loading_saving_failure = кон피그레이션 데이터를 파일 { $name }에 저장하지 못했습니다, 이유는 { $reason }입니다.\nsaving_loading_reset_configuration = 현재 설정이 초기화됨.\nsaving_loading_loading_success = 앱 설정 불러오기 성공.\nsaving_loading_failed_to_create_config_file = \"{ $path }\" 파일에 설정을 저장할 수 없습니다. 이유: \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = \"{ $path }\" 파일에서 설정을 불러올 수 없습니다. 파일이 없거나, 파일이 아닙니다.\nsaving_loading_failed_to_read_data_from_file = \"{ $path }\" 파일을 읽을 수 없습니다. 이유: \"{ $reason }\".\n# Other\nselected_all_reference_folders = 모든 디렉터리가 기준 폴더이므로, 검색을 시작할 수 없습니다\nsearching_for_data = 검색 중. 잠시만 기다려주세요...\ntext_view_messages = 알림\ntext_view_warnings = 경고\ntext_view_errors = 오류\nabout_window_motto = 이 프로그램은 무료이며, 앞으로도 항상 그럴 것이다.\nkrokiet_new_app = 'Czkawka'는 유지보수 모드에 있습니다,这意味着仅会修复关键错误且不会添加新功能。对于新功能，请查看新的Krokiet应用，该应用更加稳定性能更强并且仍在积极开发中。.\n# Various dialog\ndialogs_ask_next_time = 다음에도 묻기\nsymlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\ndelete_title_dialog = 삭제 확인\ndelete_question_label = 정말로 파일들을 삭제합니까?\ndelete_all_files_in_group_title = 그룹의 모든 파일 삭제 확인\ndelete_all_files_in_group_label1 = 일부 그룹 내에 있는 모든 파일이 선택되어 있습니다.\ndelete_all_files_in_group_label2 = 정말로 해당 파일을 모두 삭제합니까?\ndelete_items_label = { $items }개의 파일이 삭제됩니다.\ndelete_items_groups_label = { $groups }개 그룹에서 { $items }개의 파일이 삭제됩니다.\nhardlink_failed = 하드 링크를 생성하지 못했습니다 { $name }을 { $target }으로, 이유는 { $reason }입니다\nhard_sym_invalid_selection_title_dialog = 일부 그룹의 선택이 유효하지 않습니다\nhard_sym_invalid_selection_label_1 = 일부 그룹에서 1개의 항목만이 선택되었으며, 해당 항목은 무시됩니다.\nhard_sym_invalid_selection_label_2 = 하드 링크/심볼릭 링크를 생성하려면, 그룹 내에서 최소 2개의 파일이 선택되어야 합니다.\nhard_sym_invalid_selection_label_3 = 그룹 내의 첫 번째가 원본으로 설정되며, 나머지는 수정될 것입니다.\nhard_sym_link_title_dialog = 링크 생성 확인\nhard_sym_link_label = 정말로 해당 파일들을 링크합니까?\nmove_folder_failed = { $name } 폴더 이동 실패. 이유: { $reason }\nmove_file_failed = { $name } 파일 이동 실패. 이유: { $reason }\nmove_files_title_dialog = 중복 파일을 이동할 폴더를 선택하세요\nmove_files_choose_more_than_1_path = 중복 파일을 복사할 1개의 폴더만 지정해야 하지만, { $path_number }개의 경로를 선택했습니다.\nmove_stats = { $num_files }/{ $all_files }개의 항목을 이동함\nsave_results_to_file = 결과를 txt 및 json 파일로 ‘{ $name }’ 폴더에 저장했습니다.\nsearch_not_choosing_any_music = 경고: 최소한 하나의 검색 방법을 선택해야 합니다.\nsearch_not_choosing_any_broken_files = 경고: 최소한 하나 이상의 검색할 파일 분류를 선택해야 합니다.\ninclude_folders_dialog_title = 검색할 폴더 추가\nexclude_folders_dialog_title = 제외할 폴더 추가\ninclude_manually_directories_dialog_title = 수동으로 디렉터리 추가\ncache_properly_cleared = 캐시를 성공적으로 정리했습니다\ncache_clear_duplicates_title = 중복 파일 캐시 정리\ncache_clear_similar_images_title = 유사한 이미지 캐시 정리\ncache_clear_similar_videos_title = 유사한 영상 캐시 정리\ncache_clear_message_label_1 = 유효하지 않은 캐시 항목을 제거할까요?\ncache_clear_message_label_2 = 이 동작은 더 이상 유효하지 않은 파일에 대한 캐시 항목을 제거합니다.\ncache_clear_message_label_3 = 이를 통해 더 빠른 캐시 저장/불러오기가 가능할 수 있습니다.\ncache_clear_message_label_4 = 경고! 이 동작은 연결되지 않은 외장 저장장치에 위치한 모든 항목을 제거합니다. 따라서 해당 파일들에 대한 캐시는 다시 생성되어야 합니다.\n# Show preview\npreview_image_resize_failure = { $name } 이미지 크기 조정 실패.\npreview_image_opening_failure = { $name } 이미지 열기 실패. 이유: { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = 그룹 { $current_group } / { $all_groups } ({ $images_in_group } 이미지)\ncompare_move_left_button = 이전\ncompare_move_right_button = 다음\n"
  },
  {
    "path": "czkawka_gui/i18n/nl/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Instellingen\nwindow_main_title = Czkawka (Giechel)\nwindow_progress_title = Scannen\nwindow_compare_images = Vergelijk afbeeldingen\n# General\ngeneral_ok_button = OK\ngeneral_close_button = Afsluiten\n# Krokiet info dialog\nkrokiet_info_title = Introductie van Krokiet - Nieuwe versie van Czkawka\nkrokiet_info_message = \n        Krokiet is de nieuwe, verbeterde, snellere en betrouwbaardere versie van de Czkawka GTK GUI!\n\n        Het is eenvoudiger te draaien en robuuster tegen systeemwijzigingen, omdat het alleen afhankelijk is van kernbibliotheken die standaard op de meeste systemen beschikbaar zijn.\n\n        Krokiet brengt ook functies die Czkawka mist, waaronder thumbnails in video vergelijking modus, een EXIF cleaner, bestand verplaatsen/kopieer/verwijderen voortgang of uitgebreide sorteeropties.\n\n        Probeer het uit en zie het verschil!\n\n        Czkawka zal blijven ontvangen bugfixes en kleine updates van mij, maar alle nieuwe functies zullen exclusief voor Krokiet worden ontwikkeld, en iedereen is vrij om nieuwe functies toe te voegen, ontbrekende modi uit te breiden of Czkawka verder te ontwikkelen.\n\n        PS: Dit bericht zou alleen één keer moeten verschijnen. Als het weer verschijnt, stel dan de CZKAWKA_DONT_ANNOY_ME omgeving variabele in op een niet-lege waarde.\n# Main window\nmusic_title_checkbox = Aanspreektitel\nmusic_artist_checkbox = Kunstenaar\nmusic_year_checkbox = jaar\nmusic_bitrate_checkbox = Bitsnelheid\nmusic_genre_checkbox = genre\nmusic_length_checkbox = longueur\nmusic_comparison_checkbox = Geschatte vergelijking\nmusic_checking_by_tags = Labels\nmusic_checking_by_content = Inhoud\nsame_music_seconds_label = Minimale fragment tweede duur\nsame_music_similarity_label = Maximum verschil\nmusic_compare_only_in_title_group = Vergelijk binnen groepen van vergelijkbare titels\nmusic_compare_only_in_title_group_tooltip =\n    Wanneer ingeschakeld, worden bestanden gegroepeerd op titel en vervolgens vergeleken met elkaar.\n    \n    Met 10000 bestanden, in plaats van bijna 100 miljoen vergelijkingen zullen er meestal ongeveer 20000 vergelijkingen worden gemaakt.\nsame_music_tooltip =\n    Zoeken naar vergelijkbare muziekbestanden door de inhoud ervan kan worden geconfigureerd door instelling:\n    \n    - De minimale fragmenttijd waarna muziekbestanden kunnen worden geïdentificeerd als vergelijkbaar\n    - Het maximale verschil tussen twee geteste fragmenten\n    \n    De sleutel tot goede resultaten is om verstandige combinaties van deze parameters te vinden, voor opgegeven.\n    \n    Instelling van de minimale tijd op 5 en het maximale verschil op 1.0, zal zoeken naar bijna identieke fragmenten in de bestanden.\n    Een tijd van 20 en een maximaal verschil van 6,0 werkt daarentegen goed voor het vinden van remixes/live versies, enz.\n    \n    Standaard wordt elk muziekbestand met elkaar vergeleken en dit kan veel tijd in beslag nemen bij het testen van veel bestanden, dus is het meestal beter om referentie-mappen te gebruiken en aan te geven welke bestanden met elkaar moeten worden vergeleken (met dezelfde hoeveelheid bestanden, Het vergelijken van vingerafdrukken is sneller dan zonder referentiemateriaal).\nmusic_comparison_checkbox_tooltip =\n    Het zoekt naar vergelijkbare muziekbestanden met behulp van AI, die machine-leren gebruikt om haakjes uit een zin te verwijderen. Bijvoorbeeld met deze optie ingeschakeld de bestanden in kwestie zullen als duplicaten worden beschouwd:\n    \n    SØ wieľdzizive b --- S000000wie.pldzizľb (Remix Lato 2021)\nduplicate_case_sensitive_name = Kist gevoelig\nduplicate_case_sensitive_name_tooltip =\n    Wanneer ingeschakeld, groep alleen records wanneer ze precies dezelfde naam hebben, b.v. Zit ołd <-> ZØ ołd\n    \n    Uitschakelen van een dergelijke optie zal namen groeperen zonder te controleren of elke letter hetzelfde formaat heeft, bijv. zghaoŁD <-> Zit ołd\nduplicate_mode_size_name_combo_box = Grootte en naam\nduplicate_mode_name_combo_box = naam\nduplicate_mode_size_combo_box = Grootte\nduplicate_mode_hash_combo_box = Toegangssleutel\nduplicate_hash_type_tooltip =\n    Czkawka biedt 3 soorten hashes:\n    \n    Blake3 - cryptografische hash-functie. Dit is de standaard omdat het erg snel is.\n    \n    CRC32 - eenvoudige hashfunctie. Dit zou sneller moeten zijn dan Blake3, maar kan zeer zelden een botsing veroorzaken.\n    \n    XXH3 - erg vergelijkbaar in prestaties en hashkwaliteit naar Blake3 (maar niet-cryptografie). Dergelijke modi kunnen dus eenvoudig worden verwisseld.\nduplicate_check_method_tooltip =\n    Op dit moment biedt Czkawka drie soorten methode aan om duplicaten te vinden door:\n    \n    Naam - Gevonden bestanden met dezelfde naam.\n    \n    Grootte - Gevonden bestanden die dezelfde grootte hebben.\n    \n    Hash - Gevonden bestanden die dezelfde inhoud hebben. Deze modus hashet het bestand en vergelijkt deze hash later om duplicaten te vinden. Deze modus is de veiligste manier om duplicaten te vinden. App gebruikt zwaar cache, dus de tweede en verdere scans van dezelfde gegevens zou veel sneller moeten zijn dan de eerste.\nimage_hash_size_tooltip =\n    Elke gecontroleerde afbeelding produceert een speciale hash die met elkaar kan worden vergeleken en een klein verschil tussen hen betekent dat deze afbeeldingen vergelijkbaar zijn.\n    \n    8 hash size is vrij goed om afbeeldingen te vinden die maar een beetje lijken op origineel. Met een grotere set afbeeldingen (>1000) levert dit een grote hoeveelheid valse positieven op. Dus ik raad in dit geval aan een grotere hashgrootte te gebruiken.\n    \n    16 is de standaard hash-afmeting, wat een heel goed compromis is tussen het vinden van zelfs een beetje gelijksoortige afbeeldingen en het hebben van slechts een klein aantal hash-botsingen.\n    \n    32 en 64 hashes vinden slechts zeer gelijksoortige afbeeldingen, maar zouden bijna geen valse positieve motieven moeten hebben (behalve sommige afbeeldingen met alpha kanaal).\nimage_resize_filter_tooltip =\n    Om hash van de afbeelding te berekenen, moet de bibliotheek deze eerst grootschalen.\n    \n    Afhankelijk van het gekozen algoritme, zal de uiteindelijke afbeelding die gebruikt wordt om hash te berekenen er een beetje anders uitzien.\n    \n    Het snelste algoritme te gebruiken, maar ook het algoritme dat de slechtste resultaten geeft, is het dichtstbijst. Het is standaard ingeschakeld, want met 16x16 hash grootte is het niet echt zichtbaar.\n    \n    met 8x8 hash grootte is het raadzaam om een ander algoritme te gebruiken dan Nearest, om betere groepen afbeeldingen te hebben.\nimage_hash_alg_tooltip =\n    Gebruikers kunnen kiezen uit een van de vele algoritmes om de hash te berekenen.\n    \n    Elk van deze punten heeft sterke en zwakke punten en zal soms betere en soms slechtere resultaten opleveren voor verschillende afbeeldingen.\n    \n    Dus om het beste voor u te bepalen, is handmatige test vereist.\nbig_files_mode_combobox_tooltip = Maakt het mogelijk om naar kleinste/grootste bestanden te zoeken\nbig_files_mode_label = Gecontroleerde bestanden\nbig_files_mode_smallest_combo_box = De Kleinste\nbig_files_mode_biggest_combo_box = De Grootste\nmain_notebook_duplicates = Dupliceer Bestanden\nmain_notebook_empty_directories = Lege mappen\nmain_notebook_big_files = Grote bestanden\nmain_notebook_empty_files = Lege bestanden\nmain_notebook_temporary = Tijdelijke bestanden\nmain_notebook_similar_images = Vergelijkbare afbeeldingen\nmain_notebook_similar_videos = Soortgelijke video's\nmain_notebook_same_music = Muziek duplicaten\nmain_notebook_symlinks = Ongeldige Symlinks\nmain_notebook_broken_files = Kapotte Bestanden\nmain_notebook_bad_extensions = Slechte extensies\nmain_tree_view_column_file_name = Bestandsnaam\nmain_tree_view_column_folder_name = Map Naam\nmain_tree_view_column_path = Pad\nmain_tree_view_column_modification = Wijziging datum\nmain_tree_view_column_size = Grootte\nmain_tree_view_column_similarity = Vergelijkbaarheid\nmain_tree_view_column_dimensions = Mål\nmain_tree_view_column_title = Aanspreektitel\nmain_tree_view_column_artist = Kunstenaar\nmain_tree_view_column_year = jaar\nmain_tree_view_column_bitrate = Bitsnelheid\nmain_tree_view_column_length = longueur\nmain_tree_view_column_genre = genre\nmain_tree_view_column_symlink_file_name = Symlink bestandsnaam\nmain_tree_view_column_symlink_folder = Symlink map\nmain_tree_view_column_destination_path = Bestemming pad\nmain_tree_view_column_type_of_error = Type fout\nmain_tree_view_column_current_extension = Huidige extensie\nmain_tree_view_column_proper_extensions = Proper Extensie\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codec\nmain_label_check_method = Controleer methode\nmain_label_hash_type = Soort hash\nmain_label_hash_size = Hash grootte\nmain_label_size_bytes = Grootte (bytes)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Aantal getoonde bestanden\nmain_label_resize_algorithm = Algoritme aanpassen\nmain_label_similarity = Similarity{ \"   \" }\nmain_check_box_broken_files_audio = Geluid\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = Archief\nmain_check_box_broken_files_image = Afbeelding\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Gebruikt ffmpeg/ffprobe om video bestanden te valideren. Zeer traag en kan pedantische fouten detecteren zelfs als de bestand goed afspeelt.\ncheck_button_general_same_size = Negeer dezelfde grootte\ncheck_button_general_same_size_tooltip = Bestanden met identieke grootte in resultaten negeren - meestal zijn deze 1:1 duplicaten\nmain_label_size_bytes_tooltip = Grootte van bestanden die zullen worden gebruikt in scan\n# Upper window\nupper_tree_view_included_folder_column_title = Mappen om te zoeken\nupper_tree_view_included_reference_column_title = Referentie Mappen\nupper_recursive_button = Recursief\nupper_recursive_button_tooltip = Indien geselecteerd, zoek ook naar bestanden die niet direct onder de gekozen mappen worden geplaatst.\nupper_manual_add_included_button = Handmatig toevoegen\nupper_add_included_button = Toevoegen\nupper_remove_included_button = Verwijderen\nupper_manual_add_excluded_button = Handmatig toevoegen\nupper_add_excluded_button = Toevoegen\nupper_remove_excluded_button = Verwijderen\nupper_manual_add_included_button_tooltip =\n    Voeg mapnaam toe om met de hand te zoeken.\n    \n    Om meerdere paden tegelijk toe te voegen, scheiden ze met ;\n    \n    /home/roman;/home/rozkaz zal twee mappen / home/roman en /home/rozkaz toevoegen\nupper_add_included_button_tooltip = Voeg nieuwe map toe om te zoeken.\nupper_remove_included_button_tooltip = Map verwijderen uit zoekopdracht.\nupper_manual_add_excluded_button_tooltip =\n    Voeg uitgesloten mapnaam met de hand toe.\n    \n    Om meerdere paden tegelijk toe te voegen, scheid ze met\n    \n    /home/roman;/home/krokiet zal twee mappen / home/roman en /home/keokiet toevoegen\nupper_add_excluded_button_tooltip = Voeg map toe om uitgesloten te worden in zoekopdracht.\nupper_remove_excluded_button_tooltip = Verwijder map van uitgesloten.\nupper_notebook_items_configuration = Artikelen configuratie\nupper_notebook_excluded_directories = Uitgesloten Paden\nupper_notebook_included_directories = Inclusieve Paden\nupper_allowed_extensions_tooltip =\n    Toegestane extensies moeten door komma's gescheiden worden (standaard zijn alle beschikbaar).\n    \n    De volgende macro's die meerdere extensies in één keer toevoegen, zijn ook beschikbaar: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Gebruiksgebruik voorbeeld \".exe, IMAGE, VIDEO, .rar, 7z\" - dit betekent dat afbeeldingen (e. . jpg, png), video's (bijv. avi, mp4), exe, rr en 7z bestanden worden gescand.\nupper_excluded_extensions_tooltip =\n    Lijst van uitgeschakelde bestanden die genegeerd zullen worden in scan.\n    \n    Wanneer gebruik wordt gemaakt van toegestane en uitgeschakelde extensies, heeft deze hogere prioriteit, dus het bestand zal niet worden gecontroleerd.\nupper_excluded_items_tooltip = \n        Uitsluitende items moeten * wildcard bevatten en moeten gescheiden worden door komma's.\n        Dit is langzamer dan Uitsluitingspaden, dus gebruik het voorzichtig.\nupper_excluded_items = Uitgesloten artikelen:\nupper_allowed_extensions = Toegestane extensies:\nupper_excluded_extensions = Uitgeschakelde extensies:\n# Popovers\npopover_select_all = Alles selecteren\npopover_unselect_all = Selectie ongedaan maken\npopover_reverse = Omgekeerde selectie\npopover_select_all_except_shortest_path = Selecteer alles behalve de kortste route\npopover_select_all_except_longest_path = Selecteer alles behalve de langste pad\npopover_select_all_except_oldest = Alles selecteren behalve oudste\npopover_select_all_except_newest = Selecteer alles behalve nieuwste\npopover_select_one_oldest = Selecteer één oudste\npopover_select_one_newest = Selecteer een nieuwste\npopover_select_custom = Selecteer aangepaste\npopover_unselect_custom = Aangepaste deselecteer ongedaan maken\npopover_select_all_images_except_biggest = Alles selecteren behalve de grootste\npopover_select_all_images_except_smallest = Selecteer alles behalve de kleinste\npopover_custom_path_check_button_entry_tooltip =\n    Records per pad selecteren.\n    \n    Voorbeeld gebruik:\n    /home/pimpek/rzecz.txt kan worden gevonden met /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Records selecteren op bestandnamen.\n    \n    Voorbeeld gebruik:\n    /usr/ping/pong.txt kan worden gevonden met *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Select records by specified Regex.\n    \n    In this mode is searched text Path with Name.\n    \n    Voorbeeld use usage:\n    /usr/bin/ziemniak. xt kan gevonden worden met /ziem[a-z]+\n    \n    Dit gebruikt de standaard Rust regex implementatie. Je kunt hier meer over lezen: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Maakt hoofdlettergevoelige detectie mogelijk.\n    \n    Wanneer uitgeschakeld /home/* vindt zowel /HoMe/roman en /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Voorkomt dat alle records in de groep worden geselecteerd.\n    \n    Dit is standaard ingeschakeld, omdat in de meeste situaties u wilt niet zowel origineel als duplicaten verwijderen, maar ten minste één bestand achterlaten.\n    \n    WAARSCHUWING: Deze instelling werkt niet als je al handmatig alle resultaten hebt geselecteerd in een groep.\npopover_custom_regex_path_label = Pad\npopover_custom_regex_name_label = naam\npopover_custom_regex_regex_label = Regex pad + naam\npopover_custom_case_sensitive_check_button = Hoofdletter gevoelig\npopover_custom_all_in_group_label = Niet alle records in groep selecteren\npopover_custom_mode_unselect = Aangepaste deselecteren\npopover_custom_mode_select = Selecteer aangepast\npopover_sort_file_name = Bestandsnaam is vereist\npopover_sort_folder_name = Folder Name\npopover_sort_full_name = Volledige naam\npopover_sort_size = Grootte\npopover_sort_selection = Selectie\npopover_invalid_regex = Regex is ongeldig\npopover_valid_regex = Regex is geldig\n# Bottom buttons\nbottom_search_button = Zoeken\nbottom_select_button = Selecteren\nbottom_delete_button = Verwijderen\nbottom_save_button = Opslaan\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Verplaatsen\nbottom_sort_button = Sorteren\nbottom_compare_button = Vergelijk\nbottom_search_button_tooltip = Zoeken starten\nbottom_select_button_tooltip = Selecteer records. Alleen geselecteerde bestanden/mappen kunnen later worden verwerkt.\nbottom_delete_button_tooltip = Verwijder geselecteerde bestanden/mappen.\nbottom_save_button_tooltip = Gegevens opslaan van zoekopdracht naar bestand\nbottom_symlink_button_tooltip =\n    Maak symbolische links.\n    Werkt alleen wanneer ten minste twee resultaten in een groep zijn geselecteerd.\n    De eerste is ongewijzigd en de tweede is symgekoppeld naar eerst.\nbottom_hardlink_button_tooltip =\n    Maak hardlinks.\n    Werkt alleen wanneer ten minste twee resultaten in een groep worden geselecteerd.\n    De eerste is ongewijzigd en de tweede keer en later zijn vastgekoppeld aan eerst.\nbottom_hardlink_button_not_available_tooltip =\n    Maak hardlinks.\n    Knop is uitgeschakeld, omdat hardlinks niet kunnen worden gemaakt.\n    Hardlinks werkt alleen met beheerdersrechten op Windows, dus zorg ervoor dat je de app als administrator gebruikt.\n    Als de app al werkt met dergelijke privileges controle op gelijksoortige issues op Github.\nbottom_move_button_tooltip =\n    Verplaatst bestanden naar de gekozen map.\n    Het kopieert alle bestanden naar de map zonder de mapstructuur te bewaren.\n    Wanneer twee bestanden met dezelfde naam naar de map worden verplaatst, zal de tweede mislukt en de fout worden weergegeven.\nbottom_sort_button_tooltip = Sorteert bestanden/mappen op de geselecteerde methode.\nbottom_compare_button_tooltip = Afbeeldingen in de groep vergelijken.\nbottom_show_errors_tooltip = Onderste tekstvenster tonen/verbergen.\nbottom_show_upper_notebook_tooltip = Toon/Verberg bovenste notitieboekpaneel.\n# Progress Window\nprogress_stop_button = Stoppen\nprogress_stop_additional_message = Stop aangevraagd\n# About Window\nabout_repository_button_tooltip = Link naar de repository pagina met broncode.\nabout_donation_button_tooltip = Link naar donatie pagina.\nabout_instruction_button_tooltip = Link naar instructiepagina.\nabout_translation_button_tooltip = Link naar de Crowdin pagina met appvertalingen. Officieel worden Pools en Engels ondersteund.\nabout_repository_button = Bewaarplaats\nabout_donation_button = Donatie\nabout_instruction_button = Instructie\nabout_translation_button = Vertaling\n# Header\nheader_setting_button_tooltip = Opent instellingen dialoogvenster.\nheader_about_button_tooltip = Opent dialoogvenster met info over app.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Aantal gebruikte threads\nsettings_number_of_threads_tooltip = Aantal gebruikte threads, 0 betekent dat alle beschikbare threads zullen worden gebruikt.\nsettings_use_rust_preview = Gebruik externe bibliotheken in plaats daarvan om previews te laden\nsettings_use_rust_preview_tooltip =\n    Het gebruik van gtk previews zal soms sneller zijn en ondersteuning bieden voor meer formaten, maar soms kan het precies het tegenovergestelde zijn.\n    \n    Als je problemen hebt met het laden van previews, kan je proberen deze instelling te veranderen.\n    \n    Op niet-linux systemen is het aangeraden om deze optie te gebruiken, omdat gtk-pixbuf niet altijd beschikbaar is, dus het uitschakelen van deze optie zal geen voorvertoningen van sommige afbeeldingen laden.\nsettings_label_restart = U moet de app herstarten om de instellingen toe te passen!\nsettings_ignore_other_filesystems = Negeer andere bestandssystemen (alleen Linux)\nsettings_ignore_other_filesystems_tooltip =\n    negeert bestanden die niet in hetzelfde bestandssysteem zitten als gezochte mappen.\n    \n    Werkt dezelfde als -xdev optie in het zoekcommando voor Linux\nsettings_save_at_exit_button_tooltip = Configuratie opslaan in bestand bij het sluiten van de app.\nsettings_load_at_start_button_tooltip =\n    Laad de configuratie van het bestand bij het openen van de app.\n    \n    Indien niet ingeschakeld, worden standaard instellingen gebruikt.\nsettings_confirm_deletion_button_tooltip = Bevestigingsvenster tonen bij het klikken op de knop verwijderen.\nsettings_confirm_link_button_tooltip = Toon bevestigingsvenster bij het klikken op de hard/symlink knop.\nsettings_confirm_group_deletion_button_tooltip = Waarschuwingsvenster weergeven wanneer geprobeerd wordt om alle records uit de groep te verwijderen.\nsettings_show_text_view_button_tooltip = Tekstpaneel aan de onderkant van de gebruikersinterface weergeven.\nsettings_use_cache_button_tooltip = Gebruik bestandscache.\nsettings_save_also_as_json_button_tooltip = Cache opslaan in (menselijk leesbaar) JSON formaat. Het is mogelijk om de inhoud te wijzigen. Cache van dit bestand wordt automatisch gelezen door de app als er een binaire cache (met bin extensie) ontbreekt.\nsettings_use_trash_button_tooltip = Verplaatst bestanden naar prullenbak in plaats daarvan ze permanent te verwijderen.\nsettings_language_label_tooltip = Taal voor de gebruikersinterface.\nsettings_save_at_exit_button = Configuratie opslaan bij het sluiten van app\nsettings_load_at_start_button = Laad configuratie bij het openen van app\nsettings_confirm_deletion_button = Toon bevestigingsdialoog bij het verwijderen van bestanden\nsettings_confirm_link_button = Melding bevestigen bij hard/symlinks van bestanden\nsettings_confirm_group_deletion_button = Toon het bevestigingsvenster bij het verwijderen van alle bestanden in de groep\nsettings_show_text_view_button = Toon onderaan tekstpaneel\nsettings_use_cache_button = Gebruik cache\nsettings_save_also_as_json_button = Sla ook de cache op als JSON-bestand\nsettings_use_trash_button = Verwijderde bestanden verplaatsen naar prullenbak\nsettings_language_label = Taal\nsettings_multiple_delete_outdated_cache_checkbutton = Verouderde cache vermeldingen automatisch verwijderen\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Verwijder verouderde cacheresultaten die verwijzen naar niet-bestaande bestanden.\n    \n    Indien ingeschakeld, zorgt de app ervoor dat bij het laden van records, dat alle records naar geldige bestanden verwijzen (gebroken bestanden worden genegeerd).\n    \n    Dit uitschakelen zal helpen bij het scannen van bestanden op externe schijven, dus cache-items over deze zullen niet worden gewist in de volgende scan.\n    \n    In het geval van honderdduizenden records in de cache, het wordt aangeraden om dit in te schakelen, dit zal de cache laden/opslaan aan het starten/einde van de scan versnellen.\nsettings_notebook_general = Algemeen\nsettings_notebook_duplicates = Duplicaten\nsettings_notebook_images = Vergelijkbare afbeeldingen\nsettings_notebook_videos = Gelijkaardige Video\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Toont voorbeeld aan rechterkant (bij het selecteren van een afbeeldingsbestand).\nsettings_multiple_image_preview_checkbutton = Toon voorvertoning afbeelding\nsettings_multiple_clear_cache_button_tooltip =\n    Handmatig de cache van verouderde items wissen.\n    Dit mag alleen worden gebruikt als automatisch wissen is uitgeschakeld.\nsettings_multiple_clear_cache_button = Verwijder verouderde resultaten uit de cache.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Verbergt alle bestanden behalve één, als alle naar dezelfde gegevens verwijst (zijn hardlinked).\n    \n    Voorbeeld: In het geval waar er (op schijf) zeven bestanden zijn die zijn gekoppeld aan specifieke data en één ander bestand met dezelfde gegevens, maar een andere inode, dan in dubbele zoeker, slechts één uniek bestand en één bestand van de hardgelinkte bestanden zullen worden weergegeven.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Stel de minimale bestandsgrootte in die gecached zal worden.\n    \n    kiezen van een kleinere waarde zal meer records genereren. Dit zal het zoeken versnellen, maar de cache aan het laden/opslaan.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Hiermee kan het cachen van prehash (een hash berekend van een klein deel van het bestand) eerder verwijderen van niet-gedupliceerde resultaten.\n    \n    Het is standaard uitgeschakeld omdat het in sommige situaties vertraging kan veroorzaken.\n    \n    Het is sterk aanbevolen om het te gebruiken bij het scannen van honderdduizenden of miljoen bestanden, omdat het zoeken meerdere keren kan versnellen.\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimale grootte van gecachete invoer.\nsettings_duplicates_hide_hard_link_button = Verberg harde links\nsettings_duplicates_prehash_checkbutton = Gebruik prehash cache\nsettings_duplicates_minimal_size_cache_label = Minimale bestandsgrootte (in bytes) opgeslagen in de cache\nsettings_duplicates_minimal_size_cache_prehash_label = Minimale grootte van bestanden (in bytes) opgeslagen naar prehash cache\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = De huidige instellingen configuratie opslaan in bestand.\nsettings_loading_button_tooltip = Laad de instellingen uit het bestand en vervang de huidige configuratie.\nsettings_reset_button_tooltip = De huidige configuratie terugzetten naar de standaard.\nsettings_saving_button = Configuratie opslaan\nsettings_loading_button = Laad configuratie\nsettings_reset_button = Reset configuratie\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Opent de map waar de cache txt bestanden zijn opgeslagen.\n    \n    Het wijzigen van de cachebestanden kan ervoor zorgen dat ongeldige resultaten worden getoond. Het wijzigen van een pad kan echter tijd besparen bij het verplaatsen van een grote hoeveelheid bestanden naar een andere locatie.\n    \n    U kunt deze bestanden tussen computers kopiëren om tijd te besparen bij het scannen van bestanden (van natuurlijk als ze een vergelijkbare directory structuur hebben).\n    \n    In geval van problemen met de cache kunnen deze bestanden worden verwijderd. De app zal ze automatisch opnieuw genereren.\nsettings_folder_settings_open_tooltip =\n    Opent de map waar de Czkawka config is opgeslagen.\n    \n    WAARSCHUWING: Handmatig wijzigen van de configuratie kan uw workflow verbreken.\nsettings_folder_cache_open = Open cachemap\nsettings_folder_settings_open = Instellingenmap openen\n# Compute results\ncompute_stopped_by_user = Zoeken is gestopt door gebruiker\ncompute_found_duplicates_hash_size = Gevonden { $number_files } duplicaten in { $number_groups } groepen die { $size } in { $time } namen\ncompute_found_duplicates_name = Gevonden { $number_files } duplicaten in { $number_groups } groepen in { $time }\ncompute_found_empty_folders = Gevonden { $number_files } lege mappen in { $time }\ncompute_found_empty_files = Gevonden { $number_files } lege bestanden in { $time }\ncompute_found_big_files = Gevonden { $number_files } grote bestanden in { $time }\ncompute_found_temporary_files = Gevonden { $number_files } tijdelijke bestanden in { $time }\ncompute_found_images = { $number_files } soortgelijke afbeeldingen gevonden in { $number_groups } groepen in { $time }\ncompute_found_videos = Gevonden { $number_files } soortgelijke video's in { $number_groups } groepen in { $time }\ncompute_found_music = Gevonden { $number_files } soortgelijke muziekbestanden in { $number_groups } groepen in { $time }\ncompute_found_invalid_symlinks = Gevonden { $number_files } ongeldige symlinks in { $time }\ncompute_found_broken_files = Gevonden { $number_files } gebroken bestanden in { $time }\ncompute_found_bad_extensions = { $number_files } bestanden met ongeldige extensies gevonden in { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Gescande { $file_number } bestand\n       *[other] Gescande { $file_number } bestanden\n    }\nprogress_scanning_extension_of_files = Gecontroleerde extensie van { $file_checked }/{ $all_files } bestand\nprogress_scanning_broken_files = Gecontroleerd { $file_checked }/{ $all_files } bestand ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Onderbroken van { $file_checked }/{ $all_files } video\nprogress_creating_video_thumbnails = Gemaakte miniaturen van { $file_checked }/{ $all_files } video\nprogress_scanning_image = Bevroren van { $file_checked }/{ $all_files } afbeelding ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Vergeleken { $file_checked }/{ $all_files } afbeelding hash\nprogress_scanning_music_tags_end = Vergeleken tags van { $file_checked }/{ $all_files } muziekbestand\nprogress_scanning_music_tags = Lees tags van { $file_checked }/{ $all_files } muziekbestand\nprogress_scanning_music_content_end = Vergeleken vingerafdruk van { $file_checked }/{ $all_files } muziekbestand\nprogress_scanning_music_content = Berekende vingerafdruk van { $file_checked }/{ $all_files } muziekbestand ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Gescande { $folder_number } map\n       *[other] Gescande { $folder_number } mappen\n    }\nprogress_scanning_size = Gescande grootte van { $file_number } bestand\nprogress_scanning_size_name = Gescande naam en grootte van { $file_number } bestand\nprogress_scanning_name = Gescande naam van { $file_number } bestand\nprogress_analyzed_partial_hash = Geanalyseerde gedeeltelijke hash van { $file_checked }/{ $all_files } bestanden ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Volledige hash van { $file_checked }/{ $all_files } bestanden ({ $data_checked }/{ $all_data } ) geanalyseerd\nprogress_prehash_cache_loading = Prehash cache laden\nprogress_prehash_cache_saving = Opslaan van prehash cache\nprogress_hash_cache_loading = hash-cache laden\nprogress_hash_cache_saving = hash cache opslaan\nprogress_cache_loading = Cache laden\nprogress_cache_saving = Cache opslaan\nprogress_current_stage = Current Stage:{ \"  \" }\nprogress_all_stages = All Stages:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = Configuratie opgeslagen in bestand { $name }.\nsaving_loading_saving_failure = Kan de configuratiegegevens niet opslaan in het bestand { $name }, reden { $reason }.\nsaving_loading_reset_configuration = Huidige configuratie is gewist.\nsaving_loading_loading_success = Goed geladen app configuratie.\nsaving_loading_failed_to_create_config_file = Fout bij het aanmaken van het configuratiebestand \"{ $path }\", reden \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Kan configuratie niet laden van \"{ $path }\" omdat deze niet bestaat of geen bestand is.\nsaving_loading_failed_to_read_data_from_file = Kan gegevens niet lezen van bestand \"{ $path }\", reden \"{ $reason }\".\n# Other\nselected_all_reference_folders = Kan zoeken niet starten, als alle mappen als referentie mappen zijn ingesteld\nsearching_for_data = Gegevens zoeken, het kan een tijdje duren, even wachten...\ntext_view_messages = BERICHTEN\ntext_view_warnings = LET OP\ntext_view_errors = FOUTEN\nabout_window_motto = Dit programma is gratis te gebruiken en zal dat altijd zijn.\nkrokiet_new_app = Czkawka is in onderhoudsmodus, wat betekent dat alleen kritieke bugs opgelost zullen worden en er geen nieuwe functies aan toegevoegd zullen worden. Voor nieuwe functies, bekijk de nieuwe Krokiet app, die stabieler en performanter is en nog in volle ontwikkeling is.\n# Various dialog\ndialogs_ask_next_time = Volgende keer vragen\nsymlink_failed = symlink { $name } naar { $target }mislukt, reden { $reason }\ndelete_title_dialog = Bevestiging verwijderen\ndelete_question_label = Weet u zeker dat u bestanden wilt verwijderen?\ndelete_all_files_in_group_title = Bevestiging van het verwijderen van alle bestanden in groep\ndelete_all_files_in_group_label1 = In sommige groepen worden alle records geselecteerd.\ndelete_all_files_in_group_label2 = Weet u zeker dat u deze wilt verwijderen?\ndelete_items_label = { $items } bestanden worden verwijderd.\ndelete_items_groups_label = { $items } bestanden van { $groups } groepen worden verwijderd.\nhardlink_failed = Mislukt om hardlink te maken { $name } naar { $target }, reden { $reason }\nhard_sym_invalid_selection_title_dialog = Ongeldige selectie met sommige groepen\nhard_sym_invalid_selection_label_1 = In sommige groepen is er slechts één record geselecteerd en zal worden genegeerd.\nhard_sym_invalid_selection_label_2 = Om deze bestanden hard/sym te kunnen koppelen, moeten ten minste twee resultaten in de groep worden geselecteerd.\nhard_sym_invalid_selection_label_3 = Eerst wordt de groep als origineel erkend en niet veranderd, en vervolgens worden de tweede wijzigingen aangebracht.\nhard_sym_link_title_dialog = Bevestiging link\nhard_sym_link_label = Weet u zeker dat u deze bestanden wilt koppelen?\nmove_folder_failed = Map { $name }kon niet verplaatst worden, reden { $reason }\nmove_file_failed = Kon bestand niet verplaatsen { $name }, reden { $reason }\nmove_files_title_dialog = Kies de map waarnaar u gedupliceerde bestanden wilt verplaatsen\nmove_files_choose_more_than_1_path = Er kan slechts één pad geselecteerd zijn om hun gedupliceerde bestanden te kopiëren, geselecteerde { $path_number }.\nmove_stats = Naar behoren verplaatst { $num_files }/{ $all_files } items\nsave_results_to_file = Opgeslagen resultaten zowel in txt als json bestanden in de \"{ $name }\" map.\nsearch_not_choosing_any_music = FOUT: U moet ten minste één selectievakje met muziekinstypes selecteren.\nsearch_not_choosing_any_broken_files = FOUT: U moet ten minste één selectievakje selecteren met type van aangevinkte bestanden.\ninclude_folders_dialog_title = Mappen om op te nemen\nexclude_folders_dialog_title = Mappen om uit te sluiten\ninclude_manually_directories_dialog_title = Voeg map handmatig toe\ncache_properly_cleared = Cache op juiste wijze gewist\ncache_clear_duplicates_title = Duplicaten cache wissen\ncache_clear_similar_images_title = Leeg soortgelijke afbeeldingen-cache\ncache_clear_similar_videos_title = Leeg soortgelijke video cache\ncache_clear_message_label_1 = Wilt u de cache van verouderde items wissen?\ncache_clear_message_label_2 = Deze actie zal alle cache-items verwijderen die naar ongeldige bestanden wijzen.\ncache_clear_message_label_3 = Dit kan de laden/opslaan enigszins versnellen.\ncache_clear_message_label_4 = WAARSCHUWING: De bewerking zal alle opgeslagen data van externe schijven verwijderen. Daarom zal elke hash opnieuw moeten worden gegenereerd.\n# Show preview\npreview_image_resize_failure = Formaat wijzigen van afbeelding { $name } is mislukt.\npreview_image_opening_failure = Kan afbeelding { $name }niet openen, reden { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Groep { $current_group }/{ $all_groups } ({ $images_in_group } afbeeldingen)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/no/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Innstillinger\nwindow_main_title = Czkawka (Hikkup)\nwindow_progress_title = Skanner\nwindow_compare_images = Sammenlign bilder\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Lukk\n# Krokiet info dialog\nkrokiet_info_title = Introduserer Krokiet - Ny versjon av Czkawka\nkrokiet_info_message = \n        Krokiet er den nye, forbedrede, raskere og mer pålitelige versjonen av Czkawka GTK GUI!\n\n        Det er lettere å kjøre og mer motstandsdyktig mot systemendringer, siden det bare er avhengig av kjernelibbrer som er tilgjengelige på de fleste systemer som standard.\n\n        Krokiet bringer også funksjoner som Czkawka mangler, inkludert forhåndsvisninger i videoj sammenligningsmodus, en EXIF-renser, fil flytt/kopier/slett fremdrift eller utvidede sorteringsalternativer.\n\n        Prøv det og se forskjellen!\n\n        Czkawka vil fortsette å motta feilrettinger og mindre oppdateringer fra meg, men alle nye funksjoner vil bli utviklet eksklusivt for Krokiet, og alle er fri til å bidra med nye funksjoner, legge til manglende moduser eller utvide Czkawka videre.\n\n        PS: Denne meldingen skal bare vises én gang. Hvis den dukker opp igjen, sett CZKAWKA_DONT_ANNOY_ME miljøvariabelen til en hvilken som helst ikke-tom verdi.\n# Main window\nmusic_title_checkbox = Tittel\nmusic_artist_checkbox = Kunstner\nmusic_year_checkbox = År\nmusic_bitrate_checkbox = Bitratespeed\nmusic_genre_checkbox = Sjanger\nmusic_length_checkbox = Lengde\nmusic_comparison_checkbox = Omtrentlig sammenligning\nmusic_checking_by_tags = Tagger\nmusic_checking_by_content = Innhold\nsame_music_seconds_label = Minste fragment andre varighet\nsame_music_similarity_label = Maksimal differanse\nmusic_compare_only_in_title_group = Sammenlign innenfor grupper av lignende titler\nmusic_compare_only_in_title_group_tooltip =\n    Når aktivert, blir filer gruppert etter tittel og sammenlignes med hverandre.\n    \n    Med 10000 filer, vil det i stedet være nesten 100 millioner sammenligninger som regel være rundt 20000 sammenligninger.\nsame_music_tooltip =\n    Søker etter lignende musikkfiler av innholdet kan konfigureres ved å gå inn:\n    \n    - Minimumsfragmenteringstiden etter hvilken musikkfiler som kan identifiseres som lignende\n    - Maksimal forskjell mellom to testede fragmenter\n    \n    Nøkkelen til gode resultater er å finne fornuftige kombinasjoner av disse parametrene, for utlevert.\n    \n    Angir minimum tid til 5 s og maksimal forskjell til 1,0, vil se etter nesten identiske fragmenter i filene.\n    En tid på 20 s og en maksimal forskjell på 6.0, for den andre siden fungerer bra for å finne remikser/levende versjoner osv.\n    \n    Som standard kan hver musikkfil sammenlignes med hverandre, og dette kan ta mye tid når du tester mange filer, slik at det vanligvis er bedre å bruke referanselapper og spesifisere hvilke filer som skal sammenlignes med hverandre (med samme mengde filer, å sammenligne fingeravtrykk vil være raskere minst 4 x enn uten referansemapper).\nmusic_comparison_checkbox_tooltip =\n    Den søker etter lignende musikkfiler ved hjelp av AI, som bruker maskiner til å fjerne parenteser fra et frase. For eksempel, med dette alternativet er aktivert. filene du vil bli betraktet som duplikater:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Skill mellom små og store bokstaver\nduplicate_case_sensitive_name_tooltip =\n    Når aktivert, vil du bare gruppere når de har nøyaktig samme navn, f.eks. Żołd <-> Żołd\n    \n    Deaktivering av en slik opsjon vil gi deg egne navn uten å sjekke om hver bokstav er like stort, f.eks. żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Størrelse og navn\nduplicate_mode_name_combo_box = Navn\nduplicate_mode_size_combo_box = Størrelse\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Tsjekkia har 3 typer hashes:\n    \n    Blake3 - kryptografisk hash-funksjon. Dette er standard fordi den er veldig rask.\n    \n    CRC32 - enkel hash-funksjon. Dette bør være raskere enn Blake3, men kan svært sjelden ha noen kollisjoner.\n    \n    XXH3 - meget likt i ytelse og hash-kvalitet til Blake3 (men ikke-kryptografisk). Såfremt kan slike moduser endres enkelt.\nduplicate_check_method_tooltip =\n    Tsjekkka tilbyr tre typer metoder for å finne duplikater av:\n    \n    Navn - Finner filer med samme navn.\n    \n    Størrelse på funnet finner du filer med samme størrelse.\n    \n    Hash - Finner filer med samme innhold. Denne modusen ligner filen og senere sammenligner dette hashen for å finne duplikater. Denne modusen er den sikreste måten å finne duplikater. Appen bruker stort, så sekund og ytterligere skanninger av de samme dataene bør være mye raskere enn det første.\nimage_hash_size_tooltip =\n    Hvert avmerkede bilde gir en spesiell hash som kan sammenlignes med hverandre, og en liten forskjell mellom dem betyr at disse bildene er like.\n    \n    8 hash-størrelse er ganske bra for å finne bilder som er litt likt original. Med et større sett med bilder (>1000) vil dette gi svært mange falske positive. Derfor anbefaler jeg å bruke en større hash-størrelse i denne saken.\n    \n    16 er standard hash-størrelse som er et godt kompromiss mellom å finne selv små lignende bilder og å ha bare en liten mengde hash-kollisjoner.\n    \n    32 og 64 hashes finner bare lignende bilder, men bør ha nesten ingen falske positiver (kanskje unntatt bilder med alfa-kanal).\nimage_resize_filter_tooltip =\n    For å beregne hesh av bilde, må biblioteket først tilpass bilden.\n    \n    Avhengig av valgt algoritme vil det bilde som brukes for beregning av hesh se litt forskjellig ut.\n    \n    Forkortest algoritme å bruke, men også den som gir de vres resultatene, er Nearest. Den aktiveres standard, fordi med en 16x16 heshstørrelse nederste kvalitet ikke er virkelig synlig.\n    \n    Med en 8x8 heshstørrelse anbefales det å bruke en annen algoritme enn Nearest for å ha bedre grupper av bilder.\nimage_hash_alg_tooltip =\n    Brukere kan velge mellom en av mange algoritmer i beregningen av hashen.\n    \n    Hver har både sterke og svakere poeng, og vil noen ganger gi bedre og noen ganger verre resultater for ulike bilder.\n    \n    Så for å bestemme det beste for deg, kreves manuell testing.\nbig_files_mode_combobox_tooltip = Lar deg søke etter minste/største filer\nbig_files_mode_label = Avmerkede filer\nbig_files_mode_smallest_combo_box = Den minste\nbig_files_mode_biggest_combo_box = Den største\nmain_notebook_duplicates = Dupliser filer\nmain_notebook_empty_directories = Tomme mapper\nmain_notebook_big_files = Store filer\nmain_notebook_empty_files = Tomme filer\nmain_notebook_temporary = Midlertidige filer\nmain_notebook_similar_images = Lignende bilder\nmain_notebook_similar_videos = Lignende videoer\nmain_notebook_same_music = Musikk dupliserer\nmain_notebook_symlinks = Ugyldige Symlinks\nmain_notebook_broken_files = Ødelagte filer\nmain_notebook_bad_extensions = Feil utvidelser\nmain_tree_view_column_file_name = Filnavn\nmain_tree_view_column_folder_name = Mappenavn\nmain_tree_view_column_path = Sti\nmain_tree_view_column_modification = Endret dato\nmain_tree_view_column_size = Størrelse\nmain_tree_view_column_similarity = Likhet\nmain_tree_view_column_dimensions = Dimensjoner\nmain_tree_view_column_title = Tittel\nmain_tree_view_column_artist = Kjennestein\nmain_tree_view_column_year = År\nmain_tree_view_column_bitrate = Bitratespeed\nmain_tree_view_column_length = Lengde\nmain_tree_view_column_genre = Sjanger\nmain_tree_view_column_symlink_file_name = Symlink filnavn\nmain_tree_view_column_symlink_folder = Mappe for Symlink\nmain_tree_view_column_destination_path = Destinasjonssti\nmain_tree_view_column_type_of_error = Type feil\nmain_tree_view_column_current_extension = Gjeldende utvidelse\nmain_tree_view_column_proper_extensions = Riktig utvidelse\nmain_tree_view_column_fps = BPS\nmain_tree_view_column_codec = Kodek\nmain_label_check_method = Sjekkmetode\nmain_label_hash_type = Type hash\nmain_label_hash_size = Størrelse på hash\nmain_label_size_bytes = Størrelse (bytes)\nmain_label_min_size = Min\nmain_label_max_size = Maks\nmain_label_shown_files = Antall filer som vises\nmain_label_resize_algorithm = Endre algoritmen\nmain_label_similarity = Similarity{ \" \" }\nmain_check_box_broken_files_audio = Lyd\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Arkiv\nmain_check_box_broken_files_image = Bilde\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Bruker ffmpeg/ffprobe for å validere video filer. Veldig treg og kan detektere pedantiske feil selv om filen spilles fint.\ncheck_button_general_same_size = Ignorer samme størrelse\ncheck_button_general_same_size_tooltip = Ignorer filer med identisk størrelse i resultater - vanligvis disse er 1:1 duplikater\nmain_label_size_bytes_tooltip = Størrelse på filer som vil bli brukt i skanning\n# Upper window\nupper_tree_view_included_folder_column_title = Mapper å søke etter\nupper_tree_view_included_reference_column_title = Referanse mapper\nupper_recursive_button = Rekursivt\nupper_recursive_button_tooltip = Hvis valgt, søk også etter filer som ikke er plassert direkte under valgte mapper.\nupper_manual_add_included_button = Manuelt legg til\nupper_add_included_button = Legg til\nupper_remove_included_button = Fjern\nupper_manual_add_excluded_button = Manuelt legg til\nupper_add_excluded_button = Legg til\nupper_remove_excluded_button = Fjern\nupper_manual_add_included_button_tooltip =\n    Legg til mappenavn for å søke etter hånd.\n    \n    For å legge til flere baner samtidig, separer dem med ;\n    \n    /home/roman;/home/rozkaz vil legge til to kataloger /home/roman og /home/rozkaz\nupper_add_included_button_tooltip = Legg til ny mappe i søk.\nupper_remove_included_button_tooltip = Slett mappen fra søk.\nupper_manual_add_excluded_button_tooltip =\n    Legg til ekskludert mappenavn for hånd.\n    \n    For å legge til flere baner på en gang, separer dem med ;\n    \n    /home/roman;/home/krokiet vil legge til to kataloger /home/roman and /home/keokiet\nupper_add_excluded_button_tooltip = Legg til mappe som skal utelukkes i søk.\nupper_remove_excluded_button_tooltip = Slett mappen fra ekskludert.\nupper_notebook_items_configuration = Konfigurasjon av elementer\nupper_notebook_excluded_directories = Uekskluderte stier\nupper_notebook_included_directories = Inkluderte Stier\nupper_allowed_extensions_tooltip =\n    Tillatte utvidelser må være atskilt med komma (ved at alle er tilgjengelige).\n    \n    Følgende makroer, som legger til flere utvidelser samtidig, er også tilgjengelige: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Bruk eksempel \".exe, IMAGE, VIDEO, .rar, 7z\" - dette betyr at bilder (e. . jpg, png), videoer (f.eks. avi, mp4), exe, rar og 7z filer vil bli skannet.\nupper_excluded_extensions_tooltip =\n    Liste over deaktiverte filer som vil bli ignorert i skanning.\n    \n    Ved bruk av både tillatte og deaktiverte utvidelser, har denne prioriteten høyere enn prioritet, så filen vil ikke bli sjekket.\nupper_excluded_items_tooltip = \n        Uekskluderte elementer må inneholde * wildcard og skal være separert med komma.\n        Dette er tregere enn Excluded Paths, så bruk det forsiktig.\nupper_excluded_items = Ekskluderte elementer:\nupper_allowed_extensions = Tillatte utvidelser:\nupper_excluded_extensions = Deaktiverte utvidelser:\n# Popovers\npopover_select_all = Velg alle\npopover_unselect_all = Fjern alle valg\npopover_reverse = Omvendt utvalg\npopover_select_all_except_shortest_path = Velg alle unntatt korteste vei\npopover_select_all_except_longest_path = Velg alle unntatt lengste sti\npopover_select_all_except_oldest = Velg alt unntatt det eldste\npopover_select_all_except_newest = Velg alle unntatt nyeste\npopover_select_one_oldest = Velg en eldste\npopover_select_one_newest = Velg en nyeste\npopover_select_custom = Velg egendefinert\npopover_unselect_custom = Avvelg egendefinert\npopover_select_all_images_except_biggest = Velg alle unntatt største\npopover_select_all_images_except_smallest = Velg alle unntatt minste\npopover_custom_path_check_button_entry_tooltip =\n    Velg poster som sti.\n    \n    Eksempelbruk:\n    /home/pimpek/rzecz.txt kan du finne med /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Velg poster etter filnavn.\n    \n    Eksempelbruk:\n    /usr/ping/pong.txt kan finnes med *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Velg elementer ved angitt Regex.\n    \n    Med denne modus, vil søppelteksten være Sti med navn.\n    \n    Eksempel på bruk:\n    /usr/bin/ziemniak. xt finner du med /ziem[a-z]+\n    \n    Dette bruker standard Rust regex implementasjon. Du kan lese mer om den her: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Aktiverer case-sensitiv deteksjon.\n    \n    Når deaktivert/hjem/* funn både /HoMe/roman og /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Hindrer å velge alle poster i gruppen.\n    \n    Dette er aktivert som standard, fordi i de fleste situasjoner du ikke vil slette både originale og duplikatfiler, men vil forlate minst en fil.\n    \n    ADVARSEL: Denne innstillingen fungerer ikke hvis du allerede har valgt alle resultater i en gruppe.\npopover_custom_regex_path_label = Sti\npopover_custom_regex_name_label = Navn\npopover_custom_regex_regex_label = Regex sti + navn\npopover_custom_case_sensitive_check_button = Skill store og små bokstaver\npopover_custom_all_in_group_label = Ikke velg alle poster i gruppen\npopover_custom_mode_unselect = Avvelg egendefinert\npopover_custom_mode_select = Velg egendefinert\npopover_sort_file_name = Filnavn\npopover_sort_folder_name = Mappenavn\npopover_sort_full_name = Fullt navn\npopover_sort_size = Størrelse\npopover_sort_selection = Utvalg\npopover_invalid_regex = Regex er ugyldig\npopover_valid_regex = Regex er gyldig\n# Bottom buttons\nbottom_search_button = Søk\nbottom_select_button = Velg\nbottom_delete_button = Slett\nbottom_save_button = Lagre\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Flytt\nbottom_sort_button = Sorter\nbottom_compare_button = Sammenlign\nbottom_search_button_tooltip = Starte søk\nbottom_select_button_tooltip = Velg oppføringer. Bare valgte filer/mapper kan bli behandlet senere.\nbottom_delete_button_tooltip = Slett valgte filer/mapper.\nbottom_save_button_tooltip = Lagre data om søk i fil\nbottom_symlink_button_tooltip =\n    Opprett symbolske lenker.\n    Virker bare når minst to resultater i en gruppe er valgt.\n    Først er uendret og sekund og senere er symlinket til først.\nbottom_hardlink_button_tooltip =\n    Opprette fastkoblinger.\n    Virker bare når minst to resultater i en gruppe er valgt.\n    Først er uendret og annet og senere er vanskelig knyttet til først.\nbottom_hardlink_button_not_available_tooltip =\n    Opprett faste koblinger.\n    Knappen er deaktivert, fordi faste koblinger ikke kan opprettes.\n    Faste koblinger fungerer bare med administratorrettigheter i Windows, så pass på at du kjører programmet som administrator.\n    Hvis programmet allerede fungerer med slike privilegier, sjekk om lignende problemer er observert på GitHub.\nbottom_move_button_tooltip =\n    Flytter filer til valgt mappe.\n    Den kopierer alle filer til mappen uten å lagre mappetreet.\n    Når du prøver å flytte to filer med identisk navn til mappe, vil det andre feile og vise feil.\nbottom_sort_button_tooltip = Sorter filer/mapper etter valgt metode.\nbottom_compare_button_tooltip = Sammenlign bilder i gruppen.\nbottom_show_errors_tooltip = Vis/Skjul bunntekstpanelet.\nbottom_show_upper_notebook_tooltip = Vis/Skjul øvre notebook panel.\n# Progress Window\nprogress_stop_button = Stopp\nprogress_stop_additional_message = Stopp forespurt\n# About Window\nabout_repository_button_tooltip = Link til pakkesiden med kildekoden.\nabout_donation_button_tooltip = Lenke til donasjonssiden.\nabout_instruction_button_tooltip = Lenke til instruksjonssiden.\nabout_translation_button_tooltip = Link til Crowdin-siden med app oversettelser. Officialt Polsk og engelsk støttes.\nabout_repository_button = Pakkebrønn\nabout_donation_button = Donasjon\nabout_instruction_button = Instruksjon\nabout_translation_button = Oversettelse\n# Header\nheader_setting_button_tooltip = Åpner dialogboksen for innstillinger.\nheader_about_button_tooltip = Åpner dialog med info om app.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Antall brukte tråder\nsettings_number_of_threads_tooltip = Antall brukte tråder. 0 betyr at alle tilgjengelige tråder vil bli brukt.\nsettings_use_rust_preview = Bruk eksterne biblioteker i stedet gtk for å laste forhåndsvisninger\nsettings_use_rust_preview_tooltip =\n    Med gtk innledning vil noen ganger være raskere og støtte flere formater, men noen ganger kan dette være akkurat det motsatte.\n    \n    Hvis du har problemer med å laste forhåndsvisninger, kan du prøve å endre denne innstillingen.\n    \n    på ikke-linux-systemer anbefales det å bruke dette alternativet, fordi gtk-pixbuf ikke alltid finnes, slik at deaktivering av dette alternativet ikke vil laste forhåndsvisninger på noen bilder.\nsettings_label_restart = Start programmet på nytt for å bruke innstillingene!\nsettings_ignore_other_filesystems = Ignorer andre filsystemer (bare Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignorerer filer som ikke er i samme filsystem som søk-kataloger.\n    \n    Fungerer samme som -xdev alternativet i å finne kommandoen på Linux\nsettings_save_at_exit_button_tooltip = Lagre konfigurasjon som fil når appen lukkes.\nsettings_load_at_start_button_tooltip =\n    Last inn konfigurasjon fra filen når du åpner appen.\n    \n    Hvis ikke er aktivert brukes standard innstillinger.\nsettings_confirm_deletion_button_tooltip = Vis bekreftelsesdialog når du klikker på Slette-knappen.\nsettings_confirm_link_button_tooltip = Vis bekreftelsesdialog når du klikker på knappen hard/symlink.\nsettings_confirm_group_deletion_button_tooltip = Vis advarselsdialog når du prøver å slette alle poster fra gruppen.\nsettings_show_text_view_button_tooltip = Vis tekstpanelet nederst av brukergrensesnittet.\nsettings_use_cache_button_tooltip = Bruk filmellomlager.\nsettings_save_also_as_json_button_tooltip = Lagre cache til (human lesbar) JSON-format. Det er mulig å endre innholdet. Cachen fra denne filen vil automatisk bli lest av app dersom binært format mellomlager (med «bøi-utvidelsen») mangler.\nsettings_use_trash_button_tooltip = Flytter filer til papirkurv istedenfor å slette dem permanent.\nsettings_language_label_tooltip = Språk til brukergrensesnitt.\nsettings_save_at_exit_button = Lagre konfigurasjon når appen lukkes\nsettings_load_at_start_button = Last inn konfigurasjon når du åpner appen\nsettings_confirm_deletion_button = Vis bekreftelsesdialog ved sletting av filer\nsettings_confirm_link_button = Vis bekreftelsesdialog når noen filer er fast/symlinker\nsettings_confirm_group_deletion_button = Vis \"Bekreft\"-dialog når du sletter alle filer i gruppen\nsettings_show_text_view_button = Vis nederste tekstpanel\nsettings_use_cache_button = Bruk buffer\nsettings_save_also_as_json_button = Lagre også mellomlager som JSON-fil\nsettings_use_trash_button = Flytt slettede filer til papirkurv\nsettings_language_label = Språk\nsettings_multiple_delete_outdated_cache_checkbutton = Slett utdaterte cache-oppføringer automatisk\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Slett utdaterte cache-resultater som peker til ikke-eksisterende filer.\n    \n    Når aktivert sørger appen for å laste inn poster, at alle oppføringer peker til gyldige filer (ødelagte blir ignorert).\n    \n    Deaktivering av dette vil hjelpe når du skanner filer på eksterne stasjoner, så cacheoppføringer om dem vil ikke bli tømt i neste skanning.\n    \n    Når det gjelder å ha hundre og tusenvis av registreringer i cache, det er foreslått å aktivere dette, som vil øke hurtigbufferen innlasting/lagring ved start/slutten av skanningen.\nsettings_notebook_general = Generelt\nsettings_notebook_duplicates = Duplikater\nsettings_notebook_images = Lignende bilder\nsettings_notebook_videos = Lignende video\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Viser forhåndsvisning på høyre side (når du velger en bildefil).\nsettings_multiple_image_preview_checkbutton = Vis forhåndsvisning av bilde\nsettings_multiple_clear_cache_button_tooltip =\n    Manuelt tømmer hurtigbufferen av utdaterte oppføringer.\n    Dette bør bare brukes hvis automatisk tømming er deaktivert.\nsettings_multiple_clear_cache_button = Fjern utdaterte resultater fra mellomlager.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Skjuler alle filer unntatt en, hvis alle peker til samme data (knyttes sammen).\n    \n    Eksempel: I det tilfellet hvor det (på disk) er syv filer som er vanskelige å koble til bestemte data og én annen fil med samme data men en annen innhold, deretter i duplisert finer, vises bare én unik fil og én fil fra hardkoblede vil vises.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Angi minste filstørrelse som blir bufret i cachen.\n    \n    Hvis du velger en mindre verdi, genererer du flere poster. Dette vil fremskynde søk, men tregere hurtigbufferet lasting/lagring.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Aktiverer hurtigbufring av prehash (en hash beregnet fra en liten del av filen) som tillater tidligere fjerning av ikke-dupliserte resultater.\n    \n    Den er deaktivert som standard fordi den kan forårsake langsommere i noen situasjoner.\n    \n    Det anbefales å bruke det når det skannes hundre eller millioner filer, fordi det kan fremskynde søk med flere ganger.\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimal størrelse på mellomlagret oppføring.\nsettings_duplicates_hide_hard_link_button = Skjul harde linker\nsettings_duplicates_prehash_checkbutton = Bruk prehash cache\nsettings_duplicates_minimal_size_cache_label = Minimal størrelse på filer (i byte) lagret i mellomlager\nsettings_duplicates_minimal_size_cache_prehash_label = Minimal størrelse på filer (i byte) lagret i hurtigbuffer for prehash\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Lagre gjeldende innstillingskonfigurasjon til filen.\nsettings_loading_button_tooltip = Last innstillinger fra fil og erstatt gjeldende konfigurasjon med dem.\nsettings_reset_button_tooltip = Tilbakestill den gjeldende konfigurasjonen til standard.\nsettings_saving_button = Lagre konfigurasjonen\nsettings_loading_button = Last inn konfigurasjon\nsettings_reset_button = Tilbakestill konfigurasjon\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Åpner mappen der cache txt filene er lagret.\n    \n    Modifisere cachefilene kan forårsake ugyldige resultater. Imidlertid kan modifisere stien spare tid når du flytter store mengder filer til en annen posisjon.\n    \n    Du kan kopiere disse filene mellom datamaskiner for å spare tid ved skanning igjen for filer (hvis de har en lignende katalogstruktur).\n    \n    Hvis det oppstår problemer med cachen, kan disse filene fjernes. Appen vil automatisk regenerere dem.\nsettings_folder_settings_open_tooltip =\n    Åpner mappen der Czkawka konfigurasjonen er lagret.\n    \n    ADVARSEL: Manuelt endre konfigurasjonen kan ødelegge arbeidsflyten din.\nsettings_folder_cache_open = Åpne mappe for hurtigbuffer\nsettings_folder_settings_open = Åpne innstillingsmappen\n# Compute results\ncompute_stopped_by_user = Søket ble stoppet av bruker\ncompute_found_duplicates_hash_size = Fant { $number_files } duplikater i { $number_groups } grupper som tok { $size } i { $time }\ncompute_found_duplicates_name = Fant { $number_files } duplikater i { $number_groups } grupper i { $time }\ncompute_found_empty_folders = Fant { $number_files } tomme mapper i { $time }\ncompute_found_empty_files = Fant { $number_files } tomme filer i { $time }\ncompute_found_big_files = Fant { $number_files } store filer i { $time }\ncompute_found_temporary_files = Fant { $number_files } midlertidige filer i { $time }\ncompute_found_images = Fant { $number_files } lignende bilder i { $number_groups } grupper i { $time }\ncompute_found_videos = Fant { $number_files } lignende videoer i { $number_groups } grupper i { $time }\ncompute_found_music = Fant { $number_files } lignende musikkfiler i { $number_groups } grupper i { $time }\ncompute_found_invalid_symlinks = Fant { $number_files } ugyldige symlinker i { $time }\ncompute_found_broken_files = Fant { $number_files } ødelagte filer i { $time }\ncompute_found_bad_extensions = Fant { $number_files } filer med ugyldige utvidelser i { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Skannet { $file_number } fil\n       *[other] Skannet { $file_number } filer\n    }\nprogress_scanning_extension_of_files = Merket utvidelse av { $file_checked }/{ $all_files } -filen\nprogress_scanning_broken_files = Merket { $file_checked }/{ $all_files } fil ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Kastet av { $file_checked }/{ $all_files } video\nprogress_creating_video_thumbnails = Lagede miniatyrbilder av { $file_checked }/{ $all_files } video\nprogress_scanning_image = Kastet av { $file_checked }/{ $all_files } bilde ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Sammenlignet { $file_checked }/{ $all_files } bilde-hash\nprogress_scanning_music_tags_end = Sammenligne tagger med { $file_checked }/{ $all_files } musikkfil\nprogress_scanning_music_tags = Les tagger for { $file_checked }/{ $all_files } musikkfil\nprogress_scanning_music_content_end = Sammenlignet fingeravtrykk av { $file_checked }/{ $all_files } musikkfil\nprogress_scanning_music_content = Beregnet fingeravtrykk på { $file_checked }/{ $all_files } musikkfil ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Skannet { $folder_number } mappe\n       *[other] Skannet { $folder_number } mapper\n    }\nprogress_scanning_size = Skannet størrelse på { $file_number } fil\nprogress_scanning_size_name = { $file_number } fil skannet navn og størrelse\nprogress_scanning_name = { $file_number } fil skannet navn\nprogress_analyzed_partial_hash = Analyserte delvis hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Analysert full hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Laster prehash cache\nprogress_prehash_cache_saving = Lagrer prehash-cache\nprogress_hash_cache_loading = Laster hash-cache\nprogress_hash_cache_saving = Lagrer hurtigbufferen for hash\nprogress_cache_loading = Laster cache\nprogress_cache_saving = Lagrer cachen\nprogress_current_stage = Gjeldende trinn: { \" \" }\nprogress_all_stages = Alle stadier:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Lagret konfigurasjon til filen { $name }.\nsaving_loading_saving_failure = Kunne ikke lagre konfigurasjonsdata som filen { $name }, grunn { $reason }.\nsaving_loading_reset_configuration = Gjeldende konfigurasjon ble fjernet.\nsaving_loading_loading_success = Godt lastet applikasjonskonfigurasjon.\nsaving_loading_failed_to_create_config_file = Kunne ikke opprette konfigurasjonsfilen{ $path }\", grunn \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Kan ikke laste konfigurasjonen fra \"{ $path }\" fordi den ikke eksisterer eller ikke er en fil.\nsaving_loading_failed_to_read_data_from_file = Kan ikke lese data fra filen{ $path }\", grunn \"{ $reason }\".\n# Other\nselected_all_reference_folders = Kan ikke starte søk, når alle kataloger er angitt som referanselapper\nsearching_for_data = Søker data, det kan ta en stund, vennligst vent...\ntext_view_messages = MELDINGER\ntext_view_warnings = ADVARSELSER\ntext_view_errors = FEILSER\nabout_window_motto = Dette programmet er gratis å bruke og vil alltid være.\nkrokiet_new_app = Tjekkawka er i vedlikeholdsmodus, noe som betyr at bare kritiske feil utbedres, og ingen nye funksjoner legges til. For nye funksjoner, vennligst sjekk ut ny Krokiet-app, som er mer stabil og fremstår som under aktiv utvikling.\n# Various dialog\ndialogs_ask_next_time = Spør neste gang\nsymlink_failed = Kan ikke symlink { $name } til { $target }, årsak { $reason }\ndelete_title_dialog = Bekreft sletting\ndelete_question_label = Er du sikker på at du vil slette filer?\ndelete_all_files_in_group_title = Bekreftelse på sletting av alle filer i gruppen\ndelete_all_files_in_group_label1 = For noen grupper er alle poster valgt.\ndelete_all_files_in_group_label2 = Er du sikker på at du vil slette dem?\ndelete_items_label = { $items } filer vil bli slettet.\ndelete_items_groups_label = { $items } filer fra { $groups } grupper vil bli slettet.\nhardlink_failed = Kunne ikke koble { $name } til { $target }, grunn { $reason }\nhard_sym_invalid_selection_title_dialog = Ugyldig valg med noen grupper\nhard_sym_invalid_selection_label_1 = I noen grupper er det bare én post valgt og det vil bli ignorert.\nhard_sym_invalid_selection_label_2 = For å kunne feste koblingen til disse filene, må minst to resultater i gruppen velges.\nhard_sym_invalid_selection_label_3 = Først i gruppen gjenkjennes som originalen og endres ikke, men sekund og senere endres.\nhard_sym_link_title_dialog = Lenke bekreftelse\nhard_sym_link_label = Er du sikker på at du vil koble disse filene?\nmove_folder_failed = Kunne ikke flytte mappen { $name }, årsak { $reason }\nmove_file_failed = Kunne ikke flytte filen { $name }, årsak { $reason }\nmove_files_title_dialog = Velg mappen du vil flytte dupliserte filer til\nmove_files_choose_more_than_1_path = Bare én sti kan velges for å kunne kopiere sine dupliserte filer, valgt { $path_number }.\nmove_stats = Flott flyttet { $num_files }/{ $all_files } elementer\nsave_results_to_file = Lagrede resultater både i txt og json filer inn i \"{ $name }\"-mappen.\nsearch_not_choosing_any_music = FEIL: Du må velge minst en avkrysningsboks med musikk som søker.\nsearch_not_choosing_any_broken_files = FEIL: Du må velge minst en avkrysningsboks med sjekket ødelagte filer.\ninclude_folders_dialog_title = Mapper å inkludere\nexclude_folders_dialog_title = Mapper som skal ekskluderes\ninclude_manually_directories_dialog_title = Legg til mappe manuelt\ncache_properly_cleared = Riktig tømt cache\ncache_clear_duplicates_title = Tømmer duplikatene\ncache_clear_similar_images_title = Fjerner lignende bilde-mellomlager\ncache_clear_similar_videos_title = Tømmer hurtigbufferen for videoer\ncache_clear_message_label_1 = Vil du slette cachen med utdaterte oppføringer?\ncache_clear_message_label_2 = Denne operasjonen vil fjerne alle cacheoppføringer som peker til ugyldige filer.\ncache_clear_message_label_3 = Dette kan øke lasting og lagring på cache.\ncache_clear_message_label_4 = ADVARSEL: Operasjonen vil fjerne alle bufrede data fra eksterne stasjoner som ikke er koblet til. Så hver hash må regenereres.\n# Show preview\npreview_image_resize_failure = Kunne ikke endre størrelse på bildet { $name }.\npreview_image_opening_failure = Klarte ikke å åpne bilde { $name }, årsak { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Gruppe { $current_group }/{ $all_groups } ({ $images_in_group } bilder)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/pl/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Ustawienia\nwindow_main_title = Czkawka\nwindow_progress_title = Skanowanie\nwindow_compare_images = Porównywanie Obrazów\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Zamknij\n# Krokiet info dialog\nkrokiet_info_title = Wprowadzamy Krokiet - Nowa wersja Czkawki\nkrokiet_info_message = \n        Krokiet to nowa, ulepszona, szybsza i bardziej niezawodna wersja Czkawki GTK!\n\n        Jest łatwiejszy w uruchomieniu i bardziej odporny na zmiany w systemie, ponieważ polega tylko na podstawowych bibliotekach dostępnych domyślnie na większości systemów.\n\n        Krokiet oferuje również funkcje, których brakuje Czkawce, w tym miniaturki w trybie porównania wideo, czyszczenie EXIF, wyświetlanie postępu kopiowania/przenoszenia/usuwania plików czy rozszerzone opcje sortowania.\n\n        Wypróbuj go i zobacz różnicę!\n\n        Czkawka będzie nadal otrzymywać poprawki błędów i drobne aktualizacje z mojej strony, ale wszystkie nowe funkcje będą rozwijane wyłącznie dla Krokieta, lecz zachęcam każdego chętnego by jeśli chce, to by implementował na własną rękę nowe funkcje czy brakujące tryby w Czkawce.\n\n        PS: Ta wiadomość powinna pojawić się tylko raz. Jeśli pojawia się ponownie, ustaw zmienną środowiskową CZKAWKA_DONT_ANNOY_ME na dowolną niepustą wartość.\n# Main window\nmusic_title_checkbox = Tytuł\nmusic_artist_checkbox = Wykonawca\nmusic_year_checkbox = Rok\nmusic_bitrate_checkbox = Przepływność bitów\nmusic_genre_checkbox = Gatunek\nmusic_length_checkbox = Długość\nmusic_comparison_checkbox = Przybliżone Porównywanie\nmusic_checking_by_tags = Tagi\nmusic_checking_by_content = Zawartość\nsame_music_seconds_label = Minimalny czas trwania podobnego fragmentu\nsame_music_similarity_label = Maksymalna różnica\nmusic_compare_only_in_title_group = Porównaj w grupach o podobnych tytułach\nmusic_compare_only_in_title_group_tooltip =\n    Gdy włączone, pliki są pogrupowane według tytułu, a następnie porównywane do siebie.\n    \n    z 10000 plików, zamiast prawie 100 milionów porównań, zwykle będzie sprawdzone ~20000 porównań.\nsame_music_tooltip =\n    Wyszukiwanie podobnych plików muzycznych przez jego zawartość można skonfigurować przez ustawienie:\n    \n    - Minimalny czas fragmentu, po którym pliki muzyczne mogą być zidentyfikowane jako podobne\n    - Maksymalna różnica między dwoma testowanymi fragmentami\n    \n    Kluczem do dobrych wyników jest znalezienie rozsądnych kombinacji tych parametrów, do dostarczania.\n    \n    Ustawianie minimalnego czasu na 5s i maksymalnej różnicy na 1.0, będzie szukać prawie identycznych fragmentów w plikach.\n    Czas 20s i maksymalna różnica 6.0, z drugiej strony, dobrze działa w poszukiwaniu remiksów/wersji na żywo itp.\n    \n    Domyślnie każdy plik muzyczny jest porównywany ze sobą, co może zająć dużo czasu podczas testowania wielu plików, więc zwykle lepiej jest używać folderów referencyjnych i określać, które pliki mają być porównywane ze sobą (z taką samą ilością plików, porównywanie odcisków palców będzie szybsze niż bez folderów referencyjnych).\nmusic_comparison_checkbox_tooltip =\n    Wyszukuje podobne pliki muzyczne za pomocą AI, która używa nauki maszynowej, aby usunąć nawiasy z frazy. Na przykład, z tą opcją włączoną, rozpatrywane pliki będą traktowane jako duplikaty:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Uwzględnij Wielkość Liter\nduplicate_case_sensitive_name_tooltip =\n    Gdy włączone, grupowe rekordy tylko wtedy, gdy mają dokładnie taką samą nazwę, np. Żołd <-> Żołd\n    \n    Wyłączenie tej opcji spowoduje grupowanie nazw bez sprawdzania, czy każda litera ma ten sam rozmiar, np. żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Rozmiar i nazwa\nduplicate_mode_name_combo_box = Nazwa\nduplicate_mode_size_combo_box = Rozmiar\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka oferuje 3 rodzaje hashów:\n    \n    Blake3 - kryptograficzna funkcja skrótu. Jest to wartość domyślna, ponieważ jest bardzo szybka.\n    \n    CRC32 - prosta funkcja haszująca. Powinno to być szybsze od Blake3, ale bardzo rzadko może to prowadzić do kolizji.\n    \n    XXH3 - bardzo podobna pod względem wydajności i jakości hashu do Blake3 (ale niekryptograficzna). Tak więc takie tryby mogą być łatwo wymienione.\nduplicate_check_method_tooltip =\n    Na razie Czkawka oferuje trzy typy metod do znalezienia duplikatów przez:\n    \n    Nazwa - Znajduje pliki o tej samej nazwie.\n    \n    Rozmiar - Znajduje pliki o tym samym rozmiarze.\n    \n    Hash - Znajduje pliki, które mają tę samą zawartość. Ten tryb haszuje plik, a następnie porównuje utworzony skrót(hash) aby znaleźć duplikaty. Ten tryb jest najbezpieczniejszym sposobem na znalezienie duplikatów. Aplikacja używa pamięci podręcznej, więc drugie i kolejne skanowanie tych samych danych powinno być dużo szybsze niż za pierwszym razem.\nimage_hash_size_tooltip =\n    Każdy zaznaczony obraz tworzy specjalny skrót, który można porównać ze sobą, a niewielka różnica między nimi oznacza, że obrazy te są podobne.\n    \n    8 hashh rozmiar jest dość dobry do wyszukiwania obrazów, które są tylko trochę podobne do oryginału. Dzięki większemu zestawowi zdjęć (>1000), spowoduje to uzyskanie dużej ilości fałszywych dodatnich, więc zalecam użycie większego rozmiaru skrótu w tym przypadku.\n    \n    16 to domyślny rozmiar hasha, który jest dość dobrym kompromisem między znalezieniem nawet nieco podobnych obrazów a zaledwie niewielką ilością kolizji haszujących.\n    \n    32 i 64 hashy znajdują tylko bardzo podobne obrazy, ale nie powinny mieć prawie żadnych fałszywych pozytywnych (może z wyjątkiem niektórych obrazów z kanałem alfa).\nimage_resize_filter_tooltip =\n    Aby obliczyć skrót obrazu, biblioteka musi najpierw zmienić jego rozmiar.\n    \n    Dołącz do wybranego algorytmu, wynikowy obraz użyty do obliczenia skrótu będzie wyglądał nieco inaczej.\n    \n    Najszybszy algorytm do użycia, ale także ten, który daje najgorsze wyniki, jest najbardziej potrzebny. Domyślnie jest włączona, ponieważ przy rozmiarze skrótu 16x16 jego jakość nie jest naprawdę widoczna.\n    \n    Przy rozmiarze skrótu 8x8 zaleca się użycie innego algorytmu niż najbliższy, aby mieć lepsze grupy obrazów.\nimage_hash_alg_tooltip =\n    Użytkownicy mogą wybrać jeden z wielu algorytmów obliczania hashu.\n    \n    Każdy ma zarówno mocniejsze jak i słabsze punkty i czasami daje lepsze a czasami gorsze wyniki dla różnych obrazów.\n    \n    Najlepiej jest samemu potestować jaki algorytm ma najlepsze wyniki(może to nie być zawsze dobrze widoczne).\nbig_files_mode_combobox_tooltip = Pozwala na wyszukiwanie najmniejszych lub największych plików\nbig_files_mode_label = Sprawdzane pliki\nbig_files_mode_smallest_combo_box = Najmniejsze\nbig_files_mode_biggest_combo_box = Największe\nmain_notebook_duplicates = Duplikaty\nmain_notebook_empty_directories = Puste Katalogi\nmain_notebook_big_files = Duże Pliki\nmain_notebook_empty_files = Puste Pliki\nmain_notebook_temporary = Pliki Tymczasowe\nmain_notebook_similar_images = Podobne Obrazy\nmain_notebook_similar_videos = Podobne Wideo\nmain_notebook_same_music = Podobna Muzyka\nmain_notebook_symlinks = Niepoprawne Symlinki\nmain_notebook_broken_files = Zepsute Pliki\nmain_notebook_bad_extensions = Błędne rozszerzenia\nmain_tree_view_column_file_name = Nazwa\nmain_tree_view_column_folder_name = Nazwa\nmain_tree_view_column_path = Ścieżka\nmain_tree_view_column_modification = Data Modyfikacji\nmain_tree_view_column_size = Rozmiar\nmain_tree_view_column_similarity = Podobieństwo\nmain_tree_view_column_dimensions = Wymiary\nmain_tree_view_column_title = Tytuł\nmain_tree_view_column_artist = Wykonawca\nmain_tree_view_column_year = Rok\nmain_tree_view_column_bitrate = Przepływność bitów\nmain_tree_view_column_length = Długość\nmain_tree_view_column_genre = Gatunek\nmain_tree_view_column_symlink_file_name = Nazwa Symlinka\nmain_tree_view_column_symlink_folder = Folder Symlinka\nmain_tree_view_column_destination_path = Docelowa Ścieżka\nmain_tree_view_column_type_of_error = Typ Błędu\nmain_tree_view_column_current_extension = Aktualne rozszerzenie\nmain_tree_view_column_proper_extensions = Poprawne rozszerzenia\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Kodek\nmain_label_check_method = Metoda sprawdzania\nmain_label_hash_type = Typ hashu\nmain_label_hash_size = Rozmiar hashu\nmain_label_size_bytes = Rozmiar (bajty)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Liczba wyświetlanych plików\nmain_label_resize_algorithm = Algorytm zmiany rozmiaru\nmain_label_similarity = Podobieństwo{ \"   \" }\nmain_check_box_broken_files_audio = Dźwięk\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Archiwa\nmain_check_box_broken_files_image = Obraz\nmain_check_box_broken_files_video = Wideo\nmain_check_box_broken_files_video_tooltip = Używa ffmpeg/ffprobe do weryfikacji plików wideo. Jest to dość powolne i może wykryć błędy, które nie są widoczne przy zwykłym odtwarzaniu wideo.\ncheck_button_general_same_size = Ignoruj identyczny rozmiar\ncheck_button_general_same_size_tooltip = Ignoruj pliki o identycznym rozmiarze w wynikach - zazwyczaj są to duplikaty 1:1\nmain_label_size_bytes_tooltip = Rozmiar plików które będą zawarte przy przeszukiwaniu\n# Upper window\nupper_tree_view_included_folder_column_title = Foldery do Przeszukania\nupper_tree_view_included_reference_column_title = Foldery Referencyjne\nupper_recursive_button = Rekursywnie\nupper_recursive_button_tooltip = Jeśli zaznaczony, szuka plików i folderów również w katalogach wewnątrz, nawet jeśli nie znajdują się one bezpośrednio w tym folderze.\nupper_manual_add_included_button = Ręcznie Dodaj\nupper_add_included_button = Dodaj\nupper_remove_included_button = Usuń\nupper_manual_add_excluded_button = Ręcznie Dodaj\nupper_add_excluded_button = Dodaj\nupper_remove_excluded_button = Usuń\nupper_manual_add_included_button_tooltip =\n    Dodaj nazwę katalogu do ręcznego wyszukiwania.\n    \n    Aby dodać wiele ścieżek na raz, należy je oddzielić za pomocą średnika ;\n    \n    /home/roman;/home/rozkaz doda dwa katalogi /home/roman i /home/rozkaz\nupper_add_included_button_tooltip = Dodaje wybrany folder do przeskanowania.\nupper_remove_included_button_tooltip = Usuwa zaznaczony folder z listy do skanowania.\nupper_manual_add_excluded_button_tooltip =\n    Dodaj ręcznie katalog do listy wykluczonych.\n    \n    Aby dodać wiele ścieżek na raz, oddziel je średnikiem ;\n    \n    /home/roman;/home/krokiet doda dwa katalogi /home/roman i /home/keokiet\nupper_add_excluded_button_tooltip = Dodaje wybrany folder do ignorowanych.\nupper_remove_excluded_button_tooltip = Usuwa zaznaczony folder z ignorowanych.\nupper_notebook_items_configuration = Konfiguracja Skanowania\nupper_notebook_excluded_directories = Wykluczone Ścieżki\nupper_notebook_included_directories = Dołączone ścieżki\nupper_allowed_extensions_tooltip =\n    Dozwolone rozszerzenia muszą być oddzielone przecinkami (domyślnie wszystkie są dostępne).\n    \n    Istnieją makra, które umożliwiają dołączenie za jednym razem określonych typów plików IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Przykład użycia \".exe, IMAGE, VIDEO, .rar, 7z\" - oznacza że obrazy (np. jpg, png), filmy (np. avi, mp4), exe, rar i 7z zostaną sprawdzone.\nupper_excluded_extensions_tooltip =\n    Lista wyłączonych plików, które zostaną zignorowane w skanowaniu.\n    \n    Gdy używasz zarówno dozwolonych, jak i wyłączonych rozszerzeń, ten ma wyższy priorytet, więc plik nie zostanie sprawdzony.\nupper_excluded_items_tooltip = \n        Wykluczone elementy muszą zawierać znak * i powinny być oddzielone przecinkami.\n        To działa wolniej niż ustawianie wykluczonych katalogow i plików, więc używaj tego z rozwagą.\nupper_excluded_items = Ignorowane Obiekty:\nupper_allowed_extensions = Dozwolone Rozszerzenia:\nupper_excluded_extensions = Wyłączone rozszerzenia:\n# Popovers\npopover_select_all = Zaznacz wszystko\npopover_unselect_all = Odznacz wszystko\npopover_reverse = Odwróć zaznaczenie\npopover_select_all_except_shortest_path = Zaznacz wszystkie oprócz najkrótszej ścieżki\npopover_select_all_except_longest_path = Zaznacz wszystkie oprócz najdłuższego ścieżki\npopover_select_all_except_oldest = Zaznacz wszystkie oprócz najstarszego\npopover_select_all_except_newest = Zaznacz wszystkie oprócz najnowszego\npopover_select_one_oldest = Zaznacz jedno najstarsze\npopover_select_one_newest = Zaznacz jedno najnowsze\npopover_select_custom = Własne zaznaczanie\npopover_unselect_custom = Własne odznaczanie\npopover_select_all_images_except_biggest = Zaznacz wszystkie oprócz największego\npopover_select_all_images_except_smallest = Zaznacz wszystkie oprócz najmniejszego\npopover_custom_path_check_button_entry_tooltip =\n    Zaznacza rekordy według ścieżki.\n    \n    Przykładowe użycie:\n    /home/pimpek/rzecz.txt można znaleźć używając /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Zaznacza rekordy według nazw plików.\n    \n    Przykładowe użycie:\n    /usr/ping/pong.txt można znaleźć za pomocą *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Wybierz rekordy według określonego Regexa.\n    \n    W tym trybie wyszukiwanym tekstem jest pełna ścieżka(wraz z nazwą).\n    \n    Przykładowe użycie:\n    /usr/bin/ziemniak. xt można znaleźć za pomocą /ziem[a-z]+\n    \n    Używana jest tutaj domyślnej implementacja Regexa w Rust. Więcej informacji na ten temat można znaleźć tutaj: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Umożliwia wykrywanie wielkości liter.\n    \n    Wykluczenie /home/* znajdzie zarówno /HoMe/roman, jak i /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Zapobiega wybraniu wszystkich rekordów w grupie.\n    \n    Ta opcja jest domyślnie włączona, ponieważ w większości sytuacji prawdopodobnie nie chcesz usuwać zarówno oryginałów jak i duplikatów, lecz chcesz pozostawić co najmniej jeden plik.\n    \n    OSTRZEŻENIE: To ustawienie nie działa jeśli wcześniej ręcznie zostały wybrane wszystkie rekordy w grupie.\npopover_custom_regex_path_label = Ścieżka\npopover_custom_regex_name_label = Nazwa\npopover_custom_regex_regex_label = Regex - Pełna ścieżka\npopover_custom_case_sensitive_check_button = Rozróżniaj wielkość liter\npopover_custom_all_in_group_label = Nie zaznaczaj wszystkich rekordów w grupie\npopover_custom_mode_unselect = Własne odznaczanie\npopover_custom_mode_select = Własne zaznaczanie\npopover_sort_file_name = Nazwa pliku\npopover_sort_folder_name = Nazwa katalogu\npopover_sort_full_name = Pełna nazwa\npopover_sort_size = Rozmiar\npopover_sort_selection = Zaznaczanie\npopover_invalid_regex = Regex jest niepoprawny\npopover_valid_regex = Regex jest poprawny\n# Bottom buttons\nbottom_search_button = Szukaj\nbottom_select_button = Zaznacz\nbottom_delete_button = Usuń\nbottom_save_button = Zapisz\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Przenieś\nbottom_sort_button = Sortuj\nbottom_compare_button = Porównaj\nbottom_search_button_tooltip = Rozpocznij wyszukiwanie\nbottom_select_button_tooltip = Wybierz rekordy. Tylko wybrane pliki/foldery mogą być później przetwarzane.\nbottom_delete_button_tooltip = Usuń zaznaczone elementy.\nbottom_save_button_tooltip = Zapisz informacje o skanowaniu\nbottom_symlink_button_tooltip =\n    Utwórz linki symboliczne.\n    Działa tylko wtedy, gdy co najmniej dwa wyniki w grupie są zaznaczone.\n    Pierwszy jest niezmieniony, drugi i następny jest powiązywany z pierwszym.\nbottom_hardlink_button_tooltip =\n    Tworzenie twardych linków.\n    Działa tylko wtedy, gdy wybrano co najmniej dwa rekordy w grupie.\n    Pierwszy jest niezmieniony, drugi i następny jest dowiązywany z pierwszym.\nbottom_hardlink_button_not_available_tooltip =\n    Tworzenie twardych dowiązań.\n    Przycisk jest zablokowany, gdyż stworzenie twardego dowiązania nie jest możliwe.\n    Dowiązanie tego rodzaju może tworzyć administrator w systemie Windows, więc należy upewnić się że aplikacja jest uruchomiona przez z tymi uprawnieniami.\n    Jeśli aplikacja działa z nimi, należy przeszukać issues w Githubie celem znalezienia możliwych rozwiązań danego problemu.\nbottom_move_button_tooltip =\n    Przenosi pliki do wybranego katalogu.\n    Kopiuje wszystkie pliki do katalogu bez zachowania struktury plików.\n    Podczas próby przeniesienia dwóch plików o identycznej nazwie do folderu, drugi plik nie zostanie przeniesiony i pojawi się błąd.\nbottom_sort_button_tooltip = Sortuje pliki/foldery zgodnie z wybraną metodą.\nbottom_compare_button_tooltip = Porównaj obrazy w grupie.\nbottom_show_errors_tooltip = Pokaż/Ukryj dolny panel tekstowy.\nbottom_show_upper_notebook_tooltip = Pokazuje/ukrywa górny panel.\n# Progress Window\nprogress_stop_button = Zatrzymaj\nprogress_stop_additional_message = Przerywanie skanowania\n# About Window\nabout_repository_button_tooltip = Link do repozytorium z kodem źródłowym.\nabout_donation_button_tooltip = Link do strony z dotacjami.\nabout_instruction_button_tooltip = Link do strony z instrukcją.\nabout_translation_button_tooltip = Link do strony Crowdin z tłumaczeniami aplikacji. Oficialnie wspierany jest język polski i angielski.\nabout_repository_button = Repozytorium\nabout_donation_button = Dotacje\nabout_instruction_button = Instrukcja(ENG)\nabout_translation_button = Tłumaczenie\n# Header\nheader_setting_button_tooltip = Otwórz okno z ustawieniami programu.\nheader_about_button_tooltip = Otwórz okno z informacjami o programie.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Liczba używanych wątków\nsettings_number_of_threads_tooltip = Liczba używanych wątków, 0 oznacza, że zostaną użyte wszystkie dostępne wątki.\nsettings_use_rust_preview = Użyj zewnętrznych bibliotek zamiast gtk, aby załadować podgląd\nsettings_use_rust_preview_tooltip =\n    Korzystanie z podglądu gtk będzie czasem szybsze i obsługuje więcej formatów, ale czasami może to być dokładnie odwrotne.\n    \n    Jeśli masz problemy z ładowaniem podglądów, możesz spróbować zmienić to ustawienie.\n    \n    W systemach innych niż linux zaleca się użycie tej opcji, ponieważ gtk-pixbuf nie zawsze jest tam dostępny, więc wyłączenie tej opcji nie załaduje podglądu niektórych obrazów.\nsettings_label_restart = Musisz ponownie uruchomić aplikację, aby aplikacja zaciągnęła nowe ustawienia!\nsettings_ignore_other_filesystems = Ignoruj inne systemy plików (tylko Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignoruje pliki, które nie są w tym samym systemie plików co przeszukiwane katalogi.\n    \n    Działa tak samo jak opcja -xdev w komendzie find na Linux\nsettings_save_at_exit_button_tooltip = Zapisz konfigurację do pliku podczas zamykania aplikacji.\nsettings_load_at_start_button_tooltip =\n    Wczytaj konfigurację z pliku podczas otwierania aplikacji.\n    \n    Jeśli nieaktywny, zostaną użyte domyślne ustawienia.\nsettings_confirm_deletion_button_tooltip = Pokaż okno dialogowe potwierdzające usuwanie przy próbie usunięcia rekordu.\nsettings_confirm_link_button_tooltip = Pokaż dodatkowe okno dialogowe przy próbie utworzenia hard/symlinków.\nsettings_confirm_group_deletion_button_tooltip = Pokaż okno dialogowe ostrzegające przy próbie usunięcia wszystkich rekordów z grupy.\nsettings_show_text_view_button_tooltip = Pokaż dolny panel tekstowy.\nsettings_use_cache_button_tooltip = Użyj pamięci podręcznej plików.\nsettings_save_also_as_json_button_tooltip = Zapisz pamięć podręczną do formatu JSON (czytelnego dla człowieka). Można modyfikować jego zawartość. Pamięć podręczna z tego pliku zostanie odczytana automatycznie przez aplikację, jeśli brakuje pamięci podręcznej formatu binarnego (z rozszerzeniem bin).\nsettings_use_trash_button_tooltip = Przenosi pliki do kosza zamiast usuwać je na stałe.\nsettings_language_label_tooltip = Język interfejsu użytkownika.\nsettings_save_at_exit_button = Zapisz konfigurację podczas zamykania aplikacji\nsettings_load_at_start_button = Załaduj konfigurację z pliku podczas otwierania aplikacji\nsettings_confirm_deletion_button = Pokazuj okno potwierdzające usuwanie plików\nsettings_confirm_link_button = Pokazuj potwierdzenie usuwania hard/symlinków\nsettings_confirm_group_deletion_button = Pokazuj okno potwierdzające usuwanie wszystkich obiektów w grupie\nsettings_show_text_view_button = Pokazuj panel tekstowy na dole\nsettings_use_cache_button = Używaj pamięci podręcznej\nsettings_save_also_as_json_button = Zapisz pamięć podręczną również do pliku JSON\nsettings_use_trash_button = Przenoś pliki do kosza\nsettings_language_label = Język\nsettings_multiple_delete_outdated_cache_checkbutton = Usuwaj automatycznie nieaktualne rekordy z pamięci podręcznej\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Usuń nieaktualne rekordy z pamięci podręcznej, które wskazują na nieistniejące pliki.\n    \n    Po włączeniu aplikacja upewnia się, że podczas ładowania rekordów wszystkie wskazują na prawidłowe pliki (uszkodzone czy zmienione pliki są ignorowane).\n    \n    Wyłączenie tej opcji, pomoże podczas skanowania plików na zewnętrznych dyskach, więc wpisy dotyczące ich nie zostaną usunięte w następnym skanowaniu.\n    \n    W przypadku posiadania stu tysięcy rekordów w pamięci podręcznej, sugeruje się, aby włączyć tę opcję, ponieważ przyspieszy ładowanie/zapisywanie pamięci podręcznej na początku/końcu skanowania.\nsettings_notebook_general = Ogólne\nsettings_notebook_duplicates = Duplikaty\nsettings_notebook_images = Podobne Obrazy\nsettings_notebook_videos = Podobne Wideo\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Pokazuje podgląd po prawej stronie (podczas zaznaczania obrazu).\nsettings_multiple_image_preview_checkbutton = Pokazuj podgląd obrazów\nsettings_multiple_clear_cache_button_tooltip =\n    Ręcznie wyczyść pamięć podręczną przestarzałych wpisów.\n    To powinno być używane tylko wtedy, gdy automatyczne czyszczenie zostało wyłączone.\nsettings_multiple_clear_cache_button = Usuń nieaktualne wyniki z pamięci podręcznej.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Ukrywa wszystkie pliki z wyjątkiem jednego, jeśli wszystkie wskazują na te same dane (są połączone twardym dowiązaniem).\n    \n    Przykład: W przypadku gdy istnieje (na dysku) siedem plików, które są twardo dowiązane ze sobą i jeden inny plik z tymi samymi danymi, ale innym inode, wtedy w oknie wyników, wyświetlony zostanie tylko jeden unikalny plik i jeden plik z siedmiu dowiązanych ze sobą plików.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Ustaw minimalny rozmiar pliku, który zapisywany będzie do pliku z pamięcią podręcznej.\n    \n    Wybór mniejszej wartości spowoduje wygenerowanie większej ilości rekordów. To przyspieszy wyszukiwanie, ale spowolni ładowanie/zapisywanie danych do pamięci podręcznej.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Włącza zapisywanie częściowych haszów do pamięci podręcznej (hash obliczany jest tylko z małej części pliku), które pozwala na wcześniejsze odrzucenie unikalnych plików.\n    \n    Jest domyślnie wyłączona opcja, ponieważ może spowodować spowolnienie w niektórych sytuacjach.\n    \n    Zaleca się używanie tej opcji podczas skanowania setek tysięcy lub milionów plików, ponieważ może przyspieszyć wielokrotnie przeszukiwanie i wyłaczać gdy skanuje się niewielką ilość danych.\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimalny rozmiar pliku, którego cząstkowy hash będzie zapisywany do pamięci podręcznej.\nsettings_duplicates_hide_hard_link_button = Ukryj twarde dowiązania\nsettings_duplicates_prehash_checkbutton = Używaj pamięci podręcznej dla hashy cząstkowych\nsettings_duplicates_minimal_size_cache_label = Minimalny rozmiar plików (w bajtach) zapisywanych do pamięci podręcznej\nsettings_duplicates_minimal_size_cache_prehash_label = Minimalny rozmiar plików (w bajtach) przy zapisywaniu ich częściowego haszu do pamięci podręcznej\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Zapisz aktualną konfigurację ustawień do pliku.\nsettings_loading_button_tooltip = Załaduj ustawienia z pliku i nadpisz bieżącą konfigurację.\nsettings_reset_button_tooltip = Przywróć domyślną konfigurację.\nsettings_saving_button = Zapisanie ustawień\nsettings_loading_button = Załadowanie ustawień\nsettings_reset_button = Reset ustawień\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Otwiera folder gdzie przechowywana jest pamięć podręczna aplikacji.\n    \n    Ręczne modyfikowanie może powodować wyświetlanie niepoprawnych wyników lub jej uszkodzenie spowoduje konieczność ponownej generacji, lecz umożliwia też oszczędzenie czasu przy przesuwaniu większej ilości plików.\n    \n    Pliki można kopiować pomiędzy komputerami by zaoszczędzić czas na hashowaniu plików (oczywiście tylko gdy dane są przechowywane w identycznej strukturze katalogów na komputerach).\n    \n    W razie problemów z pamięcią podręczną, pliki mogą zostać usunięte. Aplikacja automatycznie je zregeneuje.\nsettings_folder_settings_open_tooltip =\n    Otwiera folder, w którym konfiguracja Czkawki jest przechowywana.\n    \n    OSTRZEŻENIE: ręczna modyfikacja konfiguracji może zakłócić przepływ twojej pracy.\nsettings_folder_cache_open = Otwórz folder pamięci podręcznej\nsettings_folder_settings_open = Otwórz folder ustawień\n# Compute results\ncompute_stopped_by_user = Przeszukiwanie zostało zatrzymane przez użytkownika\ncompute_found_duplicates_hash_size = Znaleziono { $number_files } duplikatów w { $number_groups } grupach w { $time }, które zajęły { $size }\ncompute_found_duplicates_name = Znaleziono { $number_files } duplikatów w { $number_groups } grupach w { $time }\ncompute_found_empty_folders = Znaleziono { $number_files } pustych folderów w { $time }\ncompute_found_empty_files = Znaleziono { $number_files } pustych plików w { $time }\ncompute_found_big_files = Znaleziono { $number_files } dużych plików w { $time }\ncompute_found_temporary_files = Znaleziono { $number_files } plików tymczasowych w { $time }\ncompute_found_images = Znaleziono { $number_files } podobnych obrazów w grupach { $number_groups } w { $time }\ncompute_found_videos = Znaleziono { $number_files } podobnych plików wideo w { $number_groups } grupach w { $time }\ncompute_found_music = Znaleziono { $number_files } podobnych plików muzycznych w { $number_groups } grupach w { $time }\ncompute_found_invalid_symlinks = Znaleziono { $number_files } niepoprawnych symlinków w { $time }\ncompute_found_broken_files = Znaleziono { $number_files } uszkodzonych plików w { $time }\ncompute_found_bad_extensions = Znaleziono { $number_files } plików z nieprawidłowymi rozszerzeniami w { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Przeskanowano { $file_number } plik\n       *[other] Przeskanowano { $file_number } plików\n    }\nprogress_scanning_extension_of_files = Sprawdzono rozszerzenie { $file_checked }/{ $all_files } plików\nprogress_scanning_broken_files = Sprawdzono plik { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Obliczono hashe dla { $file_checked }/{ $all_files } plików video\nprogress_creating_video_thumbnails = Utworzone miniatury filmu { $file_checked }/{ $all_files }\nprogress_scanning_image = Obliczono hashe dla { $file_checked }/{ $all_files } obrazów ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Porównano { $file_checked }/{ $all_files } hashy obrazów\nprogress_scanning_music_tags_end = Porównano { $file_checked }/{ $all_files } tagów plików audio\nprogress_scanning_music_tags = Odczytano tagi { $file_checked }/{ $all_files } plików audio\nprogress_scanning_music_content_end = Porównano hashe { $file_checked }/{ $all_files } plików audio\nprogress_scanning_music_content = Obliczono hash { $file_checked }/{ $all_files } plików audio ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Przeskanowano { $folder_number } folder\n       *[other] Przeskanowano { $folder_number } folderów\n    }\nprogress_scanning_size = Przeskano rozmiar { $file_number } plików\nprogress_scanning_size_name = Sprawdzono rozmiar i nazwę { $file_number } plików\nprogress_scanning_name = Sprawdzono nazwę { $file_number } plików\nprogress_analyzed_partial_hash = Obliczono częściowy hash { $file_checked }/{ $all_files } plików ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Obliczono pełny hash { $file_checked }/{ $all_files } plików ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Ładowanie pamięci podręcznej częściowego hashu\nprogress_prehash_cache_saving = Zapisywanie pamięci podręcznej częściowego hashu\nprogress_hash_cache_loading = Ładowanie pamięci podręcznej hash\nprogress_hash_cache_saving = Zapisywanie pamięci podręcznej skrótu\nprogress_cache_loading = Ładowanie pamięci podręcznej\nprogress_cache_saving = Zapisywanie pamięci podręcznej\nprogress_current_stage = Aktualny Etap:{ \"  \" }\nprogress_all_stages = Wszystkie Etapy:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = Zapisano konfigurację do pliku { $name }.\nsaving_loading_saving_failure = Nie udało się zapisać danych konfiguracyjnych do pliku { $name }, powód { $reason }.\nsaving_loading_reset_configuration = Przywrócono domyślą konfigurację.\nsaving_loading_loading_success = Poprawnie załadowano ustawienia aplikacji.\nsaving_loading_failed_to_create_config_file = Nie udało się utworzyć pliku konfiguracyjnego \"{ $path }\", powód \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Nie można załadować konfiguracji z \"{ $path }\" ponieważ nie istnieje lub nie jest plikiem.\nsaving_loading_failed_to_read_data_from_file = Nie można odczytać danych z pliku \"{ $path }\", powód \"{ $reason }\".\n# Other\nselected_all_reference_folders = Nie można rozpocząć wyszukiwania, gdy wszystkie katalogi są ustawione jako foldery źródłowe (referencyjne)\nsearching_for_data = Przeszukiwanie dysku, może to potrwać chwilę, proszę czekać...\ntext_view_messages = WIADOMOŚCI\ntext_view_warnings = OSTRZEŻENIA\ntext_view_errors = BŁĘDY\nabout_window_motto =\n    Program jest i będzie zawsze darmowy do użytku.\n    \n    Może interfejs programu nie jest ergonomiczny,\n    ale za to przynajmniej kod jest nieczytelny.\nkrokiet_new_app = Czkawka jest w trybie konserwacji, co oznacza, że naprawione zostaną tylko krytyczne błędy i nie zostaną dodane żadne nowe funkcje. Aby uzyskać nowe funkcje, sprawdź nową aplikację Krokiet, która jest bardziej stabilna i wydajna i nadal jest w fazie rozwoju.\n# Various dialog\ndialogs_ask_next_time = Pytaj następnym razem\nsymlink_failed = Nie udało się stworzyć symlinka { $name } do { $target }, powód { $reason }\ndelete_title_dialog = Potwierdzenie usunięcia\ndelete_question_label = Czy na pewno usunąć te pliki?\ndelete_all_files_in_group_title = Potwierdzenie usunięcia wszystkich plików w grupie\ndelete_all_files_in_group_label1 = W niektórych grupach wszystkie rekordy są zaznaczone.\ndelete_all_files_in_group_label2 = Czy na pewno je usunąć?\ndelete_items_label = { $items } plików będzie usuniętych.\ndelete_items_groups_label = { $items } plików z { $groups } grup zostanie usuniętych.\nhardlink_failed = Nie udało się połączyć z { $name } do { $target }, powód { $reason }\nhard_sym_invalid_selection_title_dialog = Niepoprawne zaznaczenie w niektórych grupach\nhard_sym_invalid_selection_label_1 = W niektórych grupach jest zaznaczony tylko jeden rekord i zostanie zignorowany.\nhard_sym_invalid_selection_label_2 = Aby móc mocno połączyć te pliki, należy wybrać co najmniej dwa rekordy w grupie.\nhard_sym_invalid_selection_label_3 = Pierwszy pozostaje nienaruszony a drugi i kolejne są dowiązywane do tego pierwszego.\nhard_sym_link_title_dialog = Potwierdzenie dowiązania\nhard_sym_link_label = Czy na pewno chcesz dowiązać te pliki?\nmove_folder_failed = Nie można przenieść folderu { $name }, powód { $reason }\nmove_file_failed = Nie można przenieść pliku { $name }, powód { $reason }\nmove_files_title_dialog = Wybierz folder, do którego zostaną przeniesione pliki\nmove_files_choose_more_than_1_path = Tylko jedna ścieżka może być wybrana, aby móc skopiować zduplikowane pliki, wybrano { $path_number }.\nmove_stats = Poprawnie przeniesiono { $num_files }/{ $all_files } elementów\nsave_results_to_file = Zapisano wyniki zarówno do plików txt, jak i json w folderze \"{ $name }\".\nsearch_not_choosing_any_music = BŁĄD: Musisz zaznaczyć przynajmniej jeden pole, według którego będą wyszukiwane podobne pliki muzyczne.\nsearch_not_choosing_any_broken_files = BŁĄD: Musisz wybrać co najmniej jedno pole wyboru z rodzajem uszkodzonych plików.\ninclude_folders_dialog_title = Foldery do przeszukiwania\nexclude_folders_dialog_title = Foldery do ignorowania\ninclude_manually_directories_dialog_title = Dodaj katalogi ręcznie\ncache_properly_cleared = Poprawnie wyczyszczono pamięć podręczną\ncache_clear_duplicates_title = Czyszczenie pamięci podręcznej duplikatów\ncache_clear_similar_images_title = Czyszczenie pamięci podręcznej podobnych obrazów\ncache_clear_similar_videos_title = Czyszczenie pamięci podręcznej podobnych plików wideo\ncache_clear_message_label_1 = Czy na pewno chcesz oczyścić pamięć podręczną z przestarzałych wpisów?\ncache_clear_message_label_2 = Ta operacja usunie wszystkie rekordy, które wskazują na nieistniejące pliki.\ncache_clear_message_label_3 = Może to nieznacznie przyspieszyć ładowanie/oszczędzanie pamięci podręcznej.\ncache_clear_message_label_4 = OSTRZEŻENIE: Operacja usunie wszystkie dane w pamięci podręcznej z wyłączonych dysków zewnętrznych. Zatem każdy hash będzie musiał zostać zregenerowany.\n# Show preview\npreview_image_resize_failure = Nie udało się zmienić rozmiaru obrazu { $name }.\npreview_image_opening_failure = Nie udało się otworzyć obrazu { $name }, powód { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Grupa { $current_group }/{ $all_groups } ({ $images_in_group } obrazów)\ncompare_move_left_button = L\ncompare_move_right_button = P\n"
  },
  {
    "path": "czkawka_gui/i18n/pt-BR/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Configurações\nwindow_main_title = Czkawka (Soluço)\nwindow_progress_title = Verificando\nwindow_compare_images = Comparar as imagens\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Fechar\n# Krokiet info dialog\nkrokiet_info_title = Apresento a você o Krokiet, a nova versão do Czkawka\nkrokiet_info_message = O Krokiet (Croquete) é a nova versão aprimorada, mais rápida e confiável, que possui menos problemas ou falhas do que o Czkawka (Soluço), que possui a sua interface gráfica desenvolvida com o conjunto de ferramentas do GTK.\n# Main window\nmusic_title_checkbox = Título\nmusic_artist_checkbox = Artista\nmusic_year_checkbox = Ano\nmusic_bitrate_checkbox = Taxa de bits\nmusic_genre_checkbox = Gênero\nmusic_length_checkbox = Comprimento\nmusic_comparison_checkbox = Comparação aproximada\nmusic_checking_by_tags = Informações do arquivo\nmusic_checking_by_content = Conteúdo\nsame_music_seconds_label = Duração mínima em segundos do fragmento\nsame_music_similarity_label = Diferença máxima\nmusic_compare_only_in_title_group = Comparar dentro dos grupos de títulos similares\nmusic_compare_only_in_title_group_tooltip =\n    Quando esta opção está ativada, os arquivos são agrupados por título, em seguida, são comparados entre si.\n    \n    Com 10.000 arquivos, em vez de se obter quase 100 milhões de comparações, normalmente, resultará em cerca de 20.000 comparações.\nsame_music_tooltip =\n    A pesquisa dos arquivos de música equivalentes por seu conteúdo pode ser defininda por meio das configurações:\n    \n    - O tempo mínimo do fragmento após o qual os arquivos de música podem ser identificados como equivalentes\n    - A diferença máxima entre os dois fragmentos dos testes\n    \n    Para obter bons resultados forneça combinações razoáveis destes parâmetros em cada teste.\n    \n    Definir o tempo mínimo para 5s e a diferença máxima para 1.0, irá pesquisar fragmentos quase idênticos nos arquivos.\n    Um tempo de 20s e uma diferença máxima de 6.0, por outro lado, funciona bem para encontrar versões ao vivo, versões modificadas (remixadas), etc.\n    \n    Por padrão, cada arquivo de música é comparado entre si, o que pode levar muito tempo para testar vários arquivos. Portanto, é melhor utilizar as pastas de referência e especificar quais são os arquivos que devem ser comparados entre si. Com a mesma quantidade de arquivos, a comparação de impressões digitais será pelo menos quatro vezes mais rápida do que sem as pastas de referência.\nmusic_comparison_checkbox_tooltip =\n    Pesquisar os arquivos de música equivalentes utilizando a inteligência artificial (IA) que utiliza o aprendizado da máquina para remover os parênteses de uma frase. Por exemplo, com esta opção ativada, os arquivos em questão que serão tratados como duplicados:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021) (Santuário --- O santuário foi remixado no verão de 2021)\nduplicate_case_sensitive_name = Diferenciar as letras maiúsculas das minúsculas\nduplicate_case_sensitive_name_tooltip = \n    Quando esta opção está ativada, agrupa apenas os registros se eles tiverem exatamente o mesmo nome. Por exemplo, pagar <-> pagar.\n    \n    Quando esta opção está desativada, agrupa os registros por nomes e sem verificar a diferença entre as letras maiúsculas das minúsculas. Por exemplo, pagar <-> Pagar\nduplicate_mode_size_name_combo_box = Tamanho e nome\nduplicate_mode_name_combo_box = Nome\nduplicate_mode_size_combo_box = Tamanho\nduplicate_mode_hash_combo_box = Integridade do arquivo\nduplicate_hash_type_tooltip =\n    O Czkawka oferece três tipos de identificação pela integridade do arquivo por meio do código ‘hash’:\n    \n    Blake3 - Esta opção possui o recurso de criptografia e está definida como padrão porque é muito rápida.\n    \n    CRC32 - Esta opção é a mais simples e deve ser mais rápida do que o Blake3, mas muito raramente pode ocorrer algumas colisões.\n    \n    XXH3 - Esta opção não possui o recurso de criptografia por ser muito similar em desempenho e qualidade do Blake3.\n    \n    Estes modos podem ser facilmente alternados.\nduplicate_check_method_tooltip =\n    Por enquanto, o Czkawka oferece três tipos de métodos para localizar os arquivos duplicados:\n    \n    Por nome - Esta opção permite localizar os arquivos que têm o mesmo nome.\n    \n    Por tamanho - Esta opção permite localizar os arquivos que têm o mesmo tamanho.\n    \n    Por integridade do arquivo - Esta opção permite localizar os arquivos que têm o mesmo conteúdo, ou seja, que possui o mesmo código ‘hash’ (o ‘hash’ de arquivo ou o valor do ‘hash’ de um arquivo é uma sequência de caracteres alfanuméricos distinta, trata-se de um valor único que corresponde ao conteúdo exato de um arquivo, permite verificar a integridade de um arquivo e é como se fosse a assinatura digital do arquivo). Este método cria a assinatura digital ou ‘hash’ do arquivo e, em seguida, compara o código da assinatura digital que foi criada para localizar os arquivos duplicados. Este método é a maneira mais segura e precisa para localizar os arquivos duplicados. O Czkawka utiliza a memória ‘cache’ (é um espaço de armazenamento das configurações, dos resultados das pesquisas, etc. que guarda os dados para que possam ser acessados mais rapidamente). Portanto, a segunda verificação e as subsequentes dos mesmos dados deverão ser muito mais rápidas do que na primeira vez.\nimage_hash_size_tooltip =\n    A cada imagem que é verificada, um arquivo de assinatura digital ou ‘hash’ é criado e pode ser comparado entre si, e se uma pequena diferença entre as imagens for encontrada, então significa que as imagens são equivalentes.\n    \n    O ‘hash’ do tamanho 8 é muito bom para localizar as imagens que são apenas um pouco equivalentes às originais. Com uma quantidade maior de imagens, maior do que 1.000 imagens, irá produzir uma grande quantidade de falsos positivos, então é recomendado utilizar um tamanho maior do ‘hash’ nestes casos.\n    \n    O ‘hash’ do tamanho 16 é o tamanho padrão por ser uma boa referência entre localizar as imagens que são um pouco equivalentes e ter uma pequena quantidade de colisões do código ‘hash’.\n    \n    O ‘hash’ do tamanho 32 e 64 localizam as imagens muito equivalentes, mas quase não deverá ocorrer os falsos positivos, talvez, exceto algumas imagens que possuem o canal alfa.\nimage_resize_filter_tooltip =\n    Para calcular o ‘hash’ de uma imagem, a biblioteca deve ser primeiro dimensionada.\n    \n    Escolha o algoritmo que será utilizado para calcular o ‘hash’ da imagem, saiba que poderá ter uma aparência um pouco diferente, dependendo do algoritmo que foi escolhido.\n    \n    O algoritmo mais rápido é o que produz os piores resultados e está ativado por padrão, porque o ‘hash’ com o tamanho de 16x16 a sua qualidade não é realmente perceptível.\n    \n    O ‘hash’ com o tamanho de 8x8, recomenda-se utilizar um algoritmo diferente do mais próximo para obter melhores resultados para as imagens.\nimage_hash_alg_tooltip =\n    É possível escolher um dos vários algoritmos para o cálculo da criação do ‘hash’.\n    \n    Cada um tem os seus pontos fortes e os seus pontos fracos, às vezes produzem resultados melhores e às vezes produzem resultados piores para imagens diferentes.\n    \n    É melhor testar qual algoritmo tem os melhores resultados para os diferentes tipos de arquivos, lembre-se de que, nem sempre é facilmente perceptível as diferenças dos resultados.\nbig_files_mode_combobox_tooltip = Permite pesquisar os arquivos menores ou maiores\nbig_files_mode_label = Arquivos a serem verificados\nbig_files_mode_smallest_combo_box = O arquivo menor\nbig_files_mode_biggest_combo_box = O arquivo maior\nmain_notebook_duplicates = Arquivos duplicados\nmain_notebook_empty_directories = Diretórios vazios\nmain_notebook_big_files = Arquivos grandes\nmain_notebook_empty_files = Arquivos vazios\nmain_notebook_temporary = Arquivos temporários\nmain_notebook_similar_images = Imagens equivalentes\nmain_notebook_similar_videos = Vídeos equivalentes\nmain_notebook_same_music = Músicas duplicadas\nmain_notebook_symlinks = Ligações simbólicas não válidas\nmain_notebook_broken_files = Arquivos corrompidos\nmain_notebook_bad_extensions = Extensões inválidas\nmain_tree_view_column_file_name = Nome do arquivo\nmain_tree_view_column_folder_name = Nome da pasta\nmain_tree_view_column_path = Caminho\nmain_tree_view_column_modification = Data da modificação\nmain_tree_view_column_size = Tamanho\nmain_tree_view_column_similarity = Equivalentes\nmain_tree_view_column_dimensions = Dimensões\nmain_tree_view_column_title = Título\nmain_tree_view_column_artist = Artista\nmain_tree_view_column_year = Ano\nmain_tree_view_column_bitrate = Taxa de bits\nmain_tree_view_column_length = Comprimento\nmain_tree_view_column_genre = Gênero\nmain_tree_view_column_symlink_file_name = Nome do arquivo da ligação simbólica\nmain_tree_view_column_symlink_folder = Pasta da ligação simbólica\nmain_tree_view_column_destination_path = Caminho do destino\nmain_tree_view_column_type_of_error = Tipo do erro\nmain_tree_view_column_current_extension = Extensão atual\nmain_tree_view_column_proper_extensions = Extensões válidas\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codec\nmain_label_check_method = Método de verificação\nmain_label_hash_type = Tipo do ‘hash’\nmain_label_hash_size = Tamanho do ‘hash’\nmain_label_size_bytes = Tamanho (em bytes)\nmain_label_min_size = Mínimo\nmain_label_max_size = Máximo\nmain_label_shown_files = Quantidade de arquivos exibidos\nmain_label_resize_algorithm = Redimensionar o algoritmo\nmain_label_similarity = Equivalentes { \"   \" }\nmain_check_box_broken_files_audio = Áudio\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = Arquivo\nmain_check_box_broken_files_image = Imagem\nmain_check_box_broken_files_video = Vídeo\nmain_check_box_broken_files_video_tooltip = Utilizar o ‘ffmpeg’ ou ‘ffprobe’ para validar os arquivos de vídeo. Esta opção é bastante lenta e pode detectar alguns erros insignificantes mesmo que o arquivo seja reproduzido corretamente.\ncheck_button_general_same_size = Ignorar os arquivos do mesmo tamanho\ncheck_button_general_same_size_tooltip = Ignorar os arquivos com o mesmo tamanho nos resultados, geralmente estes são os arquivos duplicados (1:1)\nmain_label_size_bytes_tooltip = Tamanho dos arquivos que serão utilizados na pesquisa\n# Upper window\nupper_tree_view_included_folder_column_title = Pastas para serem pesquisadas\nupper_tree_view_included_reference_column_title = Pastas de referência\nupper_recursive_button = Pesquisa recursiva\nupper_recursive_button_tooltip = Quando esta opção está ativada, a pesquisa por arquivos ocorre também nas pastas que não foram escolhidas.\nupper_manual_add_included_button = Adicionar manualmente\nupper_add_included_button = Adicionar\nupper_remove_included_button = Remover\nupper_manual_add_excluded_button = Adicionar manualmente\nupper_add_excluded_button = Adicionar\nupper_remove_excluded_button = Remover\nupper_manual_add_included_button_tooltip = \n    Adicionar manualmente os nomes dos diretórios ou das pastas para serem pesquisadas.\n    \n    Para adicionar vários caminhos de uma vez, separe-os com o ponto e vírgula ‘ ; ’.\n    \n    Por exemplo, ao utilizar ‘/home/roman;/home/rozkaz’ irá adicionar os dois diretórios ‘/home/roman’ e ‘/home/rozkaz’\nupper_add_included_button_tooltip = Adicionar um novo diretório para ser pesquisado.\nupper_remove_included_button_tooltip = Remover o diretório da pesquisa.\nupper_manual_add_excluded_button_tooltip = \n    Adicionar manualmente um diretório à lista das exceções.\n    \n    Para adicionar vários caminhos de uma vez, separe-os com o ponto e vírgula ‘ ; ’.\n    \n    Por exemplo, ao utilizar ‘/home/roman;/home/krokiet’ irá adicionar os dois diretórios ‘/home/roman’ e ‘/home/keokiet’\nupper_add_excluded_button_tooltip = Selecionar o diretório que não será incluído na pesquisa.\nupper_remove_excluded_button_tooltip = Selecionar o diretório na lista das exceções.\nupper_notebook_items_configuration = Configurações dos itens\nupper_notebook_excluded_directories = Caminho dos diretórios não incluídos\nupper_notebook_included_directories = Caminho dos diretórios incluídos\nupper_allowed_extensions_tooltip =\n    As extensões que são permitidas devem ser separadas por vírgulas, por padrão, todas as extensões estão disponíveis.\n    \n    Os macros que adicionam várias extensões de uma só vez também estão disponíveis para os arquivos de IMAGEM, VÍDEO, MÚSICA e TEXTO.\n    \n    Por exemplo, ao utilizar \".exe, IMAGE, VIDEO, .rar, 7z\", esta opção significa que os arquivos ‘.exe’, as imagens (por exemplo, .jpg, .png, etc.), os vídeos (por exemplo, .avi, .mp4, etc.), os arquivos ‘.rar’ e ‘.7z’ serão verificados.\nupper_excluded_extensions_tooltip =\n    Lista dos arquivos que serão ignorados na verificação.\n    \n    Quando você utiliza as extensões permitidas, estas tem maior prioridade em relação as outras, então o arquivo não será verificado.\nupper_excluded_items_tooltip = \n        Itens excluídos devem conter * wildcard e devem ser separados por vírgulas.\n        Isto é mais lento que Excluídas Caminhos, portanto use-o com cuidado.\nupper_excluded_items = Itens ignorados:\nupper_allowed_extensions = Extensões permitidas:\nupper_excluded_extensions = Extensões ignoradas:\n# Popovers\npopover_select_all = Selecionar todos\npopover_unselect_all = Desselecionar todos\npopover_reverse = Inverter a seleção\npopover_select_all_except_shortest_path = Selecionar todas as opções, exceto o caminho mais curto\npopover_select_all_except_longest_path = Selecionar todas as opções, exceto o caminho mais longo\npopover_select_all_except_oldest = Selecionar todos, exceto os mais antigos\npopover_select_all_except_newest = Selecionar todos, exceto os mais recentes\npopover_select_one_oldest = Selecionar o mais antigo\npopover_select_one_newest = Selecionar o mais recente\npopover_select_custom = Selecionar personalizado\npopover_unselect_custom = Desselecionar personalizado\npopover_select_all_images_except_biggest = Selecionar todos, exceto o maior\npopover_select_all_images_except_smallest = Selecionar todos, exceto o menor\npopover_custom_path_check_button_entry_tooltip =\n    Selecionar os registros por caminho.\n    \n    Por exemplo:\n    O caminho ‘/home/pimpek/rzecz.txt’ pode ser encontrado com ‘/home/pim*’\npopover_custom_name_check_button_entry_tooltip =\n    Selecionar os registros por nome de arquivo.\n    \n    Por exemplo:\n    O caminho ‘/usr/ping/pong.txt’ pode ser encontrado com ‘*ong*’\npopover_custom_regex_check_button_entry_tooltip =\n    Selecionar os registros por meio das expressões regulares.\n    \n    Com o uso das expressões regulares (ou o modo ‘Regex’) o texto da pesquisa é o caminho completo (incluindo o nome do arquivo).\n    \n    Por exemplo:\n    O caminho ‘/usr/bin/ziemniak.txt’ pode ser encontrado com ‘/ziem[a-z]+’\n    \n    Esta opção utiliza a implementação padrão das expressões regulares do ‘Rust’. Você pode obter mais informações acessando a página eletrônica https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Ativar a detecção da distinção entre as letras maiúsculas e minúsculas.\n    \n    Quando esta opção está ativada, o caminho ‘/home/*’ encontra ambos ‘HoMe/roman’ e ‘/home/roman’.\npopover_custom_not_all_check_button_tooltip =\n    Impedir que todos os registros de um grupo sejam selecionados.\n    \n    Esta opção está ativada por padrão, porque na maioria das situações, você provavelmente não quer excluir (ou apagar) os arquivos originais que estejam duplicados, mas quer manter pelo menos um dos arquivos.\n    \n    Atente-se ao seguinte detalhe, esta configuração não funcionará se você tiver selecionado manualmente todos os registros de um grupo.\npopover_custom_regex_path_label = Caminho\npopover_custom_regex_name_label = Nome\npopover_custom_regex_regex_label = Expressão regular junto com o nome\npopover_custom_case_sensitive_check_button = Diferenciar entre maiúsculas e minúsculas\npopover_custom_all_in_group_label = Não selecionar todos os registros em um grupo\npopover_custom_mode_unselect = Desselecionar personalizado\npopover_custom_mode_select = Selecionar o personalizado\npopover_sort_file_name = Nome do arquivo\npopover_sort_folder_name = Nome da pasta\npopover_sort_full_name = Nome completo\npopover_sort_size = Tamanho\npopover_sort_selection = Seleção\npopover_invalid_regex = A expressão regular não é válida\npopover_valid_regex = A expressão regular é válida\n# Bottom buttons\nbottom_search_button = Pesquisar\nbottom_select_button = Selecionar\nbottom_delete_button = Excluir\nbottom_save_button = Salvar\nbottom_symlink_button = Ligação simbólica\nbottom_hardlink_button = Ligação simbólica rígida\nbottom_move_button = Mover\nbottom_sort_button = Ordenar\nbottom_compare_button = Comparar\nbottom_search_button_tooltip = Iniciar a pesquisa\nbottom_select_button_tooltip = Ao selecionar os registros, apenas os arquivos e as pastas que foram selecionadas poderão ser processadas posteriormente.\nbottom_delete_button_tooltip = Excluir os arquivos e as pastas que foram selecionadas.\nbottom_save_button_tooltip = Salvar as informações da pesquisa em um arquivo\nbottom_symlink_button_tooltip =\n    Criar ligações simbólicas ou vínculos simbólicos (‘symbolic links’ ou ‘symlinks’ ou ‘soft links’) ou ‘atalho’ para um outro arquivo ou para um outro diretório (pasta).\n    Esta opção só funciona se pelo menos dois resultados do grupo estiverem selecionados.\n    O primeiro permanece inalterado, o segundo e os subsequentes estão vinculados ou ligados simbolicamente ao primeiro.\nbottom_hardlink_button_tooltip =\n    Criar ligações simbólicas rígidas ou vínculos simbólicos rígidos (‘hard links’ ou ‘hardlinks’) ou ‘atalho’ para um outro arquivo original ou para um outro diretório original (pasta).\n    Esta opção só funciona se pelo menos dois resultados do grupo estiverem selecionados.\n    O primeiro permanece inalterado, o segundo e os subsequentes estão vinculados ou ligados simbolicamente ao primeiro.\nbottom_hardlink_button_not_available_tooltip =\n    Criar ligações simbólicas rígidas ou vínculos simbólicos rígidos (‘hard links’ ou ‘hardlinks’) ou ‘atalho’ para um outro arquivo original ou para um outro diretório original.\n    O botão está desativado, porque as ligações simbólicas rígidas não podem ser criadas.\n    Este tipo de ligação simbólica só pode ser criada por um administrador no Windows, portanto, certifique-se de executar o programa com as permissões de administrador.\n    Se o programa estiver sendo executado com as permissões de administrador, verifique se existem problemas equivalentes no GitHub do Czkawka (https://github.com/qarmin/czkawka).\nbottom_move_button_tooltip =\n    Mover os arquivos para o diretório que foi selecionado.\n    Esta opção permite copiar todos os arquivos para o diretório sem preservar a estrutura dos diretórios e dos arquivos.\n    Ao tentar mover dois arquivos com nomes idênticos para um diretório, o segundo arquivo não será movido e ocorrerá um erro.\nbottom_sort_button_tooltip = Ordenar os arquivos ou os diretórios de acordo com o método que foi selecionado.\nbottom_compare_button_tooltip = Comparar os arquivos e os diretórios nos grupos.\nbottom_show_errors_tooltip = Exibir ou ocultar o painel de texto inferior.\nbottom_show_upper_notebook_tooltip = Exibir ou ocultar o painel de texto superior.\n# Progress Window\nprogress_stop_button = Parar\nprogress_stop_additional_message = Parar a pesquisa\n# About Window\nabout_repository_button_tooltip = Endereço da página eletrônica do repositório com o código-fonte dos programas Czkawka e Krokiet.\nabout_donation_button_tooltip = Endereço da página eletrônica para fazer uma doação ao programador do Czkawka e Krokiet.\nabout_instruction_button_tooltip = Endereço da página eletrônica para obter ajuda.\nabout_translation_button_tooltip = Endereço da página eletrônica da plataforma de tradução ‘Crowdin’ com as traduções dos programas Czkawka e Krokiet. Os idiomas polonês (pl) e inglês (en) são fornecidos oficialmente pelo Rafał Mikrut, que também é conhecido por ‘qarmin’ (https://github.com/qarmin) e o idioma português do Brasil (pt-BR) foi gentilmente traduzido por marcelocripe (https://github.com/marcelocripe e https://gitlab.com/marcelocripe) em 2024, 2025 e 2026.\nabout_repository_button = Repositório\nabout_donation_button = Faça uma doação\nabout_instruction_button = Ajuda\nabout_translation_button = Tradução\n# Header\nheader_setting_button_tooltip = Abrir a janela das configurações do programa Czkawka.\nheader_about_button_tooltip = Abrir a janela das informações sobre o programa Czkawka.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Quantidade de tópicos utilizados\nsettings_number_of_threads_tooltip = Quantidade de tópicos utilizados, o zero ‘0’ significa que todos os tópicos estão disponíveis e poderão ser utilizados.\nsettings_use_rust_preview = Utilizar as bibliotecas externas em vez do GTK para carregar a pré-visualização\nsettings_use_rust_preview_tooltip =\n    Ao utilizar a pré-visualização do GTK, às vezes é mais rápido e oferece suporte a mais formatos, mas às vezes pode ser exatamente o contrário.\n    \n    Se você tiver problemas para carregar a pré-visualização, pode tentar alterar esta configuração.\n    \n    Nos sistemas operacionais que não são da família do GNU/Linux, é recomendável utilizar esta opção, porque o pacote ‘gtk-pixbuf’ nem sempre está disponível, portanto, a desativação desta opção não irá carregar a pré-visualização de algumas imagens.\nsettings_label_restart = Você tem que reiniciar o programa para aplicar as novas configurações\nsettings_ignore_other_filesystems = Ignorar outros sistemas de arquivos (somente para o GNU/Linux)\nsettings_ignore_other_filesystems_tooltip =\n    Ignorar os arquivos que não estão no mesmo sistema de arquivos dos diretórios que estão sendo pesquisados.\n    \n    Funciona da mesma forma que a opção ‘-xdev’ no comando ‘find’ (localizar) no GNU/Linux\nsettings_save_at_exit_button_tooltip = Salvar as configurações em arquivo ao fechar o programa.\nsettings_load_at_start_button_tooltip =\n    Carregar as configurações a partir de um arquivo ao abrir o programa.\n    \n    Se esta opção não estiver ativada, as configurações padrão serão utilizadas.\nsettings_confirm_deletion_button_tooltip = Exibir a janela de confirmação de exclusão ao clicar no botão ‘Excluir’.\nsettings_confirm_link_button_tooltip = Exibir a janela de confirmação ao clicar no botão da ‘Ligação simbólica’.\nsettings_confirm_group_deletion_button_tooltip = Exibir a janela de confirmação de exclusão ao tentar excluir todos os registros de um grupo.\nsettings_show_text_view_button_tooltip = Exibir o painel de texto na parte inferior da interface gráfica do usuário.\nsettings_use_cache_button_tooltip = Utilizar o arquivo de ‘cache’.\nsettings_save_also_as_json_button_tooltip = Salvar o arquivo de ‘cache’ no formato JSON que é legível por seres humanos e que permite modificar o seu conteúdo. O arquivo de ‘cache’ será lido automaticamente pelo programa, se o formato do ‘cache’ for binário com a extensão ‘.bin’ ou se não tiver uma extensão do arquivo.\nsettings_use_trash_button_tooltip = Mover os arquivos para a lixeira em vez de excluí-los permanentemente.\nsettings_language_label_tooltip = Idioma da interface gráfica do usuário.\nsettings_save_at_exit_button = Salvar as configurações ao fechar o programa\nsettings_load_at_start_button = Carregar as configurações ao abrir o programa\nsettings_confirm_deletion_button = Exibir a janela de confirmação quando for excluir qualquer arquivo\nsettings_confirm_link_button = Exibir a janela de confirmação quando for criar qualquer arquivo de ligação simbólica ou de vínculo simbólico\nsettings_confirm_group_deletion_button = Exibir a janela de confirmação quando for excluir todos os arquivos do grupo\nsettings_show_text_view_button = Exibir o painel de texto inferior\nsettings_use_cache_button = Utilizar o arquivo de ‘cache’\nsettings_save_also_as_json_button = Salvar o arquivo de ‘cache’ com o formato JSON\nsettings_use_trash_button = Mover os arquivos excluídos para a lixeira\nsettings_language_label = Configurações do idioma\nsettings_multiple_delete_outdated_cache_checkbutton = Excluir automaticamente os registros que estejam desatualizados no arquivo de ‘cache’\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Excluir os registros que estejam desatualizados no arquivo de ‘cache’.\n    \n    Quando esta opção está ativada, o programa se certifica de que, quando os registros são carregados, todos eles apontam para os arquivos válidos, enquanto que, os arquivos corrompidos ou alterados são ignorados.\n    \n    Quando esta opção está desativada, ajudará na verificação dos arquivos que estão nos dispositivos de armazenamento externos, de modo que os registros relacionados a eles não sejam excluídos na próxima verificação.\n    \n    No caso de ter centenas de milhares de registros no arquivo de ‘cache’, recomenda-se que esta opção seja ativada, pois ela irá acelerar o carregamento ou o salvamento do ‘cache’ no início ou no fim da pesquisa.\nsettings_notebook_general = Configurações gerais\nsettings_notebook_duplicates = Arquivos duplicados\nsettings_notebook_images = Imagens equivalentes\nsettings_notebook_videos = Vídeos equivalentes\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Exibir a pré-visualização no lado direito ao selecionar um arquivo de imagem.\nsettings_multiple_image_preview_checkbutton = Exibir a pré-visualização das imagens\nsettings_multiple_clear_cache_button_tooltip =\n    Excluir manualmente as entradas que estão desatualizadas no arquivo de ‘cache’.\n    Esta opção só deve ser utilizada se a limpeza automática estiver desativada.\nsettings_multiple_clear_cache_button = Remover os resultados que estejam desatualizados no arquivo de ‘cache’.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Ocultar todos os arquivos, exceto um, se todos eles apontarem para os mesmos dados, se são ligações simbólicas rígidas ou vínculos simbólicos rígidos (‘hard links’).\n    \n    Por exemplo, se houver no dispositivo de armazenamento sete arquivos de ligações simbólicas rígidas para dados específicos e um arquivo é diferente com os mesmos dados, então, o pesquisador de arquivos duplicados irá identificar apenas um arquivo exclusivo e será exibido um arquivo de ligação simbólica rígida.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Configurar o tamanho mínimo do arquivo de ‘cache’ que será salvo no dispositivo de armazenamento.\n    \n    Se você definir um valor menor irá gerar mais registros, com isso, irá acelerar a pesquisa, mas irá tornar mais lento o carregamento ou o salvamento dos dados no arquivo de ‘cache’.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Permite que os códigos de ‘hash’ (integridade do arquivo) parciais sejam salvos no arquivo de ‘cache’ (o ‘hash’ é calculado a partir de uma pequena parte do arquivo), permitindo que os arquivos únicos sejam descartados antecipadamente dos resultados da pesquisa dos arquivos que não são duplicados.\n    \n    Esta opção está ativada por padrão, pois pode causar lentidão em algumas situações.\n    \n    Recomenda-se utilizar esta opção ao fazer a pesquisa de centenas de milhares ou de milhões de arquivos, porque esta opção pode acelerar os resultados da pesquisa e você pode desativar esta opção ao fazer a pesquisa de uma pequena quantidade de arquivos.\nsettings_duplicates_prehash_minimal_entry_tooltip = Tamanho mínimo do código ‘hash’ parcial que será gravado no arquivo de ‘cache’.\nsettings_duplicates_hide_hard_link_button = Ocultar as ligações rígidas\nsettings_duplicates_prehash_checkbutton = Utilizar o ‘hash’ parcial dos arquivo do ‘cache’\nsettings_duplicates_minimal_size_cache_label = Tamanho mínimo dos arquivos (em bytes) ao salvar o arquivo de ‘cache’\nsettings_duplicates_minimal_size_cache_prehash_label = Tamanho mínimo dos arquivos (em bytes) ao salvar o ‘hash’ parcial no arquivo de ‘cache’\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Salvar as configurações atuais no arquivo.\nsettings_loading_button_tooltip = Carregar as configurações do arquivo e substituir as configurações atuais.\nsettings_reset_button_tooltip = Restaurar as configurações padrão.\nsettings_saving_button = Salvar as configurações\nsettings_loading_button = Carregar as configurações\nsettings_reset_button = Restaurar as configurações\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Abrir a pasta onde estão armazenados os arquivos ‘.txt’ do ‘cache’ do programa.\n    \n    A modificação manual dos arquivos de ‘cache’ pode causar a exibição de resultados que não são corretos ou se ocorrer danos nos dados dos arquivos resultarão na necessidade de gerar novos arquivos de ‘cache’. No entanto, modificar o caminho pode economizar tempo ao mover uma grande quantidade de arquivos para um local diferente.\n    \n    Os arquivos de ‘cache’ podem ser copiados entre computadores diferentes para economizar o tempo na criação do ‘hash’ dos arquivos. Esta opção só é possível se os dados estiverem armazenados em uma estrutura de diretórios idêntica nos computadores.\n    \n    Se ocorrer problemas nos arquivos de ‘cache’, os arquivos podem ser excluídos permanentemente. O programa irá criar novos arquivos de ‘cache’ automaticamente.\nsettings_folder_settings_open_tooltip =\n    Abrir a pasta onde está armazenada as configurações do Czkawka.\n    \n    Tenha muito cuidado, a modificação manual das configurações pode interromper o seu fluxo de trabalho.\nsettings_folder_cache_open = Abrir a pasta do ‘cache’\nsettings_folder_settings_open = Abrir a pasta das configurações\n# Compute results\ncompute_stopped_by_user = A pesquisa foi interrompida pelo usuário\ncompute_found_duplicates_hash_size = Foram encontrados ‘{ $number_files }’ arquivos duplicados nos ‘{ $number_groups }’ grupos e ocupou o tamanho de ‘{ $size }’. A verificação durou ‘{ $time }’\ncompute_found_duplicates_name = Foram encontrados ‘{ $number_files }’ arquivos duplicados nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’\ncompute_found_empty_folders = Foram encontradas ‘{ $number_files }’ pastas vazias. A verificação durou ‘{ $time }’\ncompute_found_empty_files = Foram encontrados ‘{ $number_files }’ arquivos vazios. A verificação durou ‘{ $time }’\ncompute_found_big_files = Foram encontrados ‘{ $number_files }’ arquivos grandes. A verificação durou ‘{ $time }’\ncompute_found_temporary_files = Foram encontrados ‘{ $number_files }’ arquivos temporários. A verificação durou ‘{ $time }’\ncompute_found_images = Foram encontrados ‘{ $number_files }’ arquivos de imagens equivalentes nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’\ncompute_found_videos = Foram encontrados ‘{ $number_files }’ arquivos de vídeos equivalentes nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’\ncompute_found_music = Foram encontrados ‘{ $number_files }’ arquivos de músicas equivalentes nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’\ncompute_found_invalid_symlinks = Foram encontradas ‘{ $number_files }’ ligações simbólicas que não são válidas. A verificação durou ‘{ $time }’\ncompute_found_broken_files = Foram encontrados ‘{ $number_files }’ arquivos corrompidos. A verificação durou ‘{ $time }’\ncompute_found_bad_extensions = Foram encontrados ‘{ $number_files }’ arquivos com extensões que não são válidas. A verificação durou ‘{ $time }’\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Foi verificado ‘{ $file_number }’ arquivo\n       *[other] Foram verificados ‘{ $file_number }’ arquivos\n    }\nprogress_scanning_extension_of_files = Verificando ‘{ $file_checked }’ de ‘{ $all_files }’ por tipo da extensão dos arquivos\nprogress_scanning_broken_files = Verificando ‘{ $file_checked }’ de ‘{ $all_files }’ arquivos ‘{ $data_checked }’ de ‘{ $all_data }’\nprogress_scanning_video = Foram criados ‘{ $file_checked }’ de ‘{ $all_files }’ código do ‘hash’ dos arquivos de vídeo\nprogress_creating_video_thumbnails = Foram criadas ‘{ $file_checked }’ de ‘{ $all_files }’ miniaturas de vídeo\nprogress_scanning_image = Foram criados ‘{ $file_checked }’ de ‘{ $all_files }’ código do ‘hash’ dos arquivos de imagem ‘{ $data_checked }’ de ‘{ $all_data }’\nprogress_comparing_image_hashes = Comparando ‘{ $file_checked }’ de ‘{ $all_files }’ código ‘hash’ dos arquivos de imagem\nprogress_scanning_music_tags_end = Comparando ‘{ $file_checked }’ de ‘{ $all_files }’ informações dos arquivos de música\nprogress_scanning_music_tags = Lendo ‘{ $file_checked }’ de ‘{ $all_files }’ informações dos arquivos de música\nprogress_scanning_music_content_end = Comparando ‘{ $file_checked }’ de ‘{ $all_files }’ impressões digitais dos arquivos de música\nprogress_scanning_music_content = Foram calculados ‘{ $file_checked }’ de ‘{ $all_files }’ impressões digitais dos arquivos de música e foi verificado ‘{ $data_checked }’ de ‘{ $all_data }’\nprogress_scanning_size = Pesquisando por tamanho do arquivo nos ‘{ $file_number }’\nprogress_scanning_size_name = Pesquisando por nome e por tamanho do arquivo nos ‘{ $file_number }’\nprogress_scanning_name = Pesquisando por nome do arquivo nos ‘{ $file_number }’\nprogress_analyzed_partial_hash = O ‘hash’ parcial foi analisado nos arquivos ‘{ $file_checked }’ de ‘{ $all_files }’ e foi verificado ‘{ $data_checked }’ de ‘{ $all_data }’\nprogress_analyzed_full_hash = O ‘hash’ completo foi analisado nos arquivos ‘{ $file_checked }’ de ‘{ $all_files }’ e foi verificado ‘{ $data_checked }’ de ‘{ $all_data }’\nprogress_prehash_cache_loading = Carregando o ‘hash’ parcial dos arquivos do ‘cache’\nprogress_prehash_cache_saving = Salvando o ‘hash’ parcial dos arquivos no ‘cache’\nprogress_hash_cache_loading = Carregando o ‘hash’ dos arquivos do ‘cache’\nprogress_hash_cache_saving = Salvando o ‘hash’ dos arquivos no ‘cache’\nprogress_cache_loading = Carregando as informações do ‘cache’\nprogress_cache_saving = Salvando as informações no ‘cache’\nprogress_current_stage = Estágio atual: { \"  \" }\nprogress_all_stages = Todos os estágios: { \"  \" }\n# Saving loading \nsaving_loading_saving_success = As configurações foram salvas no arquivo ‘{ $name }’.\nsaving_loading_saving_failure = Ocorreu uma falha ao salvar os dados no arquivo de configurações ‘{ $name }’, por causa de ‘{ $reason }’.\nsaving_loading_reset_configuration = As configurações padrão foram restauradas.\nsaving_loading_loading_success = As configurações do programa foram carregadas com sucesso.\nsaving_loading_failed_to_create_config_file = Ocorreu uma falha ao criar o arquivo de configurações no caminho ‘{ $path }’, por causa de ‘{ $reason }’.\nsaving_loading_failed_to_read_config_file = Não foi possível carregar o arquivo de configurações do caminho ‘{ $path }’, porque o arquivo não existe ou porque não é um arquivo de configurações.\nsaving_loading_failed_to_read_data_from_file = Não foi possível ler os dados do arquivo do caminho ‘{ $path }’, por causa de ‘{ $reason }’.\n# Other\nselected_all_reference_folders = Não foi possível iniciar a pesquisa, porque se todas as pastas estiverem definidas como pastas de referência (ou pastas de origem)\nsearching_for_data = Os dados estão sendo pesquisados. Esta ação pode demorar bastante tempo. Por favor, aguarde a finalização.\ntext_view_messages = Exibir as mensagens\ntext_view_warnings = Exibir os avisos\ntext_view_errors = Exibir os erros\nabout_window_motto = Este programa é e sempre será de código aberto e de uso gratuito.\nkrokiet_new_app = O Czkawka está em modo de manutenção, o que significa que somente os problemas críticos serão corrigidos e nenhuma nova funcionalidade será adicionada ao programa. Para obter as novas funcionalidades, por favor, confira o novo programa chamado de Krokiet (Croquete), que é mais estável, mais eficiente e ainda está em desenvolvimento ativo.\n# Various dialog\ndialogs_ask_next_time = Perguntar novamente na próxima vez que a janela for exibida\nsymlink_failed = Ocorreu uma falha na ligação simbólica ‘{ $name }’ para ‘{ $target }’, por causa de ‘{ $reason }’\ndelete_title_dialog = Confirmação da exclusão\ndelete_question_label = Você tem certeza de que quer excluir os arquivos?\ndelete_all_files_in_group_title = Confirmação da exclusão de todos os arquivos do grupo\ndelete_all_files_in_group_label1 = Em alguns grupos, todos os registros estão selecionados.\ndelete_all_files_in_group_label2 = Você tem certeza de que quer excluí-los?\ndelete_items_label = Os ‘{ $items }’ arquivos serão excluídos.\ndelete_items_groups_label = Os ‘{ $items }’ arquivos dos ‘{ $groups }’ grupos serão excluídos.\nhardlink_failed = Ocorreu uma falha na ligação rígida ‘{ $name }’ para ‘{ $target }’, por causa de ‘{ $reason }’\nhard_sym_invalid_selection_title_dialog = Alguns grupos não são válidos para serem selecionados\nhard_sym_invalid_selection_label_1 = Em alguns grupos, existe apenas um registro que foi selecionado e será ignorado.\nhard_sym_invalid_selection_label_2 = Para criar uma ligação simbólica rígida dos arquivos, pelo menos dois registros de um grupo tem que estar selecionados.\nhard_sym_invalid_selection_label_3 = O primeiro registro no grupo é reconhecido como original e não é alterado, mas o segundo registro e os subsequentes são vinculados ou ligados ao primeiro.\nhard_sym_link_title_dialog = Confirmação da ligação simbólica\nhard_sym_link_label = Você tem certeza de que quer criar a ligação simbólica para estes arquivos?\nmove_folder_failed = Ocorreu uma falha ao mover a pasta ‘{ $name }’, por causa de ‘{ $reason }’\nmove_file_failed = Ocorreu uma falha ao mover o arquivo ‘{ $name }’, por causa de ‘{ $reason }’\nmove_files_title_dialog = Escolha a pasta que você quer mover os arquivos duplicados\nmove_files_choose_more_than_1_path = Somente um caminho pode ser selecionado para copiar os arquivos duplicados. A pasta ‘{ $path_number }’ foi selecionada.\nmove_stats = Os arquivos ‘{ $num_files }’ de ‘{ $all_files }’ foram movidos corretamente\nsave_results_to_file = Os resultados foram salvos tanto nos arquivos no formato ‘.txt’ quanto no formato ‘.json’ na pasta ‘{ $name }’.\nsearch_not_choosing_any_music = Ocorreu um erro, porque você tem que selecionar pelo menos uma caixa de seleção com o tipo dos arquivos de música que serão pesquisados.\nsearch_not_choosing_any_broken_files = Ocorreu um erro, porque você tem que selecionar pelo menos uma caixa de seleção com o tipo dos arquivos corrompidos que serão pesquisados.\ninclude_folders_dialog_title = Pastas a serem pesquisadas\nexclude_folders_dialog_title = Pastas a serem ignoradas\ninclude_manually_directories_dialog_title = Adicionar as pastas manualmente\ncache_properly_cleared = O ‘cache’ foi limpo com sucesso\ncache_clear_duplicates_title = Removendo os arquivos duplicados do ‘cache’\ncache_clear_similar_images_title = Removendo as imagens equivalentes do ‘cache’\ncache_clear_similar_videos_title = Removendo os vídeos equivalentes do ‘cache’\ncache_clear_message_label_1 = Você quer remover as entradas que estão desatualizadas no ‘cache’?\ncache_clear_message_label_2 = Esta ação irá excluir todos os registros do ‘cache’ que apontam para os arquivos que não são válidos.\ncache_clear_message_label_3 = Esta opção pode acelerar um pouco o carregamento ou o salvamento do ‘cache’.\ncache_clear_message_label_4 = Tenha muito cuidado, porque esta ação irá excluir todos os dados que estão armazenados no ‘cache’ das unidades externas que não estão conectadas. Portanto, todos os ‘hash’ terão que de ser gerados novamente.\n# Show preview\npreview_image_resize_failure = Ocorreu uma falha ao redimensionar a imagem ‘{ $name }’.\npreview_image_opening_failure = Ocorreu uma falha ao abrir a imagem ‘{ $name }’, por causa de ‘{ $reason }’\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Os ‘{ $current_group }’ de ‘{ $all_groups }’ grupos possuem ‘{ $images_in_group }’ imagens\ncompare_move_left_button = E\ncompare_move_right_button = D\n\nprogress_scanning_empty_folders = \n        {$pasta_numero ->\n        [um] Pasta {$folder_number} escaneada\n        *[outro] Pastas {$folder_number} escaneadas}"
  },
  {
    "path": "czkawka_gui/i18n/pt-PT/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Configurações\nwindow_main_title = Czkawka (Soluço)\nwindow_progress_title = Escaneando\nwindow_compare_images = Comparar Imagens\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Fechar\n# Krokiet info dialog\nkrokiet_info_title = Apresentando Krokiet - Nova versão do Czkawka\nkrokiet_info_message = \n        Krokiet é a nova, melhorada, mais rápida e mais confiável versão da interface gráfica Czkawka GTK!\n\n        É mais fácil de executar e mais resistente a alterações do sistema, pois depende apenas de bibliotecas principais disponíveis na maioria dos sistemas por padrão.\n\n        Krokiet também traz recursos que a Czkawka não possui, incluindo miniaturas no modo de comparação de vídeo, um limpador EXIF, opções de progresso de mover/copiar/excluir arquivos ou opções de classificação estendidas.\n\n        Experimente e veja a diferença!\n\n        A Czkawka continuará a receber correções de bugs e atualizações menores de mim, mas todos os novos recursos serão desenvolvidos exclusivamente para o Krokiet e qualquer pessoa é livre para contribuir com novos recursos, adicionar modos ausentes ou estender ainda mais a Czkawka.\n\n        PS: Esta mensagem deve aparecer apenas uma vez. Se ela aparecer novamente, defina a variável de ambiente CZKAWKA_DONT_ANNOY_ME para qualquer valor não vazio.\n# Main window\nmusic_title_checkbox = Título\nmusic_artist_checkbox = Artista\nmusic_year_checkbox = Ano\nmusic_bitrate_checkbox = Taxa de Bits\nmusic_genre_checkbox = Gênero\nmusic_length_checkbox = Comprimento\nmusic_comparison_checkbox = Comparação Aproximada\nmusic_checking_by_tags = Etiquetas\nmusic_checking_by_content = Conteúdo\nsame_music_seconds_label = Duração mínima de segundos do fragmento\nsame_music_similarity_label = Diferença máxima\nmusic_compare_only_in_title_group = Comparar dentro de grupos de títulos similares\nmusic_compare_only_in_title_group_tooltip =\n    Quando ativado, os ficheiros são agrupados por título e então comparados entre si.\n    \n    Com 10000 ficheiros, em vez de quase 100 milhões de comparações, haverá geralmente cerca de 20 000.\nsame_music_tooltip =\n    Buscar por arquivos de música semelhantes por seu conteúdo pode ser configurado definindo:\n    \n    - O tempo mínimo de fragmento após o qual os arquivos de música podem ser identificados como semelhantes\n    - A diferença máxima entre dois fragmentos testados\n    \n    A chave para bons resultados é achar combinações sensíveis desses parâmetros, para fornecido.\n    \n    Definir o tempo mínimo para 5s e a diferença máxima para 1.0 buscará fragmentos quase iguais nos arquivos.\n    Um tempo de 20s e uma diferença máxima de 6.0, por outro lado, funciona bem para achar versões remixes/ao vivo, etc.\n    \n    Por padrão, cada arquivo de música é comparado entre si, e isso pode levar muito tempo para testar muitos arquivos, logo, é geralmente melhor usar pastas de referência e especificar quais arquivos devem ser comparados entre si (com a mesma quantidade de arquivos, comparar impressões digitais será pelo menos 4x mais rápido do que sem pastas de referência).\nmusic_comparison_checkbox_tooltip =\n    Ele busca arquivos de música semelhantes usando IA, que usa aprendizado de máquina para remover parênteses duma frase. Por exemplo, com esta opção ativada, os arquivos em questão serão considerados duplicatas:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Sensível a Maiúsculas e Minúsculas\nduplicate_case_sensitive_name_tooltip =\n    Quando ativado, o grupo só registra quando eles têm o mesmo nome, por exemplo, Żołd <-> Żołd\n    \n    Desativar esta opção agrupará os nomes sem verificar se cada letra é do mesmo tamanho, por exemplo, żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Tamanho e Nome\nduplicate_mode_name_combo_box = Nome\nduplicate_mode_size_combo_box = Tamanho\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Blake3 - função de hash criptográfico. Este é o padrão, por ser muito rápido.\n    \n    CRC32 - função de hash simples. Isto deve ser mais rápido que Blake3, mas pode muito raramente ter algumas colisões.\n    \n    XXH3 - muito semelhante em desempenho e qualidade de hash ao Blake3 (mas não criptográfico). Logo, tais modos podem ser facilmente intercambiáveis.\nduplicate_check_method_tooltip =\n    Por ora, o Czkawka oferece três tipos de métodos para encontrar duplicatas:\n    \n    Nome - Acha arquivos que têm o mesmo nome.\n    \n    Tamanho - Acha arquivos que têm o mesmo tamanho.\n    \n    Hash - Acha arquivos que têm o mesmo conteúdo. Este modo faz o hash do arquivo e então compara este hash para achar duplicatas. Este modo é o jeito mais seguro de achar duplicatas. O aplicativo usa muito cache, logo, a segunda e outras varreduras dos mesmos dados devem ser muito mais rápidas que a primeira.\nimage_hash_size_tooltip =\n    Cada imagem marcada produz um hash especial que podem ser comparados entre si, e uma pequena diferença entre eles significa que essas imagens são parecidas.\n    \n    O tamanho de hash 8 é ótimo para achar imagens que são só um pouco semelhantes ao original. Com um maior conjunto de imagens (>1000), isso produzirá muitos falsos positivos, então recomendo usar um tamanho de hash maior neste caso.\n    \n    16 é o tamanho de hash padrão e um bom compromisso entre achar até mesmo imagens pouco semelhantes e ter poucas colisões de hash.\n    \n    Hashes 32 e 64 só acham imagens muito semelhantes, mas quase não devem ter falsos positivos (talvez, exceto algumas imagens com o canal alfa).\nimage_resize_filter_tooltip =\n    Para computar o hash da imagem, a biblioteca deve primeiro redimensioná-la.\n    \n    Dependendo do algoritmo escolhido, a imagem resultante usada para calcular o hash parecerá um pouco diferente.\n    \n    O algoritmo mais rápido a ser usado, mas também o que dá os piores resultados, é o Mais Próximo. Ele é ativado por padrão, pois com o tamanho de hash 16x16, a qualidade menor não é realmente visível.\n    \n    Com o tamanho de hash 8x8, recomenda-se usar um algoritmo diferente do Mais Próximo para ter melhores grupos de imagens.\nimage_hash_alg_tooltip =\n    Os usuários podem escolher entre um dos muitos algoritmos de cálculo do hash.\n    \n    Cada um tem pontos fortes e fracos e por vezes darão resultados melhores e por vezes piores para imagens diferentes.\n    \n    Logo, para determinar o melhor para você, são precisos testes manuais.\nbig_files_mode_combobox_tooltip = Permite a busca de arquivos menores/maiores\nbig_files_mode_label = Arquivos verificados\nbig_files_mode_smallest_combo_box = O Menor\nbig_files_mode_biggest_combo_box = O Maior\nmain_notebook_duplicates = Arquivos Duplicados\nmain_notebook_empty_directories = Diretórios Vazios\nmain_notebook_big_files = Arquivos Grandes\nmain_notebook_empty_files = Arquivos Vazios\nmain_notebook_temporary = Arquivos Temporários\nmain_notebook_similar_images = Imagens Semelhantes\nmain_notebook_similar_videos = Vídeos Similares\nmain_notebook_same_music = Músicas Duplicadas\nmain_notebook_symlinks = Ligações Simbólicas Inválidas\nmain_notebook_broken_files = Arquivos Quebrados\nmain_notebook_bad_extensions = Extensões Inválidas\nmain_tree_view_column_file_name = Nome do arquivo\nmain_tree_view_column_folder_name = Nome da Pasta\nmain_tree_view_column_path = Caminho\nmain_tree_view_column_modification = Data de Modificação\nmain_tree_view_column_size = Tamanho\nmain_tree_view_column_similarity = Similaridade\nmain_tree_view_column_dimensions = Tamanho\nmain_tree_view_column_title = Título\nmain_tree_view_column_artist = Artista\nmain_tree_view_column_year = Ano\nmain_tree_view_column_bitrate = Taxa de Bits\nmain_tree_view_column_length = Comprimento\nmain_tree_view_column_genre = Género\nmain_tree_view_column_symlink_file_name = Nome do Arquivo de Ligação Simbólica\nmain_tree_view_column_symlink_folder = Pasta da Ligação Simbólica\nmain_tree_view_column_destination_path = Caminho de Destino\nmain_tree_view_column_type_of_error = Tipo de Erro\nmain_tree_view_column_current_extension = Extensão Atual\nmain_tree_view_column_proper_extensions = Extensão Adequada\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codificador\nmain_label_check_method = Método de verificação\nmain_label_hash_type = Tipo de hash\nmain_label_hash_size = Tamanho do hash\nmain_label_size_bytes = Tamanho (bytes)\nmain_label_min_size = Mínimo\nmain_label_max_size = Máximo\nmain_label_shown_files = Número de arquivos exibidos\nmain_label_resize_algorithm = Redimensionar algoritmo\nmain_label_similarity = Similaridade{ \" \" }\nmain_check_box_broken_files_audio = Áudio\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = Arquivar\nmain_check_box_broken_files_image = Imagem\nmain_check_box_broken_files_video = Vídeo\nmain_check_box_broken_files_video_tooltip = Usa ffmpeg/ffprobe para validar arquivos de vídeo. Muito lento e pode detectar erros pedantes mesmo se o arquivo reproduzir bem.\ncheck_button_general_same_size = Ignorar do mesmo tamanho\ncheck_button_general_same_size_tooltip = Ignorar arquivos com tamanho idêntico nos resultados — geralmente estes são duplicatas 1:1\nmain_label_size_bytes_tooltip = Tamanho dos arquivos usados na verificação\n# Upper window\nupper_tree_view_included_folder_column_title = Pastas para Buscar\nupper_tree_view_included_reference_column_title = Pastas de Referência\nupper_recursive_button = Recursiva\nupper_recursive_button_tooltip = Se selecionado, buscar também arquivos que não são postos diretamente nas pastas escolhidas.\nupper_manual_add_included_button = Adicionar Manual\nupper_add_included_button = Adicionar\nupper_remove_included_button = Excluir\nupper_manual_add_excluded_button = Adicionar Manual\nupper_add_excluded_button = Adicionar\nupper_remove_excluded_button = Excluir\nupper_manual_add_included_button_tooltip =\n    Adicionar o nome do diretório à mão.\n    \n    Para adicionar vários caminhos de uma vez, separe-os por ;\n    \n    /home/roman;/home/rozkaz adicionará dois diretórios /home/roman e /home/rozkaz\nupper_add_included_button_tooltip = Adicionar novo diretório à busca.\nupper_remove_included_button_tooltip = Excluir diretório da busca.\nupper_manual_add_excluded_button_tooltip =\n    Adicionar o nome de diretório excluído à mão.\n    \n    Para adicionar vários caminhos de uma vez, separe-os por ;\n    \n    /home/roman;/home/krokiet adicionará dois diretórios /home/roman e /home/keokiet\nupper_add_excluded_button_tooltip = Adicionar diretório a ser excluído na busca.\nupper_remove_excluded_button_tooltip = Excluir diretório da exclusão.\nupper_notebook_items_configuration = Configuração dos Itens\nupper_notebook_excluded_directories = Caminhos Excluídos\nupper_notebook_included_directories = Caminhos Incluídos\nupper_allowed_extensions_tooltip =\n    Extensões permitidas devem ser separadas por vírgulas (por padrão todas estão disponíveis).\n    \n    Os seguintes Macros, que adicionam várias extensões de uma só vez, também estão disponíveis: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Exemplo de uso \".exe, IMAGE, VIDEO, .rar, 7z\" — isto significa que as imagens (ex., jpg, png), vídeos (ex., avi, mp4), exe, rar e arquivos 7z serão escaneados.\nupper_excluded_extensions_tooltip =\n    Lista de arquivos desabilitados que serão ignorados na verificação.\n    \n    Ao usar extensões permitidas e desativadas, este tem maior prioridade, então o arquivo não será marcado.\nupper_excluded_items_tooltip = \n        Itens excluídos devem conter * wildcard e devem ser separados por vírgulas.\n        Este é mais lento que Excluídas Caminhos, portanto use-o com cuidado.\nupper_excluded_items = Itens excluídos:\nupper_allowed_extensions = Extensões permitidas:\nupper_excluded_extensions = Extensões desabilitadas:\n# Popovers\npopover_select_all = Selecionar todos\npopover_unselect_all = Desmarcar todos\npopover_reverse = Seleção inversa\npopover_select_all_except_shortest_path = Selecione tudo exceto o caminho mais curto\npopover_select_all_except_longest_path = Selecione tudo exceto o caminho mais longo\npopover_select_all_except_oldest = Selecionar todos, exceto os mais antigos\npopover_select_all_except_newest = Selecionar todos, exceto os mais recentes\npopover_select_one_oldest = Selecionar um mais antigo\npopover_select_one_newest = Selecionar um mais recente\npopover_select_custom = Selecionar um customizado\npopover_unselect_custom = Desmarcar customizado\npopover_select_all_images_except_biggest = Selecionar tudo, exceto o maior\npopover_select_all_images_except_smallest = Selecionar tudo, exceto o menor\npopover_custom_path_check_button_entry_tooltip =\n    Selecionar registros por caminho.\n    \n    Exemplo de uso:\n    /home/pimpek/rzecz.txt pode ser achado com /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Selecionar registros por nomes de arquivos.\n    \n    Exemplo de uso:\n    /usr/ping/pong.txt pode ser achado com *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Selecionar registros por Regex especificada.\n    \n    Com este modo, o texto buscado é o caminho com o nome.\n    \n    Exemplo de uso:\n    /usr/bin/ziemniak.txt pode ser achado com /ziem[a-z]+\n    \n    Ele usa a implementação regex padrão do Rust. Você pode ler mais sobre isso aqui: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Ativa a deteção sensível a maiúsculas e minúsculas.\n    \n    Quando desativado, /home/* acha ambos /HoMe/roman e /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Impede a seleção de todo registro no grupo.\n    \n    Isto está ativado por padrão, pois na maioria das situações, você não quer apagar ambos arquivos originais e duplicados, mas quer deixar ao menos um arquivo.\n    \n    AVISO: Esta configuração não funciona se você já selecionou manualmente todos os resultados num grupo.\npopover_custom_regex_path_label = Caminho\npopover_custom_regex_name_label = Nome\npopover_custom_regex_regex_label = Caminho da Regex + nome\npopover_custom_case_sensitive_check_button = Sensível a maiúsculas e minúsculas\npopover_custom_all_in_group_label = Não selecionar todo registro no grupo\npopover_custom_mode_unselect = Desmarcar customizado\npopover_custom_mode_select = Selecionar customizado\npopover_sort_file_name = Nome do arquivo\npopover_sort_folder_name = Nome da pasta\npopover_sort_full_name = Nome completo\npopover_sort_size = Tamanho\npopover_sort_selection = Seleção\npopover_invalid_regex = Regex inválido\npopover_valid_regex = Expressão regular é válida\n# Bottom buttons\nbottom_search_button = Buscar\nbottom_select_button = Selecionar\nbottom_delete_button = Excluir\nbottom_save_button = Guardar\nbottom_symlink_button = Ligação simbólica\nbottom_hardlink_button = Ligação hardlink\nbottom_move_button = Mover\nbottom_sort_button = Ordenar\nbottom_compare_button = Comparar\nbottom_search_button_tooltip = Iniciar busca\nbottom_select_button_tooltip = Selecionar registros. Só arquivos/diretórios selecionados podem ser processados posteriormente.\nbottom_delete_button_tooltip = Excluir arquivos/diretórios selecionados.\nbottom_save_button_tooltip = Guardar dados sobre a busca em arquivo\nbottom_symlink_button_tooltip =\n    Criar ligações simbólicas. Só funciona quando ao menos dois resultados num grupo são selecionados.\n    O primeiro é inalterado, e no segundo e mais tarde é feita a ligação simbólica para o primeiro.\nbottom_hardlink_button_tooltip =\n    Criar ligações hardlinks.\n    Só funciona quando ao menos dois resultados num grupo são selecionados.\n    O primeiro é inalterado, e no segundo e posterior é feito o hardlink ao primeiro.\nbottom_hardlink_button_not_available_tooltip =\n    Criar ligações hardlinks.\n    O botão está desativado, pois ligações hardlinks não podem ser criadas.\n    Hardlinks só funcionam com privilégios de administrador no Windows, logo, certifique-se de executar o aplicativo como administrador.\n    Se o aplicativo já funciona com tais privilégios, verifique se há questões semelhantes no GitHub.\nbottom_move_button_tooltip =\n    Move arquivos para o diretório escolhido.\n    Ele copia todos os arquivos para o diretório sem preservar a árvore de diretório.\n    Ao tentar mover dois arquivos com nome idêntico para o diretório, a segunda falhará e exibirá um erro.\nbottom_sort_button_tooltip = Ordena arquivos/pastas de acordo com o método selecionado.\nbottom_compare_button_tooltip = Compare as imagens do grupo.\nbottom_show_errors_tooltip = Exibir/ocultar painel de texto inferior.\nbottom_show_upper_notebook_tooltip = Exibir/ocultar painel superior do caderno.\n# Progress Window\nprogress_stop_button = Parar\nprogress_stop_additional_message = Parada pedida\n# About Window\nabout_repository_button_tooltip = Link para a página do repositório com o código-fonte.\nabout_donation_button_tooltip = Link para a página de doação.\nabout_instruction_button_tooltip = Link para a página de instrução.\nabout_translation_button_tooltip = Link para a página do Crowdin com traduções do aplicativo. Oficialmente, polonês e inglês são suportados.\nabout_repository_button = Repositório\nabout_donation_button = Doação\nabout_instruction_button = Instrução\nabout_translation_button = Tradução\n# Header\nheader_setting_button_tooltip = Abre diálogo de configurações.\nheader_about_button_tooltip = Abre diálogo com informações sobre o aplicativo.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Número de threads usadas\nsettings_number_of_threads_tooltip = Numero de thread usadas. Zero significa que toda thread disponível será usada.\nsettings_use_rust_preview = Usar bibliotecas externas em vez de gtk para carregar pré-visualizações\nsettings_use_rust_preview_tooltip =\n    A utilização de pré-visualizações com GTK será por vezes mais rápida e suportará mais formatos, mas outras vezes ocorre exatamente o inverso.\n    \n    Se tiver problemas com o carregamento de pré-visualizações, tente alterar esta configuração.\n    \n    Em sistemas não-GNU/Linux, é recomendado usar esta opção porque o GTK-Pixbuf nem sempre está disponível lá, então desativar esta opção irá parar as tentativas falhadas de carregar pré-visualizações de algumas imagens.\nsettings_label_restart = Você tem de reiniciar o aplicativo para aplicar as configurações!\nsettings_ignore_other_filesystems = Ignorar outros sistemas de arquivos (só Linux)\nsettings_ignore_other_filesystems_tooltip =\n    Ignora arquivos que não estão no mesmo sistema de arquivos que os diretórios buscados.\n    \n    Funciona como a opção -xdev no comando find no Linux\nsettings_save_at_exit_button_tooltip = Guardar configuração em arquivo ao fechar o aplicativo.\nsettings_load_at_start_button_tooltip =\n    Carregar configuração do arquivo ao abrir aplicativo.\n    \n    Se não estiver ativado, as configurações padrão serão usadas.\nsettings_confirm_deletion_button_tooltip = Exibir diálogo de confirmação ao clicar no botão excluir.\nsettings_confirm_link_button_tooltip = Exibir diálogo de confirmação ao clicar no botão de ligação hardlink/simbólica.\nsettings_confirm_group_deletion_button_tooltip = Exibir caixa de diálogo de aviso ao tentar excluir todo registro do grupo.\nsettings_show_text_view_button_tooltip = Exibir painel de texto na parte inferior da interface do usuário.\nsettings_use_cache_button_tooltip = Usar cache de arquivos.\nsettings_save_also_as_json_button_tooltip = Salvar cache no formato JSON (legível por humanos). É possível modificar o seu conteúdo. O cache deste arquivo será lido automaticamente pelo aplicativo se o cache de formato binário (com extensão bin) estiver faltando.\nsettings_use_trash_button_tooltip = Move arquivos para a lixeira em vez de os excluir para sempre.\nsettings_language_label_tooltip = Idioma para a interface de usuário.\nsettings_save_at_exit_button = Guardar configuração ao fechar o aplicativo\nsettings_load_at_start_button = Carregar configuração ao abrir o aplicativo\nsettings_confirm_deletion_button = Exibir diálogo de confirmação ao excluir qualquer arquivo\nsettings_confirm_link_button = Exibir a caixa de diálogo de confirmação ao fazer a ligação hardlink/simbólica de qualquer arquivo\nsettings_confirm_group_deletion_button = Exibir diálogo de confirmação ao apagar todo arquivo no grupo\nsettings_show_text_view_button = Exibir painel de texto inferior\nsettings_use_cache_button = Usar cache\nsettings_save_also_as_json_button = Também guardar o cache como arquivo JSON\nsettings_use_trash_button = Mover os arquivos excluídos para a lixeira\nsettings_language_label = Idioma\nsettings_multiple_delete_outdated_cache_checkbutton = Excluir entradas de cache desatualizadas automaticamente\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Excluir resultados de cache desatualizados que apontam para arquivos inexistentes.\n    \n    Quando ativado, o aplicativo garante que ao carregar os registros, todos os registros apontem para arquivos válidos (aqueles com problemas são ignorados).\n    \n    Desativar isto ajudará ao escanear arquivos em unidades externas, então as entradas de cache sobre eles não serão removidas na próxima verificação.\n    \n    No caso de ter centenas de milhares de registros no cache, é sugerido ativar isto, o que acelerará o carregamento/armazenamento de cache/salvamento no início/fim do escaneamento.\nsettings_notebook_general = Geral\nsettings_notebook_duplicates = Duplicatas\nsettings_notebook_images = Imagens Semelhantes\nsettings_notebook_videos = Vídeo Semelhante\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Exibe pré-visualização no lado direito (ao selecionar um arquivo de imagem).\nsettings_multiple_image_preview_checkbutton = Exibir pré-visualização da imagem\nsettings_multiple_clear_cache_button_tooltip =\n    Limpar manualmente o cache de entradas desatualizadas.\n    Isto só deve ser usado se a limpeza automática houver sido desativada.\nsettings_multiple_clear_cache_button = Remover resultados desatualizados do cache.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Oculta todos os arquivos, exceto um, se todos apontarem para os mesmos dados (são ligados por hardlink).\n    \n    Exemplo: No caso de existirem (em disco) sete arquivos que são vinculados por hardlink a dados específicos e um arquivo diferente com os mesmos dados, mas um inode diferente, em seguida no achador de duplicatas, só um arquivo único e um arquivo dos que foi feita a ligação hardlink serão exibidos.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Definir o tamanho mínimo do arquivo que será armazenado em cache.\n    \n    Escolher um valor menor gerará mais registros. Isto acelerará a busca, mas diminuirá o carregamento/armazenamento do cache.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Permite o cache de pré-hash (um hash calculado a partir duma pequena parte do arquivo) que permite a demissão de resultados não duplicados anteriormente.\n    \n    Está desativado por padrão, pois pode causar lentidões nalguns casos.\n    \n    É altamente recomendado o usar ao escanear centenas de milhares ou milhões de arquivos, pois pode acelerar a pesquisa em várias vezes.\nsettings_duplicates_prehash_minimal_entry_tooltip = Tamanho mínimo da entrada em cache.\nsettings_duplicates_hide_hard_link_button = Ocultar links físicos\nsettings_duplicates_prehash_checkbutton = Usar cache de pré-hash\nsettings_duplicates_minimal_size_cache_label = Tamanho mínimo dos arquivos (em bytes) guardados no cache\nsettings_duplicates_minimal_size_cache_prehash_label = Tamanho mínimo dos arquivos (em bytes) guardados no cache de pré-hash\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Guardar as configurações atuais em arquivo.\nsettings_loading_button_tooltip = Carregar configurações de arquivo e substituir a configuração atual por elas.\nsettings_reset_button_tooltip = Redefinir a configuração atual para a padrão.\nsettings_saving_button = Guardar configuração\nsettings_loading_button = Carregar configuração\nsettings_reset_button = Redefinir configuração\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Abre o diretório onde os arquivos txt são armazenados.\n    \n    Modificar os arquivos de cache pode fazer com que resultados inválidos sejam exibidos. Porém, modificar o caminho pode economizar tempo ao mover uma grande quantidade de arquivos para um local diferente.\n    \n    Você pode copiar esses arquivos entre computadores para economizar tempo em outra verficação de arquivos (claro, se eles tiverem uma estrutura de diretórios semelhante).\n    \n    No caso de problemas com o cache, esses arquivos podem ser removidos. O aplicativo os regenerará automaticamente.\nsettings_folder_settings_open_tooltip =\n    Abre o diretório onde a configuração do Czkawka está armazenada.\n    \n    AVISO: Modificar manualmente a configuração pode quebrar seu fluxo de trabalho.\nsettings_folder_cache_open = Abrir diretório do cache\nsettings_folder_settings_open = Abrir diretório de configurações\n# Compute results\ncompute_stopped_by_user = A busca foi parada pelo usuário\ncompute_found_duplicates_hash_size = Encontradas { $number_files } duplicatas em { $number_groups } grupos que ocuparam { $size } em { $time }\ncompute_found_duplicates_name = Encontradas { $number_files } duplicações em { $number_groups } grupos em { $time }\ncompute_found_empty_folders = Encontradas pastas { $number_files } vazias em { $time }\ncompute_found_empty_files = Encontrados { $number_files } arquivos vazios em { $time }\ncompute_found_big_files = Encontrados { $number_files } arquivos grandes em { $time }\ncompute_found_temporary_files = { $number_files } arquivos temporários encontrados em { $time }\ncompute_found_images = Encontradas { $number_files } imagens similares em { $number_groups } grupos em { $time }\ncompute_found_videos = Encontrados { $number_files } vídeos similares em { $number_groups } grupos em { $time }\ncompute_found_music = Encontrados { $number_files } arquivos de música similares em { $number_groups } grupos em { $time }\ncompute_found_invalid_symlinks = Encontrado { $number_files } links simbólicos inválidos em { $time }\ncompute_found_broken_files = Encontrados { $number_files } arquivos quebrados em { $time }\ncompute_found_bad_extensions = Encontrados { $number_files } arquivos com extensões inválidas em { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Verificado { $file_number } arquivo\n       *[other] Escaneado { $file_number } arquivos\n    }\nprogress_scanning_extension_of_files = Extensão marcada do arquivo { $file_checked }/{ $all_files }\nprogress_scanning_broken_files = Verificado { $file_checked }/{ $all_files } arquivo ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hash de { $file_checked }/{ $all_files } de vídeo\nprogress_creating_video_thumbnails = Miniaturas criadas de { $file_checked }/{ $all_files } de vídeo\nprogress_scanning_image = Hash de { $file_checked }/{ $all_files } imagem ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Comparado a { $file_checked }/{ $all_files } hash de imagem\nprogress_scanning_music_tags_end = Etiquetas comparadas de { $file_checked }/{ $all_files } arquivo de música\nprogress_scanning_music_tags = Ler etiquetas de { $file_checked }/{ $all_files } arquivo de música\nprogress_scanning_music_content_end = Impressão digital comparada de { $file_checked }/{ $all_files } arquivo de música\nprogress_scanning_music_content = Calculada impressão digital de { $file_checked }/ arquivo de música{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Pasta { $folder_number } escaneada\n       *[other] Escaneado { $folder_number } pastas\n    }\nprogress_scanning_size = Tamanho digitalizado do arquivo { $file_number }\nprogress_scanning_size_name = Nome digitalizado e tamanho do arquivo { $file_number }\nprogress_scanning_name = Nome digitalizado do arquivo { $file_number }\nprogress_analyzed_partial_hash = Hash parcial analisado de arquivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Hash completo analisado de arquivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Carregando cache de pré-hash\nprogress_prehash_cache_saving = Salvando cache pré-hash\nprogress_hash_cache_loading = Carregando cache de hash\nprogress_hash_cache_saving = Salvando cache de hash\nprogress_cache_loading = Carregando cache\nprogress_cache_saving = Salvando cache\nprogress_current_stage = Estágio atual:{ \" \" }\nprogress_all_stages = Todo estágio:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Configuração guardada no arquivo { $name }.\nsaving_loading_saving_failure = Falhou na salvaguarda dos dados de configuração no arquivo { $name }, motivo { $reason }.\nsaving_loading_reset_configuration = A configuração atual foi limpa.\nsaving_loading_loading_success = Configuração de aplicativo devidamente carregada.\nsaving_loading_failed_to_create_config_file = Falha ao criar o arquivo de configuração \"{ $path }\", razão \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Não se pode carregar a configuração de \"{ $path }\", pois ela não existe ou não é um arquivo.\nsaving_loading_failed_to_read_data_from_file = Não se pode ler dados do arquivo \"{ $path }\", razão \"{ $reason }\".\n# Other\nselected_all_reference_folders = Não é possível iniciar a busca quando todo diretório está definido como pasta de referência\nsearching_for_data = Buscando dados, pode demorar um pouco, aguarde...\ntext_view_messages = MENSAGENS\ntext_view_warnings = AVISOS\ntext_view_errors = ERROS\nabout_window_motto = Este programa é gratuito para o uso e sempre será.\nkrokiet_new_app = Czkawka está em modo de manutenção, o que significa que apenas erros críticos serão corrigidos e nenhum novo recurso será adicionado. Para novos recursos, por favor, veja o novo aplicativo Krokiet, que é mais estável e com desempenho e ainda está em desenvolvimento ativo.\n# Various dialog\ndialogs_ask_next_time = Perguntar na próxima vez\nsymlink_failed = Falha ao link simbólico { $name } para { $target }, motivo { $reason }\ndelete_title_dialog = Confirmação de exclusão\ndelete_question_label = Tem certeza de que quer excluir arquivos?\ndelete_all_files_in_group_title = Confirmação da exclusão de todo arquivo no grupo\ndelete_all_files_in_group_label1 = Em alguns grupos todo registro está selecionado.\ndelete_all_files_in_group_label2 = Tem certeza de que quer os excluir?\ndelete_items_label = { $items } arquivos serão excluídos.\ndelete_items_groups_label = { $items } arquivos de { $groups } grupos serão excluídos.\nhardlink_failed = Falha ao hardlink { $name } para { $target }, motivo { $reason }\nhard_sym_invalid_selection_title_dialog = Seleção inválida com alguns grupos\nhard_sym_invalid_selection_label_1 = Em alguns grupos só há um registro selecionado e ele será ignorado.\nhard_sym_invalid_selection_label_2 = Para poder ligar estes arquivos, ao menos dois resultados no grupo têm de ser selecionados.\nhard_sym_invalid_selection_label_3 = O primeiro no grupo é reconhecido como original e não é mudado, mas o segundo e posterior são modificados.\nhard_sym_link_title_dialog = Link de confirmação\nhard_sym_link_label = Tem certeza de que quer vincular estes arquivos?\nmove_folder_failed = Falha ao mover a pasta { $name }, razão { $reason }\nmove_file_failed = Falha ao mover o arquivo { $name }, razão { $reason }\nmove_files_title_dialog = Escolha a pasta para a qual você quer mover arquivos duplicados\nmove_files_choose_more_than_1_path = Só um caminho pode ser selecionado para poder copiar seus arquivos duplicados, selecionado { $path_number }.\nmove_stats = Devidamente movidos { $num_files }/{ $all_files } itens\nsave_results_to_file = Resultados salvos tanto nos arquivos txt quanto json na pasta \"{ $name }\".\nsearch_not_choosing_any_music = ERRO: Você deve selecionar ao menos uma caixa de seleção com tipos de busca de música.\nsearch_not_choosing_any_broken_files = ERRO: Você deve selecionar ao menos uma caixa de seleção com tipo de arquivos quebrados.\ninclude_folders_dialog_title = Pastas para incluir\nexclude_folders_dialog_title = Pastas para excluir\ninclude_manually_directories_dialog_title = Adicionar diretório manualmente\ncache_properly_cleared = Cache devidamente limpo\ncache_clear_duplicates_title = Limpando o cache de duplicatas\ncache_clear_similar_images_title = Limpando o cache de imagens similares\ncache_clear_similar_videos_title = Limpando o cache de vídeos similares\ncache_clear_message_label_1 = Deseja limpar o cache de entradas desatualizadas?\ncache_clear_message_label_2 = Esta operação removerá toda entrada de cache que aponta para arquivos inválidos.\ncache_clear_message_label_3 = Isto pode acelerar um pouco o carregamento/salvamento para o cache.\ncache_clear_message_label_4 = AVISO: A operação removerá todo dado em cache de unidades externas desconectadas. Logo, cada hash terá de ser regenerado.\n# Show preview\npreview_image_resize_failure = Falha ao redimensionar a imagem { $name }.\npreview_image_opening_failure = Falha ao abrir a imagem { $name }, razão { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Grupo { $current_group }/{ $all_groups } ({ $images_in_group } imagens)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/ro/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Setări\nwindow_main_title = Czkawka (Sufletăciune)\nwindow_progress_title = Scanare\nwindow_compare_images = Compară imaginile\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Inchide\n# Krokiet info dialog\nkrokiet_info_title = Introducerea lui Krokiet - Noua versiune a Czkawka\nkrokiet_info_message = \n        Krokiet este noua, îmbunătățită, mai rapidă și mai fiabilă versiune a Czkawka GTK GUI!\n\n        Este mai ușor de rulat și mai rezistent la modificările sistemului, deoarece depinde doar de bibliotecile de bază disponibile pe majoritatea sistemelor implicit.\n\n        Krokiet aduce, de asemenea, funcții pe care Czkawka nu le are, inclusiv miniaturile în modul de comparare video, un curățător EXIF, progresul mutării/copierii/ștergerii fișierelor sau opțiuni extinse de sortare.\n\n        Îl testează și vezi diferența!\n\n        Czkawka va continua să primească corecții de erori și actualizări minore de la mine, dar toate noile funcții vor fi dezvoltate exclusiv pentru Krokiet, iar oricine este liber să contribuie cu noi funcții, să adauge moduri lipsă sau să extindă Czkawka în continuare.\n\n        PS: Acest mesaj ar trebui să apară doar o dată. Dacă apare din nou, setați variabila de mediu CZKAWKA_DONT_ANNOY_ME la orice valoare non-goalomptă.\n# Main window\nmusic_title_checkbox = Titlu\nmusic_artist_checkbox = Artist\nmusic_year_checkbox = An\nmusic_bitrate_checkbox = Rată de bitrate\nmusic_genre_checkbox = Gen\nmusic_length_checkbox = Lungime\nmusic_comparison_checkbox = Comparație aproximativă\nmusic_checking_by_tags = Etichete\nmusic_checking_by_content = Conținut\nsame_music_seconds_label = Fragment minim a doua durată\nsame_music_similarity_label = Diferența maximă\nmusic_compare_only_in_title_group = Compară în cadrul grupurilor de titluri similare\nmusic_compare_only_in_title_group_tooltip =\n    Când este activat, fișierele sunt grupate după titlu și apoi comparate între ele.\n    \n    Cu 10000 de fişiere, în schimb aproape 100 de milioane de comparaţii vor fi de obicei aproximativ 20000 de comparaţii.\nsame_music_tooltip =\n    Căutarea fişierelor muzicale similare după conţinutul său poate fi configurată prin setarea:\n    \n    - Timpul minim de fragment după care fişierele muzicale pot fi identificate ca fiind similare\n    - Diferenţa maximă între două fragmente testate\n    \n    Cheia pentru rezultate bune este de a găsi combinaţii rezonabile ale acestor parametri, pentru furnizare.\n    \n    Setarea timpului minim la 5 s și diferența maximă la 1.0, va căuta fragmente aproape identice în fișiere.\n    O perioadă de 20 de ani și o diferență maximă de 6,0, pe de altă parte, funcționează bine pentru a găsi remixuri/versiuni live etc.\n    \n    În mod implicit, fiecare fișier muzical este comparat unul cu altul și acest lucru poate dura mult timp când testezi mai multe fișiere, astfel încât este de obicei mai bine să se utilizeze dosare de referință și să se precizeze care fișiere trebuie comparate între ele (cu același volum de fișiere; compararea amprentelor digitale va fi mai rapidă de cel puțin 4x decât fără dosare de referință).\nmusic_comparison_checkbox_tooltip =\n    Caută fișiere muzicale similare folosind AI, care folosește învățarea mașinăriei pentru a elimina paranteze dintr-o frază. De exemplu, cu această opțiune activată, fișierele în cauză vor fi considerate duplicate:\n    \n    Remix Lato 2021)\nduplicate_case_sensitive_name = Sensibil la caz\nduplicate_case_sensitive_name_tooltip =\n    Când este activată, grupul înregistrează doar atunci când are exact același nume, de ex. Trunchiul <-> Z<unk> ołd\n    \n    Dezactivarea acestei opțiuni va grupa numele fără a verifica dacă fiecare literă are aceeași mărime, de ex. z<unk> oŁD <-> Z<unk> ołd\nduplicate_mode_size_name_combo_box = Dimensiune și nume\nduplicate_mode_name_combo_box = Nume\nduplicate_mode_size_combo_box = Dimensiune\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka oferă 3 tipuri de hash-uri:\n    \n    Blake3 - funcţie criptografică hash. Acesta este implicit pentru că este foarte rapid.\n    \n    CRC32 - funcţia simplă de hash. Acest lucru ar trebui să fie mai rapid decât Blake3, dar foarte rar poate avea unele coliziuni.\n    \n    XXH3 - foarte asemănător din punct de vedere al performanței și al calității hash-ului cu Blake3 (dar non-criptografic). Astfel de moduri pot fi ușor interschimbate.\nduplicate_check_method_tooltip =\n    Deocamdată, Czkawka oferă trei tipuri de metode pentru a găsi duplicate după:\n    \n    Nume - Găseşte fişiere care au acelaşi nume.\n    \n    Dimensiune - Găseşte fişiere cu aceeaşi dimensiune.\n    \n    Hash - Găseşte fişiere care au acelaşi conţinut. Acest mod hashează fişierul şi mai târziu compară acest hash pentru a găsi duplicate. Acest mod este cel mai sigur mod de a găsi duplicate. Aplicaţiile folosesc foarte mult cache, astfel încât scanările de la secundă şi mai departe ale aceloraşi date ar trebui să fie mult mai rapide decât primul.\nimage_hash_size_tooltip =\n    Fiecare imagine verificată produce un hash special, care poate fi comparat între ele, si o diferenta mica intre ele inseamna ca aceste imagini sunt similare.\n    \n    dimensiunea de 8 hash este destul de bună pentru a găsi imagini care sunt doar puţin similare cu originalul. Cu un set mai mare de imagini (>1000), acesta va produce o cantitate mare de fals pozitiv, Aşa că vă recomand să utilizaţi o mărime mai mare de hash în acest caz.\n    \n    16 este dimensiunea implicită a hash-ului care este un compromis destul de bun între a găsi chiar și imagini similare și a avea doar o mică coliziune a hash-ului.\n    \n    32 și 64 de hash-uri găsesc doar imagini foarte similare, dar nu ar trebui să aibă aproape nicio poziție falsă (poate cu excepția unor imagini cu un canal alfa).\nimage_resize_filter_tooltip =\n    Pentru a calcula hash of imagine, biblioteca trebuie mai întâi să o redimensioneze.\n    \n    În funcție de algoritmul ales, imaginea rezultată folosită pentru a calcula hash va arăta puțin diferit.\n    \n    Cel mai rapid algoritm de utilizat, dar şi cel care dă cele mai slabe rezultate, este Nearest. Acesta este activat în mod implicit, deoarece cu dimensiunea de 16x16 a hash-ului este de calitate mai mică decât cea vizibilă.\n    \n    Cu dimensiunea hash de 8x8 este recomandat să se folosească un algoritm diferit de Nearest, pentru a avea grupuri mai bune de imagini.\nimage_hash_alg_tooltip =\n    Utilizatorii pot alege unul dintre multele algoritmi de calculare a hash-ului.\n    \n    Fiecare are atât puncte puternice, cât şi puncte mai slabe şi va da uneori rezultate mai bune şi uneori mai proaste pentru imagini diferite.\n    \n    Deci, pentru a determina cel mai bun dintre voi, este necesară testarea manuală.\nbig_files_mode_combobox_tooltip = Permite căutarea celor mai mici/mai mari fişiere\nbig_files_mode_label = Fișiere verificate\nbig_files_mode_smallest_combo_box = Cel mai mic\nbig_files_mode_biggest_combo_box = Miggest\nmain_notebook_duplicates = Fișiere duplicate\nmain_notebook_empty_directories = Dosare goale\nmain_notebook_big_files = Fișiere mari\nmain_notebook_empty_files = Fișiere goale\nmain_notebook_temporary = Fișiere temporare\nmain_notebook_similar_images = Imagini similare\nmain_notebook_similar_videos = Video similare\nmain_notebook_same_music = Duplicate Muzică\nmain_notebook_symlinks = Simboluri invalide\nmain_notebook_broken_files = Fișiere defecte\nmain_notebook_bad_extensions = Extensii rele\nmain_tree_view_column_file_name = Numele fișierului\nmain_tree_view_column_folder_name = Nume folder\nmain_tree_view_column_path = Cale\nmain_tree_view_column_modification = Data modificării\nmain_tree_view_column_size = Dimensiune\nmain_tree_view_column_similarity = Similaritate\nmain_tree_view_column_dimensions = Dimensiuni\nmain_tree_view_column_title = Titlu\nmain_tree_view_column_artist = Artist\nmain_tree_view_column_year = An\nmain_tree_view_column_bitrate = Rată de bitrate\nmain_tree_view_column_length = Lungime\nmain_tree_view_column_genre = Gen\nmain_tree_view_column_symlink_file_name = Numele fișierului Symlink\nmain_tree_view_column_symlink_folder = Dosar Symlink\nmain_tree_view_column_destination_path = Calea destinației\nmain_tree_view_column_type_of_error = Tip de eroare\nmain_tree_view_column_current_extension = Extensia curentă\nmain_tree_view_column_proper_extensions = Extensie corectă\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Codecul\nmain_label_check_method = Metoda de verificare\nmain_label_hash_type = Tip hash\nmain_label_hash_size = Dimensiune hash\nmain_label_size_bytes = Dimensiune (octeți)\nmain_label_min_size = Minim\nmain_label_max_size = Maxim\nmain_label_shown_files = Numărul de fișiere afișate\nmain_label_resize_algorithm = Redimensionare algoritm\nmain_label_similarity = Similarity{ \"   \" }\nmain_check_box_broken_files_audio = Audio\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Arhivează\nmain_check_box_broken_files_image = Imagine\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Folosește ffmpeg/ffprobe pentru a valida fișiere video. Foarte lent și poate detecta erori pedantice chiar dacă fișierul rulează bine.\ncheck_button_general_same_size = Ignoră aceeași dimensiune\ncheck_button_general_same_size_tooltip = Ignoră fișierele cu rezultate de dimensiune identică - de obicei, acestea sunt de 1:1 duplicate\nmain_label_size_bytes_tooltip = Dimensiunea fişierelor care vor fi utilizate în scanare\n# Upper window\nupper_tree_view_included_folder_column_title = Dosare de căutat\nupper_tree_view_included_reference_column_title = Dosare de referință\nupper_recursive_button = Recursiv\nupper_recursive_button_tooltip = Dacă este selectat, caută și fișiere care nu sunt plasate direct în dosarele alese.\nupper_manual_add_included_button = Adăugare manuală\nupper_add_included_button = Adăugare\nupper_remove_included_button = Elimină\nupper_manual_add_excluded_button = Adăugare manuală\nupper_add_excluded_button = Adăugare\nupper_remove_excluded_button = Elimină\nupper_manual_add_included_button_tooltip =\n    Adăugați numele directorului pentru a căuta manual.\n    \n    Pentru a adăuga căi multiple simultan, separați-le de ;\n    \n    /home/roman;/home/rozkaz va adăuga două directoare /home/roman și /home/rozkaz\nupper_add_included_button_tooltip = Adăugați un nou director pentru căutare.\nupper_remove_included_button_tooltip = Ștergeți directorul de căutare.\nupper_manual_add_excluded_button_tooltip =\n    Adaugă numele folderului exclus manual.\n    \n    Pentru a adăuga căi multiple simultan, separați-le de ;\n    \n    /home/roman;/home/krokiet va adăuga două directoare /home/roman și /home/keokiet\nupper_add_excluded_button_tooltip = Adauga directorul pentru a fi exclus in cautare.\nupper_remove_excluded_button_tooltip = Ştergeţi directorul din excludere.\nupper_notebook_items_configuration = Configurare articole\nupper_notebook_excluded_directories = Puteți exclude căile\nupper_notebook_included_directories = Include Puteți\nupper_allowed_extensions_tooltip =\n    Extensiile permise trebuie separate prin virgulă (implicit toate sunt disponibile).\n    \n    Următoarele macro care adaugă simultan extensii multiple sunt de asemenea disponibile: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Foloseste exemplul \".exe, IMAGE, VIDEO, .rar, 7z\" - asta inseamna ca imaginile (e. . fișiere jpg, png), videoclipuri (de ex. avi, mp4), exe, rar și 7z vor fi scanate.\nupper_excluded_extensions_tooltip =\n    Lista fişierelor dezactivate care vor fi ignorate în scanare.\n    \n    La utilizarea extensiilor permise și dezactivate, aceasta are prioritate mai mare, deci fișierul nu va fi verificat.\nupper_excluded_items_tooltip = \n        Elemente excluse trebuie să conțină * wildcard și să fie separate prin virgulă.\n        Aceasta este mai lentă decât Excluded Paths, deci folosiți-o cu grijă.\nupper_excluded_items = Elemente excluse:\nupper_allowed_extensions = Extensii permise:\nupper_excluded_extensions = Extensii dezactivate:\n# Popovers\npopover_select_all = Selectează tot\npopover_unselect_all = Deselectează tot\npopover_reverse = Selectare inversă\npopover_select_all_except_shortest_path = Selectează toate, cu excepția celui mai scurt drum\npopover_select_all_except_longest_path = Selectează toate, cu excepția celui mai lung traseu\npopover_select_all_except_oldest = Selectează toate cu excepția celor mai vechi\npopover_select_all_except_newest = Selectează toate cu excepția celor noi\npopover_select_one_oldest = Selectează unul mai vechi\npopover_select_one_newest = Selectaţi unul dintre cele mai noi\npopover_select_custom = Selectare particularizată\npopover_unselect_custom = Deselectare particularizată\npopover_select_all_images_except_biggest = Selectează toate cu excepția celui mai mare\npopover_select_all_images_except_smallest = Selectează toate cu excepția celor mici\npopover_custom_path_check_button_entry_tooltip =\n    Selectaţi înregistrările după cale.\n    \n    Exemplu de utilizare:\n    /home/pimpek/rzecz.txt poate fi găsit cu /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Selectaţi înregistrările cu numele fişierelor.\n    \n    Exemplu de utilizare:\n    /usr/ping/pong.txt poate fi găsit cu *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Selectaţi înregistrările specificate de Regex.\n    \n    Cu acest mod, textul căutat este calea cu numele.\n    \n    Exemplu de utilizare:\n    /usr/bin/ziemniak. xt poate fi găsit cu /ziem[a-z]+\n    \n    Acest lucru folosește implementările implicite Rust regex. Puteți citi mai multe despre ele aici: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Activează detectarea cazurilor sensibile.\n    \n    Când este dezactivat /home/* găsește atât /HoMe/roman cât și /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Previne selectarea tuturor înregistrărilor din grup.\n    \n    Aceasta este activată în mod implicit, deoarece în majoritatea situațiilor, nu doriţi să ştergeţi atât fişierele originale, cât şi duplicate, dar doriţi să lăsaţi cel puţin un fişier.\n    \n    ATENŢIE: Această setare nu funcţionează dacă aţi selectat deja manual toate rezultatele într-un grup.\npopover_custom_regex_path_label = Cale\npopover_custom_regex_name_label = Nume\npopover_custom_regex_regex_label = Cale Regex + Nume\npopover_custom_case_sensitive_check_button = Sensibil la caz\npopover_custom_all_in_group_label = Nu selectaţi toate înregistrările în grup\npopover_custom_mode_unselect = Deselectare particularizată\npopover_custom_mode_select = Selectare particularizată\npopover_sort_file_name = Nume fișier\npopover_sort_folder_name = Nume dosar\npopover_sort_full_name = Numele complet\npopover_sort_size = Dimensiune\npopover_sort_selection = Selecţie\npopover_invalid_regex = Regex nu este valid\npopover_valid_regex = Regex este valid\n# Bottom buttons\nbottom_search_button = Caută\nbottom_select_button = Selectare\nbottom_delete_button = Ștergere\nbottom_save_button = Salvează\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Mutare\nbottom_sort_button = Sortează\nbottom_compare_button = Compară\nbottom_search_button_tooltip = Începe căutarea\nbottom_select_button_tooltip = Selectaţi înregistrările. Numai fişierele/dosarele selectate pot fi procesate ulterior.\nbottom_delete_button_tooltip = Ştergeţi fişierele/dosarele selectate.\nbottom_save_button_tooltip = Salvează datele despre căutare în fișier\nbottom_symlink_button_tooltip =\n    Creaţi link-uri simbolice.\n    Funcţionează numai atunci când cel puţin două rezultate într-un grup sunt selectate.\n    Prima este neschimbată, iar a doua și mai târziu simpatizează cu primul.\nbottom_hardlink_button_tooltip =\n    Creează link-uri hardware.\n    Funcţionează numai atunci când cel puţin două rezultate sunt selectate într-un grup.\n    Prima este neschimbată, iar a doua și mai târziu sunt greu legate mai întâi.\nbottom_hardlink_button_not_available_tooltip = \n    Creează link-uri hardware.\n    Butonul este dezactivat, deoarece hardlink-urile nu pot fi create.\n    Legăturile fizice funcționează doar cu privilegii de administrator pe Windows, așa că asigură-te că rulezi aplicația ca administrator.\n    Dacă aplicația funcționează deja cu astfel de privilegii verificați pentru probleme similare pe Giwhere,.\nbottom_move_button_tooltip =\n    Mută fișierele în directorul ales.\n    Copiază toate fișierele în director fără a păstra directorul arborescent.\n    Când se încearcă mutarea a două fișiere cu nume identic în folder, al doilea va eșua și va afișa eroarea.\nbottom_sort_button_tooltip = Sortează fișierele/dosarele în funcție de metoda selectată.\nbottom_compare_button_tooltip = Compară imaginile din grup.\nbottom_show_errors_tooltip = Arată/ascunde panoul de text de jos.\nbottom_show_upper_notebook_tooltip = Arată/Ascunde panoul de notebook-uri de sus.\n# Progress Window\nprogress_stop_button = Oprește\nprogress_stop_additional_message = Oprire solicitată\n# About Window\nabout_repository_button_tooltip = Link către pagina de depozit cu codul sursă.\nabout_donation_button_tooltip = Link la pagina de donare.\nabout_instruction_button_tooltip = Link către pagina de instrucțiuni.\nabout_translation_button_tooltip = Link catre pagina Crowdin cu traducerea aplicatiilor. Oficial, limba poloneza si engleza sunt suportate.\nabout_repository_button = Depozit\nabout_donation_button = Donație\nabout_instruction_button = Instrucțiuni\nabout_translation_button = Traducere\n# Header\nheader_setting_button_tooltip = Deschide dialogul de setări.\nheader_about_button_tooltip = Deschide dialogul cu informații despre aplicație.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Numar discutii folosite\nsettings_number_of_threads_tooltip = Numărul de teme folosite, 0 înseamnă că vor fi folosite toate temele disponibile.\nsettings_use_rust_preview = Folosește în schimb gtk librării externe pentru a încărca previzualizările\nsettings_use_rust_preview_tooltip =\n    Utilizarea de previzualizări gtk va fi uneori mai rapidă și va suporta mai multe formate, dar uneori aceasta ar putea fi exact opusul.\n    \n    Dacă aveţi probleme cu încărcarea previzualizărilor, puteţi încerca să schimbaţi această setare.\n    \n    Pe sistemele non-linux, se recomandă folosirea acestei optiuni, pentru că gtk-pixbuf nu sunt întotdeauna disponibile, astfel încât dezactivarea acestei opțiuni nu va încărca previzualizarea unor imagini.\nsettings_label_restart = Trebuie să reporniți aplicația pentru a aplica setările!\nsettings_ignore_other_filesystems = Ignorați alte sisteme de fișiere (doar Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignoră fişierele care nu se află în acelaşi sistem de fişiere ca şi directoarele căutate.\n    \n    Funcţionează la fel ca opţiunea -xdev în găsirea comenzii în Linux\nsettings_save_at_exit_button_tooltip = Salvați configurația în fișier la închiderea aplicației.\nsettings_load_at_start_button_tooltip =\n    Încarcă configurația din fișier la deschiderea aplicației.\n    \n    Dacă nu este activată, se vor folosi setările implicite.\nsettings_confirm_deletion_button_tooltip = Afișați caseta de confirmare când faceți clic pe butonul de ștergere.\nsettings_confirm_link_button_tooltip = Afișați caseta de confirmare când faceți clic pe butonul hard/symlink.\nsettings_confirm_group_deletion_button_tooltip = Arată dialogul de avertizare când se încearcă ștergerea tuturor înregistrărilor din grup.\nsettings_show_text_view_button_tooltip = Arată panoul de text în partea de jos a interfeței utilizatorului.\nsettings_use_cache_button_tooltip = Foloseşte cache-ul fişierelor.\nsettings_save_also_as_json_button_tooltip = Salvează cache-ul în formatul JSON (citibil uman). Este posibil să îi modifici conținutul. Geocutia din acest fişier va fi citită automat de aplicaţie dacă nu există geocutie în format binar (cu extensie bin).\nsettings_use_trash_button_tooltip = Mută fișierele la gunoi în loc să le ștergi definitiv.\nsettings_language_label_tooltip = Limba interfeței utilizatorului.\nsettings_save_at_exit_button = Salvați configurația la închiderea aplicației\nsettings_load_at_start_button = Încarcă configurația la deschiderea aplicației\nsettings_confirm_deletion_button = Arată dialog de confirmare la ștergerea oricăror fișiere\nsettings_confirm_link_button = Arată dialog de confirmare atunci când fişierele hard/symlink\nsettings_confirm_group_deletion_button = Arată dialog de confirmare la ștergerea tuturor fișierelor din grup\nsettings_show_text_view_button = Arată panoul de text jos\nsettings_use_cache_button = Utilizare geocutie\nsettings_save_also_as_json_button = De asemenea, salvează cache-ul ca fișier JSON\nsettings_use_trash_button = Mută fișierele șterse în gunoi\nsettings_language_label = Limba\nsettings_multiple_delete_outdated_cache_checkbutton = Şterge automat intrările învechite\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Ştergeţi rezultatele învechite ale geocutiei care indică fişierele inexistente.\n    \n    Atunci când este activată, aplicația se asigură la încărcarea înregistrărilor, că toate înregistrările indică către fișiere valide (cele decongelate sunt ignorate).\n    \n    Dezactivarea acestui lucru va ajuta la scanarea fişierelor pe unităţi externe, astfel încât intrările de cache despre acestea nu vor fi şterse în următoarea scanare.\n    \n    În cazul în care există sute de mii de înregistrări în cache; se sugerează să activezi acest lucru, care va încărca/salva cache-ul la start/end al scanării.\nsettings_notebook_general = Generalități\nsettings_notebook_duplicates = Duplicate\nsettings_notebook_images = Imagini similare\nsettings_notebook_videos = Video similar\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Afișează previzualizarea în partea dreaptă (când se selectează un fișier imagine).\nsettings_multiple_image_preview_checkbutton = Arată previzualizarea imaginii\nsettings_multiple_clear_cache_button_tooltip =\n    Curăță manual cache-ul intrărilor învechite.\n    Acest lucru ar trebui folosit doar dacă curățarea automată a fost dezactivată.\nsettings_multiple_clear_cache_button = Elimină rezultatele învechite din geocutie.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Ascunde toate fişierele, cu excepţia unuia, dacă toate arată spre aceleaşi date (sunt conectate).\n    \n    Exemplu: În cazul în care sunt (pe disc) şapte fişiere care sunt greu legate de date specifice şi un fişier diferit cu aceleaşi date, dar un alt inventar, apoi în duplicat va fi afișat un singur fișier unic și un fișier de la cele hardlink-ului.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Setaţi dimensiunea minimă a fişierului care va fi memorată în cache.\n    \n    Alegerea unei valori mai mici va genera mai multe înregistrări. (Automatic Translation) Aceasta va accelera căutarea, dar va încetini încărcarea/salvarea cache-ului.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Permite stocarea în cache a prehash (un hash calculat dintr-o mică parte a fișierului) care permite concedierea mai timpurie a rezultatelor nereplicate.\n    \n    este dezactivat implicit deoarece poate cauza încetiniri în unele situații.\n    \n    Este foarte recomandat sa il utilizezi cand scanezi sute de mii sau milioane de fisiere, pentru ca poate accelera cautarea de mai multe ori.\nsettings_duplicates_prehash_minimal_entry_tooltip = Dimensiunea minimă a intrării în cache.\nsettings_duplicates_hide_hard_link_button = Ascunde link-urile fizice\nsettings_duplicates_prehash_checkbutton = Foloseste cache-ul prehash\nsettings_duplicates_minimal_size_cache_label = Dimensiunea minimă a fişierelor (în octeţi) salvate în cache\nsettings_duplicates_minimal_size_cache_prehash_label = Dimensiunea minimă a fişierelor (în octeţi) salvate în cache de prehash\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Salvați setările curente în fișier.\nsettings_loading_button_tooltip = Încarcă setările din fișier și înlocuiește configurația curentă cu ele.\nsettings_reset_button_tooltip = Resetați configurația curentă la cea implicită.\nsettings_saving_button = Salvează configurația\nsettings_loading_button = Încarcă configurația\nsettings_reset_button = Resetare configurație\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Deschide folderul unde sunt stocate fișierele txt cache-ul.\n    \n    Modificarea fișierelor de cache poate duce la afișarea unor rezultate invalide. Cu toate acestea, modificarea traiectoriei poate salva timpul atunci când mutați un număr mare de fișiere într-o locație diferită.\n    \n    Puteţi copia aceste fişiere între computere pentru a salva timp la scanarea din nou pentru fişiere (desigur, dacă au o structură de directoare similară).\n    \n    În caz de probleme cu geocutia, aceste fişiere pot fi şterse. Aplicaţia le va regenera automat.\nsettings_folder_settings_open_tooltip =\n    Deschide folderul unde este stocată configurația Czkawka.\n    \n    AVERTISMENT: Modificarea manuală a configurației poate rupe fluxul de lucru.\nsettings_folder_cache_open = Deschide dosarul cache\nsettings_folder_settings_open = Deschide folderul de setări\n# Compute results\ncompute_stopped_by_user = Căutarea a fost oprită de utilizator\ncompute_found_duplicates_hash_size = Am găsit { $number_files } duplicate în { $number_groups } grupuri care au luat { $size } în { $time }\ncompute_found_duplicates_name = Am găsit { $number_files } duplicate în grupurile { $number_groups } în { $time }\ncompute_found_empty_folders = Folderele goale { $number_files } au fost găsite în { $time }\ncompute_found_empty_files = Fișiere goale { $number_files } găsite în { $time }\ncompute_found_big_files = Fișiere mari { $number_files } găsite în { $time }\ncompute_found_temporary_files = Fișiere temporare { $number_files } găsite în { $time }\ncompute_found_images = S-au găsit imagini similare { $number_files } în grupurile { $number_groups } în { $time }\ncompute_found_videos = S-au găsit videoclipuri similare { $number_files } în grupurile { $number_groups } în { $time }\ncompute_found_music = Am găsit { $number_files } fişiere muzicale similare în { $number_groups } grupuri { $time }\ncompute_found_invalid_symlinks = { $number_files } link-uri simboluri invalide găsite în { $time }\ncompute_found_broken_files = Fișiere defecte { $number_files } găsite în { $time }\ncompute_found_bad_extensions = Fișiere { $number_files } cu extensii invalide în { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] a scanat { $file_number } fişierul\n       *[other] Scanat { $file_number } fişiere\n    }\nprogress_scanning_extension_of_files = S-a verificat extensia fișierului { $file_checked }/{ $all_files }\nprogress_scanning_broken_files = Fişier verificat { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hashed of { $file_checked }/{ $all_files } video\nprogress_creating_video_thumbnails = Pictograme video create de { $file_checked }/{ $all_files }\nprogress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Imaginea a fost comparată { $file_checked }/{ $all_files }\nprogress_scanning_music_tags_end = Tag-uri comparate ale fișierului de muzică { $file_checked }/{ $all_files }\nprogress_scanning_music_tags = Citește etichetele fișierului de muzică { $file_checked }/{ $all_files }\nprogress_scanning_music_content_end = Față de amprenta fișierului de muzică { $file_checked }/{ $all_files }\nprogress_scanning_music_content = Amprenta calculată a fișierului de muzică { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Dosar Scanat { $folder_number }\n       *[other] Scanat { $folder_number } dosare\n    }\nprogress_scanning_size = Dimensiune scanată pentru fişierul { $file_number }\nprogress_scanning_size_name = Numele scanat şi dimensiunea fişierului { $file_number }\nprogress_scanning_name = Numele scanat al fişierului { $file_number }\nprogress_analyzed_partial_hash = S-a analizat hash parțial al fișierelor { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = S-a analizat hash complet al fişierelor { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Se încarcă cache-ul prehash\nprogress_prehash_cache_saving = Salvare cache prehash\nprogress_hash_cache_loading = Încărcare cache hash\nprogress_hash_cache_saving = Salvare cache hash\nprogress_cache_loading = Se încarcă geocutia\nprogress_cache_saving = Salvare geocutie\nprogress_current_stage = Current Stage:{ \"  \" }\nprogress_all_stages = All Stages:{ \"  \" }\n# Saving loading \nsaving_loading_saving_success = Configurație salvată în fișierul { $name }.\nsaving_loading_saving_failure = Salvarea datelor de configurare a eșuat în fișierul { $name }, motivul { $reason }.\nsaving_loading_reset_configuration = Configurația curentă a fost ștearsă.\nsaving_loading_loading_success = Configurare aplicație încărcată corespunzător.\nsaving_loading_failed_to_create_config_file = Nu s-a putut crea fișierul de configurare \"{ $path }\", motivul \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Nu se poate încărca configurația din \"{ $path }\" deoarece nu există sau nu este un fișier.\nsaving_loading_failed_to_read_data_from_file = Datele din fişierul \"{ $path }\", motivul \"{ $reason }\".\n# Other\nselected_all_reference_folders = Nu se poate începe căutarea, atunci când toate directoarele sunt setate ca dosare de referință\nsearching_for_data = Se caută date, poate dura ceva timp, vă rugăm așteptați...\ntext_view_messages = MESAJE\ntext_view_warnings = ATENȚIONĂRI\ntext_view_errors = EROARE\nabout_window_motto = Acest program este liber de utilizat și va fi întotdeauna.\nkrokiet_new_app = Czkawka este în modul de întreţinere, ceea ce înseamnă că vor fi rezolvate doar erorile critice şi că nu vor fi adăugate noi caracteristici. Pentru funcții noi, vă rugăm să consultați noua aplicație Krokiet, care este mai stabilă și mai performantă și este încă în curs de dezvoltare activă.\n# Various dialog\ndialogs_ask_next_time = Întreabă data viitoare\nsymlink_failed = Esuare simlink { $name } la { $target }, motivul { $reason }\ndelete_title_dialog = Ștergeți confirmarea\ndelete_question_label = Sunteţi sigur că doriţi să ştergeţi fişierele?\ndelete_all_files_in_group_title = Confirmarea ștergerii tuturor fișierelor din grup\ndelete_all_files_in_group_label1 = In unele grupuri, toate inregistrarile sunt selectate.\ndelete_all_files_in_group_label2 = Sunteţi sigur că doriţi să le ştergeţi?\ndelete_items_label = { $items } fișiere vor fi șterse.\ndelete_items_groups_label = { $items } fișiere din grupurile { $groups } vor fi șterse.\nhardlink_failed = Eșuare conectare { $name } la { $target }, motiv { $reason }\nhard_sym_invalid_selection_title_dialog = Selecţie invalidă cu unele grupuri\nhard_sym_invalid_selection_label_1 = În unele grupuri există doar o înregistrare selectată și va fi ignorată.\nhard_sym_invalid_selection_label_2 = Pentru a putea lega hard/sym aceste fișiere, cel puțin două rezultate în grup trebuie să fie selectate.\nhard_sym_invalid_selection_label_3 = Prima în grup este recunoscută ca fiind originală şi nu se modifică, dar se modifică a doua şi mai târziu.\nhard_sym_link_title_dialog = Confirmare link\nhard_sym_link_label = Sunteţi sigur că doriţi să conectaţi aceste fişiere?\nmove_folder_failed = Nu s-a reușit mutarea dosarului { $name }, motivul { $reason }\nmove_file_failed = Nu s-a reușit mutarea fișierului { $name }, motivul { $reason }\nmove_files_title_dialog = Alegeți directorul în care doriți să mutați fișierele duplicate\nmove_files_choose_more_than_1_path = Poate fi selectată doar o singură cale pentru a putea copia fişierele duplicate, selectate { $path_number }.\nmove_stats = Elemente corect mutate { $num_files }/{ $all_files }\nsave_results_to_file = Rezultate salvate atât pentru fişierele txt cât şi pentru fişierele json în folderul \"{ $name }\".\nsearch_not_choosing_any_music = EROARE: Trebuie să selectaţi cel puţin o casetă cu tipuri de căutare de muzică.\nsearch_not_choosing_any_broken_files = EROARE: Trebuie să selectaţi cel puţin o casetă de selectare cu tipul de fişiere bifate.\ninclude_folders_dialog_title = Dosare de inclus\nexclude_folders_dialog_title = Dosare de exclus\ninclude_manually_directories_dialog_title = Adaugă director manual\ncache_properly_cleared = Geocutie golită corect\ncache_clear_duplicates_title = Golire duplicate cache\ncache_clear_similar_images_title = Curăță cache imagini similare\ncache_clear_similar_videos_title = Curățare cache video similar\ncache_clear_message_label_1 = Vrei să ştergi memoria cache a intrărilor învechite?\ncache_clear_message_label_2 = Această operaţie va elimina toate intrările din cache-ul care indică fişiere invalide.\ncache_clear_message_label_3 = Aceasta poate încărca/salva uşor accelerat în cache.\ncache_clear_message_label_4 = AVERTISMENT: Operația va elimina toate datele stocate în cache din unplugged external drive. Deci fiecare hash va trebui să fie regenerat.\n# Show preview\npreview_image_resize_failure = Redimensionarea imaginii { $name } a eșuat.\npreview_image_opening_failure = Nu s-a reușit deschiderea imaginii { $name }, motivul { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Grup { $current_group }/{ $all_groups } ({ $images_in_group } imagini)\ncompare_move_left_button = l\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/ru/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Настройки\nwindow_main_title = Czkawka («Икота»)\nwindow_progress_title = Сканирование\nwindow_compare_images = Сравнить изображения\n# General\ngeneral_ok_button = ОК\ngeneral_close_button = Закрыть\n# Krokiet info dialog\nkrokiet_info_title = Представляем Krokiet - новая версия Czkawka\nkrokiet_info_message = \n        Крокиет – это новая, улучшенная, более быстрая и надежная версия Czkawka GTK GUI!\n\n        Его проще запускать и он более устойчив к изменениям системы, так как он зависит только от основных библиотек, доступных по умолчанию на большинстве систем.\n\n        Крокиет также предоставляет функции, которых нет в Czkawka, включая миниатюры в режиме сравнения видео, EXIF-очиститель, прогресс перемещения/копирования/удаления файлов или расширенные возможности сортировки.\n\n        Попробуйте сами и посмотрите разницу!\n\n        Czkawka продолжит получать исправления ошибок и небольшие обновления от меня, но все новые функции будут разрабатываться исключительно для Крокиета, и любой может внести свой вклад, добавив новые функции, расширив режимы или дополнительно развив Czkawka.\n\n        P.S.: Это сообщение должно появиться только один раз. Если оно снова появляется, установите переменную CZKAWKA_DONT_ANNOY_ME в любое непустое значение.\n# Main window\nmusic_title_checkbox = Заголовок\nmusic_artist_checkbox = Исполнитель\nmusic_year_checkbox = Год\nmusic_bitrate_checkbox = Битрейт\nmusic_genre_checkbox = Жанр\nmusic_length_checkbox = Длительность\nmusic_comparison_checkbox = Приблизительное сравнение\nmusic_checking_by_tags = Теги\nmusic_checking_by_content = Содержание\nsame_music_seconds_label = Минимальная длительность второго фрагмента\nsame_music_similarity_label = Максимальная разница\nmusic_compare_only_in_title_group = Сравнить внутри групп с одинаковыми названиями\nmusic_compare_only_in_title_group_tooltip =\n    Когда включено, файлы сгруппируются по заголовку, а затем сравниваются друг с другом.\n    \n    С 10000 файлов, вместо этого почти 100 миллионов сравнений обычно будет около 20000 сравнений.\nsame_music_tooltip =\n    Поиск похожих музыкальных файлов по его содержимому может быть настроен с помощью настройки:\n    \n    - Минимальное время фрагмента, после которого музыкальные файлы можно определить как похожие\n    - Максимальная разница между двумя проверенными фрагментами\n    \n    Ключ к хорошим результатам - найти разумные комбинации этих параметров, для предоставленных.\n    \n    Установка минимального времени на 5 секунд, а максимальная разница в 1.0, будет искать практически идентичные фрагменты файлов.\n    Время 20 секунд и максимальная разница в 6,0, с другой стороны, хорошо подходит для поиска ремиксов/версий и т.д.\n    \n    По умолчанию, каждый музыкальный файл сравнивается друг с другом, и это может занять много времени при тестировании множества файлов, поэтому обычно лучше использовать справочные папки и указать, какие файлы следует сравнивать друг с другом (одинаковое количество файлов), сравнение отпечатков пальцев будет быстрее по крайней мере на 4х, чем без ссылочных папок).\nmusic_comparison_checkbox_tooltip =\n    Ищет похожие музыкальные файлы с помощью ИИ, использующего машинное обучение для удаления скобок из фраз. Например, если эта опция включена, следующие файлы будут считаться дубликатами:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = С учётом регистра\nduplicate_case_sensitive_name_tooltip = \n    При включённой опции записи группируются, только если у них полностью совпадают имена с точностью до каждого символа. Например, «ХИТ Дискотека» не совпадёт с «хит дискотека».\n    \n    При отключённой опции записи группируются вне зависимости от того, заглавные или строчные буквы использовались при написании. Например, «ХИТ Дискотека», «хит дискотека», «хИт ДиСкОтЕКа» будут эквивалентны\nduplicate_mode_size_name_combo_box = Размер и имя\nduplicate_mode_name_combo_box = Имя\nduplicate_mode_size_combo_box = Размер\nduplicate_mode_hash_combo_box = Хэш\nduplicate_hash_type_tooltip =\n    В программе Czkawka можно использовать один из трёх алгоритмов хэширования:\n    \n    Blake3 — криптографическая хэш-функция. Используется по умолчанию, поскольку очень быстрый.\n    \n    CRC32 — простая хэш-функция. Ещё быстрее, чем Blake3, но возможны очень редкие совпадения хэшей неидентичных файлов.\n    \n    XXH3 — функция, похожая по производительности и надёжности хэша на Blake3, но не являющаяся криптографической, поэтому её можно использовать вместо Blake3.\nduplicate_check_method_tooltip =\n    На данный момент Czkawka предлагает три метода поиска дубликатов:\n    \n    Имя — ищет файлы с одинаковыми именами.\n    \n    Размер — ищет файлы одинакового размера.\n    \n    Хэш — ищет файлы с одинаковым содержимым. Этот режим хэширует файл, а затем сравнивает хэш для поиска дубликатов. Этот режим является самым надёжным способом поиска. Приложение активно использует кэш, поэтому второе и последующие сканирования одних и тех же данных должны быть намного быстрее, чем первое.\nimage_hash_size_tooltip =\n    Каждое проверяемое изображение производит специальный хэш, который можно сравнить друг с другом, и небольшая разница между ними означает, что эти изображения аналогичны.\n    \n    8 размер хэша достаточно хорош, чтобы найти изображения, которые немного похожи на оригинал. С большим набором изображений (>1000), это приведет к большому количеству ложных срабатываний, поэтому в данном случае я рекомендую использовать больший размер хэша.\n    \n    16 - это размер хэша по умолчанию, который является хорошим компромиссом между нахождением даже немного похожих изображений и наличием лишь небольшого количества хэш-коллизий.\n    \n    32 и 64 хэши находят только очень похожие изображения, но не должны иметь ложных срабатываний (может быть, за исключением некоторых изображений с альфа-каналом).\nimage_resize_filter_tooltip =\n    Чтобы вычислить хэш изображения, библиотека должна сначала его перемасштабировать.\n    \n    В зависимости от выбранного алгоритма полученное изображение, используемое при хэшировании, может выглядеть немного другим.\n    \n    Самый быстрый алгоритм с низким качеством — это метод ближайших соседей, Nearest. Он включён по умолчанию, потому при размере хэша 16x16 плохое качество не замечается.\n    \n    Если размер хэша 8x8, рекомендуется любой алгоритм, кроме Nearest, чтобы лучше отличать похожие изображения в группах.\nimage_hash_alg_tooltip =\n    Пользователи могут выбрать один из многих алгоритмов вычисления хэша.\n    \n    Каждый имеет сильные и слабые точки и иногда даёт более качественные и иногда хуже результаты для разных изображений.\n    \n    Поэтому для определения наилучшего из вас, требуется ручное тестирование.\nbig_files_mode_combobox_tooltip = Поиск наименьших/наибольших файлов\nbig_files_mode_label = Проверенные файлы\nbig_files_mode_smallest_combo_box = Самый маленький\nbig_files_mode_biggest_combo_box = Крупнейший\nmain_notebook_duplicates = Файлы-дубликаты\nmain_notebook_empty_directories = Пустые папки\nmain_notebook_big_files = Большие файлы\nmain_notebook_empty_files = Пустые файлы\nmain_notebook_temporary = Временные файлы\nmain_notebook_similar_images = Похожие изображения\nmain_notebook_similar_videos = Похожие видео\nmain_notebook_same_music = Музыкальные дубликаты\nmain_notebook_symlinks = Битые симв. ссылки\nmain_notebook_broken_files = Битые файлы\nmain_notebook_bad_extensions = Плохие расширения\nmain_tree_view_column_file_name = Имя файла\nmain_tree_view_column_folder_name = Имя папки\nmain_tree_view_column_path = Путь\nmain_tree_view_column_modification = Дата изменения\nmain_tree_view_column_size = Размер\nmain_tree_view_column_similarity = Сходство\nmain_tree_view_column_dimensions = Размеры\nmain_tree_view_column_title = Заголовок\nmain_tree_view_column_artist = Исполнитель\nmain_tree_view_column_year = Год\nmain_tree_view_column_bitrate = Битрейт\nmain_tree_view_column_length = Длительность\nmain_tree_view_column_genre = Жанр\nmain_tree_view_column_symlink_file_name = Имя файла символьной ссылки\nmain_tree_view_column_symlink_folder = Папка Symlink\nmain_tree_view_column_destination_path = Путь назначения\nmain_tree_view_column_type_of_error = Тип ошибки\nmain_tree_view_column_current_extension = Текущее расширение\nmain_tree_view_column_proper_extensions = Правильное расширение\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Кодек\nmain_label_check_method = Метод проверки\nmain_label_hash_type = Тип хэша\nmain_label_hash_size = Размер хэша\nmain_label_size_bytes = Размер (байт)\nmain_label_min_size = Мин\nmain_label_max_size = Макс\nmain_label_shown_files = Количество отображаемых файлов\nmain_label_resize_algorithm = Алгоритм масштабирования\nmain_label_similarity = Сходство{ \"   \" }\nmain_check_box_broken_files_audio = Звук\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Архивировать\nmain_check_box_broken_files_image = Изображение\nmain_check_box_broken_files_video = Видео\nmain_check_box_broken_files_video_tooltip = Использует ffmpeg/ffprobe для проверки видеофайлов. Очень медленно и может обнаруживать педантичные ошибки, даже если файл воспроизводится нормально.\ncheck_button_general_same_size = Игнорировать одинаковый размер\ncheck_button_general_same_size_tooltip = Игнорировать файлы с одинаковым размером в результатах - обычно это 1:1 дубликаты\nmain_label_size_bytes_tooltip = Размер файлов, которые будут просканированы\n# Upper window\nupper_tree_view_included_folder_column_title = Папки для поиска\nupper_tree_view_included_reference_column_title = Содержит оригиналы\nupper_recursive_button = В подпапках\nupper_recursive_button_tooltip = При включённой опции будут также искаться файлы, не находящиеся непосредственно в корне выбранной папки, т. е. в других подпапках данной папки и их подпапках.\nupper_manual_add_included_button = Прописать вручную\nupper_add_included_button = Добавить\nupper_remove_included_button = Удалить\nupper_manual_add_excluded_button = Ручное добавление\nupper_add_excluded_button = Добавить\nupper_remove_excluded_button = Удалить\nupper_manual_add_included_button_tooltip =\n    Добавьте имя каталога для поиска вручную.\n    \n    Чтобы добавить несколько путей одновременно, разделите их на ;\n    \n    /home/roman;/home/rozkaz добавит два каталога /home/roman и /home/rozkaz\nupper_add_included_button_tooltip = Добавить новый каталог для поиска.\nupper_remove_included_button_tooltip = Исключить каталог из поиска.\nupper_manual_add_excluded_button_tooltip =\n    Добавьте вручную исключенное имя каталога.\n    \n    Чтобы добавить несколько путей одновременно, разделите их на ;\n    \n    /home/roman;/home/krokiet добавит два каталога /home/roman и /home/keokiet\nupper_add_excluded_button_tooltip = Добавить каталог, исключаемый из поиска.\nupper_remove_excluded_button_tooltip = Убрать каталог из исключенных.\nupper_notebook_items_configuration = Параметры поиска\nupper_notebook_excluded_directories = Исключенные пути\nupper_notebook_included_directories = Включенные пути\nupper_allowed_extensions_tooltip =\n    Включаемые расширения должны быть разделены запятыми (по умолчанию ищутся файлы с любыми расширениями).\n    \n    Макросы IMAGE, VIDEO, MUSIC, TEXT добавляют сразу несколько расширений.\n    \n    Пример использования: «.exe, IMAGE, VIDEO, .rar, 7z» — это означает, что будут сканироваться файлы изображений (напр. jpg, png), видео (напр. avi, mp4), exe, rar и 7z.\nupper_excluded_extensions_tooltip =\n    Список отключенных файлов, которые будут игнорироваться в сканировании.\n    \n    При использовании разрешенных и отключенных расширений этот файл имеет более высокий приоритет, поэтому файл не будет проверяться.\nupper_excluded_items_tooltip = \n        Исключенные элементы должны содержать * wildcard и должны быть разделены запятыми.\n        Это медленнее, чем Excluded Paths, поэтому используйте его осторожно.\nupper_excluded_items = Исключённые элементы:\nupper_allowed_extensions = Допустимые расширения:\nupper_excluded_extensions = Отключенные расширения:\n# Popovers\npopover_select_all = Выбрать все\npopover_unselect_all = Снять выделение\npopover_reverse = Обратить выделение\npopover_select_all_except_shortest_path = Выбрать все, кроме кратчайшего пути\npopover_select_all_except_longest_path = Выбрать все, кроме самого длинного пути\npopover_select_all_except_oldest = Выделить все, кроме старых\npopover_select_all_except_newest = Выделить все, кроме новых\npopover_select_one_oldest = Выбрать один старый\npopover_select_one_newest = Выбрать один новый\npopover_select_custom = Выбрать произвольный\npopover_unselect_custom = Снять выбор\npopover_select_all_images_except_biggest = Выделить все, кроме наибольшего\npopover_select_all_images_except_smallest = Выделить все, кроме наименьшего\npopover_custom_path_check_button_entry_tooltip =\n    Выбор записей на основе пути.\n    \n    Пример:\n    /home/pimpek/rzecz.txt можно найти с помощью /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Выбор записей по именам файлов.\n    \n    Пример:\n    /usr/ping/pong.txt можно найти с помощью *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Выбор записей с помощью регулярного выражения.\n    \n    В этом режиме искомый текст представляет собой путь с именем.\n    \n    Пример:\n    /usr/bin/ziemniak.txt можно найти с помощью выражения /ziem[a-z]+\n    \n    По умолчанию используется синтаксис регулярных выражений Rust. Подробнее об этом можно прочитать здесь: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Включает регистрозависимый поиск.\n    \n    При отключённой опции «/home/*» будет соответствовать как «/home/roman», так и «/HoMe/roman».\npopover_custom_not_all_check_button_tooltip =\n    Запрет выбора всех записей в группе.\n    \n    Эта опция включена по умолчанию, потому что в большинстве ситуаций вам не надо удалять и оригиналы, и дубликаты — обычно оставляют хотя бы один файл.\n    \n    ВНИМАНИЕ. Этот параметр не работает, если вы уже вручную выбрали все результаты в группе.\npopover_custom_regex_path_label = Путь\npopover_custom_regex_name_label = Имя\npopover_custom_regex_regex_label = Путь с рег. выраж. + имя\npopover_custom_case_sensitive_check_button = С учётом регистра\npopover_custom_all_in_group_label = Не выбирать все записи в группе\npopover_custom_mode_unselect = Снять выбор\npopover_custom_mode_select = Выбрать произвольный\npopover_sort_file_name = Имя файла\npopover_sort_folder_name = Название папки\npopover_sort_full_name = Полное имя\npopover_sort_size = Размер\npopover_sort_selection = Выбранные объекты\npopover_invalid_regex = Некорректное регулярное выражение\npopover_valid_regex = Корректное регулярное выражение\n# Bottom buttons\nbottom_search_button = Искать\nbottom_select_button = Выбрать\nbottom_delete_button = Удалить\nbottom_save_button = Сохранить\nbottom_symlink_button = Симв. ссылка\nbottom_hardlink_button = Жёст. ссылка\nbottom_move_button = Переместить\nbottom_sort_button = Сортировать\nbottom_compare_button = Сравнить\nbottom_search_button_tooltip = Начать поиск\nbottom_select_button_tooltip = Выберите записи. Только выбранные файлы/папки будут доступны для последующей обработки.\nbottom_delete_button_tooltip = Удалить выбранные файлы/папки.\nbottom_save_button_tooltip = Сохранить данные о поиске в файл\nbottom_symlink_button_tooltip =\n    Создать символьные ссылки.\n    Работает, только когда выбрано не менее двух результатов в группе.\n    Первый результат оставляется, а второй и последующие делаются символьными ссылками на первый.\nbottom_hardlink_button_tooltip =\n    Создать жёсткие ссылки.\n    Работает, только когда выбрано не менее двух результатов в группе.\n    Первый результат оставляется, а второй и последующие делаются жёсткими ссылками на первый.\nbottom_hardlink_button_not_available_tooltip =\n    Создание жестких ссылок.\n    Кнопка отключена, так как невозможно создать жёсткие ссылки.\n    Связи работают только с правами администратора в Windows, поэтому не забудьте запустить приложение от имени администратора.\n    Если приложение уже работает с такими привилегиями, проверьте аналогичные проблемы на Github.\nbottom_move_button_tooltip =\n    Перемещение файлов в выбранный каталог.\n    Копирует все файлы в папку без сохранения структуры дерева каталогов.\n    При попытке переместить два файла с одинаковым именем в одну и ту же папку второй не будет перемещён и появится сообщение об ошибке.\nbottom_sort_button_tooltip = Сортировка файлов/папок по выбранному методу.\nbottom_compare_button_tooltip = Сравнить изображения в группе.\nbottom_show_errors_tooltip = Показать/скрыть нижнюю текстовую панель.\nbottom_show_upper_notebook_tooltip = Показать/скрыть верхнюю панель блокнота.\n# Progress Window\nprogress_stop_button = Остановить\nprogress_stop_additional_message = Стоп запрошен\n# About Window\nabout_repository_button_tooltip = Ссылка на страницу репозитория с исходным кодом.\nabout_donation_button_tooltip = Ссылка на страницу пожертвований.\nabout_instruction_button_tooltip = Ссылка на страницу инструкций.\nabout_translation_button_tooltip = Ссылка на страницу Crowdin с переводами приложений. Официально поддерживаются английский и польский языки.\nabout_repository_button = Репозиторий\nabout_donation_button = Пожертвование\nabout_instruction_button = Инструкция\nabout_translation_button = Перевод\n# Header\nheader_setting_button_tooltip = Открыть окно настроек.\nheader_about_button_tooltip = Открыть окно с информацией о приложении.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Количество использованных потоков\nsettings_number_of_threads_tooltip = Количество используемых потоков. Установите 0, чтобы использовать все доступные потоки.\nsettings_use_rust_preview = Использовать внешние библиотеки вместо gtk для загрузки предпросмотра\nsettings_use_rust_preview_tooltip =\n    Использование превью gtk иногда будет быстрее и поддерживать больше форматов, но иногда это может быть и наоборот.\n    \n    Если у вас возникли проблемы с загрузкой предпросмотра, вы можете попробовать изменить эту настройку.\n    \n    На не-linux системах рекомендуется использовать эту опцию, потому что gtk-pixbuf не всегда доступен там, поэтому отключение этой опции не будет загружать превью некоторых изображений.\nsettings_label_restart = Вам нужно перезапустить приложение, чтобы применить настройки!\nsettings_ignore_other_filesystems = Игнорировать другие файловые системы (только Linux)\nsettings_ignore_other_filesystems_tooltip =\n    игнорирует файлы, которые находятся в той же файловой системе, что и поисковые директории.\n    \n    Работает так же, как и команда 'xdev' в команде 'находить'\nsettings_save_at_exit_button_tooltip = Сохранить конфигурацию в файл при закрытии приложения.\nsettings_load_at_start_button_tooltip =\n    Загрузить конфигурацию из файла при открытии приложения.\n    \n    Если не включено, будут использоваться настройки по умолчанию.\nsettings_confirm_deletion_button_tooltip = Показать окно подтверждения при нажатии на кнопку удаления.\nsettings_confirm_link_button_tooltip = Показывать окно подтверждения при нажатии кнопки жесткой/символической ссылки.\nsettings_confirm_group_deletion_button_tooltip = Показывать окно предупреждения при попытке удалить все записи из группы.\nsettings_show_text_view_button_tooltip = Показать текстовую панель в нижней части интерфейса.\nsettings_use_cache_button_tooltip = Использовать файловый кэш.\nsettings_save_also_as_json_button_tooltip = Сохранять кэш в формат JSON (человекочитаемый). Его содержимое можно изменять. Кэш из этого файла будет автоматически прочитан приложением, если бинарный кэш (с расширением bin) отсутствует.\nsettings_use_trash_button_tooltip = Перемещать файлы в корзину вместо их безвозвратного удаления.\nsettings_language_label_tooltip = Язык пользовательского интерфейса.\nsettings_save_at_exit_button = Сохранять конфигурацию при закрытии приложения\nsettings_load_at_start_button = Загружать конфигурацию при открытии приложения\nsettings_confirm_deletion_button = Показывать подтверждение при удалении любых файлов\nsettings_confirm_link_button = Показывать окно подтверждения при создании жёстких или символьных ссылок на файлы\nsettings_confirm_group_deletion_button = Показывать подтверждение при удалении всех файлов в группе\nsettings_show_text_view_button = Показывать нижнюю текстовую панель\nsettings_use_cache_button = Использовать кэш\nsettings_save_also_as_json_button = Также сохранять кэш в файл JSON\nsettings_use_trash_button = Перемещать удаляемые файлы в корзину\nsettings_language_label = Язык\nsettings_multiple_delete_outdated_cache_checkbutton = Автоматически удалять устаревшие записи кэша\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Удалить устаревшие результаты кеша, указывающие на несуществующие файлы.\n    \n    Когда опция включена, приложение проверяет при загрузке записей, указывают ли они на доступные файлы (недостающие файлы игнорируются).\n    \n    Отключение этой опции помогает при сканировании файлов на внешних носителях, чтобы информация о них не была очищена при следующем сканировании.\n    \n    При наличии сотен тысяч записей в кэше рекомендуется включить эту опцию, чтобы ускорить загрузку и сохранение кэша в начале и конце сканирования.\nsettings_notebook_general = Общие настройки\nsettings_notebook_duplicates = Дубликаты\nsettings_notebook_images = Похожие изображения\nsettings_notebook_videos = Похожие видео\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Показывать предварительный просмотр справа (при выборе файла изображения).\nsettings_multiple_image_preview_checkbutton = Показывать предпросмотр изображения\nsettings_multiple_clear_cache_button_tooltip =\n    Очистка устаревших записей кэша вручную.\n    Следует использовать только в том случае, если автоматическая очистка отключена.\nsettings_multiple_clear_cache_button = Удалить устаревшие результаты из кэша.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Скрыть все файлы, кроме первого, если все они указывают на одни и те же данные (связаны жёсткой ссылкой).\n    \n    Пример: если (на диске) семь файлов связаны жёсткой ссылкой с определёнными данными, а ещё один файл содержат те же данные, но на другом inode, то в средстве поиска дубликатов будут показаны только этот последний уникальный файл и один файл из являющихся жёсткой ссылкой.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Установить минимальный размер кэшируемого файла.\n    \n    Выбор меньшего значения приведёт к созданию большего количества записей. Это ускорит поиск, но замедлит загрузку/сохранение кэша.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Включает кэширование предварительного хэша (предхэша), вычисляемого из небольшой части файла, что позволяет быстрее исключать из анализа отличающиеся файлы.\n    \n    По умолчанию отключено, так как в некоторых ситуациях может замедлять работу.\n    \n    Настоятельно рекомендуется использовать его при сканировании сотен тысяч или миллионов файлов, так как это может ускорить поиск в разы.\nsettings_duplicates_prehash_minimal_entry_tooltip = Минимальный размер кэшируемого элемента.\nsettings_duplicates_hide_hard_link_button = Скрыть жесткие ссылки\nsettings_duplicates_prehash_checkbutton = Кэшировать предхэш\nsettings_duplicates_minimal_size_cache_label = Минимальный размер (байт) кэшируемых файлов\nsettings_duplicates_minimal_size_cache_prehash_label = Минимальный размер (байт) файлов для кэша предхэша\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Сохранить текущую конфигурацию настроек в файл.\nsettings_loading_button_tooltip = Загрузить настройки из файла и заменить ими текущую конфигурацию.\nsettings_reset_button_tooltip = Сбросить текущую конфигурацию на конфигурацию по умолчанию.\nsettings_saving_button = Сохранить конфигурацию\nsettings_loading_button = Загрузить конфигурацию\nsettings_reset_button = Сбросить настройки\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Открыть папку, в которой хранятся текстовые файлы кеша.\n    \n    Изменение файлов кэша может привести к отображению неверных результатов, однако изменение пути может сэкономить время при перемещении большого количества файлов в другое место.\n    \n    Вы можете копировать эти файлы между компьютерами, чтобы сэкономить время на повторном сканировании файлов (конечно, если они имеют схожую структуру каталогов).\n    \n    В случае возникновения проблем с кэшем эти файлы можно удалить. Приложение автоматически пересоздаст их.\nsettings_folder_settings_open_tooltip =\n    Открывает папку, в которой хранится конфигурация Czkawka.\n    \n    ВНИМАНИЕ. Ручное изменение конфигурации может нарушить функционирование программы.\nsettings_folder_cache_open = Открыть папку кэша\nsettings_folder_settings_open = Открыть папку настроек\n# Compute results\ncompute_stopped_by_user = Поиск был остановлен пользователем\ncompute_found_duplicates_hash_size = Найдено { $number_files } дубликатов в { $number_groups } группах, которые заняли { $size } за { $time }\ncompute_found_duplicates_name = Найдено { $number_files } дубликатов в { $number_groups } группах за { $time }\ncompute_found_empty_folders = Найдено { $number_files } пустых папки в { $time }\ncompute_found_empty_files = Найдено { $number_files } пустых файла в { $time }\ncompute_found_big_files = Найдено { $number_files } больших файлов в { $time }\ncompute_found_temporary_files = Найдено { $number_files } временных файла в { $time }\ncompute_found_images = Найдено { $number_files } подобных изображения в { $number_groups } группах за { $time }\ncompute_found_videos = Найдено { $number_files } похожих видео в { $number_groups } группах за { $time }\ncompute_found_music = Найдено { $number_files } схожих музыкальных файлов в { $number_groups } группах за { $time }\ncompute_found_invalid_symlinks = Найдено { $number_files } невалидных symbolic ссылок за { $time }\ncompute_found_broken_files = Найдено { $number_files } сломанных файлов в { $time }\ncompute_found_bad_extensions = Найдено { $number_files } файлов с недопустимыми расширениями в { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Просканирован { $file_number } файл\n       *[other] Просканированы { $file_number } файлов\n    }\nprogress_scanning_extension_of_files = Проверено расширение { $file_checked }/{ $all_files } файла\nprogress_scanning_broken_files = Проверено { $file_checked }/{ $all_files } файл ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Хэш { $file_checked }/{ $all_files } видео\nprogress_creating_video_thumbnails = Созданы эскизы видео { $file_checked }/{ $all_files }\nprogress_scanning_image = Хэш { $file_checked }/{ $all_files } изображения ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Хэш изображений по сравнению { $file_checked }/{ $all_files }\nprogress_scanning_music_tags_end = По сравнению тегов музыкального файла { $file_checked }/{ $all_files }\nprogress_scanning_music_tags = Чтение тегов { $file_checked }/{ $all_files } музыкального файла\nprogress_scanning_music_content_end = По сравнению с музыкальным файлом { $file_checked }/{ $all_files }\nprogress_scanning_music_content = Вычисляется отпечаток звука { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Просканирована { $folder_number } папка\n       *[other] Просканированы { $folder_number } папок\n    }\nprogress_scanning_size = Отсканированный размер файла { $file_number }\nprogress_scanning_size_name = Отсканированное имя и размер файла { $file_number }\nprogress_scanning_name = Отсканированное имя файла { $file_number }\nprogress_analyzed_partial_hash = Частичный хэш { $file_checked }/{ $all_files } файлов ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Полный хэш { $file_checked }/{ $all_files } файлов ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Загрузка кэша prehash\nprogress_prehash_cache_saving = Сохранение кэша prehash\nprogress_hash_cache_loading = Загрузка хеш-кэша\nprogress_hash_cache_saving = Сохранение хэша\nprogress_cache_loading = Загрузка кэша\nprogress_cache_saving = Сохранение кэша\nprogress_current_stage = Текущий этап:{ \" \" }\nprogress_all_stages = Все этапы:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Конфигурация сохранена в файл { $name }.\nsaving_loading_saving_failure = Не удалось сохранить данные конфигурации в файл { $name }, причина { $reason }.\nsaving_loading_reset_configuration = Текущая конфигурация была удалена.\nsaving_loading_loading_success = Настройки приложения корректно загружены.\nsaving_loading_failed_to_create_config_file = Не удалось создать файл конфигурации «{ $path }». Причина: «{ $reason }».\nsaving_loading_failed_to_read_config_file = Невозможно загрузить конфигурацию из «{ $path }», так как или такого файла не существует, или это не файл.\nsaving_loading_failed_to_read_data_from_file = Невозможно прочитать данные из файла «{ $path }». Причина: «{ $reason }».\n# Other\nselected_all_reference_folders = Невозможно начать поиск, когда все каталоги установлены как папки со ссылками\nsearching_for_data = Поиск данных может занять некоторое время — пожалуйста, подождите...\ntext_view_messages = СООБЩЕНИЯ\ntext_view_warnings = ПРЕДУПРЕЖДЕНИЯ\ntext_view_errors = ОШИБКИ\nabout_window_motto = Эта программа бесплатна для использования и всегда будет оставаться таковой.\nkrokiet_new_app = Чкавка находится в режиме технического обслуживания, что означает, что будут исправлены только критические ошибки, и новые возможности не будут добавлены. Для новых функций ознакомьтесь с новым приложением Krokiet, которое является более стабильным и эффективным, и всё ещё находится в стадии активной разработки.\n# Various dialog\ndialogs_ask_next_time = Всегда спрашивать\nsymlink_failed = Не удалось привязать { $name } к { $target }, причина { $reason }\ndelete_title_dialog = Подтверждение удаления\ndelete_question_label = Вы уверены, что хотите удалить файлы?\ndelete_all_files_in_group_title = Подтверждение удаления всех файлов в группе\ndelete_all_files_in_group_label1 = В некоторых группах были выбраны все записи.\ndelete_all_files_in_group_label2 = Вы уверены, что хотите удалить их?\ndelete_items_label = Будет удалено файлов: { $items }.\ndelete_items_groups_label = Будет удалено файлов: { $items } (групп: { $groups }).\nhardlink_failed = Не удалось привязать { $name } к { $target }, причина { $reason }\nhard_sym_invalid_selection_title_dialog = Неверный выбор в некоторых группах\nhard_sym_invalid_selection_label_1 = В некоторых группах выбрана только одна запись — они будут проигнорированы.\nhard_sym_invalid_selection_label_2 = Чтобы жёстко или символьно связать эти файлы, необходимо выбрать как минимум два результата в группе.\nhard_sym_invalid_selection_label_3 = Первый в группе признан в качестве оригинала и не будет изменён, но второй и последующие модифицированы.\nhard_sym_link_title_dialog = Подтверждение связывания ссылкой\nhard_sym_link_label = Вы уверены, что хотите связать эти файлы?\nmove_folder_failed = Не удалось переместить папку { $name }. Причина: { $reason }\nmove_file_failed = Не удалось переместить файл { $name }. Причина: { $reason }\nmove_files_title_dialog = Выберите папку, в которую вы хотите переместить дублирующиеся файлы\nmove_files_choose_more_than_1_path = Можно выбрать только один путь для копирования дубликатов файлов, но выбрано { $path_number }.\nmove_stats = Удалось переместить без ошибок элементов: { $num_files }/{ $all_files }\nsave_results_to_file = Результаты сохранены в txt и json файлы в папку \"{ $name }\".\nsearch_not_choosing_any_music = ОШИБКА: Необходимо выбрать как минимум один флажок с типами поиска музыки.\nsearch_not_choosing_any_broken_files = ОШИБКА: Вы должны выбрать хотя бы один флажок с типом проверенных ошибочных файлов.\ninclude_folders_dialog_title = Папки для включения\nexclude_folders_dialog_title = Папки для исключения\ninclude_manually_directories_dialog_title = Добавить папку вручную\ncache_properly_cleared = Кэш успешно очищен\ncache_clear_duplicates_title = Очистка кэша дубликатов\ncache_clear_similar_images_title = Очистка кэша похожих изображений\ncache_clear_similar_videos_title = Очистка кэша похожих видео\ncache_clear_message_label_1 = Убрать из кэша устаревшие записи?\ncache_clear_message_label_2 = Это действие удалит все записи кэша, указывающие на недоступные файлы.\ncache_clear_message_label_3 = Это может немного ускорить загрузку/сохранение кэша.\ncache_clear_message_label_4 = ВНИМАНИЕ. Это действие удалит все кэшированные данные с отключённых внешних дисков. Хэши для файлов на этих носителях будет необходимо сгенерировать заново.\n# Show preview\npreview_image_resize_failure = Не удалось изменить размер изображения { $name }.\npreview_image_opening_failure = Не удалось открыть изображение { $name }. Причина: { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Группа { $current_group }/{ $all_groups } (изображений: { $images_in_group })\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/sv-SE/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Inställningar\nwindow_main_title = Czkawka (Pipade)\nwindow_progress_title = Scannar\nwindow_compare_images = Jämför bilder\n# General\ngeneral_ok_button = Ok\ngeneral_close_button = Stäng\n# Krokiet info dialog\nkrokiet_info_title = Införande av Krokiet – Ny version av Czkawka\nkrokiet_info_message = \n        Stödet är den nya, förbättrade, snabbare och mer pålitliga versionen av Czkawka GTK GUI!\n\n        Det är lättare att köra och mer motståndskraftigt mot systemändringar, eftersom det bara förlitar sig på kärnbibliotek som finns tillgängliga på de flesta system som standard.\n\n        Stödet medför också funktioner som Czkawka saknar, inklusive miniatyrbilder i videojämförelsetillstånd, en EXIF-renare, filflytt/kopiera/ta bort-framsteg eller utökade sorteringsalternativ.\n\n        Prova det och se skillnaden!\n\n        Czkawka kommer fortsätta att få buggfixar och mindre uppdateringar från mig, men alla nya funktioner kommer att utvecklas exklusivt för Stödet, och vem som helst är fri att bidra med nya funktioner, lägga till saknade lägen eller utöka Czkawka vidare.\n\n        PS: Detta meddelande bör bara visas en gång. Om det visas igen, sätt miljökvariabeln CZKAWKA_DONT_ANNOY_ME till ett värde som inte är tomt.\n# Main window\nmusic_title_checkbox = Titel\nmusic_artist_checkbox = Kunstnär\nmusic_year_checkbox = År\nmusic_bitrate_checkbox = Bitratemarkering\nmusic_genre_checkbox = Genrė\nmusic_length_checkbox = Längd\nmusic_comparison_checkbox = Ungefärlig jämförelse\nmusic_checking_by_tags = Taggar\nmusic_checking_by_content = Innehåll\nsame_music_seconds_label = Minsta fragment sekund varaktighet\nsame_music_similarity_label = Maximal skillnad\nmusic_compare_only_in_title_group = Jämför inom grupper med liknande titlar\nmusic_compare_only_in_title_group_tooltip =\n    När den är aktiverad grupperas filerna efter titel och jämförs sedan med varandra.\n    \n    Med 10000 filer, i stället nästan 100 miljoner jämförelser brukar det finnas runt 20000 jämförelser.\nsame_music_tooltip =\n    Sökning efter liknande musikfiler genom dess innehåll kan konfigureras genom att ställa in:\n    \n    - Minsta fragmenttid efter vilken musikfiler kan identifieras som liknande\n    - Maximal skillnad mellan två testade fragment\n    \n    Nyckeln till bra resultat är att hitta förnuftiga kombinationer av dessa parametrar, för tillhandahållen.\n    \n    Att ställa in den minsta tiden till 5s och den maximala skillnaden till 1.0, kommer att leta efter nästan identiska fragment i filerna.\n    En tid på 20-talet och en maximal skillnad på 6,0, å andra sidan, fungerar bra för att hitta remixer/live-versioner etc.\n    \n    Som standard jämförs varje musikfil med varandra och detta kan ta mycket tid vid testning av många filer, så är det oftast bättre att använda referensmappar och ange vilka filer som ska jämföras med varandra(med samma mängd filer, Att jämföra fingeravtryck kommer att vara snabbare minst 4x än utan referensmappar).\nmusic_comparison_checkbox_tooltip =\n    Den söker efter liknande musikfiler med AI, som använder maskininlärning för att ta bort parenteser från en fras. Till exempel, med detta alternativ aktiverat, filerna i fråga kommer att betraktas som dubbletter:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = Skiftlägeskänslig\nduplicate_case_sensitive_name_tooltip =\n    När detta är aktiverat spelar gruppen bara in när de har exakt samma namn t.ex. Żołd <-> Żołd\n    \n    Inaktivera sådana alternativ kommer gruppnamn utan att kontrollera om varje bokstav är samma storlek t.ex. żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = Storlek och namn\nduplicate_mode_name_combo_box = Namn\nduplicate_mode_size_combo_box = Storlek\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka erbjuder 3 typer av hash:\n    \n    Blake3 - kryptografisk hash-funktion. Detta är standard eftersom det är mycket snabbt.\n    \n    CRC32 - enkel hash-funktion. Detta bör vara snabbare än Blake3, men kan mycket sällan ha några kollisioner.\n    \n    XXH3 - mycket lik i prestanda och hashkvalitet till Blake3 (men icke-kryptografisk). Så, sådana lägen kan lätt bytas ut.\nduplicate_check_method_tooltip =\n    För tillfället erbjuder Czkawka tre typer av metoder för att hitta dubbletter av:\n    \n    Namn - Hittar filer som har samma namn.\n    \n    Storlek - Hittar filer som har samma storlek.\n    \n    Hash - Hittar filer som har samma innehåll. Detta läge hashar filen och senare jämför denna hash för att hitta dubbletter. Detta läge är det säkraste sättet att hitta dubbletter. Appen använder starkt cache, så andra och ytterligare skanningar av samma data bör vara mycket snabbare än den första.\nimage_hash_size_tooltip =\n    Varje kontrollerad bild ger en speciell hash som kan jämföras med varandra, och en liten skillnad mellan dem innebär att dessa bilder är liknande.\n    \n    8 hash storlek är ganska bra att hitta bilder som bara är lite liknande till originalet. Med en större uppsättning bilder (>1000), kommer detta att producera en stor mängd falska positiva, så jag rekommenderar att använda en större hash storlek i detta fall.\n    \n    16 är standard hashstorlek vilket är en ganska bra kompromiss mellan att hitta även lite liknande bilder och att bara ha en liten mängd hashkollisioner.\n    \n    32 och 64 hashen finner endast mycket liknande bilder, men bör ha nästan inga falska positiva (kanske förutom vissa bilder med alfa-kanal).\nimage_resize_filter_tooltip =\n    För att beräkna hash av bilden, måste biblioteket först ändra storlek på den.\n    \n    Beroende på vald algoritm kommer den resulterande bilden som används för att beräkna hash att se lite annorlunda ut.\n    \n    Den snabbaste algoritmen att använda, men också den som ger de sämsta resultaten, är nära! Det är aktiverat som standard, eftersom med 16x16 hash storlek lägre kvalitet är det inte riktigt synligt.\n    \n    Med 8x8 hashstorlek rekommenderas att använda en annan algoritm än Närmaste för att få bättre grupper av bilder.\nimage_hash_alg_tooltip =\n    Användare kan välja mellan en av många algoritmer för att beräkna hash.\n    \n    Var och en har både starka och svagare punkter och ger ibland bättre och ibland sämre resultat för olika bilder.\n    \n    Så, för att bestämma den bästa för dig krävs manuell testning.\nbig_files_mode_combobox_tooltip = Gör det möjligt att söka efter minsta/största filer\nbig_files_mode_label = Markerade filer\nbig_files_mode_smallest_combo_box = Den minsta\nbig_files_mode_biggest_combo_box = Den största\nmain_notebook_duplicates = Duplicera filer\nmain_notebook_empty_directories = Tomma kataloger\nmain_notebook_big_files = Stora filer\nmain_notebook_empty_files = Tomma filer\nmain_notebook_temporary = Tillfälliga filer\nmain_notebook_similar_images = Liknande bilder\nmain_notebook_similar_videos = Liknande videor\nmain_notebook_same_music = Musik Duplicerar\nmain_notebook_symlinks = Ogiltiga Symlinks\nmain_notebook_broken_files = Trasiga filer\nmain_notebook_bad_extensions = Dåliga tillägg\nmain_tree_view_column_file_name = Filnamn\nmain_tree_view_column_folder_name = Mappens namn\nmain_tree_view_column_path = Sökväg\nmain_tree_view_column_modification = Senast ändrad\nmain_tree_view_column_size = Storlek\nmain_tree_view_column_similarity = Likhet\nmain_tree_view_column_dimensions = Dimensioner\nmain_tree_view_column_title = Titel\nmain_tree_view_column_artist = Künstler\nmain_tree_view_column_year = År\nmain_tree_view_column_bitrate = Bitratencion\nmain_tree_view_column_length = Längd\nmain_tree_view_column_genre = Genrer\nmain_tree_view_column_symlink_file_name = Symlink filnamn\nmain_tree_view_column_symlink_folder = Symlink mapp\nmain_tree_view_column_destination_path = Målsökvägen\nmain_tree_view_column_type_of_error = Typ av fel\nmain_tree_view_column_current_extension = Nuvarande tillägg\nmain_tree_view_column_proper_extensions = Rätt tillägg\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = Kodek\nmain_label_check_method = Kontrollera metod\nmain_label_hash_type = Hash typ\nmain_label_hash_size = Hashstorlek\nmain_label_size_bytes = Storlek (bytes)\nmain_label_min_size = Min\nmain_label_max_size = Max\nmain_label_shown_files = Antal visade filer\nmain_label_resize_algorithm = Ändra storlek på algoritm\nmain_label_similarity = Similarity{ \" \" }\nmain_check_box_broken_files_audio = Ljud\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Arkiv\nmain_check_box_broken_files_image = Bild\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = Använder ffmpeg/ffprobe för att validera videofiler. Ganska långsam och kan detektera pedantiska fel även om filen spelas fint.\ncheck_button_general_same_size = Ignorera samma storlek\ncheck_button_general_same_size_tooltip = Ignorera filer med samma storlek i resultat - vanligtvis är dessa 1:1 dubbletter\nmain_label_size_bytes_tooltip = Storlek på filer som kommer att användas vid skanning\n# Upper window\nupper_tree_view_included_folder_column_title = Mappar att söka\nupper_tree_view_included_reference_column_title = Referens mappar\nupper_recursive_button = Rekursiv\nupper_recursive_button_tooltip = Om vald, sök även efter filer som inte placeras direkt under valda mappar.\nupper_manual_add_included_button = Manuell Lägg till\nupper_add_included_button = Lägg till\nupper_remove_included_button = Ta bort\nupper_manual_add_excluded_button = Manuell Lägg till\nupper_add_excluded_button = Lägg till\nupper_remove_excluded_button = Ta bort\nupper_manual_add_included_button_tooltip =\n    Lägg till katalognamn för att söka för hand.\n    \n    För att lägga till flera sökvägar samtidigt, separera dem med ;\n    \n    /home/roman;/home/rozkaz lägger till två kataloger /home/roman och /home/rozkaz\nupper_add_included_button_tooltip = Lägg till ny katalog att söka.\nupper_remove_included_button_tooltip = Ta bort katalog från sökning.\nupper_manual_add_excluded_button_tooltip =\n    Lägg till exkluderat katalognamn för hand.\n    \n    För att lägga till flera sökvägar samtidigt, separera dem med ;\n    \n    /home/roman;/home/krokiet kommer att lägga till två kataloger /home/roman och /home/keokiet\nupper_add_excluded_button_tooltip = Lägg till katalog som ska exkluderas i sökningen.\nupper_remove_excluded_button_tooltip = Ta bort katalog från utesluten.\nupper_notebook_items_configuration = Objekt konfiguration\nupper_notebook_excluded_directories = Exkluderade Sökvägar\nupper_notebook_included_directories = Inkluderade Sökvägar\nupper_allowed_extensions_tooltip =\n    Tillåtna tillägg måste separeras med kommatecken (som standard alla är tillgängliga).\n    \n    Följande makron som lägger till flera tillägg samtidigt, finns också: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Användningsexempel \".exe, IMAGE, VIDEO, .rar, 7z\" - det betyder att bilder (e. . jpg, png), videor (t.ex. avi, mp4), exe, rar, och 7z filer kommer att skannas.\nupper_excluded_extensions_tooltip =\n    Lista över inaktiverade filer som kommer att ignoreras i skanning.\n    \n    Vid användning av både tillåtna och inaktiverade tillägg har denna högre prioritet, så filen kommer inte att kontrolleras.\nupper_excluded_items_tooltip = \n        Uteslutna objekt måste innehålla * wildcard och ska separeras med komma.\n        Detta är långsammare än Exkluderade Sökvägar, så använd det försiktigt.\nupper_excluded_items = Exkluderade objekt:\nupper_allowed_extensions = Tillåtna tillägg:\nupper_excluded_extensions = Inaktiverade tillägg:\n# Popovers\npopover_select_all = Radera\npopover_unselect_all = Avmarkera alla\npopover_reverse = Omvänd markering\npopover_select_all_except_shortest_path = Välj allt förutom den kortaste vägen\npopover_select_all_except_longest_path = Välj allt undantaget längst väg\npopover_select_all_except_oldest = Välj alla utom äldsta\npopover_select_all_except_newest = Välj alla utom nyaste\npopover_select_one_oldest = Välj en äldsta\npopover_select_one_newest = Välj en nyaste\npopover_select_custom = Välj anpassad\npopover_unselect_custom = Avmarkera anpassade\npopover_select_all_images_except_biggest = Välj alla utom största\npopover_select_all_images_except_smallest = Välj alla utom minsta\npopover_custom_path_check_button_entry_tooltip =\n    Välj poster efter sökväg.\n    \n    Exempel användning:\n    /home/pimpek/rzecz.txt hittas med /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Välj poster efter filnamn.\n    \n    Exempel användning:\n    /usr/ping/pong.txt finns med *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Välj poster efter specificerad Regex.\n    \n    Med detta läge är sökord sökväg med namn.\n    \n    Exempel användning:\n    /usr/bin/ziemniak. xt kan hittas med /ziem[a-z]+\n    \n    Detta använder Rust regex-implementationen. Du kan läsa mer om det här: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Aktiverar skiftlägeskänslig detektion.\n    \n    När du inaktiverat /home/* hittar du både /HoMe/roman och /home/roman.\npopover_custom_not_all_check_button_tooltip =\n    Förhindrar att alla poster väljs i grupp.\n    \n    Detta är aktiverat som standard, eftersom i de flesta situationer, du inte vill ta bort både original och dubbletter filer, men vill lämna minst en fil.\n    \n    VARNING: Den här inställningen fungerar inte om du redan manuellt har valt alla resultat i en grupp.\npopover_custom_regex_path_label = Sökväg\npopover_custom_regex_name_label = Namn\npopover_custom_regex_regex_label = Regex sökväg + namn\npopover_custom_case_sensitive_check_button = Skiftlägeskänslighet\npopover_custom_all_in_group_label = Välj inte alla poster i gruppen\npopover_custom_mode_unselect = Avmarkera anpassad\npopover_custom_mode_select = Välj anpassad\npopover_sort_file_name = Filnamn\npopover_sort_folder_name = Mapp namn\npopover_sort_full_name = Fullständigt namn\npopover_sort_size = Storlek\npopover_sort_selection = Markerat\npopover_invalid_regex = Regex är ogiltigt\npopover_valid_regex = Regex är giltigt\n# Bottom buttons\nbottom_search_button = Sökning\nbottom_select_button = Välj\nbottom_delete_button = Radera\nbottom_save_button = Spara\nbottom_symlink_button = Symlink\nbottom_hardlink_button = Hardlink\nbottom_move_button = Flytta\nbottom_sort_button = Sortera\nbottom_compare_button = Jämför\nbottom_search_button_tooltip = Starta sökning\nbottom_select_button_tooltip = Välj poster. Endast valda filer/mappar kan senare bearbetas.\nbottom_delete_button_tooltip = Ta bort markerade filer/mappar.\nbottom_save_button_tooltip = Spara data om sökning till fil\nbottom_symlink_button_tooltip =\n    Skapa symboliska länkar.\n    Fungerar endast när minst två resultat i en grupp väljs.\n    Först är oförändrad och andra och senare är symanknutna till först.\nbottom_hardlink_button_tooltip =\n    Skapa hardlinks.\n    Fungerar endast när minst två resultat i en grupp är valda.\n    Först är oförändrad och andra och senare är hårt länkade till först.\nbottom_hardlink_button_not_available_tooltip =\n    Skapa hardlinks.\n    Knappen är inaktiverad, eftersom hardlinks inte kan skapas.\n    Hårdlänkar fungerar bara med administratörsrättigheter i Windows, så se till att köra appen som administratör.\n    Om appen redan fungerar med sådana rättigheter kontrollera liknande problem på Github.\nbottom_move_button_tooltip =\n    Flyttar filer till vald katalog.\n    Det kopierar alla filer till katalogen utan att bevara katalogträdet.\n    När du försöker flytta två filer med identiskt namn till mappen kommer det andra att misslyckas och visa fel.\nbottom_sort_button_tooltip = Sortera filer/mappar enligt vald metod.\nbottom_compare_button_tooltip = Jämför bilder i gruppen.\nbottom_show_errors_tooltip = Visa/Dölj undertextpanelen.\nbottom_show_upper_notebook_tooltip = Visa/Dölj övre anteckningsbokspanelen.\n# Progress Window\nprogress_stop_button = Stoppa\nprogress_stop_additional_message = Stoppa begärd\n# About Window\nabout_repository_button_tooltip = Länk till utvecklingskatalogen med källkod.\nabout_donation_button_tooltip = Länk till donationssidan.\nabout_instruction_button_tooltip = Länk till instruktionssidan.\nabout_translation_button_tooltip = Länk till Crowdin sida med appöversättningar. Officiellt stöds polska och engelska.\nabout_repository_button = Filförråd\nabout_donation_button = Donationer\nabout_instruction_button = Instruktion\nabout_translation_button = Översättning\n# Header\nheader_setting_button_tooltip = Öppnar dialogrutan för inställningar.\nheader_about_button_tooltip = Öppnar dialog med info om app.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Antal använda trådar\nsettings_number_of_threads_tooltip = Antal gängor, 0 betyder att alla gängor kommer att användas.\nsettings_use_rust_preview = Använd externa bibliotek istället gtk för att ladda förhandsvisningar\nsettings_use_rust_preview_tooltip =\n    Att använda gtk-förhandsvisningar kommer ibland att vara snabbare och stödja fler format, men ibland kan det vara precis tvärtom.\n    \n    Om du har problem med att ladda förhandsvisningar, kan du försöka ändra den här inställningen.\n    \n    På icke-Linux-system rekommenderas att använda detta alternativ, eftersom gtk-pixbuf inte alltid är tillgänglig där så inaktivera detta alternativ kommer inte att ladda förhandsvisningar av vissa bilder.\nsettings_label_restart = Du måste starta om appen för att tillämpa inställningar!\nsettings_ignore_other_filesystems = Ignorera andra filsystem (endast Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ignorerar filer som inte finns i samma filsystem som sökta kataloger.\n    \n    Fungerar samma som -xdev alternativ för att hitta kommandot på Linux\nsettings_save_at_exit_button_tooltip = Spara konfigurationen till fil när appen stängs.\nsettings_load_at_start_button_tooltip =\n    Ladda konfigurationen från filen när appen öppnas.\n    \n    Om den inte är aktiverad kommer standardinställningarna att användas.\nsettings_confirm_deletion_button_tooltip = Visa bekräftelsedialog när du klickar på knappen ta bort.\nsettings_confirm_link_button_tooltip = Visa bekräftelsedialog när du klickar på den hårda/symboliska länkknappen.\nsettings_confirm_group_deletion_button_tooltip = Visa varningsdialog när du försöker ta bort alla poster från gruppen.\nsettings_show_text_view_button_tooltip = Visa textpanelen längst ner i användargränssnittet.\nsettings_use_cache_button_tooltip = Använd filcache.\nsettings_save_also_as_json_button_tooltip = Spara cache till (läsbar) JSON-format. Det är möjligt att ändra dess innehåll. Cache från denna fil kommer att läsas automatiskt av appen om binärt format cache (med bin extension) saknas.\nsettings_use_trash_button_tooltip = Flyttar filer till papperskorgen istället ta bort dem permanent.\nsettings_language_label_tooltip = Språk för användargränssnitt.\nsettings_save_at_exit_button = Spara konfiguration när appen stängs\nsettings_load_at_start_button = Ladda konfiguration när appen öppnas\nsettings_confirm_deletion_button = Visa bekräftelsedialog vid borttagning av filer\nsettings_confirm_link_button = Visa bekräftelsedialog när hårda/symboliska länkar filer\nsettings_confirm_group_deletion_button = Visa bekräftelsedialog när alla filer tas bort i grupp\nsettings_show_text_view_button = Visa längst ned textpanel\nsettings_use_cache_button = Använd cache\nsettings_save_also_as_json_button = Spara även cache som JSON-fil\nsettings_use_trash_button = Flytta raderade filer till papperskorgen\nsettings_language_label = Språk\nsettings_multiple_delete_outdated_cache_checkbutton = Ta bort föråldrade cache-poster automatiskt\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Ta bort föråldrade cacheresultat som pekar på obefintliga filer.\n    \n    När den är aktiverad, se till att appen när du laddar poster, att alla poster pekar på giltiga filer (trasiga dem ignoreras).\n    \n    Att inaktivera detta kommer att hjälpa när du skannar filer på externa enheter, så cacheposter om dem kommer inte att rensas i nästa skanning.\n    \n    När det gäller att ha hundratusentals poster i cache, det föreslås för att aktivera detta, vilket kommer att påskynda cache-inläsning/spara vid start/slut av sökningen.\nsettings_notebook_general = Info\nsettings_notebook_duplicates = Dubbletter\nsettings_notebook_images = Liknande bilder\nsettings_notebook_videos = Liknande video\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Visar förhandsgranskning på höger sida (vid val av bildfil).\nsettings_multiple_image_preview_checkbutton = Visa förhandsgranskning av bild\nsettings_multiple_clear_cache_button_tooltip =\n    Rensa cache manuellt för föråldrade poster.\n    Detta bör endast användas om automatisk rensning har inaktiverats.\nsettings_multiple_clear_cache_button = Ta bort föråldrade resultat från cachen.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Döljer alla filer utom en, om alla pekar på samma data (är hardlinked).\n    \n    Exempel: I det fall där det finns (på disk) sju filer som är hårdkopplade till specifika data och en annan fil med samma data men ett annat inode, i dubblettsökare, kommer endast en unik fil och en fil från hårdlänkade att visas.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Ange minimal filstorlek som kommer att cachelagras.\n    \n    Att välja ett mindre värde kommer att generera fler poster. Detta kommer att snabba upp sökningen, men bromsa cache-laddning/spara.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Aktiverar cachelagring av prehash (en hash beräknad från en liten del av filen) vilket tillåter tidigare avfärdande av icke-duplicerade resultat.\n    \n    Det är inaktiverat som standard eftersom det kan orsaka nedgångar i vissa situationer.\n    \n    Det rekommenderas starkt att använda det när du skannar hundratusentals eller miljoner filer, eftersom det kan påskynda sökningen flera gånger.\nsettings_duplicates_prehash_minimal_entry_tooltip = Minimal storlek på cachad post.\nsettings_duplicates_hide_hard_link_button = Dölj hårda länkar\nsettings_duplicates_prehash_checkbutton = Använd prehash cache\nsettings_duplicates_minimal_size_cache_label = Minimal storlek på filer (i bytes) sparade i cache\nsettings_duplicates_minimal_size_cache_prehash_label = Minimal storlek på filer (i bytes) sparade för att kunna använda cache\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Spara konfigurationen för nuvarande inställningar till filen.\nsettings_loading_button_tooltip = Ladda inställningar från fil och ersätta den aktuella konfigurationen med dem.\nsettings_reset_button_tooltip = Återställ den aktuella konfigurationen till standardkonfigurationen.\nsettings_saving_button = Spara konfiguration\nsettings_loading_button = Ladda konfiguration\nsettings_reset_button = Återställ konfiguration\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Öppnar mappen där cache-txt-filer lagras.\n    \n    Ändring av cache-filer kan leda till att ogiltiga resultat visas. Dock kan ändra sökvägen spara tid när du flyttar en stor mängd filer till en annan plats.\n    \n    Du kan kopiera dessa filer mellan datorer för att spara tid på skanning igen för filer (naturligtvis om de har liknande katalogstruktur).\n    \n    Vid problem med cachen kan dessa filer tas bort. Appen kommer automatiskt att regenerera dem.\nsettings_folder_settings_open_tooltip =\n    Öppnar mappen där Czkawka-konfigurationen lagras.\n    \n    VARNING: Manuellt modifierande av konfigurationen kan bryta ditt arbetsflöde.\nsettings_folder_cache_open = Öppna cachemapp\nsettings_folder_settings_open = Öppna inställningsmapp\n# Compute results\ncompute_stopped_by_user = Sökandet stoppades av användaren\ncompute_found_duplicates_hash_size = Hittade { $number_files } dubbletter i { $number_groups } grupper som tog { $size } i { $time }\ncompute_found_duplicates_name = Hittade { $number_files } dubbletter i { $number_groups } grupper i { $time }\ncompute_found_empty_folders = Hittade { $number_files } tomma mappar i { $time }\ncompute_found_empty_files = Hittade { $number_files } tomma filer i { $time }\ncompute_found_big_files = Hittade { $number_files } stora filer i { $time }\ncompute_found_temporary_files = Hittade { $number_files } temporära filer i { $time }\ncompute_found_images = Hittade { $number_files } liknande bilder i { $number_groups } grupper i { $time }\ncompute_found_videos = Hittade { $number_files } liknande videor i { $number_groups } grupper i { $time }\ncompute_found_music = Hittade { $number_files } liknande musikfiler i { $number_groups } grupper i { $time }\ncompute_found_invalid_symlinks = Hittade { $number_files } ogiltiga symlänkar i { $time }\ncompute_found_broken_files = Hittade { $number_files } trasiga filer i { $time }\ncompute_found_bad_extensions = Hittade { $number_files } filer med ogiltiga tillägg i { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] skannade { $file_number } fil\n       *[other] skannade { $file_number } filer\n    }\nprogress_scanning_extension_of_files = Kontrollerad förlängning av { $file_checked }/{ $all_files } fil\nprogress_scanning_broken_files = Kontrollerade { $file_checked }/{ $all_files } fil ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hashad av { $file_checked }/{ $all_files } video\nprogress_creating_video_thumbnails = Skapade miniatyrbilder av { $file_checked }/{ $all_files } video\nprogress_scanning_image = Hashad av { $file_checked }/{ $all_files } bild ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Jämfört { $file_checked }/{ $all_files } bildhash\nprogress_scanning_music_tags_end = Jämförda taggar av { $file_checked }/{ $all_files } musikfil\nprogress_scanning_music_tags = Läs taggar för { $file_checked }/{ $all_files } musikfil\nprogress_scanning_music_content_end = Jämfört fingeravtryck av { $file_checked }/{ $all_files } musikfil\nprogress_scanning_music_content = Beräknat fingeravtryck av { $file_checked }/{ $all_files } musikfil ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] skannade { $folder_number } mapp\n       *[other] skannade { $folder_number } mappar\n    }\nprogress_scanning_size = Skannad storlek på { $file_number } fil\nprogress_scanning_size_name = Skannat namn och storlek på { $file_number } fil\nprogress_scanning_name = Skannat namn på { $file_number } fil\nprogress_analyzed_partial_hash = Analyserad partiell hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Analyserad full hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Laddar prehash cache\nprogress_prehash_cache_saving = Sparar Omfattande cache\nprogress_hash_cache_loading = Laddar hash-cache\nprogress_hash_cache_saving = Sparar hash-cache\nprogress_cache_loading = Laddar cache\nprogress_cache_saving = Sparar cache\nprogress_current_stage = Nuvarande steg:{ \" \" }\nprogress_all_stages = Alla etapper:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = Sparad konfiguration till filen { $name }.\nsaving_loading_saving_failure = Det gick inte att spara konfigurationsdata till filen { $name }, anledningen { $reason }.\nsaving_loading_reset_configuration = Aktuell konfiguration har rensats.\nsaving_loading_loading_success = Korrekt laddad app-konfiguration.\nsaving_loading_failed_to_create_config_file = Det gick inte att skapa konfigurationsfil \"{ $path }\", orsak \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Kan inte ladda konfiguration från \"{ $path }\" eftersom den inte finns eller inte är en fil.\nsaving_loading_failed_to_read_data_from_file = Kan inte läsa data från fil \"{ $path }\", anledning \"{ $reason }\".\n# Other\nselected_all_reference_folders = Kan inte börja söka, när alla kataloger är inställda som referensmappar\nsearching_for_data = Söker data, det kan ta en stund, vänta...\ntext_view_messages = MEDDELANDEN\ntext_view_warnings = VARNINGAR\ntext_view_errors = FEL\nabout_window_motto = Detta program är gratis att använda och kommer alltid att vara.\nkrokiet_new_app = Czkawka är i underhållsläge, vilket innebär att endast kritiska fel kommer att rättas och inga nya funktioner kommer att läggas till. För nya funktioner, vänligen kolla in den nya Krokiet-appen, som är mer stabil och presterande och som fortfarande är under aktiv utveckling.\n# Various dialog\ndialogs_ask_next_time = Fråga nästa gång\nsymlink_failed = Misslyckades att symbolisk länk { $name } till { $target }, anledning { $reason }\ndelete_title_dialog = Ta bort bekräftelse\ndelete_question_label = Är du säker på att du vill ta bort filer?\ndelete_all_files_in_group_title = Bekräftelse av att ta bort alla filer i grupp\ndelete_all_files_in_group_label1 = I vissa grupper är alla poster valda.\ndelete_all_files_in_group_label2 = Är du säker på att du vill radera dem?\ndelete_items_label = { $items } filer kommer att tas bort.\ndelete_items_groups_label = { $items } filer från { $groups } grupper kommer att raderas.\nhardlink_failed = Det gick inte att hardlink { $name } till { $target }, varför { $reason }\nhard_sym_invalid_selection_title_dialog = Ogiltigt val med vissa grupper\nhard_sym_invalid_selection_label_1 = I vissa grupper finns det bara en post vald och den kommer att ignoreras.\nhard_sym_invalid_selection_label_2 = För att kunna länka dessa filer måste minst två resultat i gruppen väljas.\nhard_sym_invalid_selection_label_3 = Först i grupp känns igen som original och ändras inte, men andra och senare ändras.\nhard_sym_link_title_dialog = Länkbekräftelse\nhard_sym_link_label = Är du säker på att du vill länka dessa filer?\nmove_folder_failed = Det gick inte att flytta mappen { $name } anledning { $reason }\nmove_file_failed = Det gick inte att flytta filen { $name } anledning { $reason }\nmove_files_title_dialog = Välj mapp som du vill flytta duplicerade filer till\nmove_files_choose_more_than_1_path = Endast en sökväg kan väljas för att kunna kopiera sina duplicerade filer, valda { $path_number }.\nmove_stats = Korrekt flyttad { $num_files }/{ $all_files } objekt\nsave_results_to_file = Sparade resultat både till txt och json filer i \"{ $name }\" mapp.\nsearch_not_choosing_any_music = FEL: Du måste välja minst en kryssruta med söktyper för musik.\nsearch_not_choosing_any_broken_files = FEL: Du måste välja minst en kryssruta med typ av markerade trasiga filer.\ninclude_folders_dialog_title = Mappar att inkludera\nexclude_folders_dialog_title = Mappar att exkludera\ninclude_manually_directories_dialog_title = Lägg till katalog manuellt\ncache_properly_cleared = Rensad cache\ncache_clear_duplicates_title = Rensar dubbletter cache\ncache_clear_similar_images_title = Rensar liknande bildcache\ncache_clear_similar_videos_title = Rensar liknande videoklipp cache\ncache_clear_message_label_1 = Vill du rensa cachen för föråldrade inlägg?\ncache_clear_message_label_2 = Denna åtgärd kommer att ta bort alla cache-poster som pekar på ogiltiga filer.\ncache_clear_message_label_3 = Detta kan något speedup ladda/spara till cache.\ncache_clear_message_label_4 = VARNING: Åtgärden kommer att ta bort alla cachade data från frånkopplade externa enheter. Så varje hash kommer att behöva regenereras.\n# Show preview\npreview_image_resize_failure = Kunde inte ändra storlek på bild { $name }.\npreview_image_opening_failure = Det gick inte att öppna bilden { $name } skäl { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Grupp { $current_group }/{ $all_groups } ({ $images_in_group } bilder)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/tr/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Ayarlar\nwindow_main_title = Czkawka (Hıçkırık)\nwindow_progress_title = Taranıyor\nwindow_compare_images = Resimleri Karşılaştır\n# General\ngeneral_ok_button = Tamam\ngeneral_close_button = Kapat\n# Krokiet info dialog\nkrokiet_info_title = Krokiet - Yeni versiyon Czkawka\nkrokiet_info_message = \n        Krokiet, Czkawka GTK GUI’nin yeni, geliştirilmiş, daha hızlı ve daha güvenilir versiyonudur!\n\n        Çalıştırması daha kolay ve sistem değişikliklerine karşı daha dirençlidir, çünkü çoğu sistemde varsayılan olarak bulunan temel kütüphanelere dayanır.\n\n        Krokiet ayrıca, Czkawka’da bulunmayan özellikler getirir, örneğin video karşılaştırma modunda alıştırmalar, bir EXIF temizleyici, dosya taşıma/kopyalama/silme ilerleme veya gelişmiş sıralama seçenekleri.\n\n        Deneyin ve farkı görün!\n\n        Czkawka’nın benim tarafımdan düzeltmeler ve küçük güncellemeler almaya devam etmesi muhtemeldir, ancak tüm yeni özellikler yalnızca Krokiet için geliştirilecek ve herkes yeni özellikler eklemek, eksik modları tamamlamak veya Czkawka’yı daha da genişletmekten serbestçe yararlanabilir.\n\n        PS: Bu mesaj yalnızca bir kez görünmelidir. Tekrar gösteriliyorsa, CZKAWKA_DONT_ANNOY_ME ortam değişkenini herhangi bir boş olmayan değere ayarlayın.\n# Main window\nmusic_title_checkbox = Başlık\nmusic_artist_checkbox = Sanatçı\nmusic_year_checkbox = Yıl\nmusic_bitrate_checkbox = Bit-hızı\nmusic_genre_checkbox = Müzik Türü\nmusic_length_checkbox = Uzunluk\nmusic_comparison_checkbox = Yaklaşık Karşılaştırma\nmusic_checking_by_tags = Etiketler\nmusic_checking_by_content = İçerik\nsame_music_seconds_label = Minimal parça saniyesel süresi\nsame_music_similarity_label = Maksimum fark\nmusic_compare_only_in_title_group = Benzer başlıklı gruplar içinde karşılaştırın\nmusic_compare_only_in_title_group_tooltip =\n    Etkinleştirildiğinde dosyalar başlığa göre gruplandırılır ve ardından birbirleriyle karşılaştırılır.\n    \n    10.000 dosya ile neredeyse 100 milyon karşılaştırma yerine genellikle 20.000 civarında karşılaştırma olacaktır.\nsame_music_tooltip =\n    İçeriğine göre benzer müzik dosyalarının aranması ayarlanarak yapılandırılabilir:\n    \n    - Müzik dosyalarının benzer olarak tanımlanabileceği minimum parça süresi\n    - Test edilen iki parça arasındaki maksimum fark\n    \n    İyi sonuçlar elde etmenin anahtarı, bu parametrelerin mantıklı kombinasyonlarını bulmaktır.\n    \n    Minimum süreyi 5 saniye ve maksimum farkı 1.0 olarak ayarlamak, dosyalarda neredeyse aynı parçaları arayacaktır.\n    Öte yandan, 20 saniyelik bir süre ve 6.0'lık bir maksimum fark, remiksleri / canlı sürümleri vb. bulmak için iyi çalışır.\n    \n    Varsayılan olarak, her müzik dosyası birbiriyle karşılaştırılır ve çok sayıda dosyayı test ederken bu çok zaman alabilir, bu nedenle genellikle referans klasörleri kullanmak ve hangi dosyaların birbiriyle karşılaştırılacağını belirtmek daha iyidir (aynı miktarda dosya ile, parmak izlerini karşılaştırmak referans klasörleri olmadan en az 4 kat daha hızlı olacaktır).\nmusic_comparison_checkbox_tooltip =\n    Yapay zeka kullanarak benzer müzik dosyalarını arar. \n    Örneğin, bir tümcenin parantezlerini kaldırmak için makine öğrenimini kullanır. \n    Bu seçenek etkinleştirildiğinde, söz konusu dosyalar kopya olarak kabul edilecektir:\n    \n    Geççek <--> Geççek (Tarkan 2022)\nduplicate_case_sensitive_name = Büyük/Küçük harfe Duyarlı\nduplicate_case_sensitive_name_tooltip =\n    Etkinleştirilse, dosya adları tam olarak aynı olduğunda eşleştirilir\n    ve bir grup oluşturulur.\n    \n    fatih.kavalci <--> fatih.kavalci\n    \n    Etkisizleştirilirse, her bir harfin büyük/küçük yazılıp yazılmadığını \n    denetlemeden aynı adları eşleyip grup oluşturur.\n    \n    fatih.kavalci <--> FatiH.KaVaLCi\nduplicate_mode_size_name_combo_box = Boyut ve Ad Karşılaştırma\nduplicate_mode_name_combo_box = Ad Karşılaştırma\nduplicate_mode_size_combo_box = Boyut Karşılaştırma\nduplicate_mode_hash_combo_box = Hash\nduplicate_hash_type_tooltip =\n    Czkawka, 3 tür Sabit Uzunlukta Çıktı (SUÇ) üretimi sunar:\n    \n    Blake3 - kriptografik SUÇ üretim işlevi. Bu varsayılandır çünkü çok hızlıdır.\n    \n    CRC32 - basit SUÇ üretim işlevi. Bu, Blake3'ten daha hızlı olmalıdır, \n    ancak kimi zaman çakışmalar olabilir.\n    \n    XXH3 - performans ve benzersiz SUÇ üretim kalitesi açısından Blake3'e çok benzer \n    (ancak kriptografik değildir). Böylece, bu tür modlar kolayca değiştirilebilir.\nduplicate_check_method_tooltip =\n    Czkawka, eş dosyaları bulmak için şimdilik üç tür yöntem sunar:\n    \n    Ad Karşılaştırma - Aynı ada sahip dosyaları bulur.\n    \n    Boyut Karşılaştırma - Aynı boyuta sahip dosyaları bulur.\n    \n    Hash (SUÇ) Karşılaştırma - Aynı içeriğe sahip dosyaları bulur. Bu mod her dosya için \n    veri analizi sonucu sabit uzunlukta benzersiz birer çıktı üretir ve daha sonra eş doşyaları \n    bulmak için bu çıktıları karşılaştırır. Bu mod, eş dosyaları bulmanın en güvenli yoludur. \n    Czkawka, önbelleği yoğun olarak kullanır. Bu nedenle aynı verilerin ikinci ve sonraki taramaları \n    ilkinden çok daha hızlı olmalıdır.\nimage_hash_size_tooltip =\n    Kontrol edilen her resim, birbiriyle karşılaştırılabilen özel bir hash üretir ve aralarındaki küçük bir fark, bu görüntülerin benzer olduğu anlamına gelir.\n    \n    8 hash boyutu, orijinaline çok az benzeyen görüntüleri bulmak için oldukça iyidir. Daha büyük bir görüntü kümesinde (>1000), bu büyük miktarda yanlış pozitif üretecektir, bu nedenle bu durumda daha büyük bir karma boyutu kullanmanızı öneririm.\n    \n    16 varsayılan hash boyutudur ve az da olsa benzer resimler bulmakla az miktarda hash çakışması olması arasında oldukça iyi bir uzlaşmadır.\n    \n    32 ve 64 hash'ler yalnızca çok benzer görüntüleri bulur, ancak neredeyse hiç piksel farkı olmamalıdır (belki alfa kanallı bazı görüntüler hariç).\nimage_resize_filter_tooltip =\n    Görüntünün hash'ini hesaplamak için kütüphanenin önce görüntüyü yeniden boyutlandırması gerekir.\n    \n    Seçilen algoritmaya bağlı olarak, hash hesaplamak için kullanılan sonuç görüntüsü biraz farklı görünecektir.\n    \n    Kullanılacak en hızlı ve aynı zamanda en kötü sonuçları veren algoritma Nearest'tir. Varsayılan olarak etkindir, çünkü 16x16 hash boyutunda daha düşük kalitede gerçekten görünmez.\n    \n    8x8 karma boyutunda, daha iyi görüntü grupları elde etmek için Nearest'ten farklı bir algoritma kullanılması önerilir.\nimage_hash_alg_tooltip =\n    Kullanıcılar, SUÇ oluşturmanın birçok algoritmasından birini seçebilir. \n    Her birinin hem güçlü hem de zayıf noktaları vardır ve farklı görüntüler için \n    bazen daha iyi, bazen daha kötü sonuçlar verir. Bu nedenle, size göre en iyisini belirlemek için \n    elle test gereklidir.\nbig_files_mode_combobox_tooltip = Boyut bakımından En Büyük/En Küçük dosyaları aramaya izin verir\nbig_files_mode_label = Denetim şekli\nbig_files_mode_smallest_combo_box = En Küçük\nbig_files_mode_biggest_combo_box = En Büyük\nmain_notebook_duplicates = Eş Dosyalar\nmain_notebook_empty_directories = Boş Dizinler\nmain_notebook_big_files = Büyük/Küçük Dosyalar\nmain_notebook_empty_files = Boş Dosyalar\nmain_notebook_temporary = Geçici Dosyalar\nmain_notebook_similar_images = Benzer Resimler\nmain_notebook_similar_videos = Benzer Videolar\nmain_notebook_same_music = Müzik Kopyaları\nmain_notebook_symlinks = Geçersiz Sembolik Bağlar\nmain_notebook_broken_files = Bozuk Dosyalar\nmain_notebook_bad_extensions = Hatalı Uzantılar\nmain_tree_view_column_file_name = Dosya Adı\nmain_tree_view_column_folder_name = Klasör Adı\nmain_tree_view_column_path = Yol\nmain_tree_view_column_modification = Düzenleme Tarihi\nmain_tree_view_column_size = Boyut\nmain_tree_view_column_similarity = Benzerlik\nmain_tree_view_column_dimensions = En x Boy\nmain_tree_view_column_title = Başlık\nmain_tree_view_column_artist = Sanatçı\nmain_tree_view_column_year = Yıl\nmain_tree_view_column_bitrate = Bit-hızı\nmain_tree_view_column_length = Uzunluk\nmain_tree_view_column_genre = Tür\nmain_tree_view_column_symlink_file_name = Sembolik Bağ Dosyası Adı\nmain_tree_view_column_symlink_folder = Sembolik Bağlantı Klasörü\nmain_tree_view_column_destination_path = Hedef Yol\nmain_tree_view_column_type_of_error = Hata türü\nmain_tree_view_column_current_extension = Geçerli Uzantı\nmain_tree_view_column_proper_extensions = Uygun Uzantı\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = KodçTürkçe: Kodç\nmain_label_check_method = Denetim yöntemi:\nmain_label_hash_type = SUÇ türü:\nmain_label_hash_size = SURÇ boyutu:\nmain_label_size_bytes = Boyut (bayt):\nmain_label_min_size = Min\nmain_label_max_size = Maks\nmain_label_shown_files = Gösterilecek Dosya Sayısı:\nmain_label_resize_algorithm = Yeniden boyutlandırma algoritması:\nmain_label_similarity = Benzerlik: { \"   \" }\nmain_check_box_broken_files_audio = Ses\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Arşiv\nmain_check_box_broken_files_image = Resim\nmain_check_box_broken_files_video = Video\nmain_check_box_broken_files_video_tooltip = ffmpeg/ffprobe kullanarak video dosyalarını doğrular. Çok yavaş ve dosyanın düzgün çalışmasına rağmen katı hataları bile tespit edebilir.\ncheck_button_general_same_size = Aynı boyutu yok say\ncheck_button_general_same_size_tooltip = Sonuçlarda aynı boyutta olan dosyaları yoksay - genellikle bunlar bire bir kopyalardır\nmain_label_size_bytes_tooltip = Taramada kullanılacak dosyaların boyutu\n# Upper window\nupper_tree_view_included_folder_column_title = Aranacak Klasörler\nupper_tree_view_included_reference_column_title = Başvuru Klasörleri\nupper_recursive_button = Özyinelemeli\nupper_recursive_button_tooltip = Seçilirse, doğrudan \"Aranacak Klasörler\" listesindeki dizin altında yer almayan (alt dizinlerdeki dosyaları da) arar.\nupper_manual_add_included_button = Dizin Gir\nupper_add_included_button = Ekle\nupper_remove_included_button = Kaldır\nupper_manual_add_excluded_button = Dizin Gir\nupper_add_excluded_button = Ekle\nupper_remove_excluded_button = Kaldır\nupper_manual_add_included_button_tooltip = \n    Arama yapılacak dizin yolunu doğrudan yazın.\n    \n    Aynı anda birden fazla girdi eklemek için bunları \";\" ile ayırın.\n    \n    /home/fatih;/home/kavalci girdisi biri /home/fatih öteki /home/kavalci \n    olmak üzere iki dizin ekleyecektir\nupper_add_included_button_tooltip = \"Aranacak Klasörler\" listesine yeni bir dizin ekler.\nupper_remove_included_button_tooltip = Seçili dizini \"Aranacak Klasörler\" listesinden kaldırır.\nupper_manual_add_excluded_button_tooltip = \n    Hariç tutulacak dizin yolunu doğrudan yazın.\n    \n    Aynı anda birden fazla girdi eklemek için bunları \";\" ile ayırın.\n    \n    /home/fatih;/home/kavalci girdisi biri /home/fatih öteki /home/kavalci \n    olmak üzere iki dizin ekleyecektir\nupper_add_excluded_button_tooltip = \"Hariç Tutulacak Klasörler\" listesine yeni bir dizin ekler.\nupper_remove_excluded_button_tooltip = Seçili dizini \"Hariç Tutulacak Klasörler\" listesinden kaldırır.\nupper_notebook_items_configuration = Öğe Yapılandırması\nupper_notebook_excluded_directories = Hariç Tutulan Yollar\nupper_notebook_included_directories = Dahil Edilen Yollar\nupper_allowed_extensions_tooltip =\n    İzin verilen uzantılar virgülle ayrılmalıdır (varsayılan olarak her uzantı kullanılır).\n    \n    Aynı anda birden fazla (aynı tür) uzantı ekleyen makrolar da kullanılabilir: IMAGE, VIDEO, MUSIC, TEXT.\n    \n    Kullanım örneği: \".exe, IMAGE, VIDEO, .rar, .7z\" -- Bu girdi, resimlerin (ör. jpg, png ...), \n    videoların (ör. avi, mp4 ...), exe, rar ve 7z dosyalarının taranacağı anlamına gelir.\nupper_excluded_extensions_tooltip =\n    Taramada göz ardı edilecek devre dışı bırakılmış dosyaların listesi.\n    \n    İzin verilen ve devre dışı bırakılan uzantılar kullanıldığında, bu daha yüksek önceliğe sahiptir, bu nedenle dosya kontrol edilmeyecektir.\nupper_excluded_items_tooltip = \n        Hariçlanan öğeler * joker karakterini içermeli ve virgülle ayrılmalıdır.\n        Bu, Hariç Yollar'dan daha yavaştır, bu nedenle dikkatli kullanılmalıdır.\nupper_excluded_items = Hariç Tutulan Öğeler:\nupper_allowed_extensions = İzin Verilen Uzantılar:\nupper_excluded_extensions = Devre Dışı Uzantılar:\n# Popovers\npopover_select_all = Tümünü seç\npopover_unselect_all = Tümünün seçimini kaldır\npopover_reverse = Seçimi Ters Çevir\npopover_select_all_except_shortest_path = Tümünü seç hariç en kısa yolu seç\npopover_select_all_except_longest_path = Tümünü seç hariç en uzun yolu seç\npopover_select_all_except_oldest = En eski olan hariç hepsini seç\npopover_select_all_except_newest = En yeni olan hariç hepsini seç\npopover_select_one_oldest = En eski olanı seç\npopover_select_one_newest = En yeni olanı seç\npopover_select_custom = Özel girdi ile seçim yap\npopover_unselect_custom = Özel girdi ile seçimi kaldır\npopover_select_all_images_except_biggest = En büyük olan hariç hepsini seç\npopover_select_all_images_except_smallest = En küçük olan hariç hepsini seç\npopover_custom_path_check_button_entry_tooltip =\n    Kayıtları, kısmi yol girdisine göre seçer.\n    \n    Örnek kullanım:\n    /home/fatih/kavalci.txt dosyası, /home/fat* girdisi ile bulunabilir\npopover_custom_name_check_button_entry_tooltip =\n    Kayıtları, kısmi dosya adı girdisine göre seçer.\n    \n    Örnek kullanım:\n    /home/fatih/kavalci.txt dosyası, *val* girdisi ile bulunabilir\npopover_custom_regex_check_button_entry_tooltip =\n    Kayıtları, belirtilen Regex girdisine göre seçer.\n    \n    Bu mod ile aranan metin, tam yol dosya adıdır.\n    \n    Örnek kullanım:\n    /home/fatih/kavalcı.txt dosyası, h/ka[a-z]+ ile bulunabilir\n    \n    Bu işlev, varsayılan Rust regex uygulamasını kullanır. \n    Daha fazla bilgi için bakınız: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Büyük/Küçük harfe duyarlı algılamayı etkinleştirir.\n    \n    Etkisizleştirilir ise;\n    /home/fatih/* girdisi, hem /home/fatih/ hem de /home/FaTiH dizinlerini algılar.\npopover_custom_not_all_check_button_tooltip =\n    Gruptaki tüm kayıtların seçilmesini engeller.\n    \n    Bu varsayılan olarak etkindir. Çünkü, çoğu durumda hem asıl dosyayı hem de kopyaları \n    silmek istemezsiniz. En az bir dosya bırakmak istersiniz.\n    \n    UYARI: Bir gruptaki tüm sonuçlar zaten elle seçilmiş ise bu ayar çalışmaz.\npopover_custom_regex_path_label = Yol\npopover_custom_regex_name_label = Ad\npopover_custom_regex_regex_label = Regex Yolu + Adı\npopover_custom_case_sensitive_check_button = Büyük/Küçük harfe duyarlı\npopover_custom_all_in_group_label = Gruptaki tüm kayıtları seçme\npopover_custom_mode_unselect = Özel Girdi ile Seçimi Kaldır\npopover_custom_mode_select = Özel Girdi ile Seç\npopover_sort_file_name = Dosya adı\npopover_sort_folder_name = Klasör adı\npopover_sort_full_name = Tam ad\npopover_sort_size = Boyut\npopover_sort_selection = Seçim\npopover_invalid_regex = Regex geçersiz (hatalı)\npopover_valid_regex = Regex geçerli (doğru)\n# Bottom buttons\nbottom_search_button = Ara\nbottom_select_button = Seç\nbottom_delete_button = Sil\nbottom_save_button = Kaydet\nbottom_symlink_button = Sembolik bağlantı\nbottom_hardlink_button = Sabit bağlantı\nbottom_move_button = Taşı\nbottom_sort_button = Sırala\nbottom_compare_button = Karşılaştır\nbottom_search_button_tooltip = Aramayı başlatır\nbottom_select_button_tooltip = Kayıtları seçer. Yalnızca seçilen dosyalara/klasörlere işlem uygulanabilir.\nbottom_delete_button_tooltip = Seçili dosyaları/klasörleri siler.\nbottom_save_button_tooltip = Aramayla ilgili verileri dosyaya kaydeder\nbottom_symlink_button_tooltip =\n    Sembolik bağlantılar oluşturur.\n    Yalnızca bir gruptaki en az iki sonuç seçildiğinde çalışır.\n    Birincisi değişmez, ikincisi ve sonrası birinciye sembolik olarak bağlanır.\nbottom_hardlink_button_tooltip =\n    Sabit bağlantılar oluşturur.\n    Yalnızca bir gruptaki en az iki sonuç seçildiğinde çalışır.\n    Birincisi değişmez, ikincisi ve sonrası birinciye sabit olarak bağlanır.\nbottom_hardlink_button_not_available_tooltip =\n    Hardlinkler oluştur.\n    Düğme devre dışı, çünkü hardlinkler oluşturulamaz.\n    Hardlinkler Windows üzerinde yalnızca administrator ayrıcalıklarıyla çalışır, bu yüzden uygulamayı yönetici olarak çalıştırdığınızdan emin olun.\n    Eğer uygulama zaten yeterli ayrıcalıklarla çalışıyorsa Github üzerindeki benzer sorunları gözden geçirin.\nbottom_move_button_tooltip =\n    Dosyaları seçilen dizine taşır.\n    Dizin ağacını korumadan tüm dosyaları dizine taşır.\n    Aynı ada sahip iki dosyayı klasöre taşımaya çalışırken, ikincisi başarısız olur ve hata gösterir.\nbottom_sort_button_tooltip = Dosyaları/Dizinleri seçilen metoda göre sırala.\nbottom_compare_button_tooltip = Gruptaki görüntüleri karşılaştır.\nbottom_show_errors_tooltip = Alt çıktı panelini göster/gizle.\nbottom_show_upper_notebook_tooltip = Üst denetim panelini göster/gizle.\n# Progress Window\nprogress_stop_button = Durdur\nprogress_stop_additional_message = İşlem durduruldu\n# About Window\nabout_repository_button_tooltip = Kaynak kodu depo sayfasına bağlanır.\nabout_donation_button_tooltip = Bağış sayfasına bağlanır.\nabout_instruction_button_tooltip = Kullanım yönergeleri sayfasına bağlanır.\nabout_translation_button_tooltip = Czkawka çevirileriyle Crowdin sayfasına bağlanır. Resmi olarak Lehçe ve İngilizce desteklenmektedir.\nabout_repository_button = Depo\nabout_donation_button = Bağış\nabout_instruction_button = Yönerge\nabout_translation_button = Çeviri\n# Header\nheader_setting_button_tooltip = Ayarlar iletişim kutusunu açar.\nheader_about_button_tooltip = Czkawka hakkında bilgi içeren iletişim kutusunu açar.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Kullanılan iş parçacığı sayısı\nsettings_number_of_threads_tooltip = Kullanılan iş parçacığı sayısı, 0 tüm uygun iş parçacıklarının kullanılacağı anlamına gelir.\nsettings_use_rust_preview = Ön izlemeleri yüklemek için gtk yerine harici kitaplıkları kullanın\nsettings_use_rust_preview_tooltip =\n    Gtk ön izlemelerini kullanmak bazen daha hızlı olabilir ve daha fazla biçimi destekler, ancak bazen bu tam tersi de olabilir.\n    \n    Ön izlemeleri yüklemede sorun yaşıyorsanız bu ayarı değiştirmeyi deneyebilirsiniz.\n    \n    Linux dışı sistemlerde bu seçeneğin kullanılması önerilir çünkü gtk-pixbuf her zaman mevcut değildir, dolayısıyla bu seçeneğin devre dışı bırakılması bazı görüntülerin ön izlemelerini yüklemeyecektir.\nsettings_label_restart = Ayarları uygulamak için uygulamayı yeniden başlatmanız gerekir!\nsettings_ignore_other_filesystems = Öteki dosya sistemlerini yoksay (sadece Linux)\nsettings_ignore_other_filesystems_tooltip = \n    Aranan dizinlerle aynı dosya sisteminde olmayan dosyaları yoksayar.\n    \n    Linux'ta find komutundaki -xdev seçeneği ile aynı şekilde çalışır\nsettings_save_at_exit_button_tooltip = Uygulamayı kapatırken yapılandırmayı dosyaya kaydeder.\nsettings_load_at_start_button_tooltip =\n    Uygulamayı açarken yapılandırmayı dosyadan yükler.\n    \n    Etkinleştirilmezse, varsayılan ayarlar kullanılır.\nsettings_confirm_deletion_button_tooltip = Sil düğmesine tıklandığında onay iletişim kutusunu gösterir.\nsettings_confirm_link_button_tooltip = Sabit/sembolik bağlantı düğmesine tıklandığında onay iletişim kutusunu göster.\nsettings_confirm_group_deletion_button_tooltip = Gruptan tüm kayıtları silmeye çalışırken uyarı iletişim kutusunu gösterir.\nsettings_show_text_view_button_tooltip = Kullanıcı arayüzünün altında çıktı panelini gösterir.\nsettings_use_cache_button_tooltip = Dosya önbelleğini kullanır.\nsettings_save_also_as_json_button_tooltip =\n    Önbelleği (kullanıcı tarafından okunabilir) JSON biçiminde kaydeder. \n    İçeriğini değiştirmek mümkündür. İkili biçim önbelleği (bin uzantılı) eksikse, \n    bu dosyadaki önbellek uygulama tarafından otomatik olarak okunacaktır.\nsettings_use_trash_button_tooltip = Dosyaları kalıcı olarak silmek yerine çöp kutusuna taşır.\nsettings_language_label_tooltip = Kullanıcı arayüzü dilini değiştirir.\nsettings_save_at_exit_button = Uygulamayı kapatırken yapılandırmayı kaydet\nsettings_load_at_start_button = Uygulamayı açarken yapılandırmayı yükle\nsettings_confirm_deletion_button = Herhangi bir dosyayı silerken onay iletişim kutusunu göster\nsettings_confirm_link_button = Herhangi bir dosyaya sabit/sembolik bağlantı yapıldığında onay iletişim kutusunu göster\nsettings_confirm_group_deletion_button = Gruptaki tüm dosyaları silerken onay iletişim kutusunu göster\nsettings_show_text_view_button = Alt çıktı panelini göster\nsettings_use_cache_button = Önbelleği kullan\nsettings_save_also_as_json_button = Önbelleği JSON dosyası olarak da kaydet\nsettings_use_trash_button = Silinen dosyaları çöp kutusuna taşı\nsettings_language_label = Dil\nsettings_multiple_delete_outdated_cache_checkbutton = Güncel olmayan önbellek girişlerini otomatik olarak sil\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Var olmayan dosyalara işaret eden eski önbellek girdilerini siler.\n    \n    Etkinleştirildiğinde, uygulama kayıtları yüklerken tüm kayıtların geçerli dosyalara \n    işaret etmesini sağlar (bozuk olanlar yoksayılır).\n    \n    Bunu devre dışı bırakmak, harici sürücülerdeki dosyaları tararken yardımcı olacaktır, \n    bu nedenle bunlarla ilgili önbellek girdileri bir sonraki taramada temizlenmez.\n    \n    Önbellekte yüzbinlerce kayıt olması durumunda, taramanın başlangıcında/sonunda \n    önbellek yükleme/kaydetme işlemini hızlandıracak olan bu özelliği etkinleştirmeniz önerilir.\nsettings_notebook_general = Genel\nsettings_notebook_duplicates = Eş Dosyalar\nsettings_notebook_images = Benzer Resimler\nsettings_notebook_videos = Benzer Videolar\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Sağ tarafta önizlemeyi gösterir (bir resim dosyası seçiliyken).\nsettings_multiple_image_preview_checkbutton = Resim önizlemesini göster\nsettings_multiple_clear_cache_button_tooltip =\n    Güncel olmayan girişlerin önbelleğini el ile temizleyin.\n    Bu, yalnızca otomatik temizleme devre dışı bırakılmışsa kullanılmalıdır.\nsettings_multiple_clear_cache_button = Güncel olmayan girdileri önbellekten kaldır.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Hepsi aynı verilere işaret ediyorsa (sabit bağlantılıysa), biri dışındaki tüm dosyaları gizler.\n    \n    Örnek: (Diskte) belirli verilere sabit bağlantılı yedi dosya ve aynı veriye ancak farklı \n    bir düğüme sahip bir farklı dosya olması durumunda, yinelenen bulucuda yalnızca bir benzersiz dosya ve \n    sabit bağlantılı dosyalardan bir dosya gösterilecektir.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Önbelleğe alınacak minimum dosya boyutunu ayarlayın.\n    \n    Daha küçük bir değer seçmek daha fazla kayıt üretecektir. \n    Bu, aramayı hızlandıracak, ancak önbellek yüklemeyi/kaydetmeyi yavaşlatacaktır.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Yinelenmeyen sonuçların daha önce reddedilmesine izin veren kısmi-SUÇ \n    (dosyanın küçük bir bölümünden hesaplanan bir SUÇ) değerinin önbelleğe alınmasını sağlar.\n    \n    Bazı durumlarda yavaşlamaya neden olabileceğinden varsayılan olarak devre dışıdır.\n    \n    Aramayı birden çok kez hızlandırabileceğinden, yüz binlerce veya milyonlarca dosyayı \n    tararken kullanılması şiddetle tavsiye edilir.\nsettings_duplicates_prehash_minimal_entry_tooltip = Önbelleğe alınacak girişlerin minimum boyutu.\nsettings_duplicates_hide_hard_link_button = Zor bağlantıları gizle\nsettings_duplicates_prehash_checkbutton = kısmi-SUÇ önbelleği kullan\nsettings_duplicates_minimal_size_cache_label = Önbelleğe kaydedilen minimum dosya boyutu (bayt cinsinden):\nsettings_duplicates_minimal_size_cache_prehash_label = kısmi-SUÇ önbelleğine kaydedilen minimum dosya boyutu (bayt cinsinden):\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Geçerli ayar yapılandırmasını dosyaya kaydeder.\nsettings_loading_button_tooltip = Dosyadan ayarları yükler ve geçerli yapılandırmayı bunlarla değiştirir.\nsettings_reset_button_tooltip = Geçerli yapılandırmayı varsayılana sıfırlar.\nsettings_saving_button = Yapılandırmayı kaydet\nsettings_loading_button = Yapılandırma yükle\nsettings_reset_button = Yapılandırmayı sıfırla\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Önbellek txt dosyalarının depolandığı klasörü açar.\n    \n    Önbellek dosyalarının değiştirilmesi geçersiz sonuçların gösterilmesine neden olabilir. \n    Ancak, büyük miktarda dosyayı farklı bir konuma taşırken yolu değiştirmek zaman kazandırabilir.\n    \n    Dosyaları tekrar taramaktan zaman kazanmak için bu dosyaları bilgisayarlar arasında \n    kopyalayabilirsiniz (tabii ki benzer dizin yapısına sahiplerse).\n    \n    Önbellekte sorun olması durumunda bu dosyalar kaldırılabilir. Uygulama onları \n    otomatik olarak yeniden oluşturacaktır.\nsettings_folder_settings_open_tooltip =\n    Czkawka yapılandırmasının depolandığı klasörü açar.\n    \n    UYARI: Yapılandırmayı elle değiştirmek iş akışınızı bozabilir.\nsettings_folder_cache_open = Önbellek klasörünü aç\nsettings_folder_settings_open = Ayarlar klasörünü aç\n# Compute results\ncompute_stopped_by_user = Arama, kullanıcı tarafından durduruldu\ncompute_found_duplicates_hash_size = { $number_files } tane ekleme { $number_groups } grupta bulunmuştur ve bu,{ $size }'ye { $time } sürede kadardır\ncompute_found_duplicates_name = { $number_files } kopya, { $number_groups } grubunda { $time } süresi içinde bulunmuştur\ncompute_found_empty_folders = { $number_files } boş klasörünü { $time } buldum\ncompute_found_empty_files = { $number_files } adet dosya { $time } içinde boş bulundu\ncompute_found_big_files = { $number_files } büyük dosya { $time } içinde bulundu\ncompute_found_temporary_files = { $number_files } geçici dosya { $time } içinde bulundu\ncompute_found_images = { $number_files } benzer görüntüyü { $number_groups } grupta { $time } süre içinde buldum\ncompute_found_videos = { $number_files } benzer videoyu { $number_groups } grupta { $time } içinde buldum\ncompute_found_music = { $number_files } benzer müzik dosyası { $number_groups } grup içinde { $time } bulunmuştur\ncompute_found_invalid_symlinks = { $number_files } geçerli olmayan simge bağlantısı { $time } içinde bulunuldu\ncompute_found_broken_files = { $number_files } bozuk dosya bulundu { $time } içinde\ncompute_found_bad_extensions = Geçersiz uzantılarla { $number_files } dosya { $time } içinde bulundu\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] { $file_number } dosya tarandı\n       *[other] { $file_number } dosya tarandı\n    }\nprogress_scanning_extension_of_files = { $file_checked }/{ $all_files } dosyasını kontrol edildi\nprogress_scanning_broken_files = Kontrol edilen { $file_checked }/{ $all_files } dosya ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hash işlemi uygulanmış { $file_checked }/{ $all_files } video\nprogress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video\nprogress_scanning_image = Hash işlemi uygulanmış { $file_checked }/{ $all_files } görsel ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = { $file_checked }/{ $all_files } görsel hash kaydı karşılaştırıldı\nprogress_scanning_music_tags_end = Compared tags of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_tags = Read tags of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_content_end = Compared fingerprint of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_content = Calculated fingerprint of { $file_checked }/{ $all_files } music file ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] { $folder_number } klasör tarandı\n       *[other] { $folder_number } klasör tarandı\n    }\nprogress_scanning_size = Taranan { $file_number } dosyasının boyutu\nprogress_scanning_size_name = Scanned name and size of { $file_number } file\nprogress_scanning_name = Scanned name of { $file_number } file\nprogress_analyzed_partial_hash = Analyzed partial hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Analyzed full hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Prehash önbelleği yükleniyor\nprogress_prehash_cache_saving = Prehash önbelleği kaydediliyor\nprogress_hash_cache_loading = Hash önbelleği yükleniyor\nprogress_hash_cache_saving = Hash önbelleği kaydediliyor\nprogress_cache_loading = Önbellek yükleniyor\nprogress_cache_saving = Önbellek kaydediliyor\nprogress_current_stage = Geçerli Aşama: { \" \" }\nprogress_all_stages = Tüm Aşamalar: { \" \" }\n# Saving loading \nsaving_loading_saving_success = Yapılandırma { $name } dosyasına kaydedildi.\nsaving_loading_saving_failure = Konfigürasyon verilerini dosya { $name }'a kaydetme başarısız oldu, sebep { $reason }.\nsaving_loading_reset_configuration = Geçerli yapılandırma temizlendi.\nsaving_loading_loading_success = Uygulama yapılandırması düzgünce yüklendi.\nsaving_loading_failed_to_create_config_file = \"{ $path }\" dizininde yapılandırma dosyası oluşturulamadı, nedeni:  \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = \"{ $path }\" dizininden yapılandırma dosyası yüklenemiyor, böyle dosya yok ya da bir dosya değil.\nsaving_loading_failed_to_read_data_from_file = \"{ $path }\" dosyasından veri okunamıyor, nedeni: \"{ $reason }\".\n# Other\nselected_all_reference_folders = Tüm dizinler, \"Başvuru Klasörü\" olarak ayarlandığında arama başlatılamaz\nsearching_for_data = İşleminiz yürütülüyor, bu biraz zaman alabilir, lütfen bekleyin...\ntext_view_messages = MESAJLAR\ntext_view_warnings = UYARILAR\ntext_view_errors = HATALAR\nabout_window_motto = Bu programın kullanımı ücretsizdir ve her zaman öyle kalacaktır.\nkrokiet_new_app = Czkawka bakım modunda, bu da sadece kritik hatalarının düzeltilmesini ve yeni özelliklerin eklenmemesi anlamına geliyor. Yeni özellikler için lütfen daha stabil ve performanslı olup hala aktif olarak geliştirilen yeni Krokiet uygulamasını kontrol ediniz.\n# Various dialog\ndialogs_ask_next_time = Bir dahaki sefere sor\nsymlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\ndelete_title_dialog = Silmeyi onaylayın\ndelete_question_label = Dosyaları silmek istediğinizden emin misiniz?\ndelete_all_files_in_group_title = Gruptaki tüm dosyaları silmeyi onaylayın\ndelete_all_files_in_group_label1 = Kimi gruplarda tüm kayıtlar seçilir.\ndelete_all_files_in_group_label2 = Bunları silmek istediğinizden emin misiniz?\ndelete_items_label = { $items } dosya silinecek.\ndelete_items_groups_label = { $groups } gruptan { $items } dosya silinecek.\nhardlink_failed = { $name }'yi { $target }'a hafıza.linklemek başarısız oldu, sebep { $reason }\nhard_sym_invalid_selection_title_dialog = Kimi gruplarda geçersiz seçim\nhard_sym_invalid_selection_label_1 = Bazı gruplarda sadece bir kayıt seçilmiştir ve bu kayıt yok sayılacaktır.\nhard_sym_invalid_selection_label_2 = Bu dosyaları sabit/sembolik bağlayabilmek için gruptaki en az iki sonucun seçilmesi gerekir.\nhard_sym_invalid_selection_label_3 = Gruptaki ilk resim asıl olarak tanınır ve değiştirilmez, ancak ikinci ve sonrakiler değiştirilir.\nhard_sym_link_title_dialog = Bağlantı vermeyi onaylayın\nhard_sym_link_label = Bu dosyaları bağlamak istediğinizden emin misiniz?\nmove_folder_failed = { $name } klasörü taşınamadı, nedeni: { $reason }\nmove_file_failed = { $name } dosyası taşınamadı, nedeni: { $reason }\nmove_files_title_dialog = Eş dosyaları taşımak istediğiniz klasörü seçin\nmove_files_choose_more_than_1_path = Eş dosyaları taşıyabilmek için yalnızca bir yol seçilebilir, { $path_number } seçildi.\nmove_stats = { $num_files }/{ $all_files } öğe düzgün şekilde taşındı\nsave_results_to_file = Saved results both to txt and json files into \"{ $name }\" folder.\nsearch_not_choosing_any_music = HATA: Müzik araması için en az bir onay kutusu seçmelisiniz.\nsearch_not_choosing_any_broken_files = HATA: Bozuk dosya araması için en az bir onay kutusu seçmelisiniz.\ninclude_folders_dialog_title = Aranacak Klasörler\nexclude_folders_dialog_title = Hariç Tutulan Klasörler\ninclude_manually_directories_dialog_title = Dizini elle ekle\ncache_properly_cleared = Önbellek, uygun şekilde temizlendi\ncache_clear_duplicates_title = Eş dosyalar önbelleğini temizle\ncache_clear_similar_images_title = Benzer resimler önbelleğini temizle\ncache_clear_similar_videos_title = Benzer videolar önbelleğini temizle\ncache_clear_message_label_1 = Güncel olmayan girişleri önbellekten temizlemek istiyor musunuz?\ncache_clear_message_label_2 = Bu işlem, geçersiz dosyalara işaret eden tüm önbellek girişlerini kaldıracak.\ncache_clear_message_label_3 = Bu, önbelleğe yükleme/kaydetme işlemini biraz hızlandırabilir.\ncache_clear_message_label_4 = UYARI: İşlem, takılı olmayan harici sürücülerden önbelleğe alınmış tüm verileri kaldıracaktır. Yani her hash kaydının yeniden oluşturulması gerekecek.\n# Show preview\npreview_image_resize_failure = { $name } adlı resim yeniden boyutlandırılamadı.\npreview_image_opening_failure = { $name } adlı resim dosyası açılamadı, nedeni: { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Grup: { $current_group }/{ $all_groups } ({ $images_in_group } resim)\ncompare_move_left_button = <-\ncompare_move_right_button = ->\n"
  },
  {
    "path": "czkawka_gui/i18n/uk/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = Налаштування\nwindow_main_title = Czkawka («Гикавка»)\nwindow_progress_title = Сканування\nwindow_compare_images = Порівняння зображень\n# General\ngeneral_ok_button = Гаразд\ngeneral_close_button = Закрити\n# Krokiet info dialog\nkrokiet_info_title = Представляємо Krokiet - Нова версія Czkawka\nkrokiet_info_message = \n        Крокієт – це нова, покращена, швидша та надійніша версія Czkawka GTK GUI!\n\n        Він легше запускається та більш стійкий до змін у системі, оскільки покладається лише на основні бібліотеки, які за замовчуванням доступні на більшості систем.\n\n        Крокієт також приносить функції, яких немає в Czkawka, включаючи мініатюри в режимі порівняння відео, EXIF очищувач, прогрес переміщення/копіювання/видалення файлів або розширені опції сортування.\n\n        Спробуйте його та подивіться на різницю!\n\n        Czkawka продовжуватиме отримувати виправлення помилок та невеликі оновлення від мене, але всі нові функції будуть розроблені виключно для Крокієта, і будь-хто вільний пропонувати нові функції, додавати відсутні режими або розширювати Czkawka далі.\n\n        P.S.: Це повідомлення має з’явитися лише один раз. Якщо воно з’являється знову, встановіть змінну середовища CZKAWKA_DONT_ANNOY_ME на будь-яке не порожнє значення.\n# Main window\nmusic_title_checkbox = Найменування\nmusic_artist_checkbox = Виконавець\nmusic_year_checkbox = Рік\nmusic_bitrate_checkbox = Бітрейт\nmusic_genre_checkbox = Жанр\nmusic_length_checkbox = Тривалість\nmusic_comparison_checkbox = Приблизне порівняння\nmusic_checking_by_tags = Мітки\nmusic_checking_by_content = Зміст\nsame_music_seconds_label = Мінімальна тривалість фрагменту\nsame_music_similarity_label = Максимальна різниця\nmusic_compare_only_in_title_group = Порівняйте у групах подібних назвах\nmusic_compare_only_in_title_group_tooltip =\n    При активованому стані файли групуються за назвою і потім порівнюються між собою.\n    \n    З 10 000 файлів, замість близько 100 зіріх порівнянь, вище є ймовірністю побути близько 20 000 порівнянь.\nsame_music_tooltip =\n    Пошук подібних музичних файлів за його вмістом може бути налаштований за налаштуванням:\n    \n    - Мінімальний час фрагменту, після якого музичні файли можна визначити як схожий\n    - Максимальна різниця між двома тестовими фрагментами\n    \n    —Що ключові з хороших результатів - знайти розумні комбінації цих параметрів, за умов.\n    \n    Встановлення мінімального часу на 5 сек і максимальна різниця складає 1.0, буде шукати майже однакові фрагменти у файлах.\n    Час 20 і максимальна різниця в 6.0, з іншого боку, добре працює для пошуку реміксиксів/живу версії і т. д.\n    \n    За замовчуванням, кожен музичний файл порівнюється один з одним, і це може зайняти багато часу при тестуванні багатьох файлів, так що використовувати референтні папки і вказати, які файли слід порівнювати один з одним (з тією ж кількістю файлів, порівняння відбитків пальців буде швидше 4x, ніж без стандартних папок).\nmusic_comparison_checkbox_tooltip =\n    Шукає схожі музичні файли за допомогою ШІ, що використовує машинне навчання для видалення дужок із фраз. Наприклад, якщо ця опція увімкнена, наступні файли будуть вважатися дублікатами:\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = З урахуванням регістру\nduplicate_case_sensitive_name_tooltip = \n    Коли увімкнено, записи групуються, тільки якщо вони повністю збігаються імена з точністю до кожного символу. Наприклад, «ХІТ Дискотека» не збігається з \"хіт дискотека\".\n    \n    Коли вимкнено, записи групуються незалежно від того, великі або малі літери використовувалися при написанні. Наприклад, «ХІТ Дискотека», «хіт дискотека», «хІт ДиСкОтЕКа» будуть еквівалентні\nduplicate_mode_size_name_combo_box = Розмір і ім'я\nduplicate_mode_name_combo_box = Ім'я\nduplicate_mode_size_combo_box = Розмір\nduplicate_mode_hash_combo_box = Хеш\nduplicate_hash_type_tooltip =\n    У програмі Czkawka можна використовувати один із трьох алгоритмів хешування:\n    \n    Blake3 — криптографічна хеш-функція. Використовується за замовчуванням, оскільки дуже швидка.\n    \n    CRC32 — проста хеш-функція. Ще швидше, ніж Blake3, але можливі рідкісні колізії хешів різних файлів.\n    \n    XXH3 — функція, схожа за продуктивністю і надійністю хеша на Blake3 (але вона не криптографічна), тому її можна використовувати замість Blake3.\nduplicate_check_method_tooltip =\n    На цей час Czkawka пропонує три методи пошуку дублікатів:\n    \n    Ім'я – шукає файли з однаковими іменами.\n    \n    Розмір – шукає файли однакового розміру.\n    \n    Хеш – шукає файли з однаковим вмістом. Цей режим хешує файл, а потім порівнює хеш для пошуку дублікатів. Цей режим є найнадійнішим способом пошуку. Додаток активно використовує кеш, тому друге та подальші сканування одних і тих же даних повинні бути набагато швидшими, ніж перше.\nimage_hash_size_tooltip =\n    Кожне перевірене зображення видає спеціальний хеш, який можна порівнювати один з одним, і невелика різниця між ними означає, що ці зображення є схожими.\n    \n    8 хешів дуже добре знайти зображення, які є трохи схожими на оригінал. При великому наборі зображень (>1000) значення дає велику кількість хибних позитивних результатів, так що я рекомендую використовувати більший розмір хешу у цьому випадку.\n    \n    16 - це хеш за замовчуванням, який є досить хороший компроміс між пошуком навіть мало схожих зображень і маючи тільки невелику кількість хеш-зіткнень.\n    \n    32 і 64 пеші знайдуть лише дуже схожі зображення, але не повинні мати практично неправильних позитивних результатів (можливо, окрім деяких зображень з альфа каналом).\nimage_resize_filter_tooltip =\n    Щоб обчислити хеш зображення, бібліотека має спочатку його перемасштабувати.\n    \n    Залежно від обраного алгоритму отримане зображення, яке використовується при хешуванні, може виглядати трохи інакше.\n    \n    Найшвидший алгоритм з низькою якістю — це метод найближчого сусіда, Nearest. Він увімкнений за замовчуванням, тому при розмірі хешу 16×16 погана якість не помітна.\n    \n    Якщо розмір хешу 8×8, рекомендується будь-який алгоритм, крім Nearest, щоб краще відрізняти подібні зображення в групах.\nimage_hash_alg_tooltip =\n    Користувачі можуть вибирати з одного з багатьох алгоритмів обчислення хешу.\n    \n    Кожен з них має і сильні і слабкі точки, і іноді може призвести до кращого і іноді гірших результатів для різних зображень.\n    \n    Таким чином, щоб визначити найкращу для вас, потрібно ручне тестування.\nbig_files_mode_combobox_tooltip = Дозволяє шукати найменші або найбільші файли\nbig_files_mode_label = Перевірені файли\nbig_files_mode_smallest_combo_box = Найменший\nbig_files_mode_biggest_combo_box = Найбільший\nmain_notebook_duplicates = Файли-дублікати\nmain_notebook_empty_directories = Порожні каталоги\nmain_notebook_big_files = Великі файли\nmain_notebook_empty_files = Порожні файли\nmain_notebook_temporary = Тимчасові файли\nmain_notebook_similar_images = Схожі зображення\nmain_notebook_similar_videos = Схожі відео\nmain_notebook_same_music = Музичні дублікати\nmain_notebook_symlinks = Пошкоджені симв. посилання\nmain_notebook_broken_files = Пошкоджені файли\nmain_notebook_bad_extensions = Помилкові розширення\nmain_tree_view_column_file_name = Ім'я файлу\nmain_tree_view_column_folder_name = Ім'я теки\nmain_tree_view_column_path = Шлях\nmain_tree_view_column_modification = Дата зміни\nmain_tree_view_column_size = Розмір\nmain_tree_view_column_similarity = Подібність\nmain_tree_view_column_dimensions = Розміри\nmain_tree_view_column_title = Найменування\nmain_tree_view_column_artist = Виконавець\nmain_tree_view_column_year = Рік\nmain_tree_view_column_bitrate = Бітрейт\nmain_tree_view_column_length = Тривалість\nmain_tree_view_column_genre = Жанр\nmain_tree_view_column_symlink_file_name = Ім'я файла символьного посилання\nmain_tree_view_column_symlink_folder = Тека символічного посилання\nmain_tree_view_column_destination_path = Шлях призначення\nmain_tree_view_column_type_of_error = Тип помилки\nmain_tree_view_column_current_extension = Поточне розширення\nmain_tree_view_column_proper_extensions = Належне розширення\nmain_tree_view_column_fps = Кадрів в секунду\nmain_tree_view_column_codec = Кодек\nmain_label_check_method = Метод перевірки\nmain_label_hash_type = Тип хешу\nmain_label_hash_size = Розмір хешу\nmain_label_size_bytes = Розмір (байт)\nmain_label_min_size = Мін\nmain_label_max_size = Макс\nmain_label_shown_files = Кількість показаних файлів\nmain_label_resize_algorithm = Алгоритм масштабування\nmain_label_similarity = Подібність{ \"   \" }\nmain_check_box_broken_files_audio = Звук\nmain_check_box_broken_files_pdf = Pdf\nmain_check_box_broken_files_archive = Архів\nmain_check_box_broken_files_image = Зображення\nmain_check_box_broken_files_video = Відео\nmain_check_box_broken_files_video_tooltip = Використовує ffmpeg/ffprobe для валідації відеофайлів. Дуже повільно і може виявляти педантичні помилки, навіть якщо файл відтворюється нормально.\ncheck_button_general_same_size = Ігнорувати однаковий розмір\ncheck_button_general_same_size_tooltip = Ігнорувати файли з однаковим розміром у результаті - зазвичай це 1:1 дублікатів\nmain_label_size_bytes_tooltip = Розмір файлів, які будуть проскановані\n# Upper window\nupper_tree_view_included_folder_column_title = Папки для пошуку\nupper_tree_view_included_reference_column_title = Містить оригінали\nupper_recursive_button = Рекурсивний\nupper_recursive_button_tooltip = Коли увімкнено, будуть шукатися файли, що не знаходяться безпосередньо в корені вибраної папки, тобто в інших підпапках даної папки та їх підпапках.\nupper_manual_add_included_button = Прописати вручну\nupper_add_included_button = Додати\nupper_remove_included_button = Видалити\nupper_manual_add_excluded_button = Ручне додавання\nupper_add_excluded_button = Додати\nupper_remove_excluded_button = Видалити\nupper_manual_add_included_button_tooltip =\n    Додавати назву теки для пошуку вручну.\n    \n    Додайте декілька шляхів відразу, розділіть їх\n    \n    /home/roman;/home/rozkaz буде додано дві папки/home/roman and /home/rozkaz\nupper_add_included_button_tooltip = Додати нову директорію для пошуку.\nupper_remove_included_button_tooltip = Видалити директорію з пошуку.\nupper_manual_add_excluded_button_tooltip =\n    Додавати виключені назви директорії.\n    \n    Додайте декілька шляхів одночасно, відокремте їх ;\n    \n    /home/roman;/home/krokiet додасть дві папки/home/roman та /home/keokiet\nupper_add_excluded_button_tooltip = Додати каталог, який виключається з пошуку.\nupper_remove_excluded_button_tooltip = Видалити каталог з виключених.\nupper_notebook_items_configuration = Параметри пошуку\nupper_notebook_excluded_directories = Виключені шляхи\nupper_notebook_included_directories = Включені шляхи\nupper_allowed_extensions_tooltip =\n    Розширення, що включаються, повинні бути розділені комами (за замовчуванням шукаються файли з будь-якими розширеннями).\n    \n    Макроси IMAGE, VIDEO, MUSIC, TEXT додають одразу кілька розширень.\n    \n    Приклад використання: «.exe, IMAGE, VIDEO, .rar, 7z» — це означає, що будуть скануватися файли зображень (напр. jpg, png), відео (напр. avi, mp4), exe, rar і 7z.\nupper_excluded_extensions_tooltip =\n    Список вимкнених файлів, які будуть ігноруватися при скануванні.\n    \n    При використанні дозволених і вимкнених розширень, цей файл має більший пріоритет, тому файл не буде відмітити.\nupper_excluded_items_tooltip = \n        Виключені елементи повинні містити * wildcard і повинні бути розділені комами.\n        Це повільніше, ніж Excluded Paths, тому використовуйте його обережно.\nupper_excluded_items = Виключені елементи:\nupper_allowed_extensions = Дозволені розширення:\nupper_excluded_extensions = Вимкнені розширення:\n# Popovers\npopover_select_all = Виділити все\npopover_unselect_all = Прибрати всі\npopover_reverse = Зворотній вибір\npopover_select_all_except_shortest_path = Виберіть все, крім найкоротшого шляху\npopover_select_all_except_longest_path = Виберіть все, крім найдовшого шляху\npopover_select_all_except_oldest = Вибрати всі, крім старіших\npopover_select_all_except_newest = Вибрати всі, крім найновіших\npopover_select_one_oldest = Вибрати один найстаріший\npopover_select_one_newest = Вибрати один найновіший\npopover_select_custom = Вибрати власний\npopover_unselect_custom = Скасувати вибір\npopover_select_all_images_except_biggest = Вибрати всі, крім найбільшого\npopover_select_all_images_except_smallest = Вибрати всі, крім найменших\npopover_custom_path_check_button_entry_tooltip =\n    Вибір записів з урахуванням шляху.\n    \n    Приклад:\n    /home/pimpek/rzecz.txt можна знайти за допомогою /home/pim*\npopover_custom_name_check_button_entry_tooltip =\n    Вибір записів на ім'я файлів.\n    \n    Приклад:\n    /usr/ping/pong.txt можна знайти за допомогою *ong*\npopover_custom_regex_check_button_entry_tooltip =\n    Вибір записів за допомогою регулярних виразів.\n    \n    У цьому режимі шуканий текст є шлях з ім'ям.\n    \n    Приклад:\n    /usr/bin/ziemniak.txt можна знайти за допомогою виразу /ziem[a-z]+\n    \n    За замовчуванням використається синтаксис регулярних виразів Rust. Докладніше про це можна прочитати тут: https://docs.rs/regex.\npopover_custom_case_sensitive_check_button_tooltip =\n    Вмикає пошук з урахуванням регістру.\n    \n    При відключеній опції «/home/*» буде відповідати як «/home/roman», так і «/HoMe/roman».\npopover_custom_not_all_check_button_tooltip =\n    Заборона вибору всіх записів у групі.\n    \n    Ця опція включена за замовчуванням, тому що в більшості ситуацій вам не треба видаляти і оригінали, і дублікати — зазвичай залишають хоча б один файл.\n    \n    УВАГА. Цей параметр не працює, якщо ви вже вручну вибрали всі результати групи.\npopover_custom_regex_path_label = Шлях\npopover_custom_regex_name_label = Ім'я\npopover_custom_regex_regex_label = Шлях із рег. виразом + ім'я\npopover_custom_case_sensitive_check_button = З урахуванням регістру\npopover_custom_all_in_group_label = Не вибирати усі записи в групі\npopover_custom_mode_unselect = Зняти вибір\npopover_custom_mode_select = Вибрати власний\npopover_sort_file_name = Ім'я файлу\npopover_sort_folder_name = Назва папки\npopover_sort_full_name = Повне ім'я\npopover_sort_size = Розмір\npopover_sort_selection = Вибрані об'єкти\npopover_invalid_regex = Неприпустимий регулярний вираз\npopover_valid_regex = Коректний регулярний вираз\n# Bottom buttons\nbottom_search_button = Пошук\nbottom_select_button = Вибрати\nbottom_delete_button = Видалити\nbottom_save_button = Зберегти\nbottom_symlink_button = Симв. посилання\nbottom_hardlink_button = Жорст. посилання\nbottom_move_button = Перемістити\nbottom_sort_button = Сортування\nbottom_compare_button = Порівняти\nbottom_search_button_tooltip = Почати пошук\nbottom_select_button_tooltip = Виберіть запис. Тільки вибрані файли/папки будуть доступні для подальшої обробки.\nbottom_delete_button_tooltip = Видалити вибрані файли/папки.\nbottom_save_button_tooltip = Зберегти дані про пошук у файл\nbottom_symlink_button_tooltip =\n    Створити символьні посилання.\n    Працює лише тоді, коли вибрано не менше двох результатів у групі.\n    Перший результат залишається, а другий та наступні робляться символьними посиланнями на перший.\nbottom_hardlink_button_tooltip =\n    Створити жорсткі посилання.\n    Працює лише тоді, коли вибрано не менше двох результатів у групі.\n    Перший результат залишається, а другий та наступні робляться жорсткими посиланнями на перший.\nbottom_hardlink_button_not_available_tooltip =\n    Створити Жорсткі посилання.\n    кнопка вимкнена, тому що не може бути створена.\n    Жорсткі посилання працюють тільки з правами адміністратора на Windows, тому не забудьте запустити додаток як адміністратор.\n    Якщо додаток вже працює з такими привілеями для подібних проблем на GitHub.\nbottom_move_button_tooltip =\n    Переміщення файлів до вибраного каталогу.\n    Копіює всі файли в теку без збереження структури дерева каталогів.\n    При спробі перемістити два файли з однаковим ім'ям в ту саму теку другий не буде переміщений, і з'явиться повідомлення про помилку.\nbottom_sort_button_tooltip = Сортує файли/теки відповідно до вибраного методу.\nbottom_compare_button_tooltip = Порівняйте зображення в групі.\nbottom_show_errors_tooltip = Показати/приховати нижню текстову панель.\nbottom_show_upper_notebook_tooltip = Показати/приховати верхню панель блокнота.\n# Progress Window\nprogress_stop_button = Зупинити\nprogress_stop_additional_message = Припинити запит\n# About Window\nabout_repository_button_tooltip = Посилання на сторінку репозиторію з вихідним кодом.\nabout_donation_button_tooltip = Посилання на сторінку пожертви.\nabout_instruction_button_tooltip = Посилання на сторінку інструкцій.\nabout_translation_button_tooltip = Посилання на сторінку Crowdin із перекладами додатків. Офіційно підтримуються англійська та польська мови.\nabout_repository_button = Репозиторій\nabout_donation_button = Пожертва\nabout_instruction_button = Інструкція\nabout_translation_button = Переклад\n# Header\nheader_setting_button_tooltip = Відкриває вікно налаштувань.\nheader_about_button_tooltip = Відкриває діалогове вікно з інформацією про додаток.\n\n# Settings\n\n\n## General\n\nsettings_number_of_threads = Кількість використаних тем\nsettings_number_of_threads_tooltip = Кількість використаних потоків; встановіть 0, щоб використовувати всі доступні потоки.\nsettings_use_rust_preview = Використовувати зовнішні бібліотеки gtk для завантаження прев'ю\nsettings_use_rust_preview_tooltip =\n    Використання gtk прев'ю іноді може бути швидшим і підтримувати більше форматів, але іноді це може бути зовсім навпаки.\n    \n    Якщо у вас виникли проблеми з завантаженням превью, можна змінити цей параметр.\n    \n    У нелінійних системах рекомендується використовувати цей параметр, тому, що gtk-pixbuf не завжди доступні, тому вимкнення цього параметру не буде завантажувати попередній перегляд деяких зображень.\nsettings_label_restart = Щоб застосувати параметри, необхідно перезапустити додаток!\nsettings_ignore_other_filesystems = Ігнорувати інші файлові системи (лише Linux)\nsettings_ignore_other_filesystems_tooltip =\n    ігнорує файли, які не знаходяться в одній файловій системі, як пошукові каталоги.\n    \n    Працює те саме, що і параметр -xdev у пошуку команди на Linux\nsettings_save_at_exit_button_tooltip = Зберегти конфігурацію в файл під час закриття додатку.\nsettings_load_at_start_button_tooltip =\n    Завантажити конфігурацію з файлу під час відкриття програми.\n    \n    Якщо не ввімкнено, будуть використовуватися налаштування за замовчуванням.\nsettings_confirm_deletion_button_tooltip = Показувати вікно підтвердження при натисканні на кнопку видалення.\nsettings_confirm_link_button_tooltip = Показувати діалогове вікно підтвердження при натисканні на кнопку твердого/символічного посилання.\nsettings_confirm_group_deletion_button_tooltip = Показувати діалогове вікно при спробі видалення всіх записів з групи.\nsettings_show_text_view_button_tooltip = Показувати панель тексту в нижній частині інтерфейсу користувача.\nsettings_use_cache_button_tooltip = Використовувати кеш файлів.\nsettings_save_also_as_json_button_tooltip = Зберігати кеш у формат JSON (читабельний). Його вміст можна змінювати. Кеш із цього файлу буде автоматично прочитаний програмою, якщо бінарний кеш (з розширенням bin) відсутній.\nsettings_use_trash_button_tooltip = Перемістити файли в смітник, а не видаляти їх назавжди.\nsettings_language_label_tooltip = Мова інтерфейсу користувача.\nsettings_save_at_exit_button = Зберегти конфігурацію під час закриття додатку\nsettings_load_at_start_button = Завантажити конфігурацію при відкритті програми\nsettings_confirm_deletion_button = Показувати вікно підтвердження при видаленні будь-якого файлу\nsettings_confirm_link_button = Показувати вікно підтвердження при складній/символьних посилань будь-які файли\nsettings_confirm_group_deletion_button = Показувати вікно підтвердження при видаленні всіх файлів групи\nsettings_show_text_view_button = Показувати нижню текстову панель\nsettings_use_cache_button = Використовувати кеш\nsettings_save_also_as_json_button = Також зберегти кеш у файл JSON\nsettings_use_trash_button = Перемістити видалені файли в смітник\nsettings_language_label = Мова\nsettings_multiple_delete_outdated_cache_checkbutton = Автоматично видаляти застарілі записи кешу\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip =\n    Видалити застарілі результати кеша, що вказують на неіснуючі файли.\n    \n    Коли опція увімкнена, програма перевіряє під час завантаження записів, чи вказують вони на доступні файли (недостачі файли ігноруються).\n    \n    Вимкнення цієї опції допомагає при скануванні файлів на зовнішніх носіях, щоб інформація про них не була очищена під час наступного сканування.\n    \n    За наявності сотень тисяч записів у кеші рекомендується увімкнути цю опцію, щоб прискорити завантаження та збереження кешу на початку та в кінці сканування.\nsettings_notebook_general = Загальні налаштування\nsettings_notebook_duplicates = Дублікати\nsettings_notebook_images = Схожі зображення\nsettings_notebook_videos = Схожий відео\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = Показувати попередній перегляд праворуч (якщо вибрано файл зображення).\nsettings_multiple_image_preview_checkbutton = Показати попередній перегляд зображення\nsettings_multiple_clear_cache_button_tooltip =\n    Очищення застарілих записів кешу вручну.\n    Слід використовувати лише в тому випадку, якщо автоматичне очищення вимкнено.\nsettings_multiple_clear_cache_button = Видалити застарілі результати з кешу.\n\n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip =\n    Приховати всі файли, крім першого, якщо всі вони вказують на ті самі дані (пов'язані жорстким посиланням).\n    \n    Приклад: якщо (на диску) сім файлів пов'язані жорстким посиланням з певними даними, а ще один файл містить ті ж дані, але на іншому inode, то в засобі пошуку дублікатів будуть показані тільки цей останній унікальний файл і один файл, що є жорстким посиланням.\nsettings_duplicates_minimal_size_entry_tooltip =\n    Встановити мінімальний розмір файлу, що кешується.\n    \n    Вибір меншого значення призведе до створення більшої кількості записів. Це прискорить пошук, але сповільнить завантаження/збереження кешу.\nsettings_duplicates_prehash_checkbutton_tooltip =\n    Включає кешування попереднього хеша (prehash), що обчислюється з невеликої частини файлу, що дозволяє швидше виключати з аналізу файли, що відрізняються.\n    \n    За замовчуванням вимкнено, оскільки в деяких ситуаціях може сповільнюватися.\n    \n    Рекомендується використовувати його при скануванні сотень тисяч або мільйонів файлів, оскільки це може прискорити пошук в рази.\nsettings_duplicates_prehash_minimal_entry_tooltip = Мінімальний розмір кешованого запису.\nsettings_duplicates_hide_hard_link_button = Приховати жорсткі посилання\nsettings_duplicates_prehash_checkbutton = Кешувати попередній хеш\nsettings_duplicates_minimal_size_cache_label = Мінімальний розмір (байт) файлів, що кешуються\nsettings_duplicates_minimal_size_cache_prehash_label = Мінімальний розмір (байт) файлів для кешування попереднього хешу\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = Зберегти поточні налаштування у файл.\nsettings_loading_button_tooltip = Завантажити параметри з файлу і замінити поточні налаштування.\nsettings_reset_button_tooltip = Скинути поточні налаштування до стандартних значень.\nsettings_saving_button = Зберегти конфігурацію\nsettings_loading_button = Завантажити конфігурацію\nsettings_reset_button = Скидання налаштувань\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip =\n    Відкрити папку, де зберігаються текстові файли кеша.\n    \n    Зміна файлів кешу може призвести до відображення неправильних результатів, однак зміна шляху може заощадити час, коли переміщується велика кількість файлів в інше місце.\n    \n    Ви можете копіювати ці файли між комп'ютерами, щоб заощадити час на повторному скануванні файлів (звичайно якщо вони мають схожу структуру каталогів).\n    \n    У разі виникнення проблем із кешем ці файли можна видалити. Програма автоматично перестворить їх.\nsettings_folder_settings_open_tooltip =\n    Відкриває теку, де зберігається конфігурація Czkawka.\n    \n    УВАГА. Ручна зміна конфігурації може порушити функціонування програми.\nsettings_folder_cache_open = Відкрити теку кешу\nsettings_folder_settings_open = Відкрити папку налаштувань\n# Compute results\ncompute_stopped_by_user = Пошук був зупинений користувачем\ncompute_found_duplicates_hash_size = Знайдено { $number_files } дублікати в { $number_groups } групах, які зайняли { $size } за { $time }\ncompute_found_duplicates_name = Знайдено { $number_files } дублікати в { $number_groups } групах за { $time }\ncompute_found_empty_folders = Знайдено { $number_files } порожніх папок в { $time }\ncompute_found_empty_files = Знайдено { $number_files } порожніх файлів за { $time }\ncompute_found_big_files = Знайдено { $number_files } великих файлів за { $time }\ncompute_found_temporary_files = Знайдено { $number_files } тимчасових файлів в { $time }\ncompute_found_images = Знайдено { $number_files } схожих зображень в { $number_groups } групах на { $time }\ncompute_found_videos = Знайдено { $number_files } подібних відео у { $number_groups } групах за { $time }\ncompute_found_music = Знайдено { $number_files } подібних музичних файлів в { $number_groups } групах за { $time }\ncompute_found_invalid_symlinks = Знайдено { $number_files } неприпустимі символьні посилання в { $time }\ncompute_found_broken_files = Знарядно { $number_files } пошкоджених файлів за час { $time }\ncompute_found_bad_extensions = Знайдено { $number_files } файли з недійсними розширеннями в { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Відсканований { $file_number } файл\n       *[other] Відсканований { $file_number } файли\n    }\nprogress_scanning_extension_of_files = Перевірено розширення { $file_checked }/{ $all_files } файлу\nprogress_scanning_broken_files = Перевірено { $file_checked }/{ $all_files } файл ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Створено відео з { $file_checked }/{ $all_files }\nprogress_creating_video_thumbnails = Створені мініатюри { $file_checked }/{ $all_files } відео\nprogress_scanning_image = Створено зображення { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Порівняо { $file_checked }/{ $all_files } хеш зображення\nprogress_scanning_music_tags_end = Порівняті теґи { $file_checked }/{ $all_files } музичний файл\nprogress_scanning_music_tags = Read tags of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_content_end = Відбиток порівняльного відбитка { $file_checked }/{ $all_files } музичного файлу\nprogress_scanning_music_content = Розраховано відбиток пальця { $file_checked }/{ $all_files } музичного файлу ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Скановано { $folder_number } папку\n       *[other] Скановано { $folder_number } папки\n    }\nprogress_scanning_size = Відсканований розмір файлу { $file_number }\nprogress_scanning_size_name = Відскановане ім'я і розмір файлу { $file_number }\nprogress_scanning_name = Відскановано ім'я файлу { $file_number }\nprogress_analyzed_partial_hash = Проаналізовано частковий хеш { $file_checked }/{ $all_files } файлів ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Проаналізовано повний хеш { $file_checked }/{ $all_files } файлів ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = Завантаження цільового кешу\nprogress_prehash_cache_saving = Збереження цілковитого кешу\nprogress_hash_cache_loading = Завантаження схованки\nprogress_hash_cache_saving = Збереження кешу кешу\nprogress_cache_loading = Завантаження кешу\nprogress_cache_saving = Збереження кешу\nprogress_current_stage = Поточний етап: { \"  \" }\nprogress_all_stages = Усі етапи: { \"  \" }\n# Saving loading \nsaving_loading_saving_success = Збережено конфігурацію в файл { $name }.\nsaving_loading_saving_failure = Не вдалося зберегти дані конфігурації у файл { $name }, причина { $reason }.\nsaving_loading_reset_configuration = Поточна конфігурація була очищена.\nsaving_loading_loading_success = Установки програми коректно завантажені.\nsaving_loading_failed_to_create_config_file = Не вдалося створити файл налаштувань \"{ $path }\", причина \"{ $reason }\".\nsaving_loading_failed_to_read_config_file = Неможливо завантажити конфігурацію з «{ $path }», тому що або такого файлу не існує, або це не файл.\nsaving_loading_failed_to_read_data_from_file = Не вдалося прочитати дані з файлу \"{ $path }\", причина \"{ $reason }\".\n# Other\nselected_all_reference_folders = Неможливо почати пошук, коли всі каталоги встановлені як папки з орієнтирами\nsearching_for_data = Пошук даних може зайняти деякий час — будь ласка, зачекайте...\ntext_view_messages = ПОВІДОМЛЕННЯ\ntext_view_warnings = ПОПЕРЕДЖЕННЯ\ntext_view_errors = ПОМИЛКИ\nabout_window_motto = Ця програма безкоштовна для використання і завжди буде такою.\nkrokiet_new_app = Чкавка перебуває у режимі технічного обслуговування, а це означає, що фіксуються лише важливі помилки, і жодна нова функція не буде додана. Для нових функцій, будь ласка, ознайомтеся з новим додатком Krokiet (більш стабільним та ефективним та активним додатком).\n# Various dialog\ndialogs_ask_next_time = Завжди запитувати\nsymlink_failed = Не вдалося прив'язати { $name } до { $target }, причина { $reason }\ndelete_title_dialog = Підтвердження видалення\ndelete_question_label = Ви впевнені, що бажаєте видалити файли?\ndelete_all_files_in_group_title = Підтвердження видалення всіх файлів у групі\ndelete_all_files_in_group_label1 = У деяких групах обрані всі записи.\ndelete_all_files_in_group_label2 = Ви впевнені, що хочете видалити їх?\ndelete_items_label = Буде видалено файлів: { $items }.\ndelete_items_groups_label = Буде видалено файлів: { $items } (груп: { $groups }).\nhardlink_failed = Не вдалося жорстке посилання { $name } на { $target }, причина { $reason }\nhard_sym_invalid_selection_title_dialog = Невірний вибір у деяких групах\nhard_sym_invalid_selection_label_1 = У деяких групах вибрано лише один запис — вони будуть проігноровані.\nhard_sym_invalid_selection_label_2 = Щоб жорстко або символьно зв'язати ці файли, необхідно вибрати щонайменше два результати групи.\nhard_sym_invalid_selection_label_3 = Перший у групі визнаний як оригінал і не буде змінено, але другий та наступні модифіковані.\nhard_sym_link_title_dialog = Підтвердження посилання\nhard_sym_link_label = Ви впевнені, що хочете зв'язати ці файли?\nmove_folder_failed = Не вдалося перемістити папку { $name }. Причина: { $reason }\nmove_file_failed = Не вдалося перемістити файл { $name }, причина { $reason }\nmove_files_title_dialog = Виберіть теку, в яку хочете перемістити дубльовані файли\nmove_files_choose_more_than_1_path = Можна вибрати лише один шлях для копіювання дублікатів файлів, але вибрано { $path_number }.\nmove_stats = Вдалося перемістити без помилок елементів: { $num_files }/{ $all_files }\nsave_results_to_file = Збережено результати як з txt, так і з json файлів в папку \"{ $name }\".\nsearch_not_choosing_any_music = Помилка: Ви повинні вибрати принаймні один прапорець з типами пошуку музики.\nsearch_not_choosing_any_broken_files = ПОМИЛКА: Ви повинні вибрати принаймні один прапорець з типом пошкоджених файлів.\ninclude_folders_dialog_title = Теки для включення\nexclude_folders_dialog_title = Теки для виключення\ninclude_manually_directories_dialog_title = Додати теку вручну\ncache_properly_cleared = Кеш успішно очищений\ncache_clear_duplicates_title = Очищення кешу дублікатів\ncache_clear_similar_images_title = Очищення кешу подібних зображень\ncache_clear_similar_videos_title = Очищення кеша схожих відео\ncache_clear_message_label_1 = Ви хочете очистити кеш від застарілих записів?\ncache_clear_message_label_2 = Ця дія видаляє всі записи кешу, що вказують на недоступні файли.\ncache_clear_message_label_3 = Це може трохи прискорити завантаження/збереження кешу.\ncache_clear_message_label_4 = УВАГА. Ця дія видаляє всі кешовані дані від вимкнених зовнішніх дисків. Хеші для файлів на цих носіях потрібно буде згенерувати заново.\n# Show preview\npreview_image_resize_failure = Не вдалося змінити розмір зображення { $name }.\npreview_image_opening_failure = Не вдалося відкрити зображення { $name }. Причина: { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = Група { $current_group }/{ $all_groups } (зображень: { $images_in_group })\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/zh-CN/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = 设定\nwindow_main_title = Czkawka - (Hiccup)\nwindow_progress_title = 正在扫描\nwindow_compare_images = 比较图像\n# General\ngeneral_ok_button = 确定\ngeneral_close_button = 关闭\n# Krokiet info dialog\nkrokiet_info_title = Introducing Krokiet - 新版本 Czkawka\nkrokiet_info_message = \n        克罗基特是 Czkawka GTK GUI 的新、改进、更快、更可靠的版本！\n \n        它更易于运行，并且对系统更改更具弹性，因为它只依赖于大多数系统默认可用的核心库。\n \n        克罗基特还带来 Czkawka 缺乏的功能，包括视频比较模式下的缩略图、EXIF 清理器、文件移动/复制/删除进度或扩展排序选项。\n \n        尝试一下并看看区别！\n \n        Czkawka 将继续收到我提供的错误修复和小更新，但所有新功能都将专门为克罗基特开发，任何人都可以在添加新功能、添加缺失模式或进一步扩展 Czkawka 时自由贡献。\n \n        PS：此消息只应出现一次。如果它再次出现，请将 CZKAWKA_DONT_ANNOY_ME 环境变量设置为任何非空值。.\n# Main window\nmusic_title_checkbox = 标题\nmusic_artist_checkbox = 艺人\nmusic_year_checkbox = 年份\nmusic_bitrate_checkbox = 码率\nmusic_genre_checkbox = 流派\nmusic_length_checkbox = 长度\nmusic_comparison_checkbox = 近似比较\nmusic_checking_by_tags = 标签\nmusic_checking_by_content = 内容\nsame_music_seconds_label = 最小分片秒持续时间\nsame_music_similarity_label = 最大差异\nmusic_compare_only_in_title_group = 在相似标题的组中比较\nmusic_compare_only_in_title_group_tooltip = \n    如果启用，文件按标题分类，然后相互比较。\n    \n    如果有一亿文件，而不是几乎1亿文件的比较，通常就会有大约2000万份比较。.\nsame_music_tooltip = \n    通过设置以下内容，可以配置按其内容搜索类似的音乐文件：\n    \n    - 可以识别音乐文件为类似文件的最小碎片时间\n    - 两个测试片段之间的最大差异度\n    \n    找到这些参数的合理组合是获得好结果的关键。\n    \n    将最小时间设置为5秒，最大差异度设置为1.0，将寻找几乎相同的文件碎片。\n    另一方面，20秒的时间和6.0的最大差异度可以很好地找到混音/现场版本等内容。\n    \n    默认情况下，每个音乐文件都会与其他音乐文件进行比较，当测试许多文件时，这可能需要很长时间，因此通常最好使用参考文件夹并指定要相互比较的文件(如有相同数量的文件，则比较指纹至少比不使用参考文件夹快4倍)。.\nmusic_comparison_checkbox_tooltip =\n    它使用AI搜索类似的音乐文件，利用机器学习从短语中移除括号。例如，启用此选项后，相关的文件将被视为重复文件：\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = 区分大小写\nduplicate_case_sensitive_name_tooltip =\n    启用时，仅当记录具有完全相同的名称时才进行分组，例如 Żołd <-> Żołd\n    \n    禁用此选项将不检查每个字母是否具有相同的大小写，例如 żoŁD <-> Żołd\nduplicate_mode_size_name_combo_box = 大小和名称\nduplicate_mode_name_combo_box = 名称\nduplicate_mode_size_combo_box = 大小\nduplicate_mode_hash_combo_box = 哈希\nduplicate_hash_type_tooltip = \n    Czkawka提供3种哈希类型：\n    \n    Blake3 - 加密散列函数。这是默认选项，因为它非常快。\n    \n    CRC32 - 简单的散列函数。这应该比Blake3更快，但极少情况下可能会有一些冲突。\n    \n    XXH3 - 与Blake3非常相似，性能和哈希质量也很高 （但不是加密的）。因此，这些模式可以很容易地互换使用。.\nduplicate_check_method_tooltip = \n    目前，Czkawka提供三种方法来查找重复：\n    \n    名称 - 查找名称相同的文件。\n    \n    大小 - 查找大小相同的文件。\n    \n    哈希 - 查找内容相同的文件。 这种模式会对文件进行哈希计算，然后将哈希值进行比较以查找重复项。这种模式是查找重复项的最安全方法。应用程序大量使用缓存，因此对相同数据进行的第二次及更多次扫描应比第一次更快。.\nimage_hash_size_tooltip = \n    每张选中的图像都产生了一个可相互比较的特殊哈希。 两者之间的小差别意味着这些图像是相似的。\n    \n    8 散列尺寸非常适合于找到仅略类似于原始图像的图像。 有了更大的一组图像 (>1000)，这将产生大量虚假的正数， 所以我建议在这种情况下使用更大的散列尺寸。\n    \n    16是默认的散列尺寸，它是一个很好的折衷，既使找到了一些小相似的图像，又仅有少量的散列碰撞。\n    \n    32和64 散列只找到非常相似的图像，但是几乎不应该有假正数  (可能只有一些带着Alpha 通道的图像)。.\nimage_resize_filter_tooltip = \n    要计算图像散列，库必须首先调整大小。\n    \n    在选定的算法上花费, 用来计算散列的结果图像看起来有点不同。\n    \n    最快使用的算法，也是结果最差的算法，是Nearest。 默认情况下启用它，因为16x16散列大小较低的质量并不真正可见。\n    \n    使用 8x8 散列大小，建议使用不同于Nearest的算法来拥有更好的图像组。.\nimage_hash_alg_tooltip = \n    用户可以从许多计算哈希值的算法中选择一种。\n    \n    每种算法都有强项和弱项，对于不同的图像，有时结果更好，有时结果更差。\n    \n    因此，为了确定最适合你的算法，需要进行人工测试。.\nbig_files_mode_combobox_tooltip = 允许搜索最小/最大的文件\nbig_files_mode_label = 已检查的文件\nbig_files_mode_smallest_combo_box = 最小的\nbig_files_mode_biggest_combo_box = 最大的\nmain_notebook_duplicates = 重复文件\nmain_notebook_empty_directories = 空目录\nmain_notebook_big_files = 大文件\nmain_notebook_empty_files = 空文件\nmain_notebook_temporary = 临时文件\nmain_notebook_similar_images = 相似图像\nmain_notebook_similar_videos = 相似视频\nmain_notebook_same_music = 重复音乐\nmain_notebook_symlinks = 无效的符号链接\nmain_notebook_broken_files = 损坏的文件\nmain_notebook_bad_extensions = 错误的扩展\nmain_tree_view_column_file_name = 文件名称\nmain_tree_view_column_folder_name = 文件夹名称\nmain_tree_view_column_path = 路径\nmain_tree_view_column_modification = 修改日期\nmain_tree_view_column_size = 大小\nmain_tree_view_column_similarity = 相似度\nmain_tree_view_column_dimensions = 尺寸\nmain_tree_view_column_title = 标题\nmain_tree_view_column_artist = 艺人\nmain_tree_view_column_year = 年份\nmain_tree_view_column_bitrate = 码率\nmain_tree_view_column_length = 长度\nmain_tree_view_column_genre = 流派\nmain_tree_view_column_symlink_file_name = 符号链接文件名\nmain_tree_view_column_symlink_folder = 符号链接文件夹\nmain_tree_view_column_destination_path = 目标路径\nmain_tree_view_column_type_of_error = 错误类型\nmain_tree_view_column_current_extension = 当前扩展\nmain_tree_view_column_proper_extensions = 合适的扩展\nmain_tree_view_column_fps = FPS\nmain_tree_view_column_codec = 编解码器\nmain_label_check_method = 检查方法\nmain_label_hash_type = 哈希类型\nmain_label_hash_size = 哈希大小\nmain_label_size_bytes = 大小 (字节)\nmain_label_min_size = 最小值\nmain_label_max_size = 最大值\nmain_label_shown_files = 显示的文件数\nmain_label_resize_algorithm = 调整算法\nmain_label_similarity = 相似性:{ \" \" }\nmain_check_box_broken_files_audio = 音频\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = 归档\nmain_check_box_broken_files_image = 图像\nmain_check_box_broken_files_video = 视频\nmain_check_box_broken_files_video_tooltip = 使用 ffmpeg/ffprobe 验证视频文件。 相当慢，并且可能检测到刻板的错误，即使文件播放正常。.\ncheck_button_general_same_size = 忽略相同的大小\ncheck_button_general_same_size_tooltip = 忽略结果中相同大小的文件 - 通常是 1:1 重复\nmain_label_size_bytes_tooltip = 将用于扫描的文件大小\n# Upper window\nupper_tree_view_included_folder_column_title = 要搜索的文件夹\nupper_tree_view_included_reference_column_title = 参考文件夹\nupper_recursive_button = 递归\nupper_recursive_button_tooltip = 如果选中，也可以搜索未直接置于选定文件夹下的文件。.\nupper_manual_add_included_button = 手动添加\nupper_add_included_button = 添加\nupper_remove_included_button = 删除\nupper_manual_add_excluded_button = 手动添加\nupper_add_excluded_button = 添加\nupper_remove_excluded_button = 删除\nupper_manual_add_included_button_tooltip =\n    手动添加目录名。\n    \n    如需一次性添加多个路径，请用分号;分隔它们\n    \n    填写 /home/roman;/home/krokiet 将添加 /home/roman 和 /home/kookiet 两个目录\nupper_add_included_button_tooltip = 添加新目录进行搜索。.\nupper_remove_included_button_tooltip = 从搜索中删除目录。.\nupper_manual_add_excluded_button_tooltip =\n    手动添加要排除的目录名称。\n    \n    如需一次性添加多个路径，请用分号;分隔它们\n    \n    填写 /home/roman;/home/krokiet 将添加 /home/roman 和  /home/kookiet 两个目录\nupper_add_excluded_button_tooltip = 添加在搜索中排除的目录。.\nupper_remove_excluded_button_tooltip = 从排除中删除目录。.\nupper_notebook_items_configuration = 项目配置\nupper_notebook_excluded_directories = 排除路径\nupper_notebook_included_directories = 包含路径\nupper_allowed_extensions_tooltip = \n    允许的扩展名必须用逗号分隔（默认情况下所有扩展名都可用）。\n    \n    还可以使用以下可一次添加多个扩展名的宏：IMAGE、VIDEO、MUSIC、TEXT。\n    \n    填写 /home/roman;/home/krokiet 将添加 /home/roman 和  /home/kookiet 两个目录\n    \n    用法示例“.exe、IMAGE、VIDEO、.rar、7z” - 这意味着将扫描图像（例如 jpg、png）、视频（例如 avi、mp4）、exe、rar 和 7z 文件。.\nupper_excluded_extensions_tooltip = \n    在扫描中忽略的已禁用文件列表。\n    \n    在使用允许和禁用的扩展时，这个扩展具有更高的优先级，所以文件将不会被检查。.\nupper_excluded_items_tooltip = \n        排除项目必须包含 * 并且应以逗号分隔。\n        这比排除路径慢，因此请谨慎使用。.\nupper_excluded_items = 排除的项目：\nupper_allowed_extensions = 允许的扩展：\nupper_excluded_extensions = 已禁用扩展：\n# Popovers\npopover_select_all = 全部选择\npopover_unselect_all = 取消全选\npopover_reverse = 反向选择\npopover_select_all_except_shortest_path = 选择所有，除了最短路径\npopover_select_all_except_longest_path = 选择所有，不包括最长路径\npopover_select_all_except_oldest = 选择除最旧以外的所有选项\npopover_select_all_except_newest = 选择除最新以外的所有选项\npopover_select_one_oldest = 选择一个最旧的\npopover_select_one_newest = 选择一个最新的\npopover_select_custom = 选择自定义\npopover_unselect_custom = 取消选择自定义\npopover_select_all_images_except_biggest = 选择除最大以外的所有选项\npopover_select_all_images_except_smallest = 选择除最小以外的所有\npopover_custom_path_check_button_entry_tooltip =\n    通过路径选择记录。\n    \n    示例用法：\n    /home/pimpek/rzecz.txt 可以通过 /home/pim* 找到\npopover_custom_name_check_button_entry_tooltip =\n    按文件名选择记录。\n    \n    示例用法：\n    /usr/ping/pong.txt 可以通过 *ong* 找到。\npopover_custom_regex_check_button_entry_tooltip = \n    按指定的正则表达式选择记录。\n    \n    使用此模式，搜索的文本是带有名称的路径。\n    \n    示例用法：\n    可以使用 /ziem[a-z]+ 查找 /usr/bin/ziemniak.txt\n    \n    这使用默认的Rust正则表达式实现。 您可以在此处阅读有关它的更多信息: https://docs.rs/regex。.\npopover_custom_case_sensitive_check_button_tooltip = \n    启用大小写检测。\n    \n    该选项禁用时，/home/* 将同时找到 /HoMe/roman 和 /home/roman。.\npopover_custom_not_all_check_button_tooltip = \n    禁止在分组中选择所有记录。\n    \n    这是默认启用的，因为在大多数情况下， 您不想删除原始文件和重复文件，而是想留下至少一个文件。\n    \n    警告：如果您已经手动选择了一个组中的所有结果，则此设置不起作用。.\npopover_custom_regex_path_label = 路径\npopover_custom_regex_name_label = 名称\npopover_custom_regex_regex_label = 正则表达式路径 + 名称\npopover_custom_case_sensitive_check_button = 区分大小写\npopover_custom_all_in_group_label = 不在组中选择所有记录\npopover_custom_mode_unselect = 取消选择自定义\npopover_custom_mode_select = 选择自定义\npopover_sort_file_name = 文件名称\npopover_sort_folder_name = 文件夹名称\npopover_sort_full_name = 全名\npopover_sort_size = 大小\npopover_sort_selection = 选择\npopover_invalid_regex = 正则表达式无效\npopover_valid_regex = 正则表达式有效\n# Bottom buttons\nbottom_search_button = 搜索\nbottom_select_button = 选择\nbottom_delete_button = 删除\nbottom_save_button = 保存\nbottom_symlink_button = 软链接\nbottom_hardlink_button = 硬链接\nbottom_move_button = 移动\nbottom_sort_button = 排序\nbottom_compare_button = 比较\nbottom_search_button_tooltip = 开始搜索\nbottom_select_button_tooltip = 选择记录。只能稍后处理选定的文件/文件夹。.\nbottom_delete_button_tooltip = 删除选中的文件/文件夹。.\nbottom_save_button_tooltip = 保存搜索数据到文件\nbottom_symlink_button_tooltip = \n    创建软链接。\n    只有在至少选择了一组中的两个结果时才起作用。\n    第一个结果保持不变，第二个及后续结果都会被软链接到第一个结果上。.\nbottom_hardlink_button_tooltip = \n    创建硬链接。\n    只有在至少选择了一组中的两个结果时才起作用。\n    第一个结果保持不变，第二个及后续结果都会被硬链接到第一个结果上。.\nbottom_hardlink_button_not_available_tooltip = \n    创建硬链接。\n    按钮已禁用，因为无法创建硬链接。\n    在 Windows 上，只有使用管理员权限才能使用硬链接，所以请确保以管理员身份运行该应用程序。\n    如果应用程序已经具有管理员权限，请在 Github 上查找类似的问题。.\nbottom_move_button_tooltip = \n    移动文件到选定的目录。\n    它复制所有文件到目录，而不保留目录树。\n    试图将两个具有相同名称的文件移动到文件夹时，第二个将失败并显示错误。.\nbottom_sort_button_tooltip = 根据选定的方法排序文件/文件夹。.\nbottom_compare_button_tooltip = 比较群组中的图像。.\nbottom_show_errors_tooltip = 显示/隐藏底部文本面板。.\nbottom_show_upper_notebook_tooltip = 显示/隐藏主笔记本面板。.\n# Progress Window\nprogress_stop_button = 停止\nprogress_stop_additional_message = 停止请求\n# About Window\nabout_repository_button_tooltip = 链接到源代码的仓库页面。.\nabout_donation_button_tooltip = 链接到捐赠页面。.\nabout_instruction_button_tooltip = 链接到指令页面。.\nabout_translation_button_tooltip = 链接到带有应用程序翻译的 Crowdin 页面。官方支持波兰语和英语。.\nabout_repository_button = 存储库\nabout_donation_button = 捐助\nabout_instruction_button = 说明\nabout_translation_button = 翻译\n# Header\nheader_setting_button_tooltip = 打开设置对话框。.\nheader_about_button_tooltip = 打开包含应用程序信息的对话框。.\n \n# Settings\n\n\n## General\n\nsettings_number_of_threads = 使用的线程数\nsettings_number_of_threads_tooltip = 使用的线程数，0表示所有可用线程都将被使用。.\nsettings_use_rust_preview = 使用外部库来加载预览\nsettings_use_rust_preview_tooltip = \n    使用 gtk 预览有时会更快，支持更多格式，但有时恰恰相反。\n    \n    如果您在加载预览时遇到问题，您可以尝试更改此设置。\n    \n    关于非Linux系统，建议使用此选项。 因为gtk-pixbuf 并不总是可用的，所以禁用此选项不会加载某些图像的预览。.\nsettings_label_restart = 您需要重新启动应用才能应用设置！\nsettings_ignore_other_filesystems = 忽略其它文件系统 (仅限Linux)\nsettings_ignore_other_filesystems_tooltip =\n    忽略与搜索的目录不在同一个文件系统中的文件。\n    \n    在 Linux 上查找命令时类似-xdev选项\nsettings_save_at_exit_button_tooltip = 关闭应用时将配置保存到文件。.\nsettings_load_at_start_button_tooltip = \n    打开应用程序时从文件加载配置。\n    \n    如果未启用，将使用默认设置。.\nsettings_confirm_deletion_button_tooltip = 点击删除按钮时显示确认对话框。.\nsettings_confirm_link_button_tooltip = 点击硬链接/符号链接按钮时显示确认对话框。.\nsettings_confirm_group_deletion_button_tooltip = 尝试从群组中删除所有记录时显示警告对话框。.\nsettings_show_text_view_button_tooltip = 在用户界面底部显示文本面板。.\nsettings_use_cache_button_tooltip = 使用文件缓存。.\nsettings_save_also_as_json_button_tooltip = 保存缓存为 (人类可读) JSON 格式。可以修改其内容。 如果缺少二进制格式缓存(带bin extensional)，此文件的缓存将被应用自动读取。.\nsettings_use_trash_button_tooltip = 将文件移至回收站，而不是将其永久删除。.\nsettings_language_label_tooltip = 用户界面的语言。.\nsettings_save_at_exit_button = 关闭应用时保存配置\nsettings_load_at_start_button = 打开应用程序时加载配置\nsettings_confirm_deletion_button = 删除任何文件时显示确认对话框\nsettings_confirm_link_button = 硬/符号链接任何文件时显示确认对话框\nsettings_confirm_group_deletion_button = 删除组中所有文件时显示确认对话框\nsettings_show_text_view_button = 显示底部文本面板\nsettings_use_cache_button = 使用缓存\nsettings_save_also_as_json_button = 同时将缓存保存为 JSON 文件\nsettings_use_trash_button = 移动已删除的文件到回收站\nsettings_language_label = 语言\nsettings_multiple_delete_outdated_cache_checkbutton = 自动删除过时的缓存条目\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip = \n    删除指向不存在文件的过期缓存结果。\n    \n    当启用时，应用程序确保在加载记录时所有记录都指向有效文件 (无法访问的文件将被忽略)。\n    \n    禁用此功能将有助于扫描外部驱动器上的文件时，避免在下一次扫描时清除与其相关的缓存条目。\n    \n    如果缓存中有数十万条记录，则建议启用此功能。这将加快扫描开始/结束时的缓存加载/保存速度。.\nsettings_notebook_general = 概况\nsettings_notebook_duplicates = 重复项\nsettings_notebook_images = 相似图像\nsettings_notebook_videos = 相似视频\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = 在右侧显示预览 (当选择图像文件时)。.\nsettings_multiple_image_preview_checkbutton = 显示图像预览\nsettings_multiple_clear_cache_button_tooltip = \n    手动清除过时条目的缓存。\n    仅在禁用自动清除时才使用。.\nsettings_multiple_clear_cache_button = 从缓存中删除过时的结果。.\n \n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip = \n    隐藏除一个以外的所有文件，如果所有文件都指向同一数据（即为硬链接）。\n    \n    示例：如果（磁盘上）有七个文件硬链接到特定数据，而一个不同文件具有相同数据但不同 inode，则在重复查找器中，将仅显示一个唯一文件和一个来自硬链接文件的文件。.\nsettings_duplicates_minimal_size_entry_tooltip = \n    设置将被缓存的最小文件大小。\n    \n    选择较小的值将会生成更多的记录。这将加快搜索速度，但会减慢缓存的加载/保存速度。.\nsettings_duplicates_prehash_checkbutton_tooltip = \n    启用预散列缓存 (从文件的一小部分计算出的哈希)，以便更早地排除非重复结果。\n    \n    默认情况下禁用它，因为在某些情况下可能会导致减慢速度。\n    \n    强烈建议在扫描数十万或数百万个文件时使用它，因为它可以使搜索速度提高数倍。.\nsettings_duplicates_prehash_minimal_entry_tooltip = 缓存条目的最小尺寸。.\nsettings_duplicates_hide_hard_link_button = 隐藏硬链接\nsettings_duplicates_prehash_checkbutton = 使用捕捉缓存\nsettings_duplicates_minimal_size_cache_label = 保存到缓存的最小文件大小 (字节)\nsettings_duplicates_minimal_size_cache_prehash_label = 文件最小尺寸(字节) 保存到逮捕缓存\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = 保存当前设置配置到文件。.\nsettings_loading_button_tooltip = 从文件加载设置并替换当前配置。.\nsettings_reset_button_tooltip = 重置当前配置为默认设置。.\nsettings_saving_button = 保存配置\nsettings_loading_button = 加载配置\nsettings_reset_button = 重置配置\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip = \n    打开存储缓存的txt文件的文件夹。\n    \n    修改缓存文件可能会导致显示无效的结果。然而，当将大量文件移动到另一个位置时，修改路径可能会节省时间。\n    \n    您可以在计算机之间复制这些文件，以节省再次扫描文件的时间 (当然，如果它们具有相似的目录结构)。\n    \n    如果出现缓存问题，可以删除这些文件。该应用程序将自动重新生成它们。.\nsettings_folder_settings_open_tooltip = \n    打开保存Czkawka配置的文件夹。\n    \n    警告：手动修改配置可能会破坏您的工作流程。.\nsettings_folder_cache_open = 打开缓存文件夹\nsettings_folder_settings_open = 打开设置文件夹\n# Compute results\ncompute_stopped_by_user = 搜索已被用户停止\ncompute_found_duplicates_hash_size = 找到 { $number_files } 重复的 { $number_groups } 个小组，这些小组在 { $size } 中占用了 { $time }\ncompute_found_duplicates_name = 在 { $number_groups } 组中找到 { $number_files } 重复的 { $time }\ncompute_found_empty_folders = 找到了 { $number_files } 个空文件夹在 { $time }\ncompute_found_empty_files = 在{ $time }找到了{ $number_files }个空文件\ncompute_found_big_files = 在 { $number_files } 中找到 { $time } 大文件\ncompute_found_temporary_files = 找到了 { $number_files } 个临时文件在 { $time }\ncompute_found_images = 在 { $number_groups } 组中找到 { $number_files } 相似的图像，于 { $time }\ncompute_found_videos = 找到了{ $number_files }个相似视频，在{ $number_groups }组中，耗时{ $time }\ncompute_found_music = 在 { $number_groups } 组中找到 { $number_files } 类似的音乐文件在 { $time }\ncompute_found_invalid_symlinks = 找到了{ $number_files }个无效的符号链接在{ $time }\ncompute_found_broken_files = 在 { $time } 中找到了 { $number_files } 个损坏文件\ncompute_found_bad_extensions = 在 { $number_files } 中发现无效扩展名的 { $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] 已掃描 { $file_number } 個文件\n       *[other] 已掃描 { $file_number } 個文件\n    }\nprogress_scanning_extension_of_files = 检查了 { $file_checked }/{ $all_files } 文件的扩展\nprogress_scanning_broken_files = 签入 { $file_checked }/{ $all_files } 文件({ $data_checked }/{ $all_data })\nprogress_scanning_video = 对 { $file_checked }/{ $all_files } 视频的哈希值\nprogress_creating_video_thumbnails = 创建 { $file_checked }/{ $all_files } 视频的缩略图\nprogress_scanning_image = 对 { $file_checked }/{ $all_files } 图像的哈希值({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = 比较 { $file_checked }/{ $all_files } 图像哈希\nprogress_scanning_music_tags_end = 对比的 { $file_checked }/{ $all_files } 音乐文件标签\nprogress_scanning_music_tags = 阅读 { $file_checked }/{ $all_files } 音乐文件的标签\nprogress_scanning_music_content_end = 比较了 { $file_checked }/{ $all_files } 音乐文件的指纹\nprogress_scanning_music_content = 计算的 { $file_checked }/{ $all_files } 音乐文件 ({ $data_checked }/{ $all_data } ) 的指纹\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] 已掃描 { $folder_number } 個資料夾\n       *[other] 已掃描 { $folder_number } 個資料夾\n    }\nprogress_scanning_size = 扫描的 { $file_number } 文件大小\nprogress_scanning_size_name = 扫描的 { $file_number } 文件的名称和大小\nprogress_scanning_name = 扫描的 { $file_number } 文件名称\nprogress_analyzed_partial_hash = 分析了 { $file_checked }/{ $all_files } 文件的部分哈希值({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = 分析了 { $file_checked }/{ $all_files } 文件的完整哈希值({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = 正在加载逮捕缓存\nprogress_prehash_cache_saving = 正在保存抓取缓存\nprogress_hash_cache_loading = 加载散列缓存\nprogress_hash_cache_saving = 保存哈希缓存\nprogress_cache_loading = 加载缓存\nprogress_cache_saving = 正在保存缓存\nprogress_current_stage = 当前阶段:{ \"  \" }\nprogress_all_stages = 所有阶段:{ \" \" }\n# Saving loading \nsaving_loading_saving_success = 配置保存到文件 { $name }。.\nsaving_loading_saving_failure = 无法将配置数据保存到文件 { $name }, 原因 { $reason }.\nsaving_loading_reset_configuration = 当前配置已被清除。.\nsaving_loading_loading_success = 正确加载应用程序配置。.\nsaving_loading_failed_to_create_config_file = 无法创建配置文件 \"{ $path }\", 原因\"{ $reason }\".\nsaving_loading_failed_to_read_config_file = 无法从 \"{ $path }\" 加载配置，因为它不存在或不是文件。.\nsaving_loading_failed_to_read_data_from_file = 无法从文件读取数据\"{ $path }\", 原因\"{ $reason }\".\n# Other\nselected_all_reference_folders = 当所有目录被设置为参考文件夹时，无法开始搜索\nsearching_for_data = 正在搜索数据，可能需要一段时间，请稍候...\ntext_view_messages = 消息\ntext_view_warnings = 警告\ntext_view_errors = 错误\nabout_window_motto = 本程序永久免费。.\nkrokiet_new_app = Czkawka正处于维护模式，这意味着只能修复关键的bug并且不会添加新的功能。 关于新的功能，请查看新的 Krokiet 应用，它更加稳定和性能更强，仍在开发中。.\n# Various dialog\ndialogs_ask_next_time = 下次询问\nsymlink_failed = 无法符号链接 { $name } 到 { $target }, 原因 { $reason }\ndelete_title_dialog = 删除确认\ndelete_question_label = 您确定要删除文件吗？\ndelete_all_files_in_group_title = 确认删除组中的所有文件\ndelete_all_files_in_group_label1 = 在某些组中，所有记录都被选中。.\ndelete_all_files_in_group_label2 = 您确定要删除它们吗？\ndelete_items_label = { $items } 文件将被删除。.\ndelete_items_groups_label = 来自 { $groups } 个组中的 { $items } 个文件将被删除。.\nhardlink_failed = 无法将 { $name } 到 { $target }链接，原因 { $reason }\nhard_sym_invalid_selection_title_dialog = 对某些组的选择无效\nhard_sym_invalid_selection_label_1 = 在某些组中，只选择了一个记录，它将被忽略。.\nhard_sym_invalid_selection_label_2 = 要能够链接到这些文件，至少需要选择两个组的结果。.\nhard_sym_invalid_selection_label_3 = 第一个组被承认为原始组别，没有改变，但是第二个组别后来被修改。.\nhard_sym_link_title_dialog = 链接确认\nhard_sym_link_label = 您确定要链接这些文件吗？\nmove_folder_failed = 无法移动文件夹 { $name }, 原因 { $reason }\nmove_file_failed = 移动文件 { $name } 失败，原因 { $reason }\nmove_files_title_dialog = 选择要移动重复文件的文件夹\nmove_files_choose_more_than_1_path = 只能选择一个路径来复制重复的文件，选择 { $path_number }。.\nmove_stats = 正确移动 { $num_files }/{ $all_files } 个项目\nsave_results_to_file = 将结果保存到 txt 和 json 文件到 \"{ $name }\" 文件夹。.\nsearch_not_choosing_any_music = 错误：您必须选择至少一个带有音乐搜索类型的复选框。.\nsearch_not_choosing_any_broken_files = 错误：您必须选择至少一个带有选中文件类型的复选框。.\ninclude_folders_dialog_title = 要包含的文件夹\nexclude_folders_dialog_title = 要排除的文件夹\ninclude_manually_directories_dialog_title = 手动添加目录\ncache_properly_cleared = 已正确清除缓存\ncache_clear_duplicates_title = 清除重复缓存\ncache_clear_similar_images_title = 清除相似图像缓存\ncache_clear_similar_videos_title = 正在清除类似视频缓存\ncache_clear_message_label_1 = 您想要清除过时条目的缓存吗？\ncache_clear_message_label_2 = 此操作将删除所有指向无效文件的缓存项。.\ncache_clear_message_label_3 = 这可能会稍微加速加载/保存到缓存。.\ncache_clear_message_label_4 = 警告：操作将从未接入的外部驱动器中移除所有缓存数据。所以每个散列都需要重新生成。.\n# Show preview\npreview_image_resize_failure = 调整图像 { $name } 的大小失败.\npreview_image_opening_failure = 打开镜像 { $name } 失败，原因 { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = 组 { $current_group }/{ $all_groups } ({ $images_in_group } 图像)\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n/zh-TW/czkawka_gui.ftl",
    "content": "# Window titles\nwindow_settings_title = 設定\nwindow_main_title = Czkawka\nwindow_progress_title = 掃描中\nwindow_compare_images = 比較影像\n# General\ngeneral_ok_button = 確定\ngeneral_close_button = 關閉\n# Krokiet info dialog\nkrokiet_info_title = 介紹 Krokiet - Czkawka 的新版本\nkrokiet_info_message = \n        考基特是新的、改良的、更快速且更可靠的Czkawka GTK GUI版本！\n \n        它更容易運行，並且更能抵抗系統變動，因為它僅依賴於大多數系統預設可用的核心函式庫。\n \n        考基特還帶來了Czkawka所沒有的功能，包括影片比較模式下的縮圖、EXIF清理器、檔案移動/複製/刪除進度或擴展的排序選項。\n \n        試試看，看看區別吧！\n \n        Czkawka將繼續由我提供錯誤修復和輕微更新，但所有新功能都將專門為考基特開發，任何人都可以在自由地貢獻新的功能、添加缺失的模式或進一步擴展Czkawka。\n \n        PS：這則訊息只應該出現一次。如果它再次出現，請將CZKAWKA_DONT_ANNOY_ME環境變數設定為任何非空值。.\n# Main window\nmusic_title_checkbox = 標題\nmusic_artist_checkbox = 藝人\nmusic_year_checkbox = 年份\nmusic_bitrate_checkbox = 位元率\nmusic_genre_checkbox = 類型\nmusic_length_checkbox = 長度\nmusic_comparison_checkbox = 近似比較\nmusic_checking_by_tags = 標籤\nmusic_checking_by_content = 內容\nsame_music_seconds_label = 最小片段秒數\nsame_music_similarity_label = 最大差異\nmusic_compare_only_in_title_group = 比較相同標題類群之間\nmusic_compare_only_in_title_group_tooltip = \n    當啟用時，檔案會按照標題分組然後相互比較。\n    \n    有萬個檔案，通常會有的近乎十億次比較，反之只會約有二萬次比較。.\nsame_music_tooltip = \n    透過以下設定，可以根據內容搜尋相似的音樂檔案：\n    \n    - 音樂檔案在超過最小片段時間後可以被識別為相似\n    - 兩個測試片段之間允許的最大差異\n    \n    要得到理想的結果，關鍵是找到這些參數的合適組合。\n    \n    例如，將最小時間設定為 5 秒，最大差異設定為 1.0，會尋找檔案中幾乎相同的片段。\n    而設定時間為 20 秒和最大差異為 6.0，則適用於尋找混音版本或現場版本等。\n    \n    預設情況下，每個音樂檔案都會與其他檔案彼此進行比較，這在測試大量檔案時會非常耗時。因此，通常更建議使用參考資料夾，並明確指定哪些檔案需要相互比較。如果檔案數量相同，使用參考資料夾進行指紋比較的速度至少會比不使用參考資料夾快 4 倍。.\nmusic_comparison_checkbox_tooltip =\n    它利用 AI 搜尋相似的音樂檔案，該 AI 使用機器學習來去除句子中的括號。例如，啟用這個選項後，以下的檔案將被視為重複檔案：\n    \n    Świędziżłób     ---     Świędziżłób (Remix Lato 2021)\nduplicate_case_sensitive_name = 區分大小寫\nduplicate_case_sensitive_name_tooltip =\n    啟用後，只有在檔案名稱完全相同的情況下才會將其分組，例如 Żołd <-> Żołd。\n    \n    停用這個選項則會在不檢查每個字母大小是否相同的情況下進行分組，例如 żoŁD <-> Żołd。\nduplicate_mode_size_name_combo_box = 大小和名稱\nduplicate_mode_name_combo_box = 名稱\nduplicate_mode_size_combo_box = 大小\nduplicate_mode_hash_combo_box = 雜湊\nduplicate_hash_type_tooltip = \n    Czkawka 提供三種類型的雜湊：\n    \n    Blake3 - 這是一種加密雜湊函式，也是預設選項，主要因為它的計算速度非常快。\n    \n    CRC32 - 這是一種簡單的雜湊函式。理論上它比 Blake3 更快，雖然機率很低但有時可能會產生碰撞。\n    \n    XXH3 - 在效能和雜湊品質上與 Blake3 非常相似，但它不是加密型的。因此，這兩種模式可以輕易地互換使用。.\nduplicate_check_method_tooltip = \n    目前，Czkawka 提供三種方法來找出重複檔案：\n    \n    名稱 - 找出名稱相同的檔案。\n    \n    大小 - 找出大小相同的檔案。\n    \n    雜湊 - 找出內容相同的檔案。這個模式會先對檔案進行雜湊運算，然後比較這些雜湊值來識別重複檔案。這是找出重複檔案最安全的方式。由於應用程式大量使用快取，對同一組資料進行的第二次及後續掃描會比第一次快得多。.\nimage_hash_size_tooltip = \n    每個檢查的圖片會產生一個可用來來互相比較的的特定的雜湊值，它們之間些微的差異則代表這些圖片是相似的。\n    \n    8 雜湊大小是相當不錯用以尋找與原版僅些微相似的圖片。對於更大規模的圖片組（>1000），則會產生大量的誤報，所以在此情形中推薦使用更大的雜湊大小。\n    \n    16 是預設的雜湊大小是相當不錯的折衷方案，對於尋找即使僅些微相似的圖片，並且只會有少量的雜湊衝突。\n    \n    32 與 64 雜湊值用於尋找非常相似的圖片，但應該幾乎沒有誤報（也許除了一些具有 Alpha 通道的圖片）。.\nimage_resize_filter_tooltip = \n    要計算圖片的雜湊值，函式庫必須先對它進行調整大小。\n    \n    取決於選取的演算法，用於計算雜湊值的圖片將會看起來有些不同。\n    \n    最快的演算法是 Nearest，但也許會給出最差結果。預設為啟用，因為 16x16 雜湊大小並不是明顯可見的較低品質。\n    \n    對於 8x8 雜湊大小，建議使用不同於 Nearest 的演算法，以獲得更好的圖片分組。.\nimage_hash_alg_tooltip = \n    使用者可以從許多計算雜湊值的演算法中選擇一種。\n    \n    每種演算法都有強項和弱項，對於不同的圖片，有時會有更好的結果，有時會有更差的結果。\n    \n    因此，為了確定最適合你的演算法，需要進行人工測試。.\nbig_files_mode_combobox_tooltip = 允許搜尋最小/最大的檔案\nbig_files_mode_label = 已檢查的檔案\nbig_files_mode_smallest_combo_box = 最小的\nbig_files_mode_biggest_combo_box = 最大的\nmain_notebook_duplicates = 重複檔案\nmain_notebook_empty_directories = 空目錄\nmain_notebook_big_files = 大檔案\nmain_notebook_empty_files = 空檔案\nmain_notebook_temporary = 臨時檔案\nmain_notebook_similar_images = 相似影像\nmain_notebook_similar_videos = 相似影片\nmain_notebook_same_music = 音樂重複\nmain_notebook_symlinks = 無效的符號連結\nmain_notebook_broken_files = 損壞的檔案\nmain_notebook_bad_extensions = 錯誤的副檔名\nmain_tree_view_column_file_name = 檔案名稱\nmain_tree_view_column_folder_name = 資料夾名稱\nmain_tree_view_column_path = 路徑\nmain_tree_view_column_modification = 修改日期\nmain_tree_view_column_size = 大小\nmain_tree_view_column_similarity = 相似度\nmain_tree_view_column_dimensions = 尺寸\nmain_tree_view_column_title = 標題\nmain_tree_view_column_artist = 藝人\nmain_tree_view_column_year = 年份\nmain_tree_view_column_bitrate = 位元率\nmain_tree_view_column_length = 長度\nmain_tree_view_column_genre = 類型\nmain_tree_view_column_symlink_file_name = 符號連結檔案名稱\nmain_tree_view_column_symlink_folder = 符號連結資料夾\nmain_tree_view_column_destination_path = 目標路徑\nmain_tree_view_column_type_of_error = 錯誤類型\nmain_tree_view_column_current_extension = 現有副檔名\nmain_tree_view_column_proper_extensions = 適當的副檔名\nmain_tree_view_column_fps = 每秒幀數\nmain_tree_view_column_codec = 編碼解碼器\nmain_label_check_method = 檢查方法\nmain_label_hash_type = 雜湊類型\nmain_label_hash_size = 雜湊大小\nmain_label_size_bytes = 大小（位元組）\nmain_label_min_size = 最小\nmain_label_max_size = 最大\nmain_label_shown_files = 顯示的檔案數\nmain_label_resize_algorithm = 調整大小的演算法\nmain_label_similarity = 相似度：{ \" \" }\nmain_check_box_broken_files_audio = 音訊\nmain_check_box_broken_files_pdf = PDF\nmain_check_box_broken_files_archive = 歸檔\nmain_check_box_broken_files_image = 影像\nmain_check_box_broken_files_video = 影片\nmain_check_box_broken_files_video_tooltip = 使用 ffmpeg/ffprobe 驗證影片檔案。相當慢，且可能偵測到刻板錯誤，即使檔案播放正常。.\ncheck_button_general_same_size = 忽略相同的大小\ncheck_button_general_same_size_tooltip = 忽略在結果中具有完全相同大小的檔案 - 通常這些是 1:1 的重複\nmain_label_size_bytes_tooltip = 將用於掃描的檔案大小\n# Upper window\nupper_tree_view_included_folder_column_title = 要搜尋的資料夾\nupper_tree_view_included_reference_column_title = 參考資料夾\nupper_recursive_button = 遞迴\nupper_recursive_button_tooltip = 如果選取，也會搜尋未直接放在選定資料夾下的檔案。.\nupper_manual_add_included_button = 手動新增\nupper_add_included_button = 新增\nupper_remove_included_button = 移除\nupper_manual_add_excluded_button = 手動新增\nupper_add_excluded_button = 新增\nupper_remove_excluded_button = 移除\nupper_manual_add_included_button_tooltip =\n    手動新增目錄名稱。\n    \n    一次新增多個路徑，用分號(;)分隔它們\n    \n    /home/roman;/home/rozkaz 將新增兩個目錄 /home/roman 和 /home/rozkaz\nupper_add_included_button_tooltip = 新增新目錄進行搜尋。.\nupper_remove_included_button_tooltip = 從搜尋中移除目錄。.\nupper_manual_add_excluded_button_tooltip =\n    手動新增要排除的目錄名稱。\n    \n    一次新增多個路徑，請用分號(;)分隔它們\n    \n    /home/roman;/home/krokiet 將新增兩個目錄 /home/roman 和 /home/krokiet\nupper_add_excluded_button_tooltip = 新增要在搜尋中排除的目錄。.\nupper_remove_excluded_button_tooltip = 從排除中移除目錄。.\nupper_notebook_items_configuration = 項目設定\nupper_notebook_excluded_directories = 排除路徑\nupper_notebook_included_directories = 包含的路徑\nupper_allowed_extensions_tooltip = \n    允許的副檔名必須用逗號分隔（預設所有可用）。\n    \n    以下的巨集也可用，可以一次新增多個副檔名：IMAGE, VIDEO, MUSIC, TEXT。\n    \n    使用範例 \".exe, IMAGE, VIDEO, .rar, 7z\" - 這表示將影像檔案（例如 .jpg, .png)、影片檔案（例如 .avi, .mp4)、.exe、.rar 和 .7z 檔案。.\nupper_excluded_extensions_tooltip = \n    在掃描中將會被忽略的禁用檔案清單。\n    \n    當使同時使用允許與禁用兩者時，此項擁有更高的優先等級，所以檔案將不會被檢查。.\nupper_excluded_items_tooltip = \n        排除的項目必須包含 * 萬位符號，並且用逗號分隔。\n        這比排除路徑慢，所以請謹慎使用。.\nupper_excluded_items = 排除的項目：\nupper_allowed_extensions = 允許的副檔名：\nupper_excluded_extensions = 禁用的副檔名：\n# Popovers\npopover_select_all = 選擇全部\npopover_unselect_all = 取消選擇全部\npopover_reverse = 反向選擇\npopover_select_all_except_shortest_path = 選擇所有，除最短路徑\npopover_select_all_except_longest_path = 選擇所有，不包括最長路徑\npopover_select_all_except_oldest = 選擇除最舊以外的全部\npopover_select_all_except_newest = 選擇除最新以外的全部\npopover_select_one_oldest = 選擇一個最舊的\npopover_select_one_newest = 選擇一個最新的\npopover_select_custom = 選擇自訂\npopover_unselect_custom = 取消選擇自訂\npopover_select_all_images_except_biggest = 選擇除最大以外的全部影像\npopover_select_all_images_except_smallest = 選擇除最小以外的全部影像\npopover_custom_path_check_button_entry_tooltip =\n    透過路徑選擇記錄。\n    \n    範例用法：\n    /home/pimpek/rzecz.txt 可以透過 /home/pim* 找到\npopover_custom_name_check_button_entry_tooltip =\n    透過檔名選擇記錄。\n    \n    範例用法：\n    /usr/ping/pong.txt 可以在 *ong* 中找到。\npopover_custom_regex_check_button_entry_tooltip = \n    透過指定的正規表達式（Regex）來選擇記錄。\n    \n    在這個模式下，被搜尋的文字是「路徑」加上「名稱」。\n    \n    範例用法：\n    使用 /ziem[a-z]+ 可以找到 /usr/bin/ziemniak.txt。\n    \n    這個功能使用的是 Rust 語言預設的正規表達式實作。更多相關資訊，您可以參考這個網址： https://docs.rs/regex。.\npopover_custom_case_sensitive_check_button_tooltip = \n    啟用區分大小寫的偵測。\n    \n    當此選項停用時，「/home/*」會同時找到「/HoMe/roman」和「/home/roman」。.\npopover_custom_not_all_check_button_tooltip = \n    防止在同一群組中全選所有記錄。\n    \n    這個選項預設是啟用的，主要是因為在多數情況下，您不會想要同時刪除原始檔案和其重複檔，而是會希望至少保留一個檔案。\n    \n    警告：如果您已經手動全選了某一群組中的所有結果，這個設定將不會生效。.\npopover_custom_regex_path_label = 路徑\npopover_custom_regex_name_label = 名稱\npopover_custom_regex_regex_label = 正規表達式路徑 + 名稱\npopover_custom_case_sensitive_check_button = 區分大小寫\npopover_custom_all_in_group_label = 不要選取群組中的所有記錄\npopover_custom_mode_unselect = 取消選擇自訂\npopover_custom_mode_select = 選擇自訂\npopover_sort_file_name = 檔案名稱\npopover_sort_folder_name = 資料夾名稱\npopover_sort_full_name = 完整名稱\npopover_sort_size = 大小\npopover_sort_selection = 選擇\npopover_invalid_regex = 正規表達式無效\npopover_valid_regex = 正規表達式有效\n# Bottom buttons\nbottom_search_button = 搜尋\nbottom_select_button = 選擇\nbottom_delete_button = 刪除\nbottom_save_button = 儲存\nbottom_symlink_button = 符號連結\nbottom_hardlink_button = 永久連結\nbottom_move_button = 移動\nbottom_sort_button = 排序\nbottom_compare_button = 比較\nbottom_search_button_tooltip = 開始搜尋\nbottom_select_button_tooltip = 選擇記錄。只能稍後處理選定的檔案/資料夾。.\nbottom_delete_button_tooltip = 刪除選取的檔案/資料夾。.\nbottom_save_button_tooltip = 儲存搜尋資料到檔案\nbottom_symlink_button_tooltip = \n    建立符號連結。\n    只有在一個群組中至少選擇了兩個結果時才會生效。\n    第一個檔案保持不變，第二個以及之後的檔案會建立為指向第一個檔案的符號連結。.\nbottom_hardlink_button_tooltip = \n    建立永久連結。\n    只有在一個群組中至少選擇了兩個結果時才會生效。\n    第一個檔案保持不變，第二個以及之後的檔案會建立為與第一個檔案的永久連結。.\nbottom_hardlink_button_not_available_tooltip = \n    建立永久連結。\n    此按鈕已被停用，因為無法建立永久連結。\n    在 Windows 上，只有擁有管理員權限才能建立永久連結，請確保以管理員身份執行應用程式。\n    如果應用程式已經具有對應的權限，請在 GitHub 上查詢相關問題。.\nbottom_move_button_tooltip = \n    將檔案移動到指定目錄。\n    會將所有檔案複製到目錄中，但不會保留原始的目錄結構。\n    如果試圖將兩個同名檔案移動到同一資料夾，第二個檔案將無法移動並會顯示錯誤。.\nbottom_sort_button_tooltip = 根據選定的方法排序檔案/資料夾。.\nbottom_compare_button_tooltip = 比較群組中的圖像。.\nbottom_show_errors_tooltip = 顯示/隱藏底部文字面板。.\nbottom_show_upper_notebook_tooltip = 顯示/隱藏主筆記本面板。.\n# Progress Window\nprogress_stop_button = 停止\nprogress_stop_additional_message = 已請求停止\n# About Window\nabout_repository_button_tooltip = 連結到原始碼的專案。.\nabout_donation_button_tooltip = 連結到贊助頁面。.\nabout_instruction_button_tooltip = 連結到指令頁面。.\nabout_translation_button_tooltip = 連結到帶有應用程式翻譯的 Crowdin 頁面。官方支援波蘭語和英語。.\nabout_repository_button = 儲存庫\nabout_donation_button = 贊助\nabout_instruction_button = 說明\nabout_translation_button = 翻譯\n# Header\nheader_setting_button_tooltip = 開啟設定對話方塊。.\nheader_about_button_tooltip = 開啟包含應用程式資訊的對話方塊。.\n \n# Settings\n\n\n## General\n\nsettings_number_of_threads = 使用的執行緒數\nsettings_number_of_threads_tooltip = 使用的執行緒數，0 表示所有可用執行緒都將被使用。.\nsettings_use_rust_preview = 使用外部庫 Instead gtk 來加載預覽\nsettings_use_rust_preview_tooltip = \n    使用 gtk 預覽通常會較快且支援更多格式，但有時這可能會正好相反。\n    \n    如果您在載入預覽時遇到問題，您可以嘗試更變這個設定。\n    \n    於非 Linux 系統中，建議使用此選項，因為 gtk-pixbuf 不總是在這些系統中可用，因而禁用此選項將無法載入某些圖像的預覽。.\nsettings_label_restart = 您需要重新啟動應用程式才能套用設定！\nsettings_ignore_other_filesystems = 忽略其它檔案系統（僅限 Linux）\nsettings_ignore_other_filesystems_tooltip =\n    忽略與搜尋的目錄不在同一個檔案系統中的檔案。\n    \n    在 Linux 上查詢命令時類似 -xdev 選項\nsettings_save_at_exit_button_tooltip = 關閉應用程式時將設定儲存到檔案。.\nsettings_load_at_start_button_tooltip = \n    開啟應用程式時從檔案載入設定。\n    \n    如果未啟用，將使用預設設定。.\nsettings_confirm_deletion_button_tooltip = 點選刪除按鈕時顯示確認對話方塊。.\nsettings_confirm_link_button_tooltip = 點選永久連結/符號連結按鈕時顯示確認對話方塊。.\nsettings_confirm_group_deletion_button_tooltip = 嘗試從群組中刪除所有記錄時顯示警告對話方塊。.\nsettings_show_text_view_button_tooltip = 在使用者介面底部顯示文字面板。.\nsettings_use_cache_button_tooltip = 使用檔案快取。.\nsettings_save_also_as_json_button_tooltip = 儲存快取為（人類可讀）JSON 格式。可以修改其內容。 如果缺少二進位制格式快取（帶bin extensional)，此檔案的快取將被應用程式自動讀取。.\nsettings_use_trash_button_tooltip = 將檔案移至回收桶，而將其永久刪除。.\nsettings_language_label_tooltip = 使用者介面的語言。.\nsettings_save_at_exit_button = 關閉應用程式時儲存設定\nsettings_load_at_start_button = 開啟應用程式時載入設定\nsettings_confirm_deletion_button = 刪除任何檔案時顯示確認對話方塊\nsettings_confirm_link_button = 硬/符號連結任何檔案時顯示確認對話方塊\nsettings_confirm_group_deletion_button = 刪除群組中所有檔案時顯示確認對話方塊\nsettings_show_text_view_button = 顯示底部文字面板\nsettings_use_cache_button = 使用快取\nsettings_save_also_as_json_button = 同時將快取儲存為 JSON 檔案\nsettings_use_trash_button = 移動已刪除的檔案到回收桶\nsettings_language_label = 語言\nsettings_multiple_delete_outdated_cache_checkbutton = 自動刪除過時的快取項目\nsettings_multiple_delete_outdated_cache_checkbutton_tooltip = \n    刪除指向不存在檔案的過時快取結果。\n    \n    啟用後，應用程式在載入記錄時會確保所有記錄都指向有效的檔案（無效的檔案會被忽略）。\n    \n    停用此選項將有助於掃描外部硬碟上的檔案，這樣下次掃描時有關這些檔案的快取項目不會被清除。\n    \n    若快取中有數十萬條記錄，建議啟用此選項，這將加速掃描開始和結束時的快取載入和儲存。.\nsettings_notebook_general = 一般\nsettings_notebook_duplicates = 重複項目\nsettings_notebook_images = 相似影像\nsettings_notebook_videos = 相似影片\n\n## Multiple - settings used in multiple tabs\n\nsettings_multiple_image_preview_checkbutton_tooltip = 在右側顯示預覽（當選擇影像檔案時）。.\nsettings_multiple_image_preview_checkbutton = 顯示影像預覽\nsettings_multiple_clear_cache_button_tooltip = \n    手動清除過時項目的快取。\n    僅在停用自動清除時才應使用。.\nsettings_multiple_clear_cache_button = 從快取中移除過時結果.\n \n## Duplicates\n\nsettings_duplicates_hide_hard_link_button_tooltip = \n    如果所有檔案都指向相同的資料（即為永久連結），則隱藏除一個以外的所有檔案。\n    \n    例如：在有七個檔案與特定資料有永久連結，以及一個具有相同資料但不同 inode 的不同檔案的情況下，重複檔案檢查工具只會顯示一個獨特的檔案和一個來自永久連結的檔案。.\nsettings_duplicates_minimal_size_entry_tooltip = \n    設定將被快取的最小檔案大小。\n    \n    選擇較小的值會產生更多記錄。這會加速搜尋，但會減慢快取的載入和儲存。.\nsettings_duplicates_prehash_checkbutton_tooltip = \n    啟用預先計算的雜湊（從檔案的一小部分計算出來）的快取，這允許更早地排除非重複的結果。\n    \n    這個選項預設是停用的，因為在某些情況下它可能會造成減速。\n    \n    當掃描數十萬或百萬個檔案時，強烈建議使用此選項，因為它可以多倍加速搜尋。.\nsettings_duplicates_prehash_minimal_entry_tooltip = 快取項目的最小大小。.\nsettings_duplicates_hide_hard_link_button = 隱藏硬連結\nsettings_duplicates_prehash_checkbutton = 使用捕捉快取\nsettings_duplicates_minimal_size_cache_label = 儲存到快取的檔案最小大小（位元組）\nsettings_duplicates_minimal_size_cache_prehash_label = 檔案最小大小（位元組）儲存到逮捕快取\n\n## Saving/Loading settings\n\nsettings_saving_button_tooltip = 儲存目前設定設定到檔案。.\nsettings_loading_button_tooltip = 從檔案載入設定並替換目前設定。.\nsettings_reset_button_tooltip = 重設目前設定為預設設定。.\nsettings_saving_button = 儲存設定\nsettings_loading_button = 載入設定\nsettings_reset_button = 重設設定\n\n## Opening cache/config folders\n\nsettings_folder_cache_open_tooltip = \n    開啟儲存快取 txt 檔案的資料夾。\n    \n    修改快取檔案可能會導致顯示無效的結果。然而，如果需要將大量檔案移動到不同位置，修改路徑可能會節省時間。\n    \n    如果兩台電腦有類似的目錄結構，您可以在它們之間複製這些檔案，以節省重新掃描檔案的時間。\n    \n    如果快取有問題，這些檔案可以被移除。應用程式會自動重新產生它們。.\nsettings_folder_settings_open_tooltip = \n    開啟儲存 Czkawka 設定的資料夾。\n    \n    警告：手動修改設定可能會影響您的工作流程。.\nsettings_folder_cache_open = 開啟快取資料夾\nsettings_folder_settings_open = 開啟設定資料夾\n# Compute results\ncompute_stopped_by_user = 搜尋已被使用者停止\ncompute_found_duplicates_hash_size = 找到{ $number_files }個重複檔案在{ $number_groups }組中，佔用了{ $size }，在{ $time }內\ncompute_found_duplicates_name = 發現{ $number_files }個重複檔在{ $number_groups }組中於{ $time }\ncompute_found_empty_folders = 找到{ $number_files }個空資料夾在{ $time }\ncompute_found_empty_files = 找到{ $number_files }個空文件在{ $time }\ncompute_found_big_files = 找到{ $number_files }個大檔案在{ $time }\ncompute_found_temporary_files = 找到 { $number_files } 個臨時文件在{ $time }\ncompute_found_images = 找到{ $number_files }張相似圖像在{ $number_groups }組中，在{ $time }內\ncompute_found_videos = 找到{ $number_files }個相似影片，在{ $number_groups }組中，耗時{ $time }\ncompute_found_music = 找到{ $number_files }首相似音樂檔案在{ $number_groups }組中，在{ $time }內\ncompute_found_invalid_symlinks = 發現 { $number_files } 個無效的符徵鏈接在 { $time }\ncompute_found_broken_files = 找到 { $number_files } 個損壞檔案在 { $time }\ncompute_found_bad_extensions = 找到{ $number_files }個擴展名無效的檔案在{ $time }\n# Progress window\nprogress_scanning_general_file =\n    { $file_number ->\n        [one] Scanned { $file_number } file\n       *[other] Scanned { $file_number } files\n    }\nprogress_scanning_extension_of_files = Checked extension of { $file_checked }/{ $all_files } file\nprogress_scanning_broken_files = Checked { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data })\nprogress_scanning_video = Hashed of { $file_checked }/{ $all_files } video\nprogress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video\nprogress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data })\nprogress_comparing_image_hashes = Compared { $file_checked }/{ $all_files } image hash\nprogress_scanning_music_tags_end = Compared tags of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_tags = Read tags of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_content_end = Compared fingerprint of { $file_checked }/{ $all_files } music file\nprogress_scanning_music_content = Calculated fingerprint of { $file_checked }/{ $all_files } music file ({ $data_checked }/{ $all_data })\nprogress_scanning_empty_folders =\n    { $folder_number ->\n        [one] Scanned { $folder_number } folder\n       *[other] Scanned { $folder_number } folders\n    }\nprogress_scanning_size = Scanned size of { $file_number } file\nprogress_scanning_size_name = Scanned name and size of { $file_number } file\nprogress_scanning_name = Scanned name of { $file_number } file\nprogress_analyzed_partial_hash = Analyzed partial hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data })\nprogress_analyzed_full_hash = Analyzed full hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data })\nprogress_prehash_cache_loading = 正在載入 PreHash 快取\nprogress_prehash_cache_saving = 正在儲存 PreHash 快取\nprogress_hash_cache_loading = 正在載入雜湊快取\nprogress_hash_cache_saving = 正在儲存雜湊快取\nprogress_cache_loading = 正在載入快取\nprogress_cache_saving = 正在儲存快取\nprogress_current_stage = 目前階段：{ \"  \" }\nprogress_all_stages = 所有階段：{ \" \" }\n# Saving loading \nsaving_loading_saving_success = 設定儲存到檔案 { $name }。.\nsaving_loading_saving_failure = 失敗將配置資料存儲至檔案 { $name }，原因 { $reason }。.\nsaving_loading_reset_configuration = 目前設定已被清除。.\nsaving_loading_loading_success = 正確載入應用程式設定。.\nsaving_loading_failed_to_create_config_file = 無法建立設定檔案 \"{ $path }\", 原因\"{ $reason }\".\nsaving_loading_failed_to_read_config_file = 無法從 \"{ $path }\" 載入設定，因為它不存在或不是檔案。.\nsaving_loading_failed_to_read_data_from_file = 無法從檔案讀取資料\"{ $path }\", 原因\"{ $reason }\".\n# Other\nselected_all_reference_folders = 當所有目錄被設定為參考資料夾時，無法開始搜尋\nsearching_for_data = 正在搜尋資料，可能需要一段時間，請稍候...\ntext_view_messages = 訊息\ntext_view_warnings = 警告\ntext_view_errors = 錯誤\nabout_window_motto = 這個程式可以永遠自由使用。.\nkrokiet_new_app = Czkawka處於維護模式，這意味著 hanya 會修復關鍵錯誤而不會添加新功能。對於新功能，請檢視新的Krokietapp，該應用更為穩定且效能更好，並且仍然處於積極開發中。.\n# Various dialog\ndialogs_ask_next_time = 下次詢問\nsymlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\ndelete_title_dialog = 刪除確認\ndelete_question_label = 您確定要刪除檔案嗎？\ndelete_all_files_in_group_title = 確認刪除群組中的所有檔案\ndelete_all_files_in_group_label1 = 在某些群組中，所有記錄都被選取。.\ndelete_all_files_in_group_label2 = 您確定要刪除它們嗎？\ndelete_items_label = { $items } 檔案將被刪除。.\ndelete_items_groups_label = { $items } 檔案來自 { $groups } 群組將被刪除。.\nhardlink_failed = 無法硬連結 { $name } 至 { $target }，理由 { $reason }\nhard_sym_invalid_selection_title_dialog = 對某些群組的選擇無效\nhard_sym_invalid_selection_label_1 = 在某些群組中，只選擇了一個記錄，它將被忽略。.\nhard_sym_invalid_selection_label_2 = 要能夠連結到這些檔案，至少需要選擇兩個群組的結果。.\nhard_sym_invalid_selection_label_3 = 第一個群組被承認為原始群組，沒有改變，但是第二個群組後來被修改。.\nhard_sym_link_title_dialog = 連結確認\nhard_sym_link_label = 您確定要連結這些檔案嗎？\nmove_folder_failed = 無法移動資料夾 { $name }, 原因 { $reason }\nmove_file_failed = 移動檔案 { $name } 失敗，原因 { $reason }\nmove_files_title_dialog = 選擇要移動重複檔案的資料夾\nmove_files_choose_more_than_1_path = 只能選擇一個路徑來複製重複的檔案，選擇 { $path_number }。.\nmove_stats = 正確移動 { $num_files }/{ $all_files } 專案\nsave_results_to_file = Saved results both to txt and json files into \"{ $name }\" folder.\nsearch_not_choosing_any_music = 錯誤：您必須選擇至少一個帶有音樂搜尋類型的核取方塊。.\nsearch_not_choosing_any_broken_files = 錯誤：您必須選擇至少一個帶有選取檔案類型的核取方塊。.\ninclude_folders_dialog_title = 要包含的資料夾\nexclude_folders_dialog_title = 要排除的資料夾\ninclude_manually_directories_dialog_title = 手動新增目錄\ncache_properly_cleared = 已正確清除快取\ncache_clear_duplicates_title = 清除重複快取\ncache_clear_similar_images_title = 清除相似影像快取\ncache_clear_similar_videos_title = 正在清除相似影片快取\ncache_clear_message_label_1 = 您想要清除過時項目的快取嗎？\ncache_clear_message_label_2 = 此操作將刪除所有指向無效檔案的快取項。.\ncache_clear_message_label_3 = 這可能會稍微加速載入/儲存到快取。.\ncache_clear_message_label_4 = 警告：操作將從未接入的外部硬碟中移除所有快取資料。所以每個雜湊都需要重新產生。.\n# Show preview\npreview_image_resize_failure = 調整影像大小失敗 { $name }.\npreview_image_opening_failure = 開啟影像 { $name } 失敗，原因 { $reason }\n# Compare images (L is short Left, R is short Right - they can't take too much space)\ncompare_groups_number = 組 { $current_group }/{ $all_groups } ({ $images_in_group } 影像）\ncompare_move_left_button = L\ncompare_move_right_button = R\n"
  },
  {
    "path": "czkawka_gui/i18n.toml",
    "content": "# (Required) The language identifier of the language used in the\n# source code for gettext system, and the primary fallback language\n# (for which all strings must be present) when using the fluent\n# system.\nfallback_language = \"en\"\n\n# Use the fluent localization system.\n[fluent]\n# (Required) The path to the assets directory.\n# The paths inside the assets directory should be structured like so:\n# `assets_dir/{language}/{domain}.ftl`\nassets_dir = \"i18n\"\n\n"
  },
  {
    "path": "czkawka_gui/src/compute_results.rs",
    "content": "use std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::rc::Rc;\nuse std::time::Duration;\n\nuse chrono::DateTime;\nuse crossbeam_channel::Receiver;\nuse czkawka_core::common::model::CheckingMethod;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::ResultEntry;\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::bad_extensions::BadExtensions;\nuse czkawka_core::tools::big_file::BigFile;\nuse czkawka_core::tools::broken_files::BrokenFiles;\nuse czkawka_core::tools::duplicate::DuplicateFinder;\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse czkawka_core::tools::empty_folder::EmptyFolder;\nuse czkawka_core::tools::invalid_symlinks::InvalidSymlinks;\nuse czkawka_core::tools::same_music::core::format_audio_duration;\nuse czkawka_core::tools::same_music::{MusicSimilarity, SameMusic};\nuse czkawka_core::tools::similar_images::core::get_string_from_similarity;\nuse czkawka_core::tools::similar_images::{ImagesEntry, SimilarImages};\nuse czkawka_core::tools::similar_videos::SimilarVideos;\nuse czkawka_core::tools::similar_videos::core::{format_bitrate_opt, format_duration_opt};\nuse czkawka_core::tools::temporary::Temporary;\nuse fun_time::fun_time;\nuse gtk4::prelude::*;\nuse gtk4::{Entry, ListStore, TextView};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\n\nuse crate::flg;\nuse crate::gui_structs::common_tree_view::{SharedModelEnum, SubView, TreeViewListStoreTrait};\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_combo_box::IMAGES_HASH_SIZE_COMBO_BOX;\nuse crate::help_functions::{HEADER_ROW_COLOR, MAIN_ROW_COLOR, TEXT_COLOR, print_text_messages_to_text_view, set_buttons};\nuse crate::helpers::enums::{\n    BottomButtonsEnum, ColumnsBadExtensions, ColumnsBigFiles, ColumnsBrokenFiles, ColumnsDuplicates, ColumnsEmptyFiles, ColumnsEmptyFolders, ColumnsInvalidSymlinks,\n    ColumnsSameMusic, ColumnsSimilarImages, ColumnsSimilarVideos, ColumnsTemporaryFiles, Message,\n};\nuse crate::helpers::list_store_operations::append_row_to_list_store;\nuse crate::notebook_enums::NotebookMainEnum;\nuse crate::notebook_info::NOTEBOOKS_INFO;\nuse crate::opening_selecting_records::{\n    select_function_always_true, select_function_duplicates, select_function_same_music, select_function_similar_images, select_function_similar_videos,\n};\n\n// Helper functions for deduplication\n\nfn handle_stopped_search<T: CommonData>(tool: &T, entry_info: &Entry) -> bool {\n    if tool.get_stopped_search() {\n        entry_info.set_text(&flg!(\"compute_stopped_by_user\"));\n        true\n    } else {\n        false\n    }\n}\n\n#[expect(clippy::unnecessary_wraps)]\nfn finalize_compute<T: Into<SharedModelEnum>>(subview: &SubView, tool: T, items_found: usize) -> Option<bool> {\n    subview.shared_model_enum.replace(tool.into());\n    Some(items_found > 0)\n}\n\nfn conditional_sort_vector<T>(vector: &[T]) -> Vec<T>\nwhere\n    T: ResultEntry + Clone + Send,\n{\n    if vector.len() >= 2 {\n        let mut vector = vector.to_vec();\n        vector.par_sort_unstable_by(|a, b| split_path_compare(a.get_path(), b.get_path()));\n        vector\n    } else {\n        vector.to_vec()\n    }\n}\n\nfn format_size_and_date(size: u64, modified_date: u64, is_header: bool, is_reference_folder: bool) -> (String, String) {\n    if is_header && !is_reference_folder {\n        (String::new(), String::new())\n    } else {\n        (format_size(size, BINARY), get_dt_timestamp_string(modified_date))\n    }\n}\n\nfn get_row_color(is_header: bool) -> &'static str {\n    if is_header { HEADER_ROW_COLOR } else { MAIN_ROW_COLOR }\n}\n\npub(crate) fn connect_compute_results(gui_data: &GuiData, result_receiver: Receiver<Message>) {\n    let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone();\n    let buttons_search = gui_data.bottom_buttons.buttons_search.clone();\n    let notebook_main = gui_data.main_notebook.notebook_main.clone();\n    let entry_info = gui_data.entry_info.clone();\n    let buttons_array = gui_data.bottom_buttons.buttons_array.clone();\n    let text_view_errors = gui_data.text_view_errors.clone();\n    let shared_buttons = gui_data.shared_buttons.clone();\n    let buttons_names = gui_data.bottom_buttons.buttons_names;\n    let window_progress = gui_data.progress_window.window_progress.clone();\n    let taskbar_state = gui_data.taskbar_state.clone();\n    let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();\n    let button_settings = gui_data.header.button_settings.clone();\n    let button_app_info = gui_data.header.button_app_info.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    let main_context = glib::MainContext::default();\n    let _guard = main_context.acquire().expect(\"Failed to acquire main context\");\n\n    glib::spawn_future_local(async move {\n        loop {\n            while let Ok(msg) = result_receiver.try_recv() {\n                buttons_search.set_visible(true);\n\n                notebook_main.set_sensitive(true);\n                notebook_upper.set_sensitive(true);\n                button_settings.set_sensitive(true);\n                button_app_info.set_sensitive(true);\n\n                window_progress.set_visible(false);\n\n                taskbar_state.borrow().hide();\n\n                let hash_size_index = combo_box_image_hash_size.active().expect(\"Failed to get active item\") as usize;\n                let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index] as u8;\n\n                let msg_type = msg.get_message_type();\n                let subview = common_tree_views.get_subview(msg_type);\n\n                let found_duplicates: Option<bool> = match msg {\n                    Message::Duplicates(df) => compute_duplicate_finder(df, &entry_info, &text_view_errors, subview),\n                    Message::EmptyFolders(ef) => compute_empty_folders(ef, &entry_info, &text_view_errors, subview),\n                    Message::EmptyFiles(vf) => compute_empty_files(vf, &entry_info, &text_view_errors, subview),\n                    Message::BigFiles(bf) => compute_big_files(bf, &entry_info, &text_view_errors, subview),\n                    Message::Temporary(tf) => compute_temporary_files(tf, &entry_info, &text_view_errors, subview),\n                    Message::SimilarImages(sf) => compute_similar_images(sf, &entry_info, &text_view_errors, subview, hash_size),\n                    Message::SimilarVideos(ff) => compute_similar_videos(ff, &entry_info, &text_view_errors, subview),\n                    Message::SameMusic(mf) => compute_same_music(mf, &entry_info, &text_view_errors, subview),\n                    Message::InvalidSymlinks(ifs) => compute_invalid_symlinks(ifs, &entry_info, &text_view_errors, subview),\n                    Message::BrokenFiles(br) => compute_broken_files(br, &entry_info, &text_view_errors, subview),\n                    Message::BadExtensions(be) => compute_bad_extensions(be, &entry_info, &text_view_errors, subview),\n                };\n\n                if let Some(found_duplicates) = found_duplicates {\n                    set_specific_buttons_as_active(&shared_buttons, msg_type, found_duplicates);\n\n                    set_buttons(\n                        &mut *shared_buttons.borrow_mut().get_mut(&msg_type).expect(\"Failed to borrow buttons\"),\n                        &buttons_array,\n                        &buttons_names,\n                    );\n                }\n            }\n            glib::timeout_future(Duration::from_millis(300)).await;\n        }\n    });\n}\n\n#[fun_time(message = \"compute_bad_extensions\", level = \"debug\")]\nfn compute_bad_extensions(be: BadExtensions, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&be, entry_info) {\n        return None;\n    }\n    let information = be.get_information();\n    let text_messages = be.get_text_messages();\n    let bad_extensions_number = information.number_of_files_with_bad_extension;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(flg!(\"compute_found_bad_extensions\", number_files = bad_extensions_number, time = scanning_time_str).as_str());\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let mut vector = be.get_bad_extensions_files().clone();\n    vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n    for file_entry in vector {\n        let (directory, file) = split_path(&file_entry.path);\n        let values: [(u32, &dyn ToValue); 7] = [\n            (ColumnsBadExtensions::SelectionButton as u32, &false),\n            (ColumnsBadExtensions::Name as u32, &file),\n            (ColumnsBadExtensions::Path as u32, &directory),\n            (ColumnsBadExtensions::CurrentExtension as u32, &file_entry.current_extension),\n            (ColumnsBadExtensions::ValidExtensions as u32, &file_entry.proper_extensions_group),\n            (ColumnsBadExtensions::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))),\n            (ColumnsBadExtensions::ModificationAsSecs as u32, &(file_entry.modified_date as i64)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, be, bad_extensions_number)\n}\n\n#[fun_time(message = \"compute_broken_files\", level = \"debug\")]\nfn compute_broken_files(br: BrokenFiles, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&br, entry_info) {\n        return None;\n    }\n    let information = br.get_information();\n    let text_messages = br.get_text_messages();\n    let broken_files_number = information.number_of_broken_files;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(flg!(\"compute_found_broken_files\", number_files = broken_files_number, time = scanning_time_str).as_str());\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let mut vector = br.get_broken_files().clone();\n    vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n    for file_entry in vector {\n        let (directory, file) = split_path(&file_entry.path);\n        let values: [(u32, &dyn ToValue); 6] = [\n            (ColumnsBrokenFiles::SelectionButton as u32, &false),\n            (ColumnsBrokenFiles::Name as u32, &file),\n            (ColumnsBrokenFiles::Path as u32, &directory),\n            (ColumnsBrokenFiles::ErrorType as u32, &file_entry.error_string),\n            (ColumnsBrokenFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))),\n            (ColumnsBrokenFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, br, broken_files_number)\n}\n\n#[fun_time(message = \"compute_invalid_symlinks\", level = \"debug\")]\nfn compute_invalid_symlinks(ifs: InvalidSymlinks, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&ifs, entry_info) {\n        return None;\n    }\n    let information = ifs.get_information();\n    let text_messages = ifs.get_text_messages();\n    let invalid_symlinks = information.number_of_invalid_symlinks;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\"compute_found_invalid_symlinks\", number_files = invalid_symlinks, time = scanning_time_str));\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let vector = conditional_sort_vector(ifs.get_invalid_symlinks());\n\n    for file_entry in vector {\n        let (directory, file) = split_path(&file_entry.path);\n        let symlink_info = file_entry.symlink_info;\n        let values: [(u32, &dyn ToValue); 7] = [\n            (ColumnsInvalidSymlinks::SelectionButton as u32, &false),\n            (ColumnsInvalidSymlinks::Name as u32, &file),\n            (ColumnsInvalidSymlinks::Path as u32, &directory),\n            (ColumnsInvalidSymlinks::DestinationPath as u32, &symlink_info.destination_path.to_string_lossy().to_string()),\n            (ColumnsInvalidSymlinks::TypeOfError as u32, &symlink_info.type_of_error.translate()),\n            (ColumnsInvalidSymlinks::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))),\n            (ColumnsInvalidSymlinks::ModificationAsSecs as u32, &(file_entry.modified_date as i64)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, ifs, invalid_symlinks)\n}\n\n#[fun_time(message = \"compute_same_music\", level = \"debug\")]\nfn compute_same_music(mf: SameMusic, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&mf, entry_info) {\n        return None;\n    }\n    if mf.get_use_reference() {\n        subview.tree_view.selection().set_select_function(select_function_always_true);\n    } else {\n        subview.tree_view.selection().set_select_function(select_function_same_music);\n    }\n\n    let information = mf.get_information();\n    let text_messages = mf.get_text_messages();\n\n    let same_music_number: usize = information.number_of_duplicates;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\n            \"compute_found_music\",\n            number_files = information.number_of_duplicates,\n            number_groups = information.number_of_groups,\n            time = scanning_time_str\n        ));\n    }\n\n    // Create GUI\n    {\n        let list_store = subview.tree_view.get_model();\n\n        let music_similarity = mf.get_params().music_similarity;\n\n        let is_track_title = (MusicSimilarity::TRACK_TITLE & music_similarity) != MusicSimilarity::NONE;\n        let is_track_artist = (MusicSimilarity::TRACK_ARTIST & music_similarity) != MusicSimilarity::NONE;\n        let is_year = (MusicSimilarity::YEAR & music_similarity) != MusicSimilarity::NONE;\n        let is_bitrate = (MusicSimilarity::BITRATE & music_similarity) != MusicSimilarity::NONE;\n        let is_length = (MusicSimilarity::LENGTH & music_similarity) != MusicSimilarity::NONE;\n        let is_genre = (MusicSimilarity::GENRE & music_similarity) != MusicSimilarity::NONE;\n\n        if mf.get_use_reference() {\n            let vector = mf.get_similar_music_referenced();\n\n            for (base_file_entry, vec_file_entry) in vector {\n                let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry);\n\n                let (directory, file) = split_path(&base_file_entry.path);\n                same_music_add_to_list_store(\n                    &list_store,\n                    &file,\n                    &directory,\n                    base_file_entry.size,\n                    base_file_entry.modified_date,\n                    &base_file_entry.track_title,\n                    &base_file_entry.track_artist,\n                    &base_file_entry.year,\n                    base_file_entry.bitrate,\n                    &format!(\"{} kbps\", base_file_entry.bitrate),\n                    &base_file_entry.genre,\n                    &format_audio_duration(base_file_entry.length),\n                    true,\n                    true,\n                );\n                for file_entry in vec_file_entry {\n                    let (directory, file) = split_path(&file_entry.path);\n                    same_music_add_to_list_store(\n                        &list_store,\n                        &file,\n                        &directory,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        &file_entry.track_title,\n                        &file_entry.track_artist,\n                        &file_entry.year,\n                        file_entry.bitrate,\n                        &format!(\"{} kbps\", file_entry.bitrate),\n                        &file_entry.genre,\n                        &format_audio_duration(file_entry.length),\n                        false,\n                        true,\n                    );\n                }\n            }\n        } else {\n            let vector = mf.get_duplicated_music_entries();\n\n            let text: &str = if mf.get_params().check_type == CheckingMethod::AudioTags { \"-----\" } else { \"\" };\n\n            for vec_file_entry in vector {\n                let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry);\n\n                same_music_add_to_list_store(\n                    &list_store,\n                    \"\",\n                    \"\",\n                    0,\n                    0,\n                    if is_track_title { text } else { \"\" },\n                    if is_track_artist { text } else { \"\" },\n                    if is_year { text } else { \"\" },\n                    0,\n                    if is_bitrate { text } else { \"\" },\n                    if is_genre { text } else { \"\" },\n                    if is_length { text } else { \"\" },\n                    true,\n                    false,\n                );\n                for file_entry in vec_file_entry {\n                    let (directory, file) = split_path(&file_entry.path);\n                    same_music_add_to_list_store(\n                        &list_store,\n                        &file,\n                        &directory,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        &file_entry.track_title,\n                        &file_entry.track_artist,\n                        &file_entry.year,\n                        file_entry.bitrate,\n                        &format!(\"{} kbps\", file_entry.bitrate),\n                        &file_entry.genre,\n                        &format_audio_duration(file_entry.length),\n                        false,\n                        false,\n                    );\n                }\n            }\n        }\n        print_text_messages_to_text_view(text_messages, text_view_errors);\n    }\n\n    finalize_compute(subview, mf, same_music_number)\n}\n\n#[fun_time(message = \"compute_similar_videos\", level = \"debug\")]\nfn compute_similar_videos(ff: SimilarVideos, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&ff, entry_info) {\n        return None;\n    }\n    if ff.get_use_reference() {\n        subview.tree_view.selection().set_select_function(select_function_always_true);\n    } else {\n        subview.tree_view.selection().set_select_function(select_function_similar_videos);\n    }\n    let information = ff.get_information();\n    let text_messages = ff.get_text_messages();\n    let found_any_duplicates = information.number_of_duplicates > 0;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\n            \"compute_found_videos\",\n            number_files = information.number_of_duplicates,\n            number_groups = information.number_of_groups,\n            time = scanning_time_str\n        ));\n    }\n\n    // Create GUI\n    {\n        let list_store = subview.tree_view.get_model();\n\n        if ff.get_use_reference() {\n            let vec_struct_similar = ff.get_similar_videos_referenced();\n\n            for (base_file_entry, vec_file_entry) in vec_struct_similar {\n                let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry);\n\n                let (directory, file) = split_path(&base_file_entry.path);\n                similar_videos_add_to_list_store(\n                    &list_store,\n                    &file,\n                    &directory,\n                    base_file_entry.size,\n                    base_file_entry.modified_date,\n                    true,\n                    true,\n                    base_file_entry.fps,\n                    base_file_entry.codec.as_deref(),\n                    base_file_entry.bitrate,\n                    base_file_entry.width,\n                    base_file_entry.height,\n                    base_file_entry.duration,\n                );\n                for file_entry in &vec_file_entry {\n                    let (directory, file) = split_path(&file_entry.path);\n                    similar_videos_add_to_list_store(\n                        &list_store,\n                        &file,\n                        &directory,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        false,\n                        true,\n                        file_entry.fps,\n                        file_entry.codec.as_deref(),\n                        file_entry.bitrate,\n                        file_entry.width,\n                        file_entry.height,\n                        file_entry.duration,\n                    );\n                }\n            }\n        } else {\n            let vec_struct_similar = ff.get_similar_videos();\n\n            for vec_file_entry in vec_struct_similar {\n                let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry);\n\n                similar_videos_add_to_list_store(&list_store, \"\", \"\", 0, 0, true, false, None, None, None, None, None, None);\n                for file_entry in &vec_file_entry {\n                    let (directory, file) = split_path(&file_entry.path);\n                    similar_videos_add_to_list_store(\n                        &list_store,\n                        &file,\n                        &directory,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        false,\n                        false,\n                        file_entry.fps,\n                        file_entry.codec.as_deref(),\n                        file_entry.bitrate,\n                        file_entry.width,\n                        file_entry.height,\n                        file_entry.duration,\n                    );\n                }\n            }\n        }\n\n        print_text_messages_to_text_view(text_messages, text_view_errors);\n    }\n\n    finalize_compute(subview, ff, found_any_duplicates as usize)\n}\n\n#[fun_time(message = \"compute_similar_images\", level = \"debug\")]\nfn compute_similar_images(sf: SimilarImages, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView, hash_size: u8) -> Option<bool> {\n    if handle_stopped_search(&sf, entry_info) {\n        return None;\n    }\n    if sf.get_use_reference() {\n        subview.tree_view.selection().set_select_function(select_function_always_true);\n    } else {\n        subview.tree_view.selection().set_select_function(select_function_similar_images);\n    }\n    let information = sf.get_information();\n    let text_messages = sf.get_text_messages();\n\n    let found_any_duplicates = information.number_of_duplicates > 0;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\n            \"compute_found_images\",\n            number_files = information.number_of_duplicates,\n            number_groups = information.number_of_groups,\n            time = scanning_time_str\n        ));\n    }\n\n    // Create GUI\n    {\n        let list_store = subview.tree_view.get_model();\n\n        if sf.get_use_reference() {\n            let vec_struct_similar: Vec<(ImagesEntry, Vec<ImagesEntry>)> = sf.get_similar_images_referenced().clone();\n            for (base_file_entry, mut vec_file_entry) in vec_struct_similar {\n                vec_file_entry.sort_by_key(|e| e.difference);\n\n                // Header\n                let (directory, file) = split_path(&base_file_entry.path);\n                similar_images_add_to_list_store(\n                    &list_store,\n                    &file,\n                    &directory,\n                    base_file_entry.size,\n                    base_file_entry.modified_date,\n                    &format!(\"{}x{}\", base_file_entry.width, base_file_entry.height),\n                    0,\n                    hash_size,\n                    true,\n                    true,\n                );\n                for file_entry in &vec_file_entry {\n                    let (directory, file) = split_path(&file_entry.path);\n                    similar_images_add_to_list_store(\n                        &list_store,\n                        &file,\n                        &directory,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        &format!(\"{}x{}\", file_entry.width, file_entry.height),\n                        file_entry.difference,\n                        hash_size,\n                        false,\n                        true,\n                    );\n                }\n            }\n        } else {\n            let vec_struct_similar = sf.get_similar_images().clone();\n            for mut vec_file_entry in vec_struct_similar {\n                vec_file_entry.sort_by_key(|e| e.difference);\n\n                similar_images_add_to_list_store(&list_store, \"\", \"\", 0, 0, \"\", 0, 0, true, false);\n                for file_entry in &vec_file_entry {\n                    let (directory, file) = split_path(&file_entry.path);\n                    similar_images_add_to_list_store(\n                        &list_store,\n                        &file,\n                        &directory,\n                        file_entry.size,\n                        file_entry.modified_date,\n                        &format!(\"{}x{}\", file_entry.width, file_entry.height),\n                        file_entry.difference,\n                        hash_size,\n                        false,\n                        false,\n                    );\n                }\n            }\n        }\n\n        print_text_messages_to_text_view(text_messages, text_view_errors);\n    }\n\n    finalize_compute(subview, sf, found_any_duplicates as usize)\n}\n\n#[fun_time(message = \"compute_temporary_files\", level = \"debug\")]\nfn compute_temporary_files(tf: Temporary, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&tf, entry_info) {\n        return None;\n    }\n    let information = tf.get_information();\n    let text_messages = tf.get_text_messages();\n    let temporary_files_number = information.number_of_temporary_files;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\"compute_found_temporary_files\", number_files = temporary_files_number, time = scanning_time_str));\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let mut vector = tf.get_temporary_files().clone();\n    vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n    for file_entry in vector {\n        let (directory, file) = split_path(&file_entry.path);\n        let values: [(u32, &dyn ToValue); 5] = [\n            (ColumnsTemporaryFiles::SelectionButton as u32, &false),\n            (ColumnsTemporaryFiles::Name as u32, &file),\n            (ColumnsTemporaryFiles::Path as u32, &directory),\n            (ColumnsTemporaryFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))),\n            (ColumnsTemporaryFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, tf, temporary_files_number)\n}\n\n#[fun_time(message = \"compute_big_files\", level = \"debug\")]\nfn compute_big_files(bf: BigFile, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&bf, entry_info) {\n        return None;\n    }\n    let information = bf.get_information();\n    let text_messages = bf.get_text_messages();\n    let biggest_files_number = information.number_of_real_files;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\"compute_found_big_files\", number_files = biggest_files_number, time = scanning_time_str));\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let vector = bf.get_big_files();\n\n    for file_entry in vector {\n        let (directory, file) = split_path(&file_entry.path);\n        let values: [(u32, &dyn ToValue); 7] = [\n            (ColumnsBigFiles::SelectionButton as u32, &false),\n            (ColumnsBigFiles::Size as u32, &(format_size(file_entry.size, BINARY))),\n            (ColumnsBigFiles::Name as u32, &file),\n            (ColumnsBigFiles::Path as u32, &directory),\n            (ColumnsBigFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))),\n            (ColumnsBigFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)),\n            (ColumnsBigFiles::SizeAsBytes as u32, &(file_entry.size)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, bf, biggest_files_number)\n}\n\n#[fun_time(message = \"compute_empty_files\", level = \"debug\")]\nfn compute_empty_files(vf: EmptyFiles, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&vf, entry_info) {\n        return None;\n    }\n    let information = vf.get_information();\n    let text_messages = vf.get_text_messages();\n    let empty_files_number = information.number_of_empty_files;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\"compute_found_empty_files\", number_files = empty_files_number, time = scanning_time_str));\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let vector = conditional_sort_vector(vf.get_empty_files());\n\n    for file_entry in vector {\n        let (directory, file) = split_path(&file_entry.path);\n        let values: [(u32, &dyn ToValue); 5] = [\n            (ColumnsEmptyFiles::SelectionButton as u32, &false),\n            (ColumnsEmptyFiles::Name as u32, &file),\n            (ColumnsEmptyFiles::Path as u32, &directory),\n            (ColumnsEmptyFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))),\n            (ColumnsEmptyFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, vf, empty_files_number)\n}\n\n#[fun_time(message = \"compute_empty_folders\", level = \"debug\")]\nfn compute_empty_folders(ef: EmptyFolder, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&ef, entry_info) {\n        return None;\n    }\n    let information = ef.get_information();\n    let text_messages = ef.get_text_messages();\n    let empty_folder_number = information.number_of_empty_folders;\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        entry_info.set_text(&flg!(\"compute_found_empty_folders\", number_files = empty_folder_number, time = scanning_time_str));\n    }\n\n    let list_store = subview.tree_view.get_model();\n    let hashmap = ef.get_empty_folder_list();\n    let mut vector = hashmap.values().collect::<Vec<_>>();\n    vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n    for fe in vector {\n        let (directory, file) = split_path(&fe.path);\n        let values: [(u32, &dyn ToValue); 5] = [\n            (ColumnsEmptyFolders::SelectionButton as u32, &false),\n            (ColumnsEmptyFolders::Name as u32, &file),\n            (ColumnsEmptyFolders::Path as u32, &directory),\n            (ColumnsEmptyFolders::Modification as u32, &(get_dt_timestamp_string(fe.modified_date))),\n            (ColumnsEmptyFolders::ModificationAsSecs as u32, &(fe.modified_date)),\n        ];\n        append_row_to_list_store(&list_store, &values);\n    }\n    print_text_messages_to_text_view(text_messages, text_view_errors);\n    finalize_compute(subview, ef, empty_folder_number)\n}\n\n#[fun_time(message = \"compute_duplicate_finder\", level = \"debug\")]\nfn compute_duplicate_finder(df: DuplicateFinder, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option<bool> {\n    if handle_stopped_search(&df, entry_info) {\n        return None;\n    }\n\n    if df.get_use_reference() {\n        subview.tree_view.selection().set_select_function(select_function_always_true);\n    } else {\n        subview.tree_view.selection().set_select_function(select_function_duplicates);\n    }\n\n    let information = df.get_information();\n    let text_messages = df.get_text_messages();\n\n    let duplicates_number: usize;\n    let duplicates_size: u64;\n    let duplicates_group: usize;\n\n    match df.get_params().check_method {\n        CheckingMethod::Name => {\n            duplicates_number = information.number_of_duplicated_files_by_name;\n            duplicates_size = 0;\n            duplicates_group = information.number_of_groups_by_name;\n        }\n        CheckingMethod::Hash => {\n            duplicates_number = information.number_of_duplicated_files_by_hash;\n            duplicates_size = information.lost_space_by_hash;\n            duplicates_group = information.number_of_groups_by_hash;\n        }\n        CheckingMethod::Size => {\n            duplicates_number = information.number_of_duplicated_files_by_size;\n            duplicates_size = information.lost_space_by_size;\n            duplicates_group = information.number_of_groups_by_size;\n        }\n        CheckingMethod::SizeName => {\n            duplicates_number = information.number_of_duplicated_files_by_size_name;\n            duplicates_size = information.lost_space_by_size;\n            duplicates_group = information.number_of_groups_by_size_name;\n        }\n        _ => unreachable!(),\n    }\n    let scanning_time_str = format_time(information.scanning_time);\n\n    if let Some(critical) = text_messages.critical.clone() {\n        entry_info.set_text(&critical);\n    } else {\n        if duplicates_size == 0 {\n            entry_info.set_text(\n                flg!(\n                    \"compute_found_duplicates_name\",\n                    number_files = duplicates_number,\n                    number_groups = duplicates_group,\n                    time = scanning_time_str\n                )\n                .as_str(),\n            );\n        } else {\n            entry_info.set_text(\n                flg!(\n                    \"compute_found_duplicates_hash_size\",\n                    number_files = duplicates_number,\n                    number_groups = duplicates_group,\n                    size = format_size(duplicates_size, BINARY),\n                    time = scanning_time_str\n                )\n                .as_str(),\n            );\n        }\n    }\n\n    // Create GUI\n    {\n        let list_store = subview.tree_view.get_model();\n\n        if df.get_use_reference() {\n            match df.get_params().check_method {\n                CheckingMethod::Name => {\n                    let btreemap = df.get_files_with_identical_name_referenced();\n\n                    for (_name, (base_file_entry, vector)) in btreemap.iter().rev() {\n                        let vector = vector_sort_unstable_entry_by_path(vector);\n                        let (directory, file) = split_path(&base_file_entry.path);\n                        duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true);\n\n                        for entry in vector {\n                            let (directory, file) = split_path(&entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true);\n                        }\n                    }\n                }\n                CheckingMethod::Hash => {\n                    let btreemap = df.get_files_with_identical_hashes_referenced();\n\n                    for (_size, vectors_vector) in btreemap.iter().rev() {\n                        for (base_file_entry, vector) in vectors_vector {\n                            let vector = vector_sort_unstable_entry_by_path(vector);\n                            let (directory, file) = split_path(&base_file_entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true);\n                            for entry in vector {\n                                let (directory, file) = split_path(&entry.path);\n                                duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true);\n                            }\n                        }\n                    }\n                }\n                CheckingMethod::Size => {\n                    let btreemap = df.get_files_with_identical_size_referenced();\n\n                    for (_size, (base_file_entry, vector)) in btreemap.iter().rev() {\n                        let vector = vector_sort_unstable_entry_by_path(vector);\n                        let (directory, file) = split_path(&base_file_entry.path);\n                        duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true);\n                        for entry in vector {\n                            let (directory, file) = split_path(&entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true);\n                        }\n                    }\n                }\n                CheckingMethod::SizeName => {\n                    let btreemap = df.get_files_with_identical_size_names_referenced();\n\n                    for (_size, (base_file_entry, vector)) in btreemap.iter().rev() {\n                        let vector = vector_sort_unstable_entry_by_path(vector);\n                        let (directory, file) = split_path(&base_file_entry.path);\n                        duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true);\n                        for entry in vector {\n                            let (directory, file) = split_path(&entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true);\n                        }\n                    }\n                }\n                _ => panic!(),\n            }\n        } else {\n            match df.get_params().check_method {\n                CheckingMethod::Name => {\n                    let btreemap = df.get_files_sorted_by_names();\n\n                    for (_name, vector) in btreemap.iter().rev() {\n                        let vector = vector_sort_unstable_entry_by_path(vector);\n                        duplicates_add_to_list_store(&list_store, \"\", \"\", 0, 0, true, false);\n                        for entry in vector {\n                            let (directory, file) = split_path(&entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false);\n                        }\n                    }\n                }\n                CheckingMethod::Hash => {\n                    let btreemap = df.get_files_sorted_by_hash();\n\n                    for (_size, vectors_vector) in btreemap.iter().rev() {\n                        for vector in vectors_vector {\n                            let vector = vector_sort_unstable_entry_by_path(vector);\n                            duplicates_add_to_list_store(&list_store, \"\", \"\", 0, 0, true, false);\n\n                            for entry in vector {\n                                let (directory, file) = split_path(&entry.path);\n                                duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false);\n                            }\n                        }\n                    }\n                }\n                CheckingMethod::Size => {\n                    let btreemap = df.get_files_sorted_by_size();\n\n                    for (_size, vector) in btreemap.iter().rev() {\n                        let vector = vector_sort_unstable_entry_by_path(vector);\n                        duplicates_add_to_list_store(&list_store, \"\", \"\", 0, 0, true, false);\n\n                        for entry in vector {\n                            let (directory, file) = split_path(&entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false);\n                        }\n                    }\n                }\n                CheckingMethod::SizeName => {\n                    let btreemap = df.get_files_sorted_by_size_name();\n\n                    for (_size, vector) in btreemap.iter().rev() {\n                        let vector = vector_sort_unstable_entry_by_path(vector);\n                        duplicates_add_to_list_store(&list_store, \"\", \"\", 0, 0, true, false);\n\n                        for entry in vector {\n                            let (directory, file) = split_path(&entry.path);\n                            duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false);\n                        }\n                    }\n                }\n                _ => panic!(),\n            }\n        }\n        print_text_messages_to_text_view(text_messages, text_view_errors);\n    }\n\n    finalize_compute(subview, df, duplicates_number)\n}\n\nfn vector_sort_unstable_entry_by_path<T>(vector: &[T]) -> Vec<T>\nwhere\n    T: ResultEntry + Clone + Send,\n{\n    if vector.len() >= 2 {\n        let mut vector = vector.to_vec();\n        vector.par_sort_unstable_by(|a, b| split_path_compare(a.get_path(), b.get_path()));\n        vector\n    } else {\n        vector.to_vec()\n    }\n}\n\nfn duplicates_add_to_list_store(list_store: &ListStore, file: &str, directory: &str, size: u64, modified_date: u64, is_header: bool, is_reference_folder: bool) {\n    const COLUMNS_NUMBER: usize = 11;\n    let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder);\n    let color = get_row_color(is_header);\n\n    let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [\n        (ColumnsDuplicates::ActivatableSelectButton as u32, &(!is_header)),\n        (ColumnsDuplicates::SelectionButton as u32, &false),\n        (ColumnsDuplicates::Size as u32, &size_str),\n        (ColumnsDuplicates::SizeAsBytes as u32, &size),\n        (ColumnsDuplicates::Name as u32, &file),\n        (ColumnsDuplicates::Path as u32, &directory),\n        (ColumnsDuplicates::Modification as u32, &string_date),\n        (ColumnsDuplicates::ModificationAsSecs as u32, &modified_date),\n        (ColumnsDuplicates::Color as u32, &color),\n        (ColumnsDuplicates::IsHeader as u32, &is_header),\n        (ColumnsDuplicates::TextColor as u32, &TEXT_COLOR),\n    ];\n    append_row_to_list_store(list_store, &values);\n}\n\nfn similar_images_add_to_list_store(\n    list_store: &ListStore,\n    file: &str,\n    directory: &str,\n    size: u64,\n    modified_date: u64,\n    dimensions: &str,\n    similarity: u32,\n    hash_size: u8,\n    is_header: bool,\n    is_reference_folder: bool,\n) {\n    const COLUMNS_NUMBER: usize = 13;\n    let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder);\n    let color = get_row_color(is_header);\n    let similarity_string = if is_header { String::new() } else { get_string_from_similarity(similarity, hash_size) };\n\n    let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [\n        (ColumnsSimilarImages::ActivatableSelectButton as u32, &(!is_header)),\n        (ColumnsSimilarImages::SelectionButton as u32, &false),\n        (ColumnsSimilarImages::Similarity as u32, &similarity_string),\n        (ColumnsSimilarImages::Size as u32, &size_str),\n        (ColumnsSimilarImages::SizeAsBytes as u32, &size),\n        (ColumnsSimilarImages::Dimensions as u32, &dimensions),\n        (ColumnsSimilarImages::Name as u32, &file),\n        (ColumnsSimilarImages::Path as u32, &directory),\n        (ColumnsSimilarImages::Modification as u32, &string_date),\n        (ColumnsSimilarImages::ModificationAsSecs as u32, &modified_date),\n        (ColumnsSimilarImages::Color as u32, &color),\n        (ColumnsSimilarImages::IsHeader as u32, &is_header),\n        (ColumnsSimilarImages::TextColor as u32, &TEXT_COLOR),\n    ];\n    append_row_to_list_store(list_store, &values);\n}\n\nfn similar_videos_add_to_list_store(\n    list_store: &ListStore,\n    file: &str,\n    directory: &str,\n    size: u64,\n    modified_date: u64,\n    is_header: bool,\n    is_reference_folder: bool,\n    fps: Option<f64>,\n    codec: Option<&str>,\n    bitrate: Option<u64>,\n    width: Option<u32>,\n    height: Option<u32>,\n    duration: Option<f64>,\n) {\n    const COLUMNS_NUMBER: usize = 16;\n    let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder);\n    let color = get_row_color(is_header);\n\n    let fps_str = fps.map(|f| format!(\"{f:.2}\")).unwrap_or_default();\n    let bitrate_str = format_bitrate_opt(bitrate);\n    let codec_str = codec.unwrap_or_default();\n    let dimensions = match (width, height) {\n        (Some(w), Some(h)) => format!(\"{w}x{h}\"),\n        _ => \"\".to_string(),\n    };\n    let duration_str = format_duration_opt(duration);\n\n    let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [\n        (ColumnsSimilarVideos::ActivatableSelectButton as u32, &(!is_header)),\n        (ColumnsSimilarVideos::SelectionButton as u32, &false),\n        (ColumnsSimilarVideos::Size as u32, &size_str),\n        (ColumnsSimilarVideos::SizeAsBytes as u32, &size),\n        (ColumnsSimilarVideos::Fps as u32, &fps_str),\n        (ColumnsSimilarVideos::Codec as u32, &codec_str),\n        (ColumnsSimilarVideos::Bitrate as u32, &bitrate_str),\n        (ColumnsSimilarVideos::Dimensions as u32, &dimensions),\n        (ColumnsSimilarVideos::Duration as u32, &duration_str),\n        (ColumnsSimilarVideos::Name as u32, &file),\n        (ColumnsSimilarVideos::Path as u32, &directory),\n        (ColumnsSimilarVideos::Modification as u32, &string_date),\n        (ColumnsSimilarVideos::ModificationAsSecs as u32, &modified_date),\n        (ColumnsSimilarVideos::Color as u32, &color),\n        (ColumnsSimilarVideos::IsHeader as u32, &is_header),\n        (ColumnsSimilarVideos::TextColor as u32, &TEXT_COLOR),\n    ];\n\n    append_row_to_list_store(list_store, &values);\n}\n\nfn same_music_add_to_list_store(\n    list_store: &ListStore,\n    file: &str,\n    directory: &str,\n    size: u64,\n    modified_date: u64,\n    track_title: &str,\n    track_artist: &str,\n    track_year: &str,\n    track_bitrate: u32,\n    bitrate_string: &str,\n    track_genre: &str,\n    track_length: &str,\n    is_header: bool,\n    is_reference_folder: bool,\n) {\n    const COLUMNS_NUMBER: usize = 18;\n    let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder);\n    let color = get_row_color(is_header);\n\n    let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [\n        (ColumnsSameMusic::ActivatableSelectButton as u32, &(!is_header)),\n        (ColumnsSameMusic::SelectionButton as u32, &false),\n        (ColumnsSameMusic::Size as u32, &size_str),\n        (ColumnsSameMusic::SizeAsBytes as u32, &size),\n        (ColumnsSameMusic::Name as u32, &file),\n        (ColumnsSameMusic::Path as u32, &directory),\n        (ColumnsSameMusic::Title as u32, &track_title),\n        (ColumnsSameMusic::Artist as u32, &track_artist),\n        (ColumnsSameMusic::Year as u32, &track_year),\n        (ColumnsSameMusic::Genre as u32, &track_genre),\n        (ColumnsSameMusic::Bitrate as u32, &bitrate_string),\n        (ColumnsSameMusic::BitrateAsNumber as u32, &track_bitrate),\n        (ColumnsSameMusic::Length as u32, &track_length),\n        (ColumnsSameMusic::Modification as u32, &string_date),\n        (ColumnsSameMusic::ModificationAsSecs as u32, &modified_date),\n        (ColumnsSameMusic::Color as u32, &color),\n        (ColumnsSameMusic::IsHeader as u32, &is_header),\n        (ColumnsSameMusic::TextColor as u32, &TEXT_COLOR),\n    ];\n\n    append_row_to_list_store(list_store, &values);\n}\n\nfn get_dt_timestamp_string(timestamp: u64) -> String {\n    DateTime::from_timestamp(timestamp as i64, 0)\n        .expect(\"Modified date always should be in valid range\")\n        .to_string()\n}\n\nfn set_specific_buttons_as_active(buttons_array: &Rc<RefCell<HashMap<NotebookMainEnum, HashMap<BottomButtonsEnum, bool>>>>, notebook_enum: NotebookMainEnum, value_to_set: bool) {\n    let mut b_mut = buttons_array.borrow_mut();\n    let butt = b_mut.get_mut(&notebook_enum).expect(\"Failed to borrow buttons\");\n    let allowed_buttons = NOTEBOOKS_INFO[notebook_enum as usize].bottom_buttons;\n    for i in allowed_buttons {\n        *butt.get_mut(i).expect(\"Failed to borrow buttons\") = value_to_set;\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_about_buttons.rs",
    "content": "use gtk4::prelude::*;\nuse log::error;\n\nuse crate::gui_structs::gui_data::GuiData;\n\nconst SPONSOR_SITE: &str = \"https://github.com/sponsors/qarmin\";\nconst REPOSITORY_SITE: &str = \"https://github.com/qarmin/czkawka\";\nconst INSTRUCTION_SITE: &str = \"https://github.com/qarmin/czkawka/blob/master/instructions/Instruction.md\";\nconst TRANSLATION_SITE: &str = \"https://crwd.in/czkawka\";\nconst KROKIET_SITE: &str = \"https://github.com/qarmin/czkawka/tree/master/krokiet\";\n\npub(crate) fn connect_about_buttons(gui_data: &GuiData) {\n    let button_donation = gui_data.about.button_donation.clone();\n    button_donation.connect_clicked(move |_| {\n        if let Err(e) = open::that(SPONSOR_SITE) {\n            error!(\"Failed to open sponsor site: {SPONSOR_SITE}, reason {e}\");\n        }\n    });\n\n    let button_instruction = gui_data.about.button_instruction.clone();\n    button_instruction.connect_clicked(move |_| {\n        if let Err(e) = open::that(INSTRUCTION_SITE) {\n            error!(\"Failed to open instruction site: {INSTRUCTION_SITE}, reason {e}\");\n        }\n    });\n\n    let button_krokiet = gui_data.about.button_krokiet.clone();\n    button_krokiet.connect_clicked(move |_| {\n        if let Err(e) = open::that(KROKIET_SITE) {\n            error!(\"Failed to open Krokiet site: {KROKIET_SITE}, reason {e}\");\n        }\n    });\n\n    let button_repository = gui_data.about.button_repository.clone();\n    button_repository.connect_clicked(move |_| {\n        if let Err(e) = open::that(REPOSITORY_SITE) {\n            error!(\"Failed to open repository site: {REPOSITORY_SITE}, reason {e}\");\n        }\n    });\n\n    let button_translation = gui_data.about.button_translation.clone();\n    button_translation.connect_clicked(move |_| {\n        if let Err(e) = open::that(TRANSLATION_SITE) {\n            error!(\"Failed to open translation site: {TRANSLATION_SITE}, reason {e}\");\n        }\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_compare.rs",
    "content": "use std::cell::RefCell;\nuse std::rc::Rc;\n\nuse czkawka_core::common::image::{ImgResizeOptions, get_dynamic_image_from_path};\nuse czkawka_core::re_exported::FirFilterType;\nuse gdk4::gdk_pixbuf::{InterpType, Pixbuf};\nuse gtk4::prelude::*;\nuse gtk4::{Align, CheckButton, Orientation, Picture, ScrolledWindow, TreeIter, TreeModel, TreePath, Widget};\nuse image::DynamicImage;\nuse log::error;\n\nuse crate::flg;\nuse crate::gtk_traits::WidgetTraits;\nuse crate::gui_structs::common_tree_view::SubView;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::{get_full_name_from_path_name, get_max_file_name};\nuse crate::helpers::image_operations::{get_pixbuf_from_dynamic_image, resize_pixbuf_dimension};\nuse crate::helpers::list_store_operations::count_number_of_groups;\nuse crate::notebook_info::NotebookObject;\n\nconst BIG_PREVIEW_SIZE: i32 = 1024;\nconst SMALL_PREVIEW_SIZE: i32 = 130;\n\npub(crate) fn connect_button_compare(gui_data: &GuiData) {\n    let button_compare = gui_data.bottom_buttons.buttons_compare.clone();\n    let window_compare = gui_data.compare_images.window_compare.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    let scrolled_window_compare_choose_images = gui_data.compare_images.scrolled_window_compare_choose_images.clone();\n\n    let label_group_info = gui_data.compare_images.label_group_info.clone();\n\n    let button_go_previous_compare_group = gui_data.compare_images.button_go_previous_compare_group.clone();\n    let button_go_next_compare_group = gui_data.compare_images.button_go_next_compare_group.clone();\n\n    let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone();\n    let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone();\n\n    let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone();\n    let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone();\n    let shared_current_path = gui_data.compare_images.shared_current_path.clone();\n    let shared_image_cache = gui_data.compare_images.shared_image_cache.clone();\n    let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone();\n\n    let image_compare_left = gui_data.compare_images.image_compare_left.clone();\n    let image_compare_right = gui_data.compare_images.image_compare_right.clone();\n\n    let check_button_settings_use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone();\n\n    window_compare.set_default_size(700, 700);\n\n    button_compare.connect_clicked(move |_| {\n        let subview = common_tree_views.get_current_subview();\n        let model = subview.tree_view.model().expect(\"Missing tree_view model\");\n\n        let group_number = count_number_of_groups(subview);\n\n        if group_number == 0 {\n            return;\n        }\n\n        // Check selected items\n        let (current_group, tree_path) = get_current_group_and_iter_from_selection(subview);\n\n        *shared_current_of_groups.borrow_mut() = current_group;\n        *shared_numbers_of_groups.borrow_mut() = group_number;\n\n        populate_groups_at_start(\n            &subview.nb_object,\n            &model,\n            &shared_current_path,\n            tree_path,\n            &image_compare_left,\n            &image_compare_right,\n            current_group,\n            group_number,\n            &check_button_left_preview_text,\n            &check_button_right_preview_text,\n            &scrolled_window_compare_choose_images,\n            &label_group_info,\n            &shared_image_cache,\n            &shared_using_for_preview,\n            &button_go_previous_compare_group,\n            &button_go_next_compare_group,\n            &check_button_settings_use_rust_preview,\n        );\n\n        window_compare.set_visible(true);\n    });\n\n    let shared_image_cache = gui_data.compare_images.shared_image_cache.clone();\n    let shared_current_path = gui_data.compare_images.shared_current_path.clone();\n    let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone();\n    let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone();\n    let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone();\n    let window_compare = gui_data.compare_images.window_compare.clone();\n    let image_compare_left = gui_data.compare_images.image_compare_left.clone();\n    let image_compare_right = gui_data.compare_images.image_compare_right.clone();\n    window_compare.connect_close_request(move |window_compare| {\n        window_compare.set_visible(false);\n        *shared_image_cache.borrow_mut() = Vec::new();\n        *shared_current_path.borrow_mut() = None;\n        *shared_current_of_groups.borrow_mut() = 0;\n        *shared_numbers_of_groups.borrow_mut() = 0;\n        *shared_using_for_preview.borrow_mut() = (None, None);\n        image_compare_left.set_pixbuf(None);\n        image_compare_right.set_pixbuf(None);\n        glib::Propagation::Stop\n    });\n\n    let button_go_previous_compare_group = gui_data.compare_images.button_go_previous_compare_group.clone();\n    let button_go_next_compare_group = gui_data.compare_images.button_go_next_compare_group.clone();\n    let label_group_info = gui_data.compare_images.label_group_info.clone();\n    let scrolled_window_compare_choose_images = gui_data.compare_images.scrolled_window_compare_choose_images.clone();\n\n    let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone();\n    let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone();\n\n    let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone();\n    let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone();\n    let shared_current_path = gui_data.compare_images.shared_current_path.clone();\n    let shared_image_cache = gui_data.compare_images.shared_image_cache.clone();\n    let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone();\n\n    let image_compare_left = gui_data.compare_images.image_compare_left.clone();\n    let image_compare_right = gui_data.compare_images.image_compare_right.clone();\n\n    let check_button_settings_use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    button_go_previous_compare_group.connect_clicked(move |button_go_previous_compare_group| {\n        let sv = common_tree_views.get_current_subview();\n        let model = sv.get_tree_model();\n\n        *shared_current_of_groups.borrow_mut() -= 1;\n\n        let current_group = *shared_current_of_groups.borrow();\n        let group_number = *shared_numbers_of_groups.borrow();\n\n        let tree_path = move_iter(\n            &model,\n            shared_current_path.borrow().as_ref().expect(\"Missing current path\"),\n            sv.nb_object.column_header.expect(\"Missing column_header\"),\n            false,\n        );\n\n        populate_groups_at_start(\n            &sv.nb_object,\n            &model,\n            &shared_current_path,\n            tree_path,\n            &image_compare_left,\n            &image_compare_right,\n            current_group,\n            group_number,\n            &check_button_left_preview_text,\n            &check_button_right_preview_text,\n            &scrolled_window_compare_choose_images,\n            &label_group_info,\n            &shared_image_cache,\n            &shared_using_for_preview,\n            button_go_previous_compare_group,\n            &button_go_next_compare_group,\n            &check_button_settings_use_rust_preview,\n        );\n    });\n\n    let button_go_previous_compare_group = gui_data.compare_images.button_go_previous_compare_group.clone();\n    let button_go_next_compare_group = gui_data.compare_images.button_go_next_compare_group.clone();\n    let label_group_info = gui_data.compare_images.label_group_info.clone();\n    let scrolled_window_compare_choose_images = gui_data.compare_images.scrolled_window_compare_choose_images.clone();\n\n    let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone();\n    let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone();\n\n    let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone();\n    let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone();\n    let shared_current_path = gui_data.compare_images.shared_current_path.clone();\n    let shared_image_cache = gui_data.compare_images.shared_image_cache.clone();\n    let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone();\n\n    let image_compare_left = gui_data.compare_images.image_compare_left.clone();\n    let image_compare_right = gui_data.compare_images.image_compare_right.clone();\n\n    let check_button_settings_use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    button_go_next_compare_group.connect_clicked(move |button_go_next_compare_group| {\n        let sv = common_tree_views.get_current_subview();\n        let model = sv.get_tree_model();\n\n        *shared_current_of_groups.borrow_mut() += 1;\n\n        let current_group = *shared_current_of_groups.borrow();\n        let group_number = *shared_numbers_of_groups.borrow();\n\n        let tree_path = move_iter(\n            &model,\n            shared_current_path.borrow().as_ref().expect(\"Missing current path\"),\n            sv.nb_object.column_header.expect(\"Missing column_header\"),\n            true,\n        );\n\n        populate_groups_at_start(\n            &sv.nb_object,\n            &model,\n            &shared_current_path,\n            tree_path,\n            &image_compare_left,\n            &image_compare_right,\n            current_group,\n            group_number,\n            &check_button_left_preview_text,\n            &check_button_right_preview_text,\n            &scrolled_window_compare_choose_images,\n            &label_group_info,\n            &shared_image_cache,\n            &shared_using_for_preview,\n            &button_go_previous_compare_group,\n            button_go_next_compare_group,\n            &check_button_settings_use_rust_preview,\n        );\n    });\n\n    let button_replace_group = gui_data.compare_images.button_replace_group.clone();\n    let image_compare_right = gui_data.compare_images.image_compare_right.clone();\n    let image_compare_left = gui_data.compare_images.image_compare_left.clone();\n    button_replace_group.connect_clicked(move |_| {\n        let tmp = image_compare_left.paintable();\n        image_compare_left.set_paintable(image_compare_right.paintable().as_ref());\n        image_compare_right.set_paintable(tmp.as_ref());\n    });\n\n    let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone();\n    let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone();\n    let shared_current_path = gui_data.compare_images.shared_current_path.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    check_button_left_preview_text.connect_toggled(move |check_button_left_preview_text| {\n        let sv = common_tree_views.get_current_subview();\n        let model = sv.get_model();\n\n        let main_tree_path = shared_current_path.borrow().as_ref().expect(\"Missing current path\").clone();\n        let this_tree_path = shared_using_for_preview.borrow().0.clone().expect(\"Missing left preview path\");\n        if main_tree_path == this_tree_path {\n            return; // Selected header, so we don't need to select result in treeview\n            // TODO this should be handled by disabling entirely check box\n        }\n\n        let is_active = check_button_left_preview_text.is_active();\n        model.set_value(\n            &model.iter(&this_tree_path).expect(\"Using invalid tree_path\"),\n            sv.nb_object.column_selection as u32,\n            &is_active.to_value(),\n        );\n    });\n\n    let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone();\n    let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone();\n    let shared_current_path = gui_data.compare_images.shared_current_path.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    check_button_right_preview_text.connect_toggled(move |check_button_right_preview_text| {\n        let sv = common_tree_views.get_current_subview();\n        let model = sv.get_model();\n\n        let main_tree_path = shared_current_path.borrow().as_ref().expect(\"Missing current path\").clone();\n        let this_tree_path = shared_using_for_preview.borrow().1.clone().expect(\"Missing right preview path\");\n        if main_tree_path == this_tree_path {\n            return; // Selected header, so we don't need to select result in treeview\n            // TODO this should be handled by disabling entirely check box\n        }\n\n        let is_active = check_button_right_preview_text.is_active();\n        model.set_value(\n            &model.iter(&this_tree_path).expect(\"Using invalid tree_path\"),\n            sv.nb_object.column_selection as u32,\n            &is_active.to_value(),\n        );\n    });\n}\n\nfn populate_groups_at_start(\n    nb_object: &NotebookObject,\n    model: &TreeModel,\n    shared_current_path: &Rc<RefCell<Option<TreePath>>>,\n    tree_path: TreePath,\n    image_compare_left: &Picture,\n    image_compare_right: &Picture,\n    current_group: u32,\n    group_number: u32,\n    check_button_left_preview_text: &CheckButton,\n    check_button_right_preview_text: &CheckButton,\n    scrolled_window_compare_choose_images: &ScrolledWindow,\n    label_group_info: &gtk4::Label,\n    shared_image_cache: &Rc<RefCell<Vec<(String, String, Picture, Picture, TreePath)>>>,\n    shared_using_for_preview: &Rc<RefCell<(Option<TreePath>, Option<TreePath>)>>,\n    button_go_previous_compare_group: &gtk4::Button,\n    button_go_next_compare_group: &gtk4::Button,\n    check_button_settings_use_rust_preview: &CheckButton,\n) {\n    if current_group == 1 {\n        button_go_previous_compare_group.set_sensitive(false);\n    } else {\n        button_go_previous_compare_group.set_sensitive(true);\n    }\n    if current_group == group_number {\n        button_go_next_compare_group.set_sensitive(false);\n    } else {\n        button_go_next_compare_group.set_sensitive(true);\n    }\n\n    let all_vec = get_all_path(\n        model,\n        &tree_path,\n        nb_object.column_header.expect(\"Missing column_header\"),\n        nb_object.column_path,\n        nb_object.column_name,\n    );\n    *shared_current_path.borrow_mut() = Some(tree_path);\n\n    let cache_all_images = generate_cache_for_results(all_vec, check_button_settings_use_rust_preview.is_active());\n\n    // This is safe, because cache have at least 2 results\n    image_compare_left.set_paintable(cache_all_images[0].2.paintable().as_ref());\n    image_compare_right.set_paintable(cache_all_images[1].2.paintable().as_ref());\n\n    *shared_using_for_preview.borrow_mut() = (Some(cache_all_images[0].4.clone()), Some(cache_all_images[1].4.clone()));\n\n    check_button_left_preview_text.set_label(Some(&format!(\"1. {}\", get_max_file_name(&cache_all_images[0].0, 60))));\n    check_button_right_preview_text.set_label(Some(&format!(\"2. {}\", get_max_file_name(&cache_all_images[1].0, 60))));\n\n    label_group_info.set_text(\n        flg!(\n            \"compare_groups_number\",\n            current_group = current_group,\n            all_groups = group_number,\n            images_in_group = cache_all_images.len()\n        )\n        .as_str(),\n    );\n\n    populate_similar_scrolled_view(\n        scrolled_window_compare_choose_images,\n        &cache_all_images,\n        image_compare_left,\n        image_compare_right,\n        shared_using_for_preview,\n        shared_image_cache,\n        check_button_left_preview_text,\n        check_button_right_preview_text,\n        model,\n        nb_object.column_selection,\n    );\n\n    *shared_image_cache.borrow_mut() = cache_all_images.clone();\n\n    let mut found = false;\n    for i in scrolled_window_compare_choose_images\n        .child()\n        .expect(\"Failed to get child of scrolled_window_compare_choose_images\")\n        .downcast::<gtk4::Viewport>()\n        .expect(\"Failed to downcast to Viewport\")\n        .get_all_direct_children()\n    {\n        if i.widget_name() == \"all_box\" {\n            let gtk_box = i.downcast::<gtk4::Box>().expect(\"Failed to downcast to Box\");\n            update_bottom_buttons(&gtk_box, shared_using_for_preview, shared_image_cache);\n            found = true;\n            break;\n        }\n    }\n    assert!(found);\n\n    let is_active = model.get::<bool>(&model.iter(&cache_all_images[0].4).expect(\"Using invalid tree_path\"), nb_object.column_selection);\n    check_button_left_preview_text.set_active(is_active);\n    let is_active = model.get::<bool>(&model.iter(&cache_all_images[1].4).expect(\"Using invalid tree_path\"), nb_object.column_selection);\n    check_button_right_preview_text.set_active(is_active);\n}\n\nfn generate_cache_for_results(vector_with_path: Vec<(String, String, TreePath)>, use_rust_loader: bool) -> Vec<(String, String, Picture, Picture, TreePath)> {\n    // TODO use here threads,\n    // For now threads cannot be used because Image and TreeIter cannot be used in threads\n    let mut cache_all_images = Vec::new();\n    for (full_path, name, tree_path) in vector_with_path {\n        let small_img = Picture::new();\n        let big_img = Picture::new();\n\n        let mut pixbuf = get_pixbuf_from_dynamic_image(DynamicImage::new_rgb8(1, 1)).expect(\"Failed to create pixbuf\");\n\n        if use_rust_loader {\n            match get_dynamic_image_from_path(\n                &full_path,\n                Some(ImgResizeOptions {\n                    max_width: BIG_PREVIEW_SIZE as u32,\n                    max_height: BIG_PREVIEW_SIZE as u32,\n                    filter: FirFilterType::Bilinear,\n                }),\n            )\n            .and_then(|e| get_pixbuf_from_dynamic_image(e.image))\n            {\n                Ok(t) => {\n                    pixbuf = t;\n                }\n                Err(e) => {\n                    error!(\"Failed to open image {full_path}, reason {e}\");\n                }\n            }\n        } else {\n            match Pixbuf::from_file(&full_path) {\n                Ok(t) => {\n                    pixbuf = t;\n                }\n                Err(e) => {\n                    error!(\"Failed to open image {full_path}, reason {e}\");\n                }\n            }\n        }\n\n        #[expect(clippy::never_loop)]\n        loop {\n            let Some(pixbuf_big) = resize_pixbuf_dimension(&pixbuf, (BIG_PREVIEW_SIZE, BIG_PREVIEW_SIZE), InterpType::Bilinear) else {\n                error!(\"Failed to resize image {full_path}.\");\n                break;\n            };\n            let Some(pixbuf_small) = resize_pixbuf_dimension(&pixbuf_big, (SMALL_PREVIEW_SIZE, SMALL_PREVIEW_SIZE), InterpType::Bilinear) else {\n                error!(\"Failed to resize image {full_path}.\");\n                break;\n            };\n\n            big_img.set_pixbuf(Some(&pixbuf_big));\n            small_img.set_pixbuf(Some(&pixbuf_small));\n            break;\n        }\n\n        cache_all_images.push((full_path, name, big_img, small_img, tree_path));\n    }\n    cache_all_images\n}\n\nfn get_all_path(model: &TreeModel, current_path: &TreePath, column_header: i32, column_path: i32, column_name: i32) -> Vec<(String, String, TreePath)> {\n    let mut used_iter = model.iter(current_path).expect(\"Using invalid tree_path\");\n\n    assert!(model.get::<bool>(&used_iter, column_header));\n    let using_reference = !model.get::<String>(&used_iter, column_path).is_empty();\n\n    let mut returned_vector = Vec::new();\n\n    if using_reference {\n        let name = model.get::<String>(&used_iter, column_name);\n        let path = model.get::<String>(&used_iter, column_path);\n\n        let full_name = get_full_name_from_path_name(&path, &name);\n\n        returned_vector.push((full_name, name, model.path(&used_iter)));\n    }\n\n    assert!(model.iter_next(&mut used_iter), \"Found only header!\");\n\n    loop {\n        let name = model.get::<String>(&used_iter, column_name);\n        let path = model.get::<String>(&used_iter, column_path);\n\n        let full_name = get_full_name_from_path_name(&path, &name);\n\n        returned_vector.push((full_name, name, model.path(&used_iter)));\n\n        if !model.iter_next(&mut used_iter) {\n            break;\n        }\n\n        if model.get::<bool>(&used_iter, column_header) {\n            break;\n        }\n    }\n\n    assert!(returned_vector.len() > 1);\n\n    returned_vector\n}\n\nfn move_iter(model: &TreeModel, tree_path: &TreePath, column_header: i32, go_next: bool) -> TreePath {\n    let mut tree_iter = model.iter(tree_path).expect(\"Using invalid tree_path\");\n\n    assert!(model.get::<bool>(&tree_iter, column_header));\n\n    if go_next {\n        assert!(model.iter_next(&mut tree_iter), \"Found only header!\");\n    } else {\n        assert!(model.iter_previous(&mut tree_iter), \"Found only header!\");\n    }\n\n    loop {\n        if go_next {\n            if !model.iter_next(&mut tree_iter) {\n                break;\n            }\n        } else if !model.iter_previous(&mut tree_iter) {\n            break;\n        }\n\n        if model.get::<bool>(&tree_iter, column_header) {\n            break;\n        }\n    }\n    model.path(&tree_iter)\n}\n\nfn populate_similar_scrolled_view(\n    scrolled_window: &ScrolledWindow,\n    image_cache: &[(String, String, Picture, Picture, TreePath)],\n    image_compare_left: &Picture,\n    image_compare_right: &Picture,\n    shared_using_for_preview: &Rc<RefCell<(Option<TreePath>, Option<TreePath>)>>,\n    shared_image_cache: &Rc<RefCell<Vec<(String, String, Picture, Picture, TreePath)>>>,\n    check_button_left_preview_text: &CheckButton,\n    check_button_right_preview_text: &CheckButton,\n    model: &TreeModel,\n    column_selection: i32,\n) {\n    scrolled_window.set_child(None::<&Widget>);\n\n    let all_gtk_box = gtk4::Box::new(Orientation::Horizontal, 5);\n    all_gtk_box.set_widget_name(\"all_box\");\n    all_gtk_box.set_halign(Align::Fill);\n    all_gtk_box.set_valign(Align::Fill);\n\n    for (number, (path, _name, big_thumbnail, small_thumbnail, tree_path)) in image_cache.iter().enumerate() {\n        let small_box = gtk4::Box::new(Orientation::Vertical, 3);\n\n        let smaller_box = gtk4::Box::new(Orientation::Horizontal, 2);\n\n        let button_left = gtk4::Button::builder().label(&flg!(\"compare_move_left_button\")).build();\n        let label = gtk4::Label::builder().label((number + 1).to_string()).build();\n        let button_right = gtk4::Button::builder().label(&flg!(\"compare_move_right_button\")).build();\n\n        let image_compare_left = image_compare_left.clone();\n        let image_compare_right = image_compare_right.clone();\n\n        let big_thumbnail_clone = big_thumbnail.clone();\n        let tree_path_clone = tree_path.clone();\n        let all_gtk_box_clone = all_gtk_box.clone();\n        let shared_using_for_preview_clone = shared_using_for_preview.clone();\n        let shared_image_cache_clone = shared_image_cache.clone();\n        let check_button_left_preview_text_clone = check_button_left_preview_text.clone();\n        let model_clone = model.clone();\n        let path_clone = path.clone();\n\n        button_left.connect_clicked(move |_button_left| {\n            shared_using_for_preview_clone.borrow_mut().0 = Some(tree_path_clone.clone());\n            update_bottom_buttons(&all_gtk_box_clone, &shared_using_for_preview_clone, &shared_image_cache_clone);\n            image_compare_left.set_paintable(big_thumbnail_clone.paintable().as_ref());\n\n            let is_active = model_clone.get::<bool>(&model_clone.iter(&tree_path_clone).expect(\"Invalid tree_path\"), column_selection);\n            check_button_left_preview_text_clone.set_active(is_active);\n            check_button_left_preview_text_clone.set_label(Some(&format!(\"{}. {}\", number + 1, get_max_file_name(&path_clone, 60))));\n        });\n\n        let big_thumbnail_clone = big_thumbnail.clone();\n        let tree_path_clone = tree_path.clone();\n        let all_gtk_box_clone = all_gtk_box.clone();\n        let shared_using_for_preview_clone = shared_using_for_preview.clone();\n        let shared_image_cache_clone = shared_image_cache.clone();\n        let check_button_right_preview_text_clone = check_button_right_preview_text.clone();\n        let model_clone = model.clone();\n        let path_clone = path.clone();\n\n        button_right.connect_clicked(move |_button_right| {\n            shared_using_for_preview_clone.borrow_mut().1 = Some(tree_path_clone.clone());\n            update_bottom_buttons(&all_gtk_box_clone, &shared_using_for_preview_clone, &shared_image_cache_clone);\n            image_compare_right.set_paintable(big_thumbnail_clone.paintable().as_ref());\n\n            let is_active = model_clone.get::<bool>(&model_clone.iter(&tree_path_clone).expect(\"Invalid tree_path\"), column_selection);\n            check_button_right_preview_text_clone.set_active(is_active);\n            check_button_right_preview_text_clone.set_label(Some(&format!(\"{}. {}\", number + 1, get_max_file_name(&path_clone, 60))));\n        });\n\n        smaller_box.append(&button_left);\n        smaller_box.append(&label);\n        smaller_box.append(&button_right);\n\n        small_box.append(&smaller_box);\n        small_box.set_halign(Align::Fill);\n        small_box.set_valign(Align::Fill);\n        small_box.set_hexpand_set(true);\n        small_box.set_vexpand_set(true);\n        small_thumbnail.set_halign(Align::Fill);\n        small_thumbnail.set_valign(Align::Fill);\n        small_thumbnail.set_hexpand(true);\n        small_thumbnail.set_hexpand_set(true);\n        small_thumbnail.set_vexpand(true);\n        small_thumbnail.set_vexpand_set(true);\n\n        small_box.append(small_thumbnail);\n\n        all_gtk_box.append(&small_box);\n    }\n\n    all_gtk_box.set_visible(true);\n    scrolled_window.set_child(Some(&all_gtk_box));\n}\n\nfn update_bottom_buttons(\n    all_gtk_box: &gtk4::Box,\n    shared_using_for_preview: &Rc<RefCell<(Option<TreePath>, Option<TreePath>)>>,\n    image_cache: &Rc<RefCell<Vec<(String, String, Picture, Picture, TreePath)>>>,\n) {\n    let left_tree_view = shared_using_for_preview.borrow().0.clone().expect(\"Left tree_view not set\");\n    let right_tree_view = shared_using_for_preview.borrow().1.clone().expect(\"Right tree_view not set\");\n\n    for (number, i) in all_gtk_box.get_all_direct_children().into_iter().enumerate() {\n        let cache_tree_path = (*image_cache.borrow())[number].4.clone();\n        let is_chosen = cache_tree_path != right_tree_view && cache_tree_path != left_tree_view;\n\n        let bx = i.downcast::<gtk4::Box>().expect(\"Not Box\");\n        let smaller_bx = bx.first_child().expect(\"No first child\").downcast::<gtk4::Box>().expect(\"First child is not Box\");\n        for items in smaller_bx.get_all_direct_children() {\n            if let Ok(btn) = items.downcast::<gtk4::Button>() {\n                btn.set_sensitive(is_chosen);\n            }\n        }\n    }\n}\n\nfn get_current_group_and_iter_from_selection(sv: &SubView) -> (u32, TreePath) {\n    let mut current_group = 1;\n    let mut possible_group = 1;\n    let mut header_clone: TreeIter;\n    let mut possible_header: TreeIter;\n\n    let column_header = sv.nb_object.column_header.expect(\"Missing column_header\");\n    let model = sv.get_tree_model();\n    let selection = sv.get_tree_selection();\n\n    let selected_records = selection.selected_rows().0;\n\n    let mut iter = model.iter_first().expect(\"Model is no empty, so should have first item\"); // Checking that treeview is not empty should be done before\n    header_clone = iter; // if nothing selected, use first group\n    possible_header = iter; // if nothing selected, use first group\n    assert!(model.get::<bool>(&iter, column_header)); // First element should be header\n\n    if !selected_records.is_empty() {\n        let first_selected_record = selected_records[0].clone();\n        loop {\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n\n            if model.get::<bool>(&iter, column_header) {\n                possible_group += 1;\n                possible_header = iter;\n            }\n\n            if model.path(&iter) == first_selected_record {\n                header_clone = possible_header;\n                current_group = possible_group;\n            }\n        }\n    }\n\n    (current_group, model.path(&header_clone))\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_delete.rs",
    "content": "use czkawka_core::common::{remove_folder_if_contains_only_empty_folders, remove_single_file};\nuse gtk4::prelude::*;\nuse gtk4::{Align, CheckButton, Dialog, Orientation, ResponseType, TextView};\nuse log::debug;\nuse rayon::prelude::*;\n\nuse crate::flg;\nuse crate::gui_structs::common_tree_view::SubView;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::get_full_name_from_path_name;\nuse crate::helpers::list_store_operations::{check_how_much_elements_is_selected, clean_invalid_headers};\nuse crate::helpers::model_iter::iter_list;\nuse crate::notebook_enums::NotebookMainEnum;\n\n// TODO add support for checking if really symlink doesn't point to correct directory/file\n\npub(crate) fn connect_button_delete(gui_data: &GuiData) {\n    let buttons_delete = gui_data.bottom_buttons.buttons_delete.clone();\n\n    let gui_data = gui_data.clone(); // TODO this maybe can be replaced, not sure if worth to clone everything\n\n    buttons_delete.connect_clicked(move |_| {\n        glib::MainContext::default().spawn_local(delete_things(gui_data.clone()));\n    });\n}\n\npub async fn delete_things(gui_data: GuiData) {\n    let window_main = gui_data.window_main.clone();\n    let check_button_settings_confirm_deletion = gui_data.settings.check_button_settings_confirm_deletion.clone();\n    let check_button_settings_confirm_group_deletion = gui_data.settings.check_button_settings_confirm_group_deletion.clone();\n\n    let check_button_settings_use_trash = gui_data.settings.check_button_settings_use_trash.clone();\n\n    let text_view_errors = gui_data.text_view_errors.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    let sv = gui_data.main_notebook.common_tree_views.get_current_subview();\n\n    let (number_of_selected_items, number_of_selected_groups) = check_how_much_elements_is_selected(sv);\n\n    // Nothing is selected\n    if number_of_selected_items == 0 {\n        return;\n    }\n\n    if !check_if_can_delete_files(&check_button_settings_confirm_deletion, &window_main, number_of_selected_items, number_of_selected_groups).await {\n        return;\n    }\n\n    if let Some(column_header) = sv.nb_object.column_header {\n        if !check_button_settings_confirm_group_deletion.is_active() || !check_if_deleting_all_files_in_group(sv, &window_main, &check_button_settings_confirm_group_deletion).await\n        {\n            tree_remove(sv, column_header, &check_button_settings_use_trash, &text_view_errors);\n        }\n    } else if sv.nb_object.notebook_type == NotebookMainEnum::EmptyDirectories {\n        empty_folder_remover(sv, &check_button_settings_use_trash, &text_view_errors);\n    } else {\n        basic_remove(sv, &check_button_settings_use_trash, &text_view_errors);\n    }\n\n    common_tree_views.hide_preview();\n}\n\npub async fn check_if_can_delete_files(\n    check_button_settings_confirm_deletion: &CheckButton,\n    window_main: &gtk4::Window,\n    number_of_selected_items: u64,\n    number_of_selected_groups: u64,\n) -> bool {\n    if check_button_settings_confirm_deletion.is_active() {\n        let (confirmation_dialog_delete, check_button) = create_dialog_ask_for_deletion(window_main, number_of_selected_items, number_of_selected_groups);\n\n        let response_type = confirmation_dialog_delete.run_future().await;\n        if response_type == ResponseType::Ok {\n            if !check_button.is_active() {\n                check_button_settings_confirm_deletion.set_active(false);\n            }\n            confirmation_dialog_delete.set_visible(false);\n            confirmation_dialog_delete.close();\n        } else {\n            confirmation_dialog_delete.set_visible(false);\n            confirmation_dialog_delete.close();\n            return false;\n        }\n    }\n    true\n}\n\nfn create_dialog_ask_for_deletion(window_main: &gtk4::Window, number_of_selected_items: u64, number_of_selected_groups: u64) -> (Dialog, CheckButton) {\n    let dialog = Dialog::builder().title(flg!(\"delete_title_dialog\")).transient_for(window_main).modal(true).build();\n    let button_ok = dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n    dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n    dialog.set_default_size(300, 0);\n\n    let label: gtk4::Label = gtk4::Label::new(Some(&flg!(\"delete_question_label\")));\n    let label2: gtk4::Label = match number_of_selected_groups {\n        0 => gtk4::Label::new(Some(&flg!(\"delete_items_label\", items = number_of_selected_items))),\n        _ => gtk4::Label::new(Some(&flg!(\n            \"delete_items_groups_label\",\n            items = number_of_selected_items,\n            groups = number_of_selected_groups\n        ))),\n    };\n\n    let check_button: CheckButton = CheckButton::builder()\n        .label(flg!(\"dialogs_ask_next_time\"))\n        .active(true)\n        .halign(Align::Center)\n        .margin_top(5)\n        .build();\n\n    button_ok.grab_focus();\n\n    let parent = button_ok.parent().expect(\"Hack 1\").parent().expect(\"Hack 2\").downcast::<gtk4::Box>().expect(\"Hack 3\"); // TODO Hack, but not so ugly as before\n    parent.set_orientation(Orientation::Vertical);\n    parent.insert_child_after(&label, None::<&gtk4::Widget>);\n    parent.insert_child_after(&label2, Some(&label));\n    parent.insert_child_after(&check_button, Some(&label2));\n\n    dialog.set_visible(true);\n    (dialog, check_button)\n}\n\nfn create_dialog_group_deletion(window_main: &gtk4::Window) -> (Dialog, CheckButton) {\n    let dialog = Dialog::builder()\n        .title(flg!(\"delete_all_files_in_group_title\"))\n        .transient_for(window_main)\n        .modal(true)\n        .build();\n    let button_ok = dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n    dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n    let label: gtk4::Label = gtk4::Label::new(Some(&flg!(\"delete_all_files_in_group_label1\")));\n    let label2: gtk4::Label = gtk4::Label::new(Some(&flg!(\"delete_all_files_in_group_label2\")));\n    let check_button: CheckButton = CheckButton::builder().label(flg!(\"dialogs_ask_next_time\")).active(true).halign(Align::Center).build();\n\n    button_ok.grab_focus();\n\n    let parent = button_ok.parent().expect(\"Hack 1\").parent().expect(\"Hack 2\").downcast::<gtk4::Box>().expect(\"Hack 3\"); // TODO Hack, but not so ugly as before\n    parent.set_orientation(Orientation::Vertical);\n    parent.insert_child_after(&label, None::<&gtk4::Widget>);\n    parent.insert_child_after(&label2, Some(&label));\n    parent.insert_child_after(&check_button, Some(&label2));\n\n    dialog.set_visible(true);\n    (dialog, check_button)\n}\n\npub async fn check_if_deleting_all_files_in_group(sv: &SubView, window_main: &gtk4::Window, check_button_settings_confirm_group_deletion: &CheckButton) -> bool {\n    let column_header = sv.nb_object.column_header.expect(\"Column header must exist here\");\n    let model = sv.get_model();\n\n    let mut selected_all_records: bool = true;\n\n    if let Some(mut iter) = model.iter_first() {\n        assert!(model.get::<bool>(&iter, column_header)); // First element should be header\n\n        // It is safe to remove any number of files in reference mode\n        if !model.get::<String>(&iter, sv.nb_object.column_path).is_empty() {\n            return false;\n        }\n\n        loop {\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n\n            if model.get::<bool>(&iter, column_header) {\n                if selected_all_records {\n                    break;\n                }\n                selected_all_records = true;\n            } else if !model.get::<bool>(&iter, sv.nb_object.column_selection) {\n                selected_all_records = false;\n            }\n        }\n    } else {\n        return false;\n    }\n\n    if !selected_all_records {\n        return false;\n    }\n\n    let (confirmation_dialog_group_delete, check_button) = create_dialog_group_deletion(window_main);\n\n    let response_type = confirmation_dialog_group_delete.run_future().await;\n    if response_type == ResponseType::Ok {\n        if !check_button.is_active() {\n            check_button_settings_confirm_group_deletion.set_active(false);\n        }\n    } else {\n        confirmation_dialog_group_delete.set_visible(false);\n        confirmation_dialog_group_delete.close();\n        return true;\n    }\n    confirmation_dialog_group_delete.set_visible(false);\n    confirmation_dialog_group_delete.close();\n\n    false\n}\n\npub(crate) fn empty_folder_remover(sv: &SubView, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView) {\n    common_file_remove(sv, check_button_settings_use_trash, text_view_errors, None, false);\n}\n\npub(crate) fn basic_remove(sv: &SubView, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView) {\n    common_file_remove(sv, check_button_settings_use_trash, text_view_errors, None, true);\n}\n\npub(crate) fn tree_remove(sv: &SubView, column_header: i32, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView) {\n    common_file_remove(sv, check_button_settings_use_trash, text_view_errors, Some(column_header), true);\n\n    clean_invalid_headers(&sv.get_model(), column_header, sv.nb_object.column_path);\n}\n\npub(crate) fn common_file_remove(sv: &SubView, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView, column_header: Option<i32>, file_remove: bool) {\n    let use_trash = check_button_settings_use_trash.is_active();\n\n    let model = sv.get_model();\n\n    let mut messages: String = String::new();\n\n    let mut selected_rows = Vec::new();\n\n    iter_list(&model, |m, i| {\n        if m.get::<bool>(i, sv.nb_object.column_selection) {\n            if let Some(column_header) = column_header {\n                if !m.get::<bool>(i, column_header) {\n                    selected_rows.push(m.path(i));\n                } else {\n                    panic!(\"Header row shouldn't be selected, please report bug.\");\n                }\n            } else {\n                selected_rows.push(m.path(i));\n            }\n        }\n    });\n\n    if selected_rows.is_empty() {\n        return; // No selected rows\n    }\n\n    debug!(\"Starting to delete {} files\", selected_rows.len());\n    let start_time = std::time::Instant::now();\n\n    let to_remove = selected_rows\n        .iter()\n        .enumerate()\n        .map(|(idx, tree_path)| {\n            let iter = model.iter(tree_path).expect(\"Using invalid tree_path\");\n\n            let name = model.get::<String>(&iter, sv.nb_object.column_name);\n            let path = model.get::<String>(&iter, sv.nb_object.column_path);\n\n            (idx, get_full_name_from_path_name(&path, &name))\n        })\n        .collect::<Vec<_>>();\n\n    let (mut removed, failed_to_remove): (Vec<usize>, Vec<String>) = to_remove\n        .into_par_iter()\n        .map(|(idx, path)| {\n            if file_remove {\n                remove_single_file(&path, use_trash)?;\n            } else {\n                remove_folder_if_contains_only_empty_folders(&path, use_trash)?;\n            }\n            Ok(idx)\n        })\n        .partition_map(|res| match res {\n            Ok(entry) => itertools::Either::Left(entry),\n            Err(err) => itertools::Either::Right(err),\n        });\n\n    for failed in &failed_to_remove {\n        messages += failed;\n        messages += \"\\n\";\n    }\n\n    removed.sort_unstable();\n    removed.reverse(); // Must be deleted from end to start\n    let deleted_files = removed.len();\n\n    for idx in removed {\n        let iter = model.iter(&selected_rows[idx]).expect(\"Using invalid tree_path\");\n        model.remove(&iter);\n    }\n\n    debug!(\n        \"Deleted {deleted_files}/{} items({} tab) in {:?}\",\n        selected_rows.len(),\n        sv.nb_object.name,\n        start_time.elapsed()\n    );\n\n    text_view_errors.buffer().set_text(messages.as_str());\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_hardlink.rs",
    "content": "use czkawka_core::common::{make_file_symlink, make_hard_link};\nuse gtk4::prelude::*;\nuse gtk4::{Align, CheckButton, Dialog, Orientation, ResponseType, TextView, TreeIter, TreePath};\nuse rayon::prelude::*;\n\nuse crate::flg;\nuse crate::gui_structs::common_tree_view::SubView;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::{add_text_to_text_view, get_full_name_from_path_name, reset_text_view};\nuse crate::helpers::list_store_operations::clean_invalid_headers;\nuse crate::helpers::model_iter::{iter_list, iter_list_break_with_init};\n\n#[derive(PartialEq, Eq, Copy, Clone)]\nenum TypeOfTool {\n    Hardlinking,\n    Symlinking,\n}\n\n#[derive(Debug)]\nstruct SymHardlinkData {\n    original_data: String,\n    files_to_symhardlink: Vec<String>,\n}\n\npub(crate) fn connect_button_hardlink_symlink(gui_data: &GuiData) {\n    // Hardlinking\n    {\n        let buttons_hardlink = gui_data.bottom_buttons.buttons_hardlink.clone();\n\n        let gui_data = gui_data.clone();\n\n        buttons_hardlink.connect_clicked(move |_| {\n            glib::MainContext::default().spawn_local(sym_hard_link_things(gui_data.clone(), TypeOfTool::Hardlinking));\n        });\n    }\n\n    // Symlinking\n    {\n        let buttons_symlink = gui_data.bottom_buttons.buttons_symlink.clone();\n\n        let gui_data = gui_data.clone();\n\n        buttons_symlink.connect_clicked(move |_| {\n            glib::MainContext::default().spawn_local(sym_hard_link_things(gui_data.clone(), TypeOfTool::Symlinking));\n        });\n    }\n}\n\nasync fn sym_hard_link_things(gui_data: GuiData, hardlinking: TypeOfTool) {\n    let text_view_errors = gui_data.text_view_errors.clone();\n    let window_main = gui_data.window_main.clone();\n\n    let common_tree_views = &gui_data.main_notebook.common_tree_views.clone();\n    let sv = common_tree_views.get_current_subview();\n\n    let check_button_settings_confirm_link = gui_data.settings.check_button_settings_confirm_link.clone();\n\n    if !check_if_anything_is_selected_async(sv) {\n        return;\n    }\n\n    if !check_if_can_link_files(&check_button_settings_confirm_link, &window_main).await {\n        return;\n    }\n\n    if !check_if_changing_one_item_in_group_and_continue(sv, &window_main).await {\n        return;\n    }\n\n    hardlink_symlink(sv, hardlinking, &text_view_errors);\n\n    common_tree_views.hide_preview();\n}\n\nfn hardlink_symlink(sv: &SubView, hardlinking: TypeOfTool, text_view_errors: &TextView) {\n    reset_text_view(text_view_errors);\n\n    let column_header = sv.nb_object.column_header.expect(\"Linking can be only used for tree views with grouped results\");\n    let model = sv.get_model();\n\n    let mut vec_tree_path_to_remove: Vec<TreePath> = Vec::new(); // List of hardlinked files without its root\n    let mut vec_symhardlink_data: Vec<SymHardlinkData> = Vec::new();\n\n    let mut current_iter: TreeIter = match model.iter_first() {\n        Some(t) => t,\n        None => return, // No records\n    };\n\n    let mut selected_rows = Vec::new();\n    iter_list(&model, |m, i| {\n        if m.get::<bool>(i, sv.nb_object.column_selection) {\n            if !m.get::<bool>(i, column_header) {\n                selected_rows.push(m.path(i));\n            } else {\n                panic!(\"Header row shouldn't be selected, please report bug.\");\n            }\n        }\n    });\n\n    if selected_rows.is_empty() {\n        return; // No selected rows\n    }\n\n    let mut current_symhardlink_data: Option<SymHardlinkData> = None;\n    let mut current_selected_index = 0;\n    loop {\n        if model.get::<bool>(&current_iter, column_header) {\n            if let Some(current_symhardlink_data) = current_symhardlink_data\n                && !current_symhardlink_data.files_to_symhardlink.is_empty()\n            {\n                vec_symhardlink_data.push(current_symhardlink_data);\n            }\n\n            current_symhardlink_data = None;\n            assert!(model.iter_next(&mut current_iter), \"HEADER, shouldn't be a last item.\");\n            continue;\n        }\n\n        if model.path(&current_iter) == selected_rows[current_selected_index] {\n            let file_name = model.get::<String>(&current_iter, sv.nb_object.column_name);\n            let path = model.get::<String>(&current_iter, sv.nb_object.column_path);\n            let full_file_path = get_full_name_from_path_name(&path, &file_name);\n\n            if let Some(mut current_data) = current_symhardlink_data {\n                vec_tree_path_to_remove.push(model.path(&current_iter));\n                current_data.files_to_symhardlink.push(full_file_path);\n                current_symhardlink_data = Some(current_data);\n            } else {\n                current_symhardlink_data = Some(SymHardlinkData {\n                    original_data: full_file_path,\n                    files_to_symhardlink: Vec::new(),\n                });\n            }\n\n            if current_selected_index != selected_rows.len() - 1 {\n                current_selected_index += 1;\n            } else {\n                if let Some(current_symhardlink_data) = current_symhardlink_data\n                    && !current_symhardlink_data.files_to_symhardlink.is_empty()\n                {\n                    vec_symhardlink_data.push(current_symhardlink_data);\n                }\n                break; // There is no more selected items, so we just end checking\n            }\n        }\n\n        if !model.iter_next(&mut current_iter) {\n            if let Some(current_symhardlink_data) = current_symhardlink_data\n                && !current_symhardlink_data.files_to_symhardlink.is_empty()\n            {\n                vec_symhardlink_data.push(current_symhardlink_data);\n            }\n\n            break;\n        }\n    }\n\n    let errors = vec_symhardlink_data\n        .into_par_iter()\n        .flat_map(|symhardlink_data| {\n            let mut err = Vec::new();\n            for file_to_be_replaced in symhardlink_data.files_to_symhardlink {\n                if hardlinking == TypeOfTool::Symlinking {\n                    if let Err(e) = make_file_symlink(&symhardlink_data.original_data, &file_to_be_replaced) {\n                        err.push(flg!(\n                            \"symlink_failed\",\n                            name = symhardlink_data.original_data.clone(),\n                            target = file_to_be_replaced,\n                            reason = e.to_string()\n                        ));\n                    }\n                } else {\n                    if let Err(e) = make_hard_link(&symhardlink_data.original_data, &file_to_be_replaced) {\n                        err.push(flg!(\n                            \"hardlink_failed\",\n                            name = symhardlink_data.original_data.clone(),\n                            target = file_to_be_replaced,\n                            reason = e.to_string()\n                        ));\n                    }\n                }\n            }\n            err\n        })\n        .collect::<Vec<_>>();\n\n    for error in errors {\n        add_text_to_text_view(text_view_errors, &error);\n    }\n\n    for tree_path in vec_tree_path_to_remove.iter().rev() {\n        model.remove(&model.iter(tree_path).expect(\"Using invalid tree_path\"));\n    }\n\n    clean_invalid_headers(&model, column_header, sv.nb_object.column_path);\n}\n\nfn create_dialog_non_group(window_main: &gtk4::Window) -> Dialog {\n    let dialog = Dialog::builder()\n        .title(flg!(\"hard_sym_invalid_selection_title_dialog\"))\n        .transient_for(window_main)\n        .modal(true)\n        .build();\n    let button_ok = dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n    dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n    let label: gtk4::Label = gtk4::Label::new(Some(&flg!(\"hard_sym_invalid_selection_label_1\")));\n    let label2: gtk4::Label = gtk4::Label::new(Some(&flg!(\"hard_sym_invalid_selection_label_2\")));\n    let label3: gtk4::Label = gtk4::Label::new(Some(&flg!(\"hard_sym_invalid_selection_label_3\")));\n\n    button_ok.grab_focus();\n\n    let parent = button_ok.parent().expect(\"Hack 1\").parent().expect(\"Hack 2\").downcast::<gtk4::Box>().expect(\"Hack 3\"); // TODO Hack, but not so ugly as before\n    parent.set_orientation(Orientation::Vertical);\n    parent.insert_child_after(&label, None::<&gtk4::Widget>);\n    parent.insert_child_after(&label2, Some(&label));\n    parent.insert_child_after(&label3, Some(&label2));\n\n    dialog.set_visible(true);\n    dialog\n}\n\npub async fn check_if_changing_one_item_in_group_and_continue(sv: &SubView, window_main: &gtk4::Window) -> bool {\n    let model = sv.get_model();\n    let column_header = sv.nb_object.column_header.expect(\"Column header must exists for linking\");\n\n    let mut selected_values_in_group = 0;\n\n    if let Some(mut iter) = model.iter_first() {\n        assert!(model.get::<bool>(&iter, column_header)); // First element should be header\n\n        loop {\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n\n            if model.get::<bool>(&iter, column_header) {\n                if selected_values_in_group == 1 {\n                    break;\n                }\n                selected_values_in_group = 0;\n            } else if model.get::<bool>(&iter, sv.nb_object.column_selection) {\n                selected_values_in_group += 1;\n            }\n        }\n    } else {\n        return false; // No available records\n    }\n\n    if selected_values_in_group == 1 {\n        let confirmation_dialog = create_dialog_non_group(window_main);\n\n        let response_type = confirmation_dialog.run_future().await;\n        if response_type != ResponseType::Ok {\n            confirmation_dialog.set_visible(false);\n            confirmation_dialog.close();\n            return false;\n        }\n        confirmation_dialog.set_visible(false);\n        confirmation_dialog.close();\n    }\n\n    true\n}\n\npub(crate) fn check_if_anything_is_selected_async(sv: &SubView) -> bool {\n    let model = sv.get_model();\n\n    let column_header = sv.nb_object.column_header.expect(\"Column header must exists for linking\");\n\n    let mut non_header_selected = false;\n\n    iter_list_break_with_init(\n        &model,\n        |m, i| {\n            assert!(m.get::<bool>(i, column_header)); // First element should be header\n        },\n        |m, i| {\n            if !m.get::<bool>(i, column_header) && m.get::<bool>(i, sv.nb_object.column_selection) {\n                non_header_selected = true;\n                return false;\n            }\n            true\n        },\n    );\n\n    non_header_selected\n}\n\npub async fn check_if_can_link_files(check_button_settings_confirm_link: &CheckButton, window_main: &gtk4::Window) -> bool {\n    if check_button_settings_confirm_link.is_active() {\n        let (confirmation_dialog_link, check_button) = create_dialog_ask_for_linking(window_main);\n\n        let response_type = confirmation_dialog_link.run_future().await;\n        if response_type == ResponseType::Ok {\n            if !check_button.is_active() {\n                check_button_settings_confirm_link.set_active(false);\n            }\n            confirmation_dialog_link.set_visible(false);\n            confirmation_dialog_link.close();\n        } else {\n            confirmation_dialog_link.set_visible(false);\n            confirmation_dialog_link.close();\n            return false;\n        }\n    }\n    true\n}\n\nfn create_dialog_ask_for_linking(window_main: &gtk4::Window) -> (Dialog, CheckButton) {\n    let dialog = Dialog::builder().title(flg!(\"hard_sym_link_title_dialog\")).transient_for(window_main).modal(true).build();\n    let button_ok = dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n    dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n    let label: gtk4::Label = gtk4::Label::new(Some(&flg!(\"hard_sym_link_label\")));\n    let check_button: CheckButton = CheckButton::builder().label(flg!(\"dialogs_ask_next_time\")).active(true).halign(Align::Center).build();\n\n    button_ok.grab_focus();\n\n    let parent = button_ok.parent().expect(\"Hack 1\").parent().expect(\"Hack 2\").downcast::<gtk4::Box>().expect(\"Hack 3\"); // TODO Hack, but not so ugly as before\n    parent.set_orientation(Orientation::Vertical);\n    parent.insert_child_after(&label, None::<&gtk4::Widget>);\n    parent.insert_child_after(&check_button, Some(&label));\n\n    dialog.set_visible(true);\n    (dialog, check_button)\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_move.rs",
    "content": "use std::path::Path;\n\nuse fs_extra::dir::CopyOptions;\nuse gtk4::prelude::*;\nuse gtk4::{ResponseType, TreePath};\nuse log::debug;\n\nuse crate::connect_things::file_chooser_helpers::extract_paths_from_file_chooser;\nuse crate::flg;\nuse crate::gui_structs::common_tree_view::SubView;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::{add_text_to_text_view, get_full_name_from_path_name, reset_text_view};\nuse crate::helpers::list_store_operations::{check_how_much_elements_is_selected, clean_invalid_headers};\nuse crate::helpers::model_iter::iter_list;\n\npub(crate) fn connect_button_move(gui_data: &GuiData) {\n    let buttons_move = gui_data.bottom_buttons.buttons_move.clone();\n\n    let entry_info = gui_data.entry_info.clone();\n    let text_view_errors = gui_data.text_view_errors.clone();\n\n    let file_dialog_move_to_folder = gui_data.file_dialog_move_to_folder.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    file_dialog_move_to_folder.connect_response(move |file_chooser, response_type| {\n        let sv = common_tree_views.get_current_subview();\n\n        let (number_of_selected_items, _number_of_selected_groups) = check_how_much_elements_is_selected(sv);\n\n        // Nothing is selected\n        if number_of_selected_items == 0 {\n            return;\n        }\n\n        reset_text_view(&text_view_errors);\n\n        if response_type == ResponseType::Accept {\n            let folders = extract_paths_from_file_chooser(file_chooser);\n\n            if folders.len() != 1 {\n                add_text_to_text_view(&text_view_errors, flg!(\"move_files_choose_more_than_1_path\", path_number = folders.len()).as_str());\n            } else {\n                let folder = folders[0].clone();\n                if sv.nb_object.column_header.is_some() {\n                    move_with_tree(sv, &folder, &entry_info, &text_view_errors);\n                } else {\n                    move_with_list(sv, &folder, &entry_info, &text_view_errors);\n                }\n            }\n        }\n        common_tree_views.hide_preview();\n    });\n\n    buttons_move.connect_clicked(move |_| {\n        file_dialog_move_to_folder.set_visible(true);\n    });\n}\n\nfn move_with_tree(sv: &SubView, destination_folder: &Path, entry_info: &gtk4::Entry, text_view_errors: &gtk4::TextView) {\n    let model = sv.get_model();\n    let column_header = sv.nb_object.column_header.expect(\"Using move_with_tree without header column\");\n\n    let mut selected_rows = Vec::new();\n\n    iter_list(&model, |m, i| {\n        if m.get::<bool>(i, sv.nb_object.column_selection) {\n            if !m.get::<bool>(i, column_header) {\n                selected_rows.push(m.path(i));\n            } else {\n                panic!(\"Header row shouldn't be selected, please report bug.\");\n            }\n        }\n    });\n\n    if selected_rows.is_empty() {\n        return; // No selected rows\n    }\n\n    move_files_common(\n        &selected_rows,\n        &model,\n        sv.nb_object.column_name,\n        sv.nb_object.column_path,\n        destination_folder,\n        entry_info,\n        text_view_errors,\n    );\n\n    clean_invalid_headers(&model, column_header, sv.nb_object.column_path);\n}\n\nfn move_with_list(sv: &SubView, destination_folder: &Path, entry_info: &gtk4::Entry, text_view_errors: &gtk4::TextView) {\n    let model = sv.get_model();\n\n    let mut selected_rows = Vec::new();\n\n    iter_list(&model, |m, i| {\n        if m.get::<bool>(i, sv.nb_object.column_selection) {\n            selected_rows.push(m.path(i));\n        }\n    });\n\n    if selected_rows.is_empty() {\n        return; // No selected rows\n    }\n\n    move_files_common(\n        &selected_rows,\n        &model,\n        sv.nb_object.column_name,\n        sv.nb_object.column_path,\n        destination_folder,\n        entry_info,\n        text_view_errors,\n    );\n}\n\nfn move_files_common(\n    selected_rows: &[TreePath],\n    model: &gtk4::ListStore,\n    column_file_name: i32,\n    column_path: i32,\n    destination_folder: &Path,\n    entry_info: &gtk4::Entry,\n    text_view_errors: &gtk4::TextView,\n) {\n    let mut messages: String = String::new();\n\n    let mut moved_files: u32 = 0;\n\n    debug!(\"Starting to move {} files\", selected_rows.len());\n    let start_time = std::time::Instant::now();\n\n    // Save to variable paths of files, and remove it when not removing all occurrences.\n    'next_result: for tree_path in selected_rows.iter().rev() {\n        let iter = model.iter(tree_path).expect(\"Using invalid tree_path\");\n\n        let file_name = model.get::<String>(&iter, column_file_name);\n        let path = model.get::<String>(&iter, column_path);\n\n        let thing = get_full_name_from_path_name(&path, &file_name);\n        let destination_file = destination_folder.join(&file_name);\n        if Path::new(&thing).is_dir() {\n            if let Err(e) = fs_extra::dir::move_dir(&thing, &destination_file, &CopyOptions::new()) {\n                messages += flg!(\"move_folder_failed\", name = thing, reason = e.to_string()).as_str();\n                messages += \"\\n\";\n                continue 'next_result;\n            }\n        } else if let Err(e) = fs_extra::file::move_file(&thing, &destination_file, &fs_extra::file::CopyOptions::new()) {\n            messages += flg!(\"move_file_failed\", name = thing, reason = e.to_string()).as_str();\n            messages += \"\\n\";\n\n            continue 'next_result;\n        }\n        model.remove(&iter);\n        moved_files += 1;\n    }\n\n    debug!(\"Moved {moved_files} files in {:?}\", start_time.elapsed());\n\n    entry_info.set_text(flg!(\"move_stats\", num_files = moved_files, all_files = selected_rows.len()).as_str());\n\n    text_view_errors.buffer().set_text(messages.as_str());\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_save.rs",
    "content": "use std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::env;\nuse std::rc::Rc;\n\nuse gtk4::prelude::*;\nuse gtk4::{Button, Entry};\n\nuse crate::flg;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::helpers::enums::BottomButtonsEnum;\nuse crate::notebook_enums::NotebookMainEnum;\n\npub(crate) fn connect_button_save(gui_data: &GuiData) {\n    let buttons_save = gui_data.bottom_buttons.buttons_save.clone();\n    let shared_buttons = gui_data.shared_buttons.clone();\n    let entry_info = gui_data.entry_info.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    buttons_save.connect_clicked(move |buttons_save| {\n        let mut current_path = match env::current_dir() {\n            Ok(t) => t.to_string_lossy().to_string(),\n            Err(_) => \"__unknown__\".to_string(),\n        };\n        if [\"Windows\"].iter().any(|item| current_path.contains(item)) {\n            current_path = directories_next::UserDirs::new()\n                .and_then(|d| d.desktop_dir().map(|d| d.to_string_lossy().to_string()))\n                .unwrap_or_else(|| current_path.clone());\n        }\n\n        let subview = common_tree_views.get_current_subview();\n\n        if let Err(e) = subview.shared_model_enum.save_all_in_one(&current_path) {\n            entry_info.set_text(&format!(\"Failed to save results to folder {current_path}, reason {e}\"));\n            return;\n        }\n\n        post_save_things(subview.enum_value, &shared_buttons, &entry_info, buttons_save, current_path);\n    });\n}\n\nfn post_save_things(\n    type_of_tab: NotebookMainEnum,\n    shared_buttons: &Rc<RefCell<HashMap<NotebookMainEnum, HashMap<BottomButtonsEnum, bool>>>>,\n    entry_info: &Entry,\n    buttons_save: &Button,\n    current_path: String,\n) {\n    entry_info.set_text(&flg!(\"save_results_to_file\", name = current_path));\n    // Set state\n    {\n        buttons_save.set_visible(false);\n        *shared_buttons\n            .borrow_mut()\n            .get_mut(&type_of_tab)\n            .expect(\"Failed to get current tab\")\n            .get_mut(&BottomButtonsEnum::Save)\n            .expect(\"Failed to get save button\") = false;\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_search.rs",
    "content": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::model::CheckingMethod;\nuse czkawka_core::common::progress_data::ProgressData;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::Search;\nuse czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsParameters};\nuse czkawka_core::tools::big_file::{BigFile, BigFileParameters};\nuse czkawka_core::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes};\nuse czkawka_core::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters};\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse czkawka_core::tools::empty_folder::EmptyFolder;\nuse czkawka_core::tools::invalid_symlinks::InvalidSymlinks;\nuse czkawka_core::tools::same_music::{MusicSimilarity, SameMusic, SameMusicParameters};\nuse czkawka_core::tools::similar_images::{SimilarImages, SimilarImagesParameters};\nuse czkawka_core::tools::similar_videos::{DEFAULT_CROP_DETECT, DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION, SimilarVideos, SimilarVideosParameters};\nuse czkawka_core::tools::temporary::Temporary;\nuse fun_time::fun_time;\nuse gtk4::Grid;\nuse gtk4::prelude::*;\n\nuse crate::gui_structs::common_tree_view::TreeViewListStoreTrait;\nuse crate::gui_structs::common_upper_tree_view::UpperTreeViewEnum;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_combo_box::{\n    AUDIO_TYPE_CHECK_METHOD_COMBO_BOX, BIG_FILES_CHECK_METHOD_COMBO_BOX, DUPLICATES_CHECK_METHOD_COMBO_BOX, DUPLICATES_HASH_TYPE_COMBO_BOX, IMAGES_HASH_SIZE_COMBO_BOX,\n    IMAGES_HASH_TYPE_COMBO_BOX, IMAGES_RESIZE_ALGORITHM_COMBO_BOX,\n};\nuse crate::help_functions::{get_path_buf_from_vector_of_strings, hide_all_buttons, reset_text_view, set_buttons};\nuse crate::helpers::enums::{ColumnsExcludedDirectory, ColumnsIncludedDirectory, Message};\nuse crate::helpers::list_store_operations::{check_if_list_store_column_have_all_same_values, get_string_from_list_store};\nuse crate::helpers::model_iter::iter_list;\nuse crate::notebook_enums::NotebookMainEnum;\nuse crate::taskbar_progress::tbp_flags::TBPF_NOPROGRESS;\nuse crate::{DEFAULT_MAXIMAL_FILE_SIZE, DEFAULT_MINIMAL_CACHE_SIZE, DEFAULT_MINIMAL_FILE_SIZE, flg};\n\npub(crate) fn connect_button_search(gui_data: &GuiData, result_sender: Sender<Message>, progress_sender: Sender<ProgressData>) {\n    let buttons_array = gui_data.bottom_buttons.buttons_array.clone();\n    let buttons_search_clone = gui_data.bottom_buttons.buttons_search.clone();\n    let grid_progress = gui_data.progress_window.grid_progress.clone();\n    let label_stage = gui_data.progress_window.label_stage.clone();\n    let notebook_main = gui_data.main_notebook.notebook_main.clone();\n    let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();\n    let progress_bar_all_stages = gui_data.progress_window.progress_bar_all_stages.clone();\n    let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone();\n    let stop_flag = gui_data.stop_flag.clone();\n    let taskbar_state = gui_data.taskbar_state.clone();\n    let text_view_errors = gui_data.text_view_errors.clone();\n    let tree_view_included_directories = gui_data\n        .upper_notebook\n        .common_upper_tree_views\n        .get_tree_view(UpperTreeViewEnum::IncludedDirectories)\n        .clone();\n    let window_progress = gui_data.progress_window.window_progress.clone();\n    let entry_info = gui_data.entry_info.clone();\n    let button_settings = gui_data.header.button_settings.clone();\n    let button_app_info = gui_data.header.button_app_info.clone();\n\n    let gui_data = gui_data.clone();\n    buttons_search_clone.connect_clicked(move |_| {\n        let loaded_commons = LoadedCommonItems::load_items(&gui_data);\n\n        // Check if user selected all referenced folders\n        let list_store_included_directories = tree_view_included_directories.get_model();\n        if check_if_list_store_column_have_all_same_values(&list_store_included_directories, ColumnsIncludedDirectory::ReferenceButton as i32, true) {\n            entry_info.set_text(&flg!(\"selected_all_reference_folders\"));\n            return;\n        }\n\n        let show_dialog = Arc::new(AtomicBool::new(true));\n\n        window_progress.set_title(Some(&flg!(\"window_progress_title\")));\n\n        hide_all_buttons(&buttons_array);\n\n        notebook_main.set_sensitive(false);\n        notebook_upper.set_sensitive(false);\n        button_settings.set_sensitive(false);\n        button_app_info.set_sensitive(false);\n\n        entry_info.set_text(&flg!(\"searching_for_data\"));\n\n        // Resets progress bars\n        progress_bar_all_stages.set_fraction(0f64);\n        progress_bar_current_stage.set_fraction(0f64);\n\n        reset_text_view(&text_view_errors);\n\n        let result_sender = result_sender.clone();\n        let stop_flag = stop_flag.clone();\n        // Clear stop flag\n        stop_flag.store(false, Ordering::Relaxed);\n\n        label_stage.set_visible(true);\n\n        let progress_sender = progress_sender.clone();\n\n        let current_data = gui_data.main_notebook.common_tree_views.clone();\n        match current_data.get_current_page() {\n            NotebookMainEnum::Duplicate => duplicate_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::EmptyFiles => empty_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::EmptyDirectories => empty_dirs_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::BigFiles => big_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::Temporary => temporary_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::SimilarImages => similar_image_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::SimilarVideos => similar_video_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::SameMusic => same_music_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender, &show_dialog),\n            NotebookMainEnum::Symlinks => bad_symlinks_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n            NotebookMainEnum::BrokenFiles => broken_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender, &show_dialog),\n            NotebookMainEnum::BadExtensions => bad_extensions_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender),\n        }\n\n        window_progress.set_default_size(1, 1);\n\n        // Show progress dialog\n        if show_dialog.load(Ordering::Relaxed) {\n            window_progress.show();\n            taskbar_state.borrow().show();\n            taskbar_state.borrow().set_progress_state(TBPF_NOPROGRESS);\n        }\n    });\n}\n\nstruct LoadedCommonItems {\n    included_directories: Vec<PathBuf>,\n    excluded_directories: Vec<PathBuf>,\n    reference_directories: Vec<PathBuf>,\n    recursive_search: bool,\n    excluded_items: Vec<String>,\n    allowed_extensions: String,\n    excluded_extensions: String,\n    hide_hard_links: bool,\n    use_cache: bool,\n    save_also_as_json: bool,\n    minimal_cache_file_size: u64,\n    minimal_file_size: u64,\n    maximal_file_size: u64,\n    ignore_other_filesystems: bool,\n}\n\nimpl LoadedCommonItems {\n    fn load_items(gui_data: &GuiData) -> Self {\n        let check_button_settings_one_filesystem = gui_data.settings.check_button_settings_one_filesystem.clone();\n        let check_button_recursive = gui_data.upper_notebook.check_button_recursive.clone();\n        let check_button_settings_hide_hard_links = gui_data.settings.check_button_settings_hide_hard_links.clone();\n        let check_button_settings_use_cache = gui_data.settings.check_button_settings_use_cache.clone();\n        let entry_allowed_extensions = gui_data.upper_notebook.entry_allowed_extensions.clone();\n        let entry_excluded_extensions = gui_data.upper_notebook.entry_excluded_extensions.clone();\n        let entry_excluded_items = gui_data.upper_notebook.entry_excluded_items.clone();\n        let entry_general_maximal_size = gui_data.upper_notebook.entry_general_maximal_size.clone();\n        let entry_general_minimal_size = gui_data.upper_notebook.entry_general_minimal_size.clone();\n        let entry_settings_cache_file_minimal_size = gui_data.settings.entry_settings_cache_file_minimal_size.clone();\n        let tree_view_excluded_directories = gui_data\n            .upper_notebook\n            .common_upper_tree_views\n            .get_tree_view(UpperTreeViewEnum::ExcludedDirectories)\n            .clone();\n        let tree_view_included_directories = gui_data\n            .upper_notebook\n            .common_upper_tree_views\n            .get_tree_view(UpperTreeViewEnum::IncludedDirectories)\n            .clone();\n        let check_button_settings_save_also_json = gui_data.settings.check_button_settings_save_also_json.clone();\n\n        let included_directories = get_path_buf_from_vector_of_strings(&get_string_from_list_store(&tree_view_included_directories, ColumnsIncludedDirectory::Path as i32, None));\n        let excluded_directories = get_path_buf_from_vector_of_strings(&get_string_from_list_store(&tree_view_excluded_directories, ColumnsExcludedDirectory::Path as i32, None));\n        let reference_directories = get_path_buf_from_vector_of_strings(&get_string_from_list_store(\n            &tree_view_included_directories,\n            ColumnsIncludedDirectory::Path as i32,\n            Some(ColumnsIncludedDirectory::ReferenceButton as i32),\n        ));\n        let recursive_search = check_button_recursive.is_active();\n        let excluded_items = entry_excluded_items.text().as_str().split(',').map(ToString::to_string).collect::<Vec<String>>();\n        let allowed_extensions = entry_allowed_extensions.text().as_str().to_string();\n        let excluded_extensions = entry_excluded_extensions.text().as_str().to_string();\n        let hide_hard_links = check_button_settings_hide_hard_links.is_active();\n        let use_cache = check_button_settings_use_cache.is_active();\n        let save_also_as_json = check_button_settings_save_also_json.is_active();\n        let minimal_cache_file_size = entry_settings_cache_file_minimal_size\n            .text()\n            .as_str()\n            .parse::<u64>()\n            .unwrap_or_else(|_| DEFAULT_MINIMAL_CACHE_SIZE.parse::<u64>().expect(\"Failed to parse minimal_cache_file_size\"));\n\n        let minimal_file_size_txt = entry_general_minimal_size.text().trim().to_string();\n        let minimal_file_size = if minimal_file_size_txt.is_empty() {\n            0u64\n        } else {\n            minimal_file_size_txt\n                .parse::<u64>()\n                .unwrap_or_else(|_| DEFAULT_MINIMAL_FILE_SIZE.parse::<u64>().expect(\"Failed to parse minimal_file_size\"))\n        };\n        let maximal_file_size = entry_general_maximal_size\n            .text()\n            .as_str()\n            .parse::<u64>()\n            .unwrap_or_else(|_| DEFAULT_MAXIMAL_FILE_SIZE.parse::<u64>().expect(\"Failed to parse maximal_file_size\"));\n        let ignore_other_filesystems = check_button_settings_one_filesystem.is_active();\n\n        Self {\n            included_directories,\n            excluded_directories,\n            reference_directories,\n            recursive_search,\n            excluded_items,\n            allowed_extensions,\n            excluded_extensions,\n            hide_hard_links,\n            use_cache,\n            save_also_as_json,\n            minimal_cache_file_size,\n            minimal_file_size,\n            maximal_file_size,\n            ignore_other_filesystems,\n        }\n    }\n}\n\nfn duplicate_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(true);\n\n    let combo_box_duplicate_check_method = gui_data.main_notebook.combo_box_duplicate_check_method.clone();\n    let combo_box_duplicate_hash_type = gui_data.main_notebook.combo_box_duplicate_hash_type.clone();\n    let check_button_duplicates_use_prehash_cache = gui_data.settings.check_button_duplicates_use_prehash_cache.clone();\n    let check_button_duplicate_case_sensitive_name: gtk4::CheckButton = gui_data.main_notebook.check_button_duplicate_case_sensitive_name.clone();\n    let check_button_settings_duplicates_delete_outdated_cache = gui_data.settings.check_button_settings_duplicates_delete_outdated_cache.clone();\n    let entry_settings_prehash_cache_file_minimal_size = gui_data.settings.entry_settings_prehash_cache_file_minimal_size.clone();\n    let image_preview_duplicates = gui_data.main_notebook.image_preview_duplicates.clone();\n\n    image_preview_duplicates.set_visible(false);\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    let check_method_index = combo_box_duplicate_check_method.active().expect(\"Failed to get active search\") as usize;\n    let check_method = DUPLICATES_CHECK_METHOD_COMBO_BOX[check_method_index].check_method;\n\n    let hash_type_index = combo_box_duplicate_hash_type.active().expect(\"Failed to get active search\") as usize;\n    let hash_type = DUPLICATES_HASH_TYPE_COMBO_BOX[hash_type_index].hash_type;\n\n    let use_prehash_cache = check_button_duplicates_use_prehash_cache.is_active();\n    let minimal_prehash_cache_file_size = entry_settings_prehash_cache_file_minimal_size.text().as_str().parse::<u64>().unwrap_or(0);\n\n    let case_sensitive_name_comparison = check_button_duplicate_case_sensitive_name.is_active();\n\n    let delete_outdated_cache = check_button_settings_duplicates_delete_outdated_cache.is_active();\n\n    // Find duplicates\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = DuplicateFinderParameters::new(\n                check_method,\n                hash_type,\n                use_prehash_cache,\n                loaded_commons.minimal_cache_file_size,\n                minimal_prehash_cache_file_size,\n                case_sensitive_name_comparison,\n            );\n            let mut tool = DuplicateFinder::new(params);\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.set_delete_outdated_cache(delete_outdated_cache);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::Duplicates(tool)).expect(\"Failed to send Duplicates message\");\n        })\n        .expect(\"Failed to spawn DuplicateFinder thread\");\n}\n\nfn empty_files_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(false);\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n    // Find empty files\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = EmptyFiles::new();\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::EmptyFiles(tool)).expect(\"Failed to send EmptyFiles message\");\n        })\n        .expect(\"Failed to spawn EmptyFiles thread\");\n}\n\nfn empty_dirs_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(false);\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = EmptyFolder::new();\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::EmptyFolders(tool)).expect(\"Failed to send EmptyFolders message\");\n        })\n        .expect(\"Failed to spawn EmptyFolders thread\");\n}\n\nfn big_files_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(false);\n\n    let combo_box_big_files_mode = gui_data.main_notebook.combo_box_big_files_mode.clone();\n    let entry_big_files_number = gui_data.main_notebook.entry_big_files_number.clone();\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    let big_files_mode_index = combo_box_big_files_mode.active().expect(\"Failed to get active search\") as usize;\n    let big_files_mode = BIG_FILES_CHECK_METHOD_COMBO_BOX[big_files_mode_index].check_method;\n\n    let numbers_of_files_to_check = entry_big_files_number.text().as_str().parse::<usize>().unwrap_or(50);\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = BigFileParameters::new(numbers_of_files_to_check, big_files_mode);\n            let mut tool = BigFile::new(params);\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::BigFiles(tool)).expect(\"Failed to send BigFiles message\");\n        })\n        .expect(\"Failed to spawn BigFiles thread\");\n}\n\nfn temporary_files_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(false);\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = Temporary::new();\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::Temporary(tool)).expect(\"Failed to send Temporary message\");\n        })\n        .expect(\"Failed to spawn Temporary thread\");\n}\n\nfn same_music_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n    show_dialog: &Arc<AtomicBool>,\n) {\n    grid_progress.set_visible(true);\n\n    let check_button_music_artist: gtk4::CheckButton = gui_data.main_notebook.check_button_music_artist.clone();\n    let check_button_music_title: gtk4::CheckButton = gui_data.main_notebook.check_button_music_title.clone();\n    let check_button_music_year: gtk4::CheckButton = gui_data.main_notebook.check_button_music_year.clone();\n    let check_button_music_genre: gtk4::CheckButton = gui_data.main_notebook.check_button_music_genre.clone();\n    let check_button_music_length: gtk4::CheckButton = gui_data.main_notebook.check_button_music_length.clone();\n    let check_button_music_bitrate: gtk4::CheckButton = gui_data.main_notebook.check_button_music_bitrate.clone();\n    let combo_box_audio_check_type = gui_data.main_notebook.combo_box_audio_check_type.clone();\n    let check_button_music_approximate_comparison = gui_data.main_notebook.check_button_music_approximate_comparison.clone();\n    let check_button_music_compare_only_in_title_group = gui_data.main_notebook.check_button_music_compare_only_in_title_group.clone();\n    let scale_seconds_same_music = gui_data.main_notebook.scale_seconds_same_music.clone();\n    let scale_similarity_same_music = gui_data.main_notebook.scale_similarity_same_music.clone();\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    let approximate_comparison = check_button_music_approximate_comparison.is_active();\n    let comparison_only_in_title_group = check_button_music_compare_only_in_title_group.is_active();\n\n    let mut music_similarity: MusicSimilarity = MusicSimilarity::NONE;\n\n    if check_button_music_title.is_active() {\n        music_similarity |= MusicSimilarity::TRACK_TITLE;\n    }\n    if check_button_music_artist.is_active() {\n        music_similarity |= MusicSimilarity::TRACK_ARTIST;\n    }\n    if check_button_music_year.is_active() {\n        music_similarity |= MusicSimilarity::YEAR;\n    }\n    if check_button_music_bitrate.is_active() {\n        music_similarity |= MusicSimilarity::BITRATE;\n    }\n    if check_button_music_genre.is_active() {\n        music_similarity |= MusicSimilarity::GENRE;\n    }\n    if check_button_music_length.is_active() {\n        music_similarity |= MusicSimilarity::LENGTH;\n    }\n\n    let check_method_index = combo_box_audio_check_type.active().expect(\"Failed to get active search\") as usize;\n    let check_method = AUDIO_TYPE_CHECK_METHOD_COMBO_BOX[check_method_index].check_method;\n\n    let maximum_difference = scale_similarity_same_music.value();\n    let minimum_segment_duration = scale_seconds_same_music.value() as f32;\n\n    if music_similarity != MusicSimilarity::NONE || check_method == CheckingMethod::AudioContent {\n        thread::Builder::new()\n            .stack_size(DEFAULT_THREAD_SIZE)\n            .spawn(move || {\n                let params = SameMusicParameters::new(\n                    music_similarity,\n                    approximate_comparison,\n                    check_method,\n                    minimum_segment_duration,\n                    maximum_difference,\n                    comparison_only_in_title_group,\n                );\n                let mut tool = SameMusic::new(params);\n\n                set_common_settings(&mut tool, &loaded_commons);\n                tool.search(&stop_flag, Some(&progress_data_sender));\n                result_sender.send(Message::SameMusic(tool)).expect(\"Failed to send SameMusic message\");\n            })\n            .expect(\"Failed to spawn SameMusic thread\");\n    } else {\n        let shared_buttons = gui_data.shared_buttons.clone();\n        let buttons_array = gui_data.bottom_buttons.buttons_array.clone();\n        let buttons_names = gui_data.bottom_buttons.buttons_names;\n        let entry_info = gui_data.entry_info.clone();\n        let notebook_main = gui_data.main_notebook.notebook_main.clone();\n        let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();\n        let button_settings = gui_data.header.button_settings.clone();\n        let button_app_info = gui_data.header.button_app_info.clone();\n\n        set_buttons(\n            &mut *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SameMusic).expect(\"Failed to get SameMusic button\"),\n            &buttons_array,\n            &buttons_names,\n        );\n        entry_info.set_text(&flg!(\"search_not_choosing_any_music\"));\n        show_dialog.store(false, Ordering::Relaxed);\n\n        notebook_main.set_sensitive(true);\n        notebook_upper.set_sensitive(true);\n        button_settings.set_sensitive(true);\n        button_app_info.set_sensitive(true);\n    }\n}\n\nfn broken_files_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n    show_dialog: &Arc<AtomicBool>,\n) {\n    grid_progress.set_visible(true);\n\n    let check_button_broken_files_archive: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_archive.clone();\n    let check_button_broken_files_pdf: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_pdf.clone();\n    let check_button_broken_files_audio: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_audio.clone();\n    let check_button_broken_files_image: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_image.clone();\n    let check_button_broken_files_video: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_video.clone();\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    let mut checked_types: CheckedTypes = CheckedTypes::NONE;\n\n    if check_button_broken_files_audio.is_active() {\n        checked_types |= CheckedTypes::AUDIO;\n    }\n    if check_button_broken_files_pdf.is_active() {\n        checked_types |= CheckedTypes::PDF;\n    }\n    if check_button_broken_files_image.is_active() {\n        checked_types |= CheckedTypes::IMAGE;\n    }\n    if check_button_broken_files_archive.is_active() {\n        checked_types |= CheckedTypes::ARCHIVE;\n    }\n    if check_button_broken_files_video.is_active() {\n        checked_types |= CheckedTypes::VIDEO;\n    }\n\n    if checked_types != CheckedTypes::NONE {\n        thread::Builder::new()\n            .stack_size(DEFAULT_THREAD_SIZE)\n            .spawn(move || {\n                let params = BrokenFilesParameters::new(checked_types);\n                let mut tool = BrokenFiles::new(params);\n\n                set_common_settings(&mut tool, &loaded_commons);\n                tool.search(&stop_flag, Some(&progress_data_sender));\n                result_sender.send(Message::BrokenFiles(tool)).expect(\"Failed to send BrokenFiles message\");\n            })\n            .expect(\"Failed to spawn BrokenFiles thread\");\n    } else {\n        let shared_buttons = gui_data.shared_buttons.clone();\n        let buttons_array = gui_data.bottom_buttons.buttons_array.clone();\n        let buttons_names = gui_data.bottom_buttons.buttons_names;\n        let entry_info = gui_data.entry_info.clone();\n        let notebook_main = gui_data.main_notebook.notebook_main.clone();\n        let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();\n        let button_settings = gui_data.header.button_settings.clone();\n        let button_app_info = gui_data.header.button_app_info.clone();\n\n        set_buttons(\n            &mut *shared_buttons\n                .borrow_mut()\n                .get_mut(&NotebookMainEnum::BrokenFiles)\n                .expect(\"Failed to get BrokenFiles button\"),\n            &buttons_array,\n            &buttons_names,\n        );\n        entry_info.set_text(&flg!(\"search_not_choosing_any_broken_files\"));\n        show_dialog.store(false, Ordering::Relaxed);\n\n        notebook_main.set_sensitive(true);\n        notebook_upper.set_sensitive(true);\n        button_settings.set_sensitive(true);\n        button_app_info.set_sensitive(true);\n    }\n}\n\nfn similar_image_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(true);\n\n    let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone();\n    let combo_box_image_hash_algorithm = gui_data.main_notebook.combo_box_image_hash_algorithm.clone();\n    let combo_box_image_resize_algorithm = gui_data.main_notebook.combo_box_image_resize_algorithm.clone();\n    let check_button_image_ignore_same_size = gui_data.main_notebook.check_button_image_ignore_same_size.clone();\n    let check_button_settings_similar_images_delete_outdated_cache = gui_data.settings.check_button_settings_similar_images_delete_outdated_cache.clone();\n    let image_preview_similar_images = gui_data.main_notebook.image_preview_similar_images.clone();\n    let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n    image_preview_similar_images.set_visible(false);\n\n    let hash_size_index = combo_box_image_hash_size.active().expect(\"Failed to get active search\") as usize;\n    let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index] as u8;\n\n    let image_filter_index = combo_box_image_resize_algorithm.active().expect(\"Failed to get active search\") as usize;\n    let image_filter = IMAGES_RESIZE_ALGORITHM_COMBO_BOX[image_filter_index].filter;\n\n    let hash_alg_index = combo_box_image_hash_algorithm.active().expect(\"Failed to get active search\") as usize;\n    let hash_alg = IMAGES_HASH_TYPE_COMBO_BOX[hash_alg_index].hash_alg;\n\n    let ignore_same_size = check_button_image_ignore_same_size.is_active();\n\n    let similarity = scale_similarity_similar_images.value() as u32;\n\n    let delete_outdated_cache = check_button_settings_similar_images_delete_outdated_cache.is_active();\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = SimilarImagesParameters::new(similarity, hash_size, hash_alg, image_filter, ignore_same_size);\n            let mut tool = SimilarImages::new(params);\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.set_delete_outdated_cache(delete_outdated_cache);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::SimilarImages(tool)).expect(\"Failed to send SimilarImages message\");\n        })\n        .expect(\"Failed to spawn SimilarImages thread\");\n}\n\nfn similar_video_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(true);\n\n    let check_button_video_ignore_same_size = gui_data.main_notebook.check_button_video_ignore_same_size.clone();\n    let check_button_settings_similar_videos_delete_outdated_cache = gui_data.settings.check_button_settings_similar_videos_delete_outdated_cache.clone();\n    let scale_similarity_similar_videos = gui_data.main_notebook.scale_similarity_similar_videos.clone();\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    let tolerance = scale_similarity_similar_videos.value() as i32;\n\n    let delete_outdated_cache = check_button_settings_similar_videos_delete_outdated_cache.is_active();\n\n    let ignore_same_size = check_button_video_ignore_same_size.is_active();\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = SimilarVideosParameters::new(\n                tolerance,\n                ignore_same_size,\n                DEFAULT_SKIP_FORWARD_AMOUNT,\n                DEFAULT_VID_HASH_DURATION,\n                DEFAULT_CROP_DETECT,\n                false, // Not implemented in gtk gui\n                10,    // Not implemented in gtk gui\n                false, // Not implemented in gtk gui\n                2,     // Not implemented in gtk gui\n            );\n            let mut tool = SimilarVideos::new(params);\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.set_delete_outdated_cache(delete_outdated_cache);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::SimilarVideos(tool)).expect(\"Failed to send SimilarVideos message\");\n        })\n        .expect(\"Failed to spawn SimilarVideos thread\");\n}\n\nfn bad_symlinks_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(false);\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = InvalidSymlinks::new();\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::InvalidSymlinks(tool)).expect(\"Failed to send InvalidSymlinks message\");\n        })\n        .expect(\"Failed to spawn InvalidSymlinks thread\");\n}\n\nfn bad_extensions_search(\n    gui_data: &GuiData,\n    loaded_commons: LoadedCommonItems,\n    stop_flag: Arc<AtomicBool>,\n    result_sender: Sender<Message>,\n    grid_progress: &Grid,\n    progress_data_sender: Sender<ProgressData>,\n) {\n    grid_progress.set_visible(true);\n\n    clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view);\n\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = BadExtensionsParameters::new();\n            let mut tool = BadExtensions::new(params);\n\n            set_common_settings(&mut tool, &loaded_commons);\n            tool.search(&stop_flag, Some(&progress_data_sender));\n            result_sender.send(Message::BadExtensions(tool)).expect(\"Failed to send BadExtensions message\");\n        })\n        .expect(\"Failed to spawn BadExtensions thread\");\n}\n\nfn set_common_settings<T>(component: &mut T, loaded_commons: &LoadedCommonItems)\nwhere\n    T: CommonData,\n{\n    component.set_included_paths(loaded_commons.included_directories.clone());\n    component.set_excluded_paths(loaded_commons.excluded_directories.clone());\n    component.set_reference_paths(loaded_commons.reference_directories.clone());\n    component.set_recursive_search(loaded_commons.recursive_search);\n    component.set_allowed_extensions(loaded_commons.allowed_extensions.split(',').map(str::to_string).collect());\n    component.set_excluded_extensions(loaded_commons.excluded_extensions.split(',').map(str::to_string).collect());\n    component.set_excluded_items(loaded_commons.excluded_items.clone());\n    component.set_exclude_other_filesystems(loaded_commons.ignore_other_filesystems);\n    component.set_use_cache(loaded_commons.use_cache);\n    component.set_save_also_as_json(loaded_commons.save_also_as_json);\n    component.set_minimal_file_size(loaded_commons.minimal_file_size);\n    component.set_maximal_file_size(loaded_commons.maximal_file_size);\n    component.set_hide_hard_links(loaded_commons.hide_hard_links);\n}\n\n#[fun_time(message = \"clean_tree_view\", level = \"debug\")]\nfn clean_tree_view(tree_view: &gtk4::TreeView) {\n    let list_store = tree_view.get_model();\n    let mut all_iters: Vec<gtk4::TreeIter> = Vec::new();\n    iter_list(&list_store, |_m, i| {\n        all_iters.push(*i);\n    });\n    all_iters.reverse();\n    for iter in all_iters {\n        list_store.remove(&iter);\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_select.rs",
    "content": "use gtk4::prelude::*;\n\nuse crate::gui_structs::common_tree_view::SubView;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::gui_structs::gui_popovers_select::GuiSelectPopovers;\nuse crate::helpers::enums::PopoverTypes;\n\npub(crate) fn connect_button_select(gui_data: &GuiData) {\n    let popovers_select = gui_data.popovers_select.clone();\n    let gc_buttons_select = gui_data.bottom_buttons.gc_buttons_select.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    gc_buttons_select.connect_pressed(move |_, _, _, _| {\n        show_required_popovers(&popovers_select, common_tree_views.get_current_subview());\n    });\n}\n\nfn show_required_popovers(popovers_select: &GuiSelectPopovers, sv: &SubView) {\n    let buttons_popover_select_all = popovers_select.buttons_popover_select_all.clone();\n    let buttons_popover_unselect_all = popovers_select.buttons_popover_unselect_all.clone();\n    let buttons_popover_reverse = popovers_select.buttons_popover_reverse.clone();\n    let buttons_popover_select_all_except_shortest_path = popovers_select.buttons_popover_select_all_except_shortest_path.clone();\n    let buttons_popover_select_all_except_longest_path = popovers_select.buttons_popover_select_all_except_longest_path.clone();\n    let buttons_popover_select_all_except_oldest = popovers_select.buttons_popover_select_all_except_oldest.clone();\n    let buttons_popover_select_all_except_newest = popovers_select.buttons_popover_select_all_except_newest.clone();\n    let buttons_popover_select_one_oldest = popovers_select.buttons_popover_select_one_oldest.clone();\n    let buttons_popover_select_one_newest = popovers_select.buttons_popover_select_one_newest.clone();\n    let buttons_popover_select_custom = popovers_select.buttons_popover_select_custom.clone();\n    let buttons_popover_unselect_custom = popovers_select.buttons_popover_unselect_custom.clone();\n    let buttons_popover_select_all_images_except_biggest = popovers_select.buttons_popover_select_all_images_except_biggest.clone();\n    let buttons_popover_select_all_images_except_smallest = popovers_select.buttons_popover_select_all_images_except_smallest.clone();\n\n    let separator_select_shortest_path = popovers_select.separator_select_shortest_path.clone();\n    let separator_select_custom = popovers_select.separator_select_custom.clone();\n    let separator_select_date = popovers_select.separator_select_date.clone();\n    let separator_select_image_size = popovers_select.separator_select_image_size.clone();\n    let separator_select_reverse = popovers_select.separator_select_reverse.clone();\n\n    let arr = sv.nb_object.available_modes;\n\n    if arr.contains(&PopoverTypes::All) {\n        buttons_popover_select_all.set_visible(true);\n        buttons_popover_unselect_all.set_visible(true);\n    } else {\n        buttons_popover_select_all.set_visible(false);\n        buttons_popover_unselect_all.set_visible(false);\n    }\n\n    if arr.contains(&PopoverTypes::Size) {\n        buttons_popover_select_all_images_except_biggest.set_visible(true);\n        buttons_popover_select_all_images_except_smallest.set_visible(true);\n        separator_select_image_size.set_visible(true);\n    } else {\n        buttons_popover_select_all_images_except_biggest.set_visible(false);\n        buttons_popover_select_all_images_except_smallest.set_visible(false);\n        separator_select_image_size.set_visible(false);\n    }\n\n    if arr.contains(&PopoverTypes::Reverse) {\n        buttons_popover_reverse.set_visible(true);\n        separator_select_reverse.set_visible(true);\n    } else {\n        buttons_popover_reverse.set_visible(false);\n        separator_select_reverse.set_visible(false);\n    }\n\n    if arr.contains(&PopoverTypes::Custom) {\n        buttons_popover_select_custom.set_visible(true);\n        buttons_popover_unselect_custom.set_visible(true);\n        separator_select_custom.set_visible(true);\n    } else {\n        buttons_popover_select_custom.set_visible(false);\n        buttons_popover_unselect_custom.set_visible(false);\n        separator_select_custom.set_visible(false);\n    }\n\n    if arr.contains(&PopoverTypes::Date) {\n        buttons_popover_select_all_except_oldest.set_visible(true);\n        buttons_popover_select_all_except_newest.set_visible(true);\n        buttons_popover_select_one_oldest.set_visible(true);\n        buttons_popover_select_one_newest.set_visible(true);\n        separator_select_date.set_visible(true);\n    } else {\n        buttons_popover_select_all_except_oldest.set_visible(false);\n        buttons_popover_select_all_except_newest.set_visible(false);\n        buttons_popover_select_one_oldest.set_visible(false);\n        buttons_popover_select_one_newest.set_visible(false);\n        separator_select_date.set_visible(false);\n    }\n\n    if arr.contains(&PopoverTypes::PathLength) {\n        buttons_popover_select_all_except_shortest_path.set_visible(true);\n        buttons_popover_select_all_except_longest_path.set_visible(true);\n        separator_select_shortest_path.set_visible(true);\n    } else {\n        buttons_popover_select_all_except_shortest_path.set_visible(false);\n        buttons_popover_select_all_except_longest_path.set_visible(false);\n        separator_select_shortest_path.set_visible(false);\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_sort.rs",
    "content": "use gtk4::prelude::*;\n\nuse crate::gui_structs::common_tree_view::SubView;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::gui_structs::gui_popovers_sort::GuiSortPopovers;\nuse crate::helpers::enums::PopoverTypes;\n\npub(crate) fn connect_button_sort(gui_data: &GuiData) {\n    let popovers_sort = gui_data.popovers_sort.clone();\n    let gc_buttons_sort = gui_data.bottom_buttons.gc_buttons_sort.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    gc_buttons_sort.connect_pressed(move |_, _, _, _| {\n        show_required_popovers(&popovers_sort, common_tree_views.get_current_subview());\n    });\n}\n\nfn show_required_popovers(popovers_sort: &GuiSortPopovers, sv: &SubView) {\n    let buttons_popover_sort_file_name = popovers_sort.buttons_popover_sort_file_name.clone();\n    let buttons_popover_sort_size = popovers_sort.buttons_popover_sort_size.clone();\n    let buttons_popover_sort_folder_name = popovers_sort.buttons_popover_sort_folder_name.clone();\n    let buttons_popover_sort_full_name = popovers_sort.buttons_popover_sort_full_name.clone();\n    let buttons_popover_sort_selection = popovers_sort.buttons_popover_sort_selection.clone();\n\n    let arr = sv.nb_object.available_modes;\n\n    buttons_popover_sort_full_name.set_visible(false);\n\n    if arr.contains(&PopoverTypes::All) {\n        buttons_popover_sort_selection.set_visible(true);\n        buttons_popover_sort_file_name.set_visible(true);\n        buttons_popover_sort_folder_name.set_visible(true);\n        // buttons_popover_sort_full_name.set_visible(true); // TODO, this needs to be handled a little different\n    } else {\n        buttons_popover_sort_selection.set_visible(false);\n        buttons_popover_sort_file_name.set_visible(false);\n        buttons_popover_sort_folder_name.set_visible(false);\n        // buttons_popover_sort_full_name.set_visible(false);\n    }\n\n    if arr.contains(&PopoverTypes::Size) {\n        buttons_popover_sort_size.set_visible(true);\n    } else {\n        buttons_popover_sort_size.set_visible(false);\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_button_stop.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse gtk4::prelude::*;\n\nuse crate::flg;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::KEY_ENTER;\n\nfn send_stop_message(stop_flag: &Arc<AtomicBool>) {\n    stop_flag.store(true, std::sync::atomic::Ordering::Relaxed);\n}\n\npub(crate) fn connect_button_stop(gui_data: &GuiData) {\n    let evk_button_stop_in_dialog = gui_data.progress_window.evk_button_stop_in_dialog.clone();\n    let stop_dialog = gui_data.progress_window.window_progress.clone();\n    let stop_flag = gui_data.stop_flag.clone();\n    evk_button_stop_in_dialog.connect_key_released(move |_, _, key_code, _| {\n        if key_code == KEY_ENTER {\n            stop_dialog.set_title(Some(&format!(\"{} ({})\", flg!(\"window_progress_title\"), flg!(\"progress_stop_additional_message\"))));\n            send_stop_message(&stop_flag);\n        }\n    });\n\n    let button_stop_in_dialog = gui_data.progress_window.button_stop_in_dialog.clone();\n    let stop_dialog = gui_data.progress_window.window_progress.clone();\n    let stop_flag = gui_data.stop_flag.clone();\n\n    button_stop_in_dialog.connect_clicked(move |_a| {\n        stop_dialog.set_title(Some(&format!(\"{} ({})\", flg!(\"window_progress_title\"), flg!(\"progress_stop_additional_message\"))));\n        send_stop_message(&stop_flag);\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_change_language.rs",
    "content": "use gtk4::prelude::*;\nuse i18n_embed::DesktopLanguageRequester;\nuse i18n_embed::unic_langid::LanguageIdentifier;\nuse log::error;\n\nuse crate::language_functions::get_language_from_combo_box_text;\nuse crate::{GuiData, LANGUAGES_ALL, localizer_gui};\n\n// use i18n_embed::{DesktopLanguageRequester, Localizer};\n\npub(crate) fn connect_change_language(gui_data: &GuiData) {\n    change_language(gui_data);\n\n    let combo_box_settings_language = gui_data.settings.combo_box_settings_language.clone();\n    let gui_data = gui_data.clone();\n    combo_box_settings_language.connect_changed(move |_| {\n        change_language(&gui_data);\n    });\n}\n\nfn change_language(gui_data: &GuiData) {\n    let localizers = vec![\n        (\"czkawka_core\", czkawka_core::localizer_core::localizer_core()),\n        (\"czkawka_gui\", localizer_gui::localizer_gui()),\n    ];\n\n    let lang_short = get_language_from_combo_box_text(&gui_data.settings.combo_box_settings_language.active_text().expect(\"No active text\")).short_text;\n\n    let lang_identifier = vec![LanguageIdentifier::from_bytes(lang_short.as_bytes()).expect(\"Failed to create LanguageIdentifier\")];\n    for (lib, localizer) in localizers {\n        if let Err(error) = localizer.select(&lang_identifier) {\n            error!(\"Error while loading languages for {lib} {error:?}\");\n        }\n    }\n    gui_data.update_language();\n}\n\npub(crate) fn load_system_language(gui_data: &GuiData) {\n    let requested_languages = DesktopLanguageRequester::requested_languages();\n\n    if let Some(language) = requested_languages.first() {\n        let old_short_lang = language.to_string();\n        let mut short_lang = String::new();\n        // removes from e.g. en_zb, ending _zd since Czkawka doesn't support this (maybe could add this in future)\n        for i in old_short_lang.chars() {\n            if i.is_ascii_alphabetic() {\n                short_lang.push(i);\n            } else {\n                break;\n            }\n        }\n        for (index, lang) in LANGUAGES_ALL.iter().enumerate() {\n            if lang.short_text == short_lang {\n                gui_data.settings.combo_box_settings_language.set_active(Some(index as u32));\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_duplicate_buttons.rs",
    "content": "use czkawka_core::common::model::CheckingMethod;\nuse gtk4::prelude::*;\n\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_combo_box::DUPLICATES_CHECK_METHOD_COMBO_BOX;\n\npub(crate) fn connect_duplicate_combo_box(gui_data: &GuiData) {\n    let combo_box_duplicate_check_method = gui_data.main_notebook.combo_box_duplicate_check_method.clone();\n    let combo_box_duplicate_hash_type = gui_data.main_notebook.combo_box_duplicate_hash_type.clone();\n    let label_duplicate_hash_type = gui_data.main_notebook.label_duplicate_hash_type.clone();\n    let check_button_duplicate_case_sensitive_name = gui_data.main_notebook.check_button_duplicate_case_sensitive_name.clone();\n    combo_box_duplicate_check_method.connect_changed(move |combo_box_duplicate_check_method| {\n        // None active can be if when adding elements(this signal is activated when e.g. adding new fields or removing them)\n        if let Some(chosen_index) = combo_box_duplicate_check_method.active() {\n            if DUPLICATES_CHECK_METHOD_COMBO_BOX[chosen_index as usize].check_method == CheckingMethod::Hash {\n                combo_box_duplicate_hash_type.set_visible(true);\n                label_duplicate_hash_type.set_visible(true);\n            } else {\n                combo_box_duplicate_hash_type.set_visible(false);\n                label_duplicate_hash_type.set_visible(false);\n            }\n\n            if [CheckingMethod::Name, CheckingMethod::SizeName].contains(&DUPLICATES_CHECK_METHOD_COMBO_BOX[chosen_index as usize].check_method) {\n                check_button_duplicate_case_sensitive_name.set_visible(true);\n            } else {\n                check_button_duplicate_case_sensitive_name.set_visible(false);\n            }\n        }\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_header_buttons.rs",
    "content": "use gtk4::prelude::*;\n\nuse crate::gui_structs::gui_data::GuiData;\n\npub(crate) fn connect_button_about(gui_data: &GuiData) {\n    let about_dialog = gui_data.about.about_dialog.clone();\n    let button_app_info = gui_data.header.button_app_info.clone();\n    button_app_info.connect_clicked(move |_| {\n        about_dialog.set_visible(true);\n\n        // Prevent from deleting dialog after close\n        about_dialog.connect_close_request(|dialog| {\n            dialog.set_visible(false);\n            glib::Propagation::Stop\n        });\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_krokiet_info_dialog.rs",
    "content": "use gtk4::prelude::*;\nuse gtk4::{Align, Dialog, Orientation, ResponseType};\n\nuse crate::flg;\n\npub fn show_krokiet_info_dialog(window_main: &gtk4::Window) {\n    let dialog = Dialog::builder().title(flg!(\"krokiet_info_title\")).transient_for(window_main).modal(true).build();\n\n    let button_ok = dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n\n    dialog.set_default_size(500, 0);\n\n    let label = gtk4::Label::builder()\n        .label(&flg!(\"krokiet_info_message\"))\n        .wrap(true)\n        .justify(gtk4::Justification::Center)\n        .halign(Align::Center)\n        .margin_top(10)\n        .margin_bottom(10)\n        .margin_start(10)\n        .margin_end(10)\n        .build();\n\n    let link = gtk4::Label::builder()\n        .label(\"<a href=\\\"https://github.com/qarmin/czkawka/tree/master/krokiet\\\">https://github.com/qarmin/czkawka/tree/master/krokiet</a> / <a href=\\\"https://github.com/qarmin/czkawka/releases\\\">https://github.com/qarmin/czkawka/releases</a>\")\n        .use_markup(true)\n        .halign(Align::Center)\n        .margin_top(5)\n        .margin_bottom(10)\n        .build();\n\n    button_ok.grab_focus();\n\n    let parent = button_ok\n        .parent()\n        .expect(\"Button should have parent\")\n        .parent()\n        .expect(\"Button parent should have parent\")\n        .downcast::<gtk4::Box>()\n        .expect(\"Should be a Box\");\n\n    parent.set_orientation(Orientation::Vertical);\n    parent.set_halign(Align::Fill);\n    parent.set_margin_start(10);\n    parent.set_margin_end(10);\n    parent.set_margin_top(10);\n    parent.set_margin_bottom(10);\n\n    parent.insert_child_after(&label, None::<&gtk4::Widget>);\n    parent.insert_child_after(&link, Some(&label));\n\n    if let Some(action_area) = button_ok.parent() {\n        action_area.set_halign(Align::Center);\n    }\n\n    dialog.set_visible(true);\n\n    dialog.connect_response(move |dialog, response_type| {\n        if response_type == ResponseType::Ok {\n            dialog.close();\n        }\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_notebook_tabs.rs",
    "content": "use crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::set_buttons;\nuse crate::notebook_enums::to_notebook_main_enum;\n\npub(crate) fn connect_notebook_tabs(gui_data: &GuiData) {\n    let shared_buttons = gui_data.shared_buttons.clone();\n    let buttons_array = gui_data.bottom_buttons.buttons_array.clone();\n    let notebook_main_clone = gui_data.main_notebook.notebook_main.clone();\n    let buttons_names = gui_data.bottom_buttons.buttons_names;\n\n    notebook_main_clone.connect_switch_page(move |_, _, number| {\n        let current_tab_in_main_notebook = to_notebook_main_enum(number);\n\n        // Buttons\n        set_buttons(\n            &mut *shared_buttons.borrow_mut().get_mut(&current_tab_in_main_notebook).expect(\"Failed to get current tab\"),\n            &buttons_array,\n            &buttons_names,\n        );\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_popovers_select.rs",
    "content": "use czkawka_core::common::items::new_excluded_item;\nuse czkawka_core::common::regex_check;\nuse gtk4::prelude::*;\nuse gtk4::{ResponseType, TreeIter, Window};\nuse log::error;\nuse regex::Regex;\n\nuse crate::flg;\nuse crate::gtk_traits::DialogTraits;\nuse crate::gui_structs::common_tree_view::{SubView, TreeViewListStoreTrait};\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::{change_dimension_to_krotka, get_full_name_from_path_name};\nuse crate::helpers::model_iter::iter_list;\n\n// File length variable allows users to choose duplicates which have shorter file name\n// e.g. 'tar.gz' will be selected instead 'tar.gz (copy)' etc.\n\nfn popover_select_all(popover: &gtk4::Popover, tree_view: &gtk4::TreeView, column_button_selection: u32, column_header: Option<i32>) {\n    let model = tree_view.get_model();\n\n    if let Some(mut iter) = model.iter_first() {\n        if let Some(column_header) = column_header {\n            loop {\n                if !model.get::<bool>(&iter, column_header) {\n                    model.set_value(&iter, column_button_selection, &true.to_value());\n                }\n                if !model.iter_next(&mut iter) {\n                    break;\n                }\n            }\n        } else {\n            loop {\n                model.set_value(&iter, column_button_selection, &true.to_value());\n\n                if !model.iter_next(&mut iter) {\n                    break;\n                }\n            }\n        }\n    }\n    popover.popdown();\n}\n\nfn popover_unselect_all(popover: &gtk4::Popover, tree_view: &gtk4::TreeView, column_button_selection: u32) {\n    let model = tree_view.get_model();\n\n    iter_list(&model, |m, i| {\n        m.set_value(i, column_button_selection, &false.to_value());\n    });\n    popover.popdown();\n}\n\nfn popover_reverse(popover: &gtk4::Popover, tree_view: &gtk4::TreeView, column_button_selection: u32, column_header: Option<i32>) {\n    let model = tree_view.get_model();\n\n    if let Some(mut iter) = model.iter_first() {\n        if let Some(column_header) = column_header {\n            loop {\n                if !model.get::<bool>(&iter, column_header) {\n                    let current_value: bool = model.get::<bool>(&iter, column_button_selection as i32);\n                    model.set_value(&iter, column_button_selection, &(!current_value).to_value());\n                }\n                if !model.iter_next(&mut iter) {\n                    break;\n                }\n            }\n        } else {\n            loop {\n                let current_value: bool = model.get::<bool>(&iter, column_button_selection as i32);\n                model.set_value(&iter, column_button_selection, &(!current_value).to_value());\n\n                if !model.iter_next(&mut iter) {\n                    break;\n                }\n            }\n        }\n    }\n    popover.popdown();\n}\n\nfn popover_all_except_longest_shortest_path(\n    popover: &gtk4::Popover,\n    tree_view: &gtk4::TreeView,\n    column_header: i32,\n    column_path: i32,\n    column_button_selection: u32,\n    except_longest: bool,\n) {\n    let model = tree_view.get_model();\n\n    if let Some(mut iter) = model.iter_first() {\n        let mut end: bool = false;\n        loop {\n            let mut tree_iter_array: Vec<TreeIter> = Vec::new();\n            let mut used_index: Option<usize> = None;\n            let mut current_index: usize = 0;\n\n            let mut path_extreme: usize = if except_longest { usize::MAX } else { 0 };\n\n            loop {\n                if model.get::<bool>(&iter, column_header) {\n                    if !model.iter_next(&mut iter) {\n                        end = true;\n                    }\n                    break;\n                }\n                tree_iter_array.push(iter);\n                let path_length = model.get::<String>(&iter, column_path).len();\n                if except_longest {\n                    if path_length < path_extreme {\n                        path_extreme = path_length;\n                        used_index = Some(current_index);\n                    }\n                } else if path_length > path_extreme {\n                    path_extreme = path_length;\n                    used_index = Some(current_index);\n                }\n                current_index += 1;\n\n                if !model.iter_next(&mut iter) {\n                    end = true;\n                    break;\n                }\n            }\n            let Some(used_index) = used_index else {\n                continue;\n            };\n            for (index, tree_iter) in tree_iter_array.iter().enumerate() {\n                if index != used_index {\n                    model.set_value(tree_iter, column_button_selection, &true.to_value());\n                } else {\n                    model.set_value(tree_iter, column_button_selection, &false.to_value());\n                }\n            }\n\n            if end {\n                break;\n            }\n        }\n    }\n\n    popover.popdown();\n}\n\nfn popover_all_except_oldest_newest(\n    popover: &gtk4::Popover,\n    tree_view: &gtk4::TreeView,\n    column_header: i32,\n    column_modification_as_secs: i32,\n    column_file_name: i32,\n    column_button_selection: u32,\n    except_oldest: bool,\n) {\n    let model = tree_view.get_model();\n\n    if let Some(mut iter) = model.iter_first() {\n        let mut end: bool = false;\n        loop {\n            let mut tree_iter_array: Vec<TreeIter> = Vec::new();\n            let mut used_index: Option<usize> = None;\n            let mut current_index: usize = 0;\n\n            let mut modification_time_min_max: u64 = if except_oldest { u64::MAX } else { 0 };\n\n            let mut file_length: usize = 0;\n\n            loop {\n                if model.get::<bool>(&iter, column_header) {\n                    if !model.iter_next(&mut iter) {\n                        end = true;\n                    }\n                    break;\n                }\n                tree_iter_array.push(iter);\n                let modification = model.get::<u64>(&iter, column_modification_as_secs);\n                let current_file_length = model.get::<String>(&iter, column_file_name).len();\n                if except_oldest {\n                    if modification < modification_time_min_max || (modification == modification_time_min_max && current_file_length < file_length) {\n                        file_length = current_file_length;\n                        modification_time_min_max = modification;\n                        used_index = Some(current_index);\n                    }\n                } else if modification > modification_time_min_max || (modification == modification_time_min_max && current_file_length < file_length) {\n                    file_length = current_file_length;\n                    modification_time_min_max = modification;\n                    used_index = Some(current_index);\n                }\n                current_index += 1;\n\n                if !model.iter_next(&mut iter) {\n                    end = true;\n                    break;\n                }\n            }\n            let Some(used_index) = used_index else {\n                continue;\n            };\n            for (index, tree_iter) in tree_iter_array.iter().enumerate() {\n                if index != used_index {\n                    model.set_value(tree_iter, column_button_selection, &true.to_value());\n                } else {\n                    model.set_value(tree_iter, column_button_selection, &false.to_value());\n                }\n            }\n\n            if end {\n                break;\n            }\n        }\n    }\n\n    popover.popdown();\n}\n\nfn popover_one_oldest_newest(\n    popover: &gtk4::Popover,\n    sv: &SubView,\n    // tree_view: &gtk4::TreeView,\n    // column_header: i32,\n    // column_modification_as_secs: i32,\n    // column_file_name: i32,\n    // column_button_selection: u32,\n    check_oldest: bool,\n) {\n    let model = sv.get_model();\n    let column_header = sv.nb_object.column_header.expect(\"OO/ON can't be used without headers\");\n    let column_modification_as_secs = sv.nb_object.column_modification_as_secs.expect(\"OO/ON needs modification as secs column\");\n\n    if let Some(mut iter) = model.iter_first() {\n        let mut end: bool = false;\n        loop {\n            let mut tree_iter_array: Vec<TreeIter> = Vec::new();\n            let mut used_index: Option<usize> = None;\n            let mut current_index: usize = 0;\n            let mut modification_time_min_max: u64 = if check_oldest { u64::MAX } else { 0 };\n\n            let mut file_length: usize = 0;\n\n            loop {\n                if model.get::<bool>(&iter, column_header) {\n                    if !model.iter_next(&mut iter) {\n                        end = true;\n                    }\n                    break;\n                }\n                tree_iter_array.push(iter);\n                let modification = model.get::<u64>(&iter, column_modification_as_secs);\n                let current_file_length = model.get::<String>(&iter, sv.nb_object.column_name).len();\n                if check_oldest {\n                    if modification < modification_time_min_max || (modification == modification_time_min_max && current_file_length > file_length) {\n                        file_length = current_file_length;\n                        modification_time_min_max = modification;\n                        used_index = Some(current_index);\n                    }\n                } else if modification > modification_time_min_max || (modification == modification_time_min_max && current_file_length > file_length) {\n                    file_length = current_file_length;\n                    modification_time_min_max = modification;\n                    used_index = Some(current_index);\n                }\n\n                current_index += 1;\n\n                if !model.iter_next(&mut iter) {\n                    end = true;\n                    break;\n                }\n            }\n            let Some(used_index) = used_index else {\n                continue;\n            };\n\n            for (index, tree_iter) in tree_iter_array.iter().enumerate() {\n                if index == used_index {\n                    model.set_value(tree_iter, sv.nb_object.column_selection as u32, &true.to_value());\n                } else {\n                    model.set_value(tree_iter, sv.nb_object.column_selection as u32, &false.to_value());\n                }\n            }\n\n            if end {\n                break;\n            }\n        }\n    }\n\n    popover.popdown();\n}\n\nfn popover_custom_select_unselect(\n    popover: &gtk4::Popover,\n    window_main: &Window,\n    sv: &SubView,\n    // tree_view: &gtk4::TreeView,\n    // column_header: Option<i32>,\n    // column_file_name: i32,\n    // column_path: i32,\n    // column_button_selection: u32,\n    select_things: bool,\n) {\n    popover.popdown();\n\n    let window_title = if select_things {\n        flg!(\"popover_custom_mode_select\")\n    } else {\n        flg!(\"popover_custom_mode_unselect\")\n    };\n\n    // Dialog for select/unselect items\n    {\n        let dialog = gtk4::Dialog::builder().title(window_title).transient_for(window_main).modal(true).build();\n        dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n        dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n        let check_button_path = gtk4::CheckButton::builder()\n            .label(flg!(\"popover_custom_regex_path_label\"))\n            .tooltip_text(flg!(\"popover_custom_path_check_button_entry_tooltip\"))\n            .build();\n        let check_button_name = gtk4::CheckButton::builder()\n            .label(flg!(\"popover_custom_regex_name_label\"))\n            .tooltip_text(flg!(\"popover_custom_name_check_button_entry_tooltip\"))\n            .build();\n        let check_button_rust_regex = gtk4::CheckButton::builder()\n            .label(flg!(\"popover_custom_regex_regex_label\"))\n            .tooltip_text(flg!(\"popover_custom_regex_check_button_entry_tooltip\"))\n            .build();\n\n        let check_button_case_sensitive = gtk4::CheckButton::builder()\n            .label(flg!(\"popover_custom_case_sensitive_check_button\"))\n            .tooltip_text(flg!(\"popover_custom_case_sensitive_check_button_tooltip\"))\n            .active(false)\n            .build();\n\n        let check_button_select_not_all_results = gtk4::CheckButton::builder()\n            .label(flg!(\"popover_custom_all_in_group_label\"))\n            .tooltip_text(flg!(\"popover_custom_not_all_check_button_tooltip\"))\n            .active(true)\n            .build();\n\n        let entry_path = gtk4::Entry::builder().tooltip_text(flg!(\"popover_custom_path_check_button_entry_tooltip\")).build();\n        let entry_name = gtk4::Entry::builder().tooltip_text(flg!(\"popover_custom_name_check_button_entry_tooltip\")).build();\n        let entry_rust_regex = gtk4::Entry::builder()\n            .tooltip_text(flg!(\"popover_custom_regex_check_button_entry_tooltip\"))\n            .sensitive(false)\n            .build(); // By default check button regex is disabled\n\n        let label_regex_valid = gtk4::Label::new(None);\n\n        {\n            let label_regex_valid = label_regex_valid.clone();\n            entry_rust_regex.connect_changed(move |entry_rust_regex| {\n                let message;\n                let text_to_check = entry_rust_regex.text().to_string();\n                if text_to_check.is_empty() {\n                    message = String::new();\n                } else {\n                    match Regex::new(&text_to_check) {\n                        Ok(_) => message = flg!(\"popover_valid_regex\"),\n                        Err(_) => message = flg!(\"popover_invalid_regex\"),\n                    }\n                }\n\n                // TODO add red and green color to text\n                // let attributes_list = AttrList::new();\n                // let p_a = PangoAttribute::init();\n                // let attribute = PangoAttrFontDesc { attr };\n                // attributes_list.insert(attribute);\n                // label_regex_valid.set_attributes(Some(&attributes_list));\n                label_regex_valid.set_text(&message);\n            });\n        }\n\n        // Disable other modes when Rust Regex is enabled\n        {\n            let check_button_path = check_button_path.clone();\n            let check_button_name = check_button_name.clone();\n            let entry_path = entry_path.clone();\n            let entry_name = entry_name.clone();\n            let entry_rust_regex = entry_rust_regex.clone();\n            check_button_rust_regex.connect_toggled(move |check_button_rust_regex| {\n                if check_button_rust_regex.is_active() {\n                    check_button_path.set_sensitive(false);\n                    check_button_name.set_sensitive(false);\n                    entry_path.set_sensitive(false);\n                    entry_name.set_sensitive(false);\n                    entry_rust_regex.set_sensitive(true);\n                } else {\n                    check_button_path.set_sensitive(true);\n                    check_button_name.set_sensitive(true);\n                    entry_path.set_sensitive(true);\n                    entry_name.set_sensitive(true);\n                    entry_rust_regex.set_sensitive(false);\n                }\n            });\n        }\n\n        // Configure look of things\n        {\n            // TODO Label should have const width, and rest should fill entry, but for now is 50%-50%\n            let grid = gtk4::Grid::builder().row_homogeneous(true).column_homogeneous(true).build();\n\n            grid.attach(&check_button_name, 0, 1, 1, 1);\n            grid.attach(&check_button_path, 0, 2, 1, 1);\n            grid.attach(&check_button_rust_regex, 0, 3, 1, 1);\n\n            grid.attach(&entry_name, 1, 1, 1, 1);\n            grid.attach(&entry_path, 1, 2, 1, 1);\n            grid.attach(&entry_rust_regex, 1, 3, 1, 1);\n\n            grid.attach(&label_regex_valid, 0, 4, 2, 1);\n\n            grid.attach(&check_button_case_sensitive, 0, 5, 2, 1);\n\n            if select_things {\n                grid.attach(&check_button_select_not_all_results, 0, 6, 2, 1);\n            }\n\n            let box_widget = dialog.get_box_child();\n            box_widget.append(&grid);\n\n            dialog.set_visible(true);\n        }\n\n        let sv = sv.clone();\n        dialog.connect_response(move |confirmation_dialog_select_unselect, response_type| {\n            let name_wildcard = entry_name.text().trim().to_string();\n            let path_wildcard = entry_path.text().trim().to_string();\n            let regex_wildcard = entry_rust_regex.text().trim().to_string();\n\n            #[cfg(target_family = \"windows\")]\n            let name_wildcard = name_wildcard.replace(\"/\", \"\\\\\");\n            #[cfg(target_family = \"windows\")]\n            let path_wildcard = path_wildcard.replace(\"/\", \"\\\\\");\n\n            let name_wildcard_excluded = new_excluded_item(&name_wildcard);\n            let name_wildcard_lowercase_excluded = new_excluded_item(&name_wildcard.to_lowercase());\n            let path_wildcard_excluded = new_excluded_item(&path_wildcard);\n            let path_wildcard_lowercase_excluded = new_excluded_item(&path_wildcard.to_lowercase());\n\n            if response_type == ResponseType::Ok {\n                let check_path = check_button_path.is_active();\n                let check_name = check_button_name.is_active();\n                let check_regex = check_button_rust_regex.is_active();\n                let case_sensitive = check_button_case_sensitive.is_active();\n\n                let check_all_selected = check_button_select_not_all_results.is_active();\n\n                if check_button_path.is_active() || check_button_name.is_active() || check_button_rust_regex.is_active() {\n                    let compiled_regex = if check_regex {\n                        if let Ok(t) = Regex::new(&regex_wildcard) {\n                            t\n                        } else {\n                            error!(\"What? Regex should compile properly.\");\n                            confirmation_dialog_select_unselect.close();\n                            return;\n                        }\n                    } else {\n                        // Trivial regex is used, because I need here regex\n                        #[expect(clippy::trivial_regex)]\n                        Regex::new(\"\").expect(\"Empty regex should compile properly.\")\n                    };\n\n                    let model = sv.get_model();\n\n                    let Some(mut iter) = model.iter_first() else {\n                        confirmation_dialog_select_unselect.close();\n                        return;\n                    };\n                    let using_reference_folders =\n                        sv.nb_object.column_header.is_some_and(|e| model.get::<bool>(&iter, e)) && !model.get::<String>(&iter, sv.nb_object.column_name).is_empty();\n\n                    let mut number_of_all_things = 0;\n                    let mut number_of_already_selected_things = 0;\n                    let mut vec_of_iters: Vec<TreeIter> = Vec::new();\n                    loop {\n                        // If went to header and all previous items were selected, then deselect last item\n                        if let Some(column_header) = sv.nb_object.column_header\n                            && model.get::<bool>(&iter, column_header)\n                        {\n                            if select_things {\n                                if !using_reference_folders && check_all_selected && (number_of_all_things - number_of_already_selected_things == vec_of_iters.len()) {\n                                    vec_of_iters.pop();\n                                }\n                                for iter in vec_of_iters {\n                                    model.set_value(&iter, sv.nb_object.column_selection as u32, &true.to_value());\n                                }\n                            } else {\n                                for iter in vec_of_iters {\n                                    model.set_value(&iter, sv.nb_object.column_selection as u32, &false.to_value());\n                                }\n                            }\n\n                            if !model.iter_next(&mut iter) {\n                                break;\n                            }\n\n                            number_of_all_things = 0;\n                            number_of_already_selected_things = 0;\n                            vec_of_iters = Vec::new();\n                            continue;\n                        }\n\n                        let is_selected = model.get::<bool>(&iter, sv.nb_object.column_selection);\n                        let path = model.get::<String>(&iter, sv.nb_object.column_path);\n                        let name = model.get::<String>(&iter, sv.nb_object.column_name);\n\n                        let path_and_name = get_full_name_from_path_name(&path, &name);\n\n                        let mut need_to_change_thing: bool = false;\n\n                        number_of_all_things += 1;\n                        if check_regex && compiled_regex.find(&path_and_name).is_some() {\n                            need_to_change_thing = true;\n                        } else {\n                            if check_name {\n                                if case_sensitive {\n                                    if regex_check(&name_wildcard_excluded, &name) {\n                                        need_to_change_thing = true;\n                                    }\n                                } else if regex_check(&name_wildcard_lowercase_excluded, &name.to_lowercase()) {\n                                    need_to_change_thing = true;\n                                }\n                            }\n                            if check_path {\n                                if case_sensitive {\n                                    if regex_check(&path_wildcard_excluded, &path) {\n                                        need_to_change_thing = true;\n                                    }\n                                } else if regex_check(&path_wildcard_lowercase_excluded, &path.to_lowercase()) {\n                                    need_to_change_thing = true;\n                                }\n                            }\n                        }\n\n                        if select_things {\n                            if is_selected {\n                                number_of_already_selected_things += 1;\n                            } else if need_to_change_thing {\n                                vec_of_iters.push(iter);\n                            }\n                        } else if need_to_change_thing {\n                            vec_of_iters.push(iter);\n                        }\n\n                        // If went to last item and all previous items were selected, then deselect last item\n                        if !model.iter_next(&mut iter) {\n                            if select_things {\n                                if !using_reference_folders && check_all_selected && (number_of_all_things - number_of_already_selected_things == vec_of_iters.len()) {\n                                    vec_of_iters.pop();\n                                }\n                                for iter in vec_of_iters {\n                                    model.set_value(&iter, sv.nb_object.column_selection as u32, &true.to_value());\n                                }\n                            } else {\n                                for iter in vec_of_iters {\n                                    model.set_value(&iter, sv.nb_object.column_selection as u32, &false.to_value());\n                                }\n                            }\n                            break;\n                        }\n                    }\n                }\n            }\n            confirmation_dialog_select_unselect.close();\n        });\n    }\n}\n\nfn popover_all_except_biggest_smallest(\n    popover: &gtk4::Popover,\n    sv: &SubView,\n    // tree_view: &gtk4::TreeView,\n    // column_header: i32,\n    // column_size_as_bytes: i32,\n    // column_dimensions: Option<i32>,\n    // column_button_selection: u32,\n    except_biggest: bool,\n) {\n    let model = sv.get_model();\n    let column_header = sv.nb_object.column_header.expect(\"AEB/AES can't be used without headers\");\n    let column_size_as_bytes = sv.nb_object.column_size_as_bytes.expect(\"AEB/AES needs size as bytes column\");\n\n    if let Some(mut iter) = model.iter_first() {\n        let mut end: bool = false;\n        loop {\n            let mut tree_iter_array: Vec<TreeIter> = Vec::new();\n            let mut used_index: Option<usize> = None;\n            let mut current_index: usize = 0;\n            let mut size_as_bytes_min_max: u64 = if except_biggest { 0 } else { u64::MAX };\n            let mut number_of_pixels_min_max: u64 = if except_biggest { 0 } else { u64::MAX };\n\n            loop {\n                if model.get::<bool>(&iter, column_header) {\n                    if !model.iter_next(&mut iter) {\n                        end = true;\n                    }\n                    break;\n                }\n                tree_iter_array.push(iter);\n                let size_as_bytes = model.get::<u64>(&iter, column_size_as_bytes);\n\n                // If dimension exists, then needs to be checked images\n                if let Some(column_dimensions) = sv.nb_object.column_dimensions {\n                    let dimensions_string = model.get::<String>(&iter, column_dimensions);\n\n                    let dimensions = change_dimension_to_krotka(&dimensions_string);\n                    let number_of_pixels = dimensions.0 * dimensions.1;\n\n                    if except_biggest {\n                        if number_of_pixels > number_of_pixels_min_max || (number_of_pixels == number_of_pixels_min_max && size_as_bytes > size_as_bytes_min_max) {\n                            number_of_pixels_min_max = number_of_pixels;\n                            size_as_bytes_min_max = size_as_bytes;\n                            used_index = Some(current_index);\n                        }\n                    } else if number_of_pixels < number_of_pixels_min_max || (number_of_pixels == number_of_pixels_min_max && size_as_bytes < size_as_bytes_min_max) {\n                        number_of_pixels_min_max = number_of_pixels;\n                        size_as_bytes_min_max = size_as_bytes;\n                        used_index = Some(current_index);\n                    }\n                } else if except_biggest {\n                    if size_as_bytes > size_as_bytes_min_max {\n                        size_as_bytes_min_max = size_as_bytes;\n                        used_index = Some(current_index);\n                    }\n                } else if size_as_bytes < size_as_bytes_min_max {\n                    size_as_bytes_min_max = size_as_bytes;\n                    used_index = Some(current_index);\n                }\n\n                current_index += 1;\n\n                if !model.iter_next(&mut iter) {\n                    end = true;\n                    break;\n                }\n            }\n            let Some(used_index) = used_index else {\n                continue;\n            };\n            for (index, tree_iter) in tree_iter_array.iter().enumerate() {\n                if index != used_index {\n                    model.set_value(tree_iter, sv.nb_object.column_selection as u32, &true.to_value());\n                } else {\n                    model.set_value(tree_iter, sv.nb_object.column_selection as u32, &false.to_value());\n                }\n            }\n\n            if end {\n                break;\n            }\n        }\n    }\n\n    popover.popdown();\n}\n\npub(crate) fn connect_popover_select(gui_data: &GuiData) {\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all = gui_data.popovers_select.buttons_popover_select_all.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_select_all(&popover_select, &sv.tree_view, sv.nb_object.column_selection as u32, sv.nb_object.column_header);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_unselect_all = gui_data.popovers_select.buttons_popover_unselect_all.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_unselect_all.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_unselect_all(&popover_select, &sv.tree_view, sv.nb_object.column_selection as u32);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_reverse = gui_data.popovers_select.buttons_popover_reverse.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_reverse.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_reverse(&popover_select, &sv.tree_view, sv.nb_object.column_selection as u32, sv.nb_object.column_header);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all_except_oldest = gui_data.popovers_select.buttons_popover_select_all_except_oldest.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all_except_oldest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_all_except_oldest_newest(\n            &popover_select,\n            &sv.tree_view,\n            sv.nb_object.column_header.expect(\"AEO can't be used without headers\"),\n            sv.nb_object.column_modification_as_secs.expect(\"AEO needs modification as secs column\"),\n            sv.nb_object.column_name,\n            sv.nb_object.column_selection as u32,\n            true,\n        );\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all_except_newest = gui_data.popovers_select.buttons_popover_select_all_except_newest.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all_except_newest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_all_except_oldest_newest(\n            &popover_select,\n            &sv.tree_view,\n            sv.nb_object.column_header.expect(\"AEN can't be used without headers\"),\n            sv.nb_object.column_modification_as_secs.expect(\"AEN needs modification as secs column\"),\n            sv.nb_object.column_name,\n            sv.nb_object.column_selection as u32,\n            false,\n        );\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all_except_shortest = gui_data.popovers_select.buttons_popover_select_all_except_shortest_path.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all_except_shortest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_all_except_longest_shortest_path(\n            &popover_select,\n            &sv.tree_view,\n            sv.nb_object.column_header.expect(\"AES can't be used without headers\"),\n            sv.nb_object.column_path,\n            sv.nb_object.column_selection as u32,\n            true,\n        );\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all_except_longest = gui_data.popovers_select.buttons_popover_select_all_except_longest_path.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all_except_longest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_all_except_longest_shortest_path(\n            &popover_select,\n            &sv.tree_view,\n            sv.nb_object.column_header.expect(\"AES can't be used without headers\"),\n            sv.nb_object.column_path,\n            sv.nb_object.column_selection as u32,\n            false,\n        );\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_one_oldest = gui_data.popovers_select.buttons_popover_select_one_oldest.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_one_oldest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_one_oldest_newest(&popover_select, sv, true);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_one_newest = gui_data.popovers_select.buttons_popover_select_one_newest.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_one_newest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_one_oldest_newest(&popover_select, sv, false);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_custom = gui_data.popovers_select.buttons_popover_select_custom.clone();\n\n    let window_main = gui_data.window_main.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_custom.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_custom_select_unselect(&popover_select, &window_main, sv, true);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_unselect_custom = gui_data.popovers_select.buttons_popover_unselect_custom.clone();\n\n    let window_main = gui_data.window_main.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_unselect_custom.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_custom_select_unselect(&popover_select, &window_main, sv, false);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all_images_except_biggest = gui_data.popovers_select.buttons_popover_select_all_images_except_biggest.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all_images_except_biggest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_all_except_biggest_smallest(&popover_select, sv, true);\n    });\n\n    let popover_select = gui_data.popovers_select.popover_select.clone();\n    let buttons_popover_select_all_images_except_smallest = gui_data.popovers_select.buttons_popover_select_all_images_except_smallest.clone();\n\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_select_all_images_except_smallest.connect_clicked(move |_| {\n        let sv = common_tree_views.get_current_subview();\n\n        popover_all_except_biggest_smallest(&popover_select, sv, false);\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_popovers_sort.rs",
    "content": "use std::fmt::Debug;\n\nuse gtk4::prelude::*;\nuse gtk4::{ListStore, TreeIter};\n\nuse crate::gui_structs::common_tree_view::{SubView, TreeViewListStoreTrait};\nuse crate::gui_structs::gui_data::GuiData;\n\nfn popover_sort_general_abs<T>(popover: &gtk4::Popover, sv: &SubView)\nwhere\n    T: Ord + for<'b> glib::value::FromValue<'b> + 'static + Debug,\n{\n    popover_sort_general::<T>(\n        popover,\n        &sv.tree_view,\n        sv.nb_object.column_size_as_bytes.expect(\"Failed to get size as bytes column\"),\n        sv.nb_object.column_header.expect(\"Failed to get header column\"),\n    );\n}\n\nfn popover_sort_general<T>(popover: &gtk4::Popover, tree_view: &gtk4::TreeView, column_sort: i32, column_header: i32)\nwhere\n    T: Ord + for<'b> glib::value::FromValue<'b> + 'static + Debug,\n{\n    let model = tree_view.get_model();\n\n    if let Some(mut curr_iter) = model.iter_first() {\n        assert!(model.get::<bool>(&curr_iter, column_header));\n        assert!(model.iter_next(&mut curr_iter));\n        loop {\n            let mut iters = Vec::new();\n            let mut all_have = false;\n            let mut local_iter = curr_iter;\n            loop {\n                if model.get::<bool>(&local_iter, column_header) {\n                    if !model.iter_next(&mut local_iter) {\n                        all_have = true;\n                    }\n                    break;\n                }\n                iters.push(local_iter);\n                if !model.iter_next(&mut local_iter) {\n                    all_have = true;\n                    break;\n                }\n            }\n            if iters.len() == 1 {\n                curr_iter = local_iter;\n                if all_have {\n                    break;\n                }\n                continue;\n            }\n            sort_iters::<T>(&model, iters, column_sort);\n            curr_iter = local_iter;\n            if all_have {\n                break;\n            }\n        }\n    }\n    popover.popdown();\n}\n\nfn sort_iters<T>(model: &ListStore, mut iters: Vec<TreeIter>, column_sort: i32)\nwhere\n    T: Ord + for<'b> glib::value::FromValue<'b> + 'static + Debug,\n{\n    assert!(iters.len() >= 2);\n    loop {\n        let mut changed_item = false;\n        for idx in 0..(iters.len() - 1) {\n            if model.get::<T>(&iters[idx], column_sort) > model.get::<T>(&iters[idx + 1], column_sort) {\n                model.swap(&iters[idx], &iters[idx + 1]);\n                iters.swap(idx, idx + 1);\n                changed_item = true;\n            }\n        }\n        if !changed_item {\n            return;\n        }\n    }\n}\n\npub(crate) fn connect_popover_sort(gui_data: &GuiData) {\n    let popover_sort = gui_data.popovers_sort.popover_sort.clone();\n    let buttons_popover_file_name = gui_data.popovers_sort.buttons_popover_sort_file_name.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    buttons_popover_file_name.connect_clicked(move |_| {\n        popover_sort_general_abs::<String>(&popover_sort, common_tree_views.get_current_subview());\n    });\n\n    let popover_sort = gui_data.popovers_sort.popover_sort.clone();\n    let buttons_popover_sort_folder_name = gui_data.popovers_sort.buttons_popover_sort_folder_name.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    buttons_popover_sort_folder_name.connect_clicked(move |_| {\n        popover_sort_general_abs::<String>(&popover_sort, common_tree_views.get_current_subview());\n    });\n\n    let popover_sort = gui_data.popovers_sort.popover_sort.clone();\n    let buttons_popover_sort_selection = gui_data.popovers_sort.buttons_popover_sort_selection.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n\n    buttons_popover_sort_selection.connect_clicked(move |_| {\n        popover_sort_general_abs::<bool>(&popover_sort, common_tree_views.get_current_subview());\n    });\n\n    let popover_sort = gui_data.popovers_sort.popover_sort.clone();\n    let buttons_popover_sort_size = gui_data.popovers_sort.buttons_popover_sort_size.clone();\n    let common_tree_views = gui_data.main_notebook.common_tree_views.clone();\n    buttons_popover_sort_size.connect_clicked(move |_| {\n        popover_sort_general_abs::<u64>(&popover_sort, common_tree_views.get_current_subview());\n    });\n}\n\n#[cfg(test)]\nmod test {\n    use glib::types::Type;\n    use gtk4::prelude::*;\n    use gtk4::{Popover, TreeView};\n    use rand::random;\n\n    use super::*;\n    use crate::helpers::list_store_operations::append_row_to_list_store;\n\n    #[gtk4::test]\n    fn test_sort_iters() {\n        let columns_types: &[Type] = &[Type::U32, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        let values_to_add: &[&[(u32, &dyn ToValue)]] = &[&[(0, &2), (1, &\"AAA\")], &[(0, &3), (1, &\"CCC\")], &[(0, &1), (1, &\"BBB\")]];\n        for i in values_to_add {\n            append_row_to_list_store(&list_store, i);\n        }\n        let mut iters = Vec::new();\n        let mut iter = list_store.iter_first().expect(\"Failed to get first iter\");\n        iters.push(iter);\n        list_store.iter_next(&mut iter);\n        iters.push(iter);\n        list_store.iter_next(&mut iter);\n        iters.push(iter);\n\n        sort_iters::<String>(&list_store, iters, 1);\n\n        let expected = [(2, \"AAA\"), (1, \"BBB\"), (3, \"CCC\")];\n        let mut curr_iter = list_store.iter_first().expect(\"Failed to get first iter\");\n        for exp in expected {\n            let real_0 = list_store.get::<u32>(&curr_iter, 0);\n            assert_eq!(real_0, exp.0);\n            let real_1 = list_store.get::<String>(&curr_iter, 1);\n            assert_eq!(real_1, exp.1);\n            list_store.iter_next(&mut curr_iter);\n        }\n    }\n\n    #[gtk4::test]\n    pub(crate) fn test_popover_sort_general_simple() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n        let tree_view = TreeView::builder().model(&list_store).build();\n        let popover = Popover::new();\n\n        let values_to_add: &[&[(u32, &dyn ToValue)]] = &[&[(0, &true), (1, &\"DDD\")], &[(0, &false), (1, &\"CCC\")], &[(0, &false), (1, &\"BBB\")]];\n        for i in values_to_add {\n            append_row_to_list_store(&list_store, i);\n        }\n\n        popover_sort_general::<String>(&popover, &tree_view, 1, 0);\n\n        let expected = [\"DDD\", \"BBB\", \"CCC\"];\n        let mut curr_iter = list_store.iter_first().expect(\"Failed to get first iter\");\n        for exp in expected {\n            let real = list_store.get::<String>(&curr_iter, 1);\n            assert_eq!(real, exp);\n            list_store.iter_next(&mut curr_iter);\n        }\n    }\n\n    #[gtk4::test]\n    pub(crate) fn test_popover_sort_general() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n        let tree_view = TreeView::builder().model(&list_store).build();\n        let popover = Popover::new();\n\n        let values_to_add: &[&[(u32, &dyn ToValue)]] = &[\n            &[(0, &true), (1, &\"AAA\")],\n            &[(0, &false), (1, &\"CCC\")],\n            &[(0, &false), (1, &\"BBB\")],\n            &[(0, &true), (1, &\"TTT\")],\n            &[(0, &false), (1, &\"PPP\")],\n            &[(0, &false), (1, &\"AAA\")],\n            &[(0, &true), (1, &\"RRR\")],\n            &[(0, &false), (1, &\"WWW\")],\n            &[(0, &false), (1, &\"ZZZ\")],\n        ];\n        for i in values_to_add {\n            append_row_to_list_store(&list_store, i);\n        }\n\n        popover_sort_general::<String>(&popover, &tree_view, 1, 0);\n\n        let expected = [\"AAA\", \"BBB\", \"CCC\", \"TTT\", \"AAA\", \"PPP\", \"RRR\", \"WWW\", \"ZZZ\"];\n        let mut curr_iter = list_store.iter_first().expect(\"Failed to get first iter\");\n        for exp in expected {\n            let real = list_store.get::<String>(&curr_iter, 1);\n            assert_eq!(real, exp);\n            list_store.iter_next(&mut curr_iter);\n        }\n    }\n\n    #[gtk4::test]\n    pub(crate) fn fuzzer_test() {\n        for _ in 0..1000 {\n            let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n            let list_store = gtk4::ListStore::new(columns_types);\n            let tree_view = TreeView::builder().model(&list_store).build();\n            let popover = Popover::new();\n\n            // Always start with a header\n            let first_row: &[(u32, &dyn ToValue)] = &[(0, &true), (1, &\"AAA\")];\n            append_row_to_list_store(&list_store, first_row);\n\n            let mut since_last_header = 0;\n            let mut need_header = false;\n            let num_rows = (random::<u32>() % 10 + 5) as usize;\n            let mut i = 0;\n            while i < num_rows {\n                if need_header {\n                    // Insert a header only if last was not a header\n                    let a: Vec<(u32, &dyn ToValue)> = vec![(0, &true), (1, &\"HEADER\")];\n                    append_row_to_list_store(&list_store, &a);\n                    since_last_header = 0;\n                    need_header = false;\n                    i += 1;\n                    continue;\n                }\n                // Insert a non-header row\n                let string_val = rand::random::<u32>().to_string();\n                let a: Vec<(u32, &dyn ToValue)> = vec![(0, &false), (1, &string_val)];\n                append_row_to_list_store(&list_store, &a);\n                since_last_header += 1;\n                // After at least 2 non-header rows, randomly decide to insert a header next\n                if since_last_header >= 2 && random::<u8>().is_multiple_of(3) {\n                    need_header = true;\n                }\n                i += 1;\n            }\n\n            // Ensure at least one non-header after the last header\n            let mut last_iter = list_store.iter_first().expect(\"TEST\");\n            let mut last_is_header;\n            loop {\n                last_is_header = list_store.get::<bool>(&last_iter, 0);\n                if !list_store.iter_next(&mut last_iter) {\n                    break;\n                }\n            }\n            if last_is_header {\n                let a: Vec<(u32, &dyn ToValue)> = vec![(0, &false), (1, &\"FINALROW\")];\n                append_row_to_list_store(&list_store, &a);\n            }\n\n            popover_sort_general::<String>(&popover, &tree_view, 1, 0);\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_progress_window.rs",
    "content": "use std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::rc::Rc;\nuse std::time::Duration;\n\nuse crossbeam_channel::Receiver;\nuse czkawka_core::common::model::ToolType;\nuse czkawka_core::common::progress_data::{CurrentStage, ProgressData};\nuse glib::MainContext;\nuse gtk4::ProgressBar;\nuse gtk4::prelude::*;\nuse humansize::{BINARY, format_size};\n\nuse crate::flg;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::localizer_core::generate_translation_hashmap;\nuse crate::taskbar_progress::TaskbarProgress;\nuse crate::taskbar_progress::tbp_flags::TBPF_INDETERMINATE;\n\npub(crate) fn connect_progress_window(gui_data: &GuiData, progress_receiver: Receiver<ProgressData>) {\n    let main_context = MainContext::default();\n    let _guard = main_context.acquire().expect(\"Failed to acquire main context\");\n\n    let gui_data = gui_data.clone();\n\n    let future = async move {\n        loop {\n            loop {\n                let item = progress_receiver.try_recv();\n                if let Ok(item) = item {\n                    if item.current_stage_idx == 0 {\n                        progress_collect_items(&gui_data, &item, item.tool_type != ToolType::EmptyFolders);\n                    } else if item.sstage.check_if_loading_saving_cache() {\n                        progress_save_load_cache(&gui_data, &item);\n                    } else {\n                        progress_default(&gui_data, &item);\n                    }\n                } else {\n                    break;\n                }\n            }\n            glib::timeout_future(Duration::from_millis(300)).await;\n        }\n    };\n\n    main_context.spawn_local(future);\n}\n\nfn progress_save_load_cache(gui_data: &GuiData, item: &ProgressData) {\n    let label_stage = gui_data.progress_window.label_stage.clone();\n    let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone();\n    let taskbar_state = gui_data.taskbar_state.clone();\n\n    progress_bar_current_stage.set_visible(false);\n    taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE);\n\n    let text = match item.sstage {\n        CurrentStage::SameMusicCacheLoadingFingerprints | CurrentStage::SameMusicCacheLoadingTags => {\n            flg!(\"progress_cache_loading\")\n        }\n        CurrentStage::SameMusicCacheSavingFingerprints | CurrentStage::SameMusicCacheSavingTags => {\n            flg!(\"progress_cache_saving\")\n        }\n        CurrentStage::DuplicateCacheLoading => {\n            flg!(\"progress_hash_cache_loading\")\n        }\n        CurrentStage::DuplicateCacheSaving => {\n            flg!(\"progress_hash_cache_saving\")\n        }\n        CurrentStage::DuplicatePreHashCacheLoading => {\n            flg!(\"progress_prehash_cache_loading\")\n        }\n        CurrentStage::DuplicatePreHashCacheSaving => {\n            flg!(\"progress_prehash_cache_saving\")\n        }\n        CurrentStage::ExifRemoverCacheLoading | CurrentStage::ExifRemoverCacheSaving | CurrentStage::ExifRemoverExtractingTags | CurrentStage::CleaningExif => {\n            panic!(\"Exif remover not implemented in gtk version\")\n        }\n        _ => panic!(\"Invalid stage {:?}\", item.sstage),\n    };\n\n    label_stage.set_text(&text);\n}\n\nfn progress_collect_items(gui_data: &GuiData, item: &ProgressData, files: bool) {\n    let label_stage = gui_data.progress_window.label_stage.clone();\n    let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone();\n    let taskbar_state = gui_data.taskbar_state.clone();\n\n    progress_bar_current_stage.set_visible(false);\n    taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE);\n\n    match item.sstage {\n        CurrentStage::DuplicateScanningName => {\n            label_stage.set_text(&flg!(\"progress_scanning_name\", file_number_tm(item)));\n        }\n        CurrentStage::DuplicateScanningSizeName => {\n            label_stage.set_text(&flg!(\"progress_scanning_size_name\", file_number_tm(item)));\n        }\n        CurrentStage::DuplicateScanningSize => {\n            label_stage.set_text(&flg!(\"progress_scanning_size\", file_number_tm(item)));\n        }\n        _ => {\n            if files {\n                label_stage.set_text(&flg!(\"progress_scanning_general_file\", file_number_tm(item)));\n            } else {\n                label_stage.set_text(&flg!(\"progress_scanning_empty_folders\", folder_number = item.entries_checked));\n            }\n        }\n    }\n}\n\nfn progress_default(gui_data: &GuiData, item: &ProgressData) {\n    let label_stage = gui_data.progress_window.label_stage.clone();\n    let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone();\n    let progress_bar_all_stages = gui_data.progress_window.progress_bar_all_stages.clone();\n    let taskbar_state = gui_data.taskbar_state.clone();\n\n    progress_bar_current_stage.set_visible(true);\n    common_set_data(item, &progress_bar_all_stages, &progress_bar_current_stage, &taskbar_state);\n    taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE);\n\n    match item.sstage {\n        CurrentStage::SameMusicReadingTags => {\n            label_stage.set_text(&flg!(\"progress_scanning_music_tags\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SameMusicCalculatingFingerprints => {\n            label_stage.set_text(&flg!(\"progress_scanning_music_content\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SameMusicComparingTags => {\n            label_stage.set_text(&flg!(\"progress_scanning_music_tags_end\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SameMusicComparingFingerprints => {\n            label_stage.set_text(&flg!(\"progress_scanning_music_content_end\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SimilarImagesCalculatingHashes => {\n            label_stage.set_text(&flg!(\"progress_scanning_image\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SimilarImagesComparingHashes => {\n            label_stage.set_text(&flg!(\"progress_comparing_image_hashes\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SimilarVideosCalculatingHashes => {\n            label_stage.set_text(&flg!(\"progress_scanning_video\", progress_ratio_tm(item)));\n        }\n        CurrentStage::SimilarVideosCreatingThumbnails => {\n            label_stage.set_text(&flg!(\"progress_creating_video_thumbnails\", progress_ratio_tm(item)));\n        }\n        CurrentStage::BrokenFilesChecking => {\n            label_stage.set_text(&flg!(\"progress_scanning_broken_files\", progress_ratio_tm(item)));\n        }\n        CurrentStage::BadExtensionsChecking => {\n            label_stage.set_text(&flg!(\"progress_scanning_extension_of_files\", progress_ratio_tm(item)));\n        }\n        CurrentStage::DuplicatePreHashing => {\n            label_stage.set_text(&flg!(\"progress_analyzed_partial_hash\", progress_ratio_tm(item)));\n        }\n        CurrentStage::DuplicateFullHashing => {\n            label_stage.set_text(&flg!(\"progress_analyzed_full_hash\", progress_ratio_tm(item)));\n        }\n        _ => unreachable!(\"Invalid stage {:?}\", item.sstage),\n    }\n}\n\nfn common_set_data(item: &ProgressData, progress_bar_all_stages: &ProgressBar, progress_bar_current_stage: &ProgressBar, taskbar_state: &Rc<RefCell<TaskbarProgress>>) {\n    let (current_items_checked, current_stage_items_to_check) = if item.bytes_to_check > 0 {\n        (item.bytes_checked, item.bytes_to_check)\n    } else {\n        (item.entries_checked as u64, item.entries_to_check as u64)\n    };\n\n    if item.entries_to_check != 0 {\n        let all_stages = (item.current_stage_idx as f64 + current_items_checked as f64 / current_stage_items_to_check as f64) / (item.max_stage_idx + 1) as f64;\n        let all_stages = all_stages.min(0.99);\n        progress_bar_all_stages.set_fraction(all_stages);\n        progress_bar_current_stage.set_fraction(current_items_checked as f64 / current_stage_items_to_check as f64);\n\n        taskbar_state.borrow().set_progress_value(\n            (item.current_stage_idx as u64) * current_stage_items_to_check + current_items_checked,\n            current_stage_items_to_check * (item.max_stage_idx + 1) as u64,\n        );\n    } else {\n        let all_stages = (item.current_stage_idx as f64) / (item.max_stage_idx + 1) as f64;\n        let all_stages = all_stages.min(0.99);\n        progress_bar_all_stages.set_fraction(all_stages);\n        progress_bar_current_stage.set_fraction(0f64);\n        taskbar_state.borrow().set_progress_value(item.current_stage_idx as u64, 1 + item.max_stage_idx as u64);\n    }\n}\n\nfn file_number_tm(item: &ProgressData) -> HashMap<&'static str, String> {\n    generate_translation_hashmap(vec![(\"file_number\", item.entries_checked.to_string())])\n}\n\nfn progress_ratio_tm(item: &ProgressData) -> HashMap<&'static str, String> {\n    let mut v = vec![(\"file_checked\", item.entries_checked.to_string()), (\"all_files\", item.entries_to_check.to_string())];\n    if item.bytes_to_check != 0 {\n        v.push((\"data_checked\", format_size(item.bytes_checked, BINARY)));\n        v.push((\"all_data\", format_size(item.bytes_to_check, BINARY)));\n    }\n    generate_translation_hashmap(v)\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_same_music_mode_changed.rs",
    "content": "use czkawka_core::common::model::CheckingMethod;\nuse gtk4::prelude::*;\nuse gtk4::{CheckButton, Widget};\n\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_combo_box::AUDIO_TYPE_CHECK_METHOD_COMBO_BOX;\nuse crate::help_functions::scale_set_min_max_values;\n\nconst MINIMUM_SECONDS: f64 = 0.5;\nconst MAXIMUM_SECONDS: f64 = 180.0;\nconst DEFAULT_SECONDS: f64 = 15.0;\nconst MINIMUM_SIMILARITY: f64 = 0.0;\nconst MAXIMUM_SIMILARITY: f64 = 10.0;\nconst DEFAULT_SIMILARITY: f64 = 5.0;\n\npub(crate) fn connect_same_music_change_mode(gui_data: &GuiData) {\n    let check_button_music_title = gui_data.main_notebook.check_button_music_title.clone();\n    let check_button_music_approximate_comparison = gui_data.main_notebook.check_button_music_approximate_comparison.clone();\n    let check_button_music_bitrate = gui_data.main_notebook.check_button_music_bitrate.clone();\n    let check_button_music_artist = gui_data.main_notebook.check_button_music_artist.clone();\n    let check_button_music_genre = gui_data.main_notebook.check_button_music_genre.clone();\n    let check_button_music_length = gui_data.main_notebook.check_button_music_length.clone();\n    let check_button_music_year = gui_data.main_notebook.check_button_music_year.clone();\n    let buttons = [\n        check_button_music_title,\n        check_button_music_approximate_comparison,\n        check_button_music_bitrate,\n        check_button_music_artist,\n        check_button_music_genre,\n        check_button_music_year,\n        check_button_music_length,\n    ];\n\n    let check_button_music_compare_only_in_title_group = gui_data.main_notebook.check_button_music_compare_only_in_title_group.clone();\n    let reversed_buttons = [check_button_music_compare_only_in_title_group];\n\n    let scale_seconds_same_music = gui_data.main_notebook.scale_seconds_same_music.clone();\n    let scale_similarity_same_music = gui_data.main_notebook.scale_similarity_same_music.clone();\n    let label_same_music_similarity = gui_data.main_notebook.label_same_music_similarity.clone();\n    let label_same_music_seconds = gui_data.main_notebook.label_same_music_seconds.clone();\n\n    scale_set_min_max_values(&scale_seconds_same_music, MINIMUM_SECONDS, MAXIMUM_SECONDS, DEFAULT_SECONDS, None);\n    scale_set_min_max_values(&scale_similarity_same_music, MINIMUM_SIMILARITY, MAXIMUM_SIMILARITY, DEFAULT_SIMILARITY, None);\n\n    let scales_and_labels = [\n        scale_seconds_same_music.into(),\n        scale_similarity_same_music.into(),\n        label_same_music_similarity.into(),\n        label_same_music_seconds.into(),\n    ];\n\n    let combo_box_audio_check_type = gui_data.main_notebook.combo_box_audio_check_type.clone();\n\n    let check_method_index = combo_box_audio_check_type.active().expect(\"Failed to get active item\") as usize;\n    let check_method = AUDIO_TYPE_CHECK_METHOD_COMBO_BOX[check_method_index].check_method;\n\n    disable_enable_buttons(&buttons, &reversed_buttons, &scales_and_labels, check_method);\n    combo_box_audio_check_type.connect_changed(move |combo_box_text| {\n        if let Some(active) = combo_box_text.active() {\n            let check_method = AUDIO_TYPE_CHECK_METHOD_COMBO_BOX[active as usize].check_method;\n\n            disable_enable_buttons(&buttons, &reversed_buttons, &scales_and_labels, check_method);\n        }\n    });\n}\n\nfn disable_enable_buttons(buttons: &[CheckButton; 7], reverse_buttons: &[CheckButton; 1], scales: &[Widget; 4], current_mode: CheckingMethod) {\n    match current_mode {\n        CheckingMethod::AudioTags => {\n            buttons.iter().for_each(WidgetExt::show);\n            reverse_buttons.iter().for_each(WidgetExt::hide);\n            scales.iter().for_each(WidgetExt::hide);\n        }\n        CheckingMethod::AudioContent => {\n            buttons.iter().for_each(WidgetExt::hide);\n            reverse_buttons.iter().for_each(WidgetExt::show);\n            scales.iter().for_each(WidgetExt::show);\n        }\n        _ => panic!(),\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_selection_of_directories.rs",
    "content": "use std::collections::HashSet;\nuse std::path::PathBuf;\n\n#[cfg(target_family = \"windows\")]\nuse czkawka_core::common::normalize_windows_path;\nuse gdk4::{DragAction, FileList};\nuse gtk4::prelude::*;\nuse gtk4::{DropTarget, FileChooserNative, Notebook, Orientation, ResponseType, TreeView, Window};\n\nuse crate::connect_things::file_chooser_helpers::extract_paths_from_file_chooser;\nuse crate::flg;\nuse crate::gui_structs::common_tree_view::TreeViewListStoreTrait;\nuse crate::gui_structs::common_upper_tree_view::UpperTreeViewEnum;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::helpers::enums::{ColumnsExcludedDirectory, ColumnsIncludedDirectory};\nuse crate::helpers::list_store_operations::{append_row_to_list_store, check_if_value_is_in_list_store};\nuse crate::notebook_enums::{NotebookUpperEnum, to_notebook_upper_enum};\n\npub(crate) fn connect_selection_of_directories(gui_data: &GuiData) {\n    let tree_view_included_directories = gui_data.upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::IncludedDirectories);\n    let tree_view_excluded_directories = gui_data.upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::ExcludedDirectories);\n\n    // Add manually directory\n    {\n        let tree_view_included_directories = tree_view_included_directories.clone();\n        let window_main = gui_data.window_main.clone();\n        let buttons_manual_add_included_directory = gui_data.upper_notebook.buttons_manual_add_included_directory.clone();\n        buttons_manual_add_included_directory.connect_clicked(move |_| {\n            add_manually_directories(&window_main, &tree_view_included_directories, false);\n        });\n    }\n    // Add manually excluded directory\n    {\n        let tree_view_excluded_directories = tree_view_excluded_directories.clone();\n        let window_main = gui_data.window_main.clone();\n        let buttons_manual_add_excluded_directory = gui_data.upper_notebook.buttons_manual_add_excluded_directory.clone();\n        buttons_manual_add_excluded_directory.connect_clicked(move |_| {\n            add_manually_directories(&window_main, &tree_view_excluded_directories, true);\n        });\n    }\n    // Add included directory\n    {\n        let buttons_add_included_directory = gui_data.upper_notebook.buttons_add_included_directory.clone();\n        let file_dialog_include_exclude_folder_selection = gui_data.file_dialog_include_exclude_folder_selection.clone();\n        buttons_add_included_directory.connect_clicked(move |_| {\n            file_dialog_include_exclude_folder_selection.set_visible(true);\n            file_dialog_include_exclude_folder_selection.set_title(&flg!(\"include_folders_dialog_title\"));\n        });\n    }\n    // Add excluded directory\n    {\n        let buttons_add_excluded_directory = gui_data.upper_notebook.buttons_add_excluded_directory.clone();\n        let file_dialog_include_exclude_folder_selection = gui_data.file_dialog_include_exclude_folder_selection.clone();\n        buttons_add_excluded_directory.connect_clicked(move |_| {\n            file_dialog_include_exclude_folder_selection.set_visible(true);\n            file_dialog_include_exclude_folder_selection.set_title(&flg!(\"exclude_folders_dialog_title\"));\n        });\n    }\n    // Connect\n    {\n        let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();\n        let tree_view_included_directories = tree_view_included_directories.clone();\n        let tree_view_excluded_directories = tree_view_excluded_directories.clone();\n        let file_dialog_include_exclude_folder_selection = gui_data.file_dialog_include_exclude_folder_selection.clone();\n        connect_file_dialog(\n            &file_dialog_include_exclude_folder_selection,\n            tree_view_included_directories,\n            tree_view_excluded_directories,\n            notebook_upper,\n        );\n    }\n    // Drag and drop\n    {\n        configure_directory_drop(tree_view_included_directories, false);\n        configure_directory_drop(tree_view_excluded_directories, true);\n    }\n    // Remove Excluded Folder\n    {\n        let buttons_remove_excluded_directory = gui_data.upper_notebook.buttons_remove_excluded_directory.clone();\n        let tree_view_excluded_directories = tree_view_excluded_directories.clone();\n        buttons_remove_excluded_directory.connect_clicked(move |_| {\n            remove_item_directory(&tree_view_excluded_directories);\n        });\n    }\n    // Remove Included Folder\n    {\n        let buttons_remove_included_directory = gui_data.upper_notebook.buttons_remove_included_directory.clone();\n        let tree_view_included_directories = tree_view_included_directories.clone();\n        buttons_remove_included_directory.connect_clicked(move |_| {\n            remove_item_directory(&tree_view_included_directories);\n        });\n    }\n}\n\nfn remove_item_directory(tree_view: &TreeView) {\n    let list_store = tree_view.get_model();\n    let selection = tree_view.selection();\n\n    let (vec_tree_path, _tree_model) = selection.selected_rows();\n\n    for tree_path in vec_tree_path.iter().rev() {\n        list_store.remove(&list_store.iter(tree_path).expect(\"Using invalid tree_path\"));\n    }\n}\n\nfn configure_directory_drop(tree_view: &TreeView, excluded_items: bool) {\n    let tv = tree_view.clone();\n    let drop_target = DropTarget::builder().name(\"file-drop-target\").actions(DragAction::COPY).build();\n    drop_target.set_types(&[FileList::static_type()]);\n    drop_target.connect_drop(move |_, value, _, _| {\n        if let Ok(file_list) = value.get::<FileList>() {\n            let mut folders: HashSet<PathBuf> = HashSet::new();\n            for f in file_list.files() {\n                if let Some(path) = f.path() {\n                    if path.is_dir() {\n                        folders.insert(path);\n                    } else if let Some(parent) = path.parent()\n                        && parent.is_dir()\n                    {\n                        folders.insert(parent.to_path_buf());\n                    }\n                }\n            }\n            add_directories(&tv, &folders.into_iter().collect(), excluded_items);\n        }\n        true\n    });\n\n    tree_view.add_controller(drop_target);\n}\n\nfn connect_file_dialog(file_dialog_include_exclude_folder_selection: &FileChooserNative, include_tree_view: TreeView, exclude_tree_view: TreeView, notebook_upper: Notebook) {\n    file_dialog_include_exclude_folder_selection.connect_response(move |file_chooser, response_type| {\n        if response_type == ResponseType::Accept {\n            let excluded_items;\n            let tree_view = match to_notebook_upper_enum(notebook_upper.current_page().expect(\"Current page not set\")) {\n                NotebookUpperEnum::IncludedDirectories => {\n                    excluded_items = false;\n                    &include_tree_view\n                }\n                NotebookUpperEnum::ExcludedDirectories => {\n                    excluded_items = true;\n                    &exclude_tree_view\n                }\n                NotebookUpperEnum::ItemsConfiguration => panic!(),\n            };\n\n            let folders = extract_paths_from_file_chooser(file_chooser);\n\n            add_directories(tree_view, &folders, excluded_items);\n        }\n    });\n}\n\nfn add_directories(tree_view: &TreeView, folders: &Vec<PathBuf>, excluded_items: bool) {\n    let list_store = tree_view.get_model();\n\n    if excluded_items {\n        for file_entry in folders {\n            let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &file_entry.to_string_lossy().to_string())];\n            append_row_to_list_store(&list_store, &values);\n        }\n    } else {\n        for file_entry in folders {\n            let values: [(u32, &dyn ToValue); 2] = [\n                (ColumnsIncludedDirectory::Path as u32, &file_entry.to_string_lossy().to_string()),\n                (ColumnsIncludedDirectory::ReferenceButton as u32, &false),\n            ];\n            append_row_to_list_store(&list_store, &values);\n        }\n    }\n}\n\nfn add_manually_directories(window_main: &Window, tree_view: &TreeView, excluded_items: bool) {\n    let dialog = gtk4::Dialog::builder()\n        .title(flg!(\"include_manually_directories_dialog_title\"))\n        .transient_for(window_main)\n        .modal(true)\n        .build();\n\n    dialog.set_default_size(300, 0);\n\n    let entry: gtk4::Entry = gtk4::Entry::new();\n\n    let added_button = dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n    dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n    let parent = added_button.parent().expect(\"Hack 1\").parent().expect(\"Hack 2\").downcast::<gtk4::Box>().expect(\"Hack 3\"); // TODO Hack, but not so ugly as before\n    parent.set_orientation(Orientation::Vertical);\n    parent.insert_child_after(&entry, None::<&gtk4::Widget>);\n\n    dialog.set_visible(true);\n\n    let tree_view = tree_view.clone();\n    dialog.connect_response(move |dialog, response_type| {\n        if response_type == ResponseType::Ok {\n            for text in entry.text().split(';') {\n                let text = text.trim().to_string();\n                #[cfg(target_family = \"windows\")]\n                let text = normalize_windows_path(text).to_string_lossy().to_string();\n                let mut text = text;\n\n                remove_ending_slashes(&mut text);\n\n                if !text.is_empty() {\n                    let list_store = tree_view.get_model();\n\n                    if excluded_items {\n                        if !check_if_value_is_in_list_store(&list_store, ColumnsExcludedDirectory::Path as i32, &text) {\n                            let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &text)];\n                            append_row_to_list_store(&list_store, &values);\n                        }\n                    } else if !check_if_value_is_in_list_store(&list_store, ColumnsIncludedDirectory::Path as i32, &text) {\n                        let values: [(u32, &dyn ToValue); 2] = [(ColumnsIncludedDirectory::Path as u32, &text), (ColumnsIncludedDirectory::ReferenceButton as u32, &false)];\n                        append_row_to_list_store(&list_store, &values);\n                    }\n                }\n            }\n        }\n        dialog.close();\n    });\n}\n\nfn remove_ending_slashes(original_string: &mut String) {\n    let mut windows_disk_path: bool = false;\n    let mut chars = original_string.chars();\n    if let Some(first_character) = chars.next()\n        && first_character.is_alphabetic()\n        && let Some(second_character) = chars.next()\n        && second_character == ':'\n    {\n        windows_disk_path = true;\n        original_string.push('/'); // In case of adding window path without ending slash e.g. C: instead C:/ or C:\\\n    }\n\n    while (original_string != \"/\" && (original_string.ends_with('/') || original_string.ends_with('\\\\'))) && (!windows_disk_path || original_string.len() > 3) {\n        original_string.pop();\n    }\n}\n\n#[test]\npub(crate) fn test_remove_ending_slashes() {\n    let mut original = \"/home/rafal\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/home/rafal\");\n\n    let mut original = \"/home/rafal/\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/home/rafal\");\n\n    let mut original = \"/home/rafal\\\\\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/home/rafal\");\n\n    let mut original = \"/home/rafal/////////\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/home/rafal\");\n\n    let mut original = \"/home/rafal/\\\\//////\\\\\\\\\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/home/rafal\");\n\n    let mut original = \"/home/rafal\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/home/rafal\");\n\n    let mut original = \"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"\");\n\n    let mut original = \"//////////\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"/\");\n\n    let mut original = \"C:/\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"C:/\");\n\n    let mut original = \"C:\\\\\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"C:\\\\\");\n\n    let mut original = \"C://////////\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"C:/\");\n\n    let mut original = \"C:/roman/function/\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"C:/roman/function\");\n\n    let mut original = \"C:/staszek/without\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"C:/staszek/without\");\n\n    let mut original = \"C:\\\\\\\\\\\\\\\\\\\\\".to_string();\n    remove_ending_slashes(&mut original);\n    assert_eq!(&original, \"C:\\\\\");\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_settings.rs",
    "content": "use std::collections::BTreeMap;\nuse std::default::Default;\n\nuse czkawka_core::common::cache::{load_cache_from_file_generalized_by_path, load_cache_from_file_generalized_by_size, save_cache_to_file_generalized};\nuse czkawka_core::common::config_cache_path::get_config_cache_path;\nuse czkawka_core::common::model::HashType;\nuse czkawka_core::helpers::messages::{MessageLimit, Messages};\nuse czkawka_core::re_exported::HashAlg;\nuse czkawka_core::tools::duplicate::DuplicateEntry;\nuse czkawka_core::tools::duplicate::core::get_duplicate_cache_file;\nuse czkawka_core::tools::similar_images::core::get_similar_images_cache_file;\nuse czkawka_core::tools::similar_videos::core::get_similar_videos_cache_file;\nuse czkawka_core::tools::similar_videos::{DEFAULT_CROP_DETECT, DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION};\nuse gtk4::prelude::*;\nuse gtk4::{Label, ResponseType, Window};\nuse image::imageops::FilterType;\nuse log::error;\n\nuse crate::flg;\nuse crate::gtk_traits::DialogTraits;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::saving_loading::{load_configuration, reset_configuration, save_configuration};\n\npub(crate) fn connect_settings(gui_data: &GuiData) {\n    // Connect scale\n    {\n        let label_restart_needed = gui_data.settings.label_restart_needed.clone();\n        gui_data.settings.scale_settings_number_of_threads.connect_value_changed(move |_| {\n            if label_restart_needed.label().is_empty() {\n                label_restart_needed.set_label(&flg!(\"settings_label_restart\"));\n            }\n        });\n    }\n    // Connect button settings\n    {\n        let button_settings = gui_data.header.button_settings.clone();\n        let window_settings = gui_data.settings.window_settings.clone();\n        button_settings.connect_clicked(move |_| {\n            window_settings.set_visible(true);\n        });\n\n        let window_settings = gui_data.settings.window_settings.clone();\n\n        window_settings.connect_close_request(move |window| {\n            window.set_visible(false);\n            glib::Propagation::Stop\n        });\n    }\n\n    // Connect save configuration button\n    {\n        let upper_notebook = gui_data.upper_notebook.clone();\n        let settings = gui_data.settings.clone();\n        let main_notebook = gui_data.main_notebook.clone();\n        let text_view_errors = gui_data.text_view_errors.clone();\n        let button_settings_save_configuration = gui_data.settings.button_settings_save_configuration.clone();\n        button_settings_save_configuration.connect_clicked(move |_| {\n            save_configuration(true, &upper_notebook, &main_notebook, &settings, &text_view_errors);\n        });\n    }\n    // Connect load configuration button\n    {\n        let upper_notebook = gui_data.upper_notebook.clone();\n        let settings = gui_data.settings.clone();\n        let main_notebook = gui_data.main_notebook.clone();\n        let text_view_errors = gui_data.text_view_errors.clone();\n        let button_settings_load_configuration = gui_data.settings.button_settings_load_configuration.clone();\n        let scrolled_window_errors = gui_data.scrolled_window_errors.clone();\n        button_settings_load_configuration.connect_clicked(move |_| {\n            load_configuration(true, &upper_notebook, &main_notebook, &settings, &text_view_errors, &scrolled_window_errors, None);\n        });\n    }\n    // Connect reset configuration button\n    {\n        let upper_notebook = gui_data.upper_notebook.clone();\n        let settings = gui_data.settings.clone();\n        let main_notebook = gui_data.main_notebook.clone();\n        let text_view_errors = gui_data.text_view_errors.clone();\n        let button_settings_reset_configuration = gui_data.settings.button_settings_reset_configuration.clone();\n        button_settings_reset_configuration.connect_clicked(move |_| {\n            reset_configuration(true, &upper_notebook, &main_notebook, &settings, &text_view_errors);\n        });\n    }\n    // Connect button for opening cache\n    {\n        let button_settings_open_cache_folder = gui_data.settings.button_settings_open_cache_folder.clone();\n        button_settings_open_cache_folder.connect_clicked(move |_| {\n            if let Some(config_cache_path) = get_config_cache_path() {\n                if let Err(e) = open::that(&config_cache_path.cache_folder) {\n                    error!(\"Failed to open config folder \\\"{}\\\", reason {e}\", config_cache_path.cache_folder.to_string_lossy());\n                }\n            } else {\n                error!(\"Failed to get cache folder path\");\n            }\n        });\n    }\n    // Connect button for opening settings\n    {\n        let button_settings_open_settings_folder = gui_data.settings.button_settings_open_settings_folder.clone();\n        button_settings_open_settings_folder.connect_clicked(move |_| {\n            if let Some(config_cache_path) = get_config_cache_path() {\n                if let Err(e) = open::that(&config_cache_path.config_folder) {\n                    error!(\"Failed to open config folder \\\"{}\\\", reason {e}\", config_cache_path.config_folder.to_string_lossy());\n                }\n            } else {\n                error!(\"Failed to get settings folder path\");\n            }\n        });\n    }\n    // Connect clear cache methods\n    {\n        {\n            let button_settings_duplicates_clear_cache = gui_data.settings.button_settings_duplicates_clear_cache.clone();\n            let settings_window = gui_data.settings.window_settings.clone();\n            let text_view_errors = gui_data.text_view_errors.clone();\n            let entry_settings_cache_file_minimal_size = gui_data.settings.entry_settings_cache_file_minimal_size.clone();\n\n            button_settings_duplicates_clear_cache.connect_clicked(move |_| {\n                let dialog = create_clear_cache_dialog(&flg!(\"cache_clear_duplicates_title\"), &settings_window);\n                dialog.set_visible(true);\n\n                let text_view_errors = text_view_errors.clone();\n                let entry_settings_cache_file_minimal_size = entry_settings_cache_file_minimal_size.clone();\n\n                dialog.connect_response(move |dialog, response_type| {\n                    if response_type == ResponseType::Ok {\n                        let mut messages: Messages = Messages::new();\n                        for use_prehash in [true, false] {\n                            for type_of_hash in [HashType::Xxh3, HashType::Blake3, HashType::Crc32] {\n                                let file_name = get_duplicate_cache_file(type_of_hash, use_prehash);\n                                let (mut messages, loaded_items) = load_cache_from_file_generalized_by_size::<DuplicateEntry>(&file_name, true, &Default::default());\n\n                                if let Some(cache_entries) = loaded_items {\n                                    let mut hashmap_to_save: BTreeMap<String, DuplicateEntry> = Default::default();\n                                    for (_, vec_file_entry) in cache_entries {\n                                        for file_entry in vec_file_entry {\n                                            hashmap_to_save.insert(file_entry.path.to_string_lossy().to_string(), file_entry);\n                                        }\n                                    }\n\n                                    let minimal_cache_size = entry_settings_cache_file_minimal_size.text().as_str().parse::<u64>().unwrap_or(2 * 1024 * 1024);\n\n                                    let save_messages = save_cache_to_file_generalized(&file_name, &hashmap_to_save, false, minimal_cache_size);\n                                    messages.extend_with_another_messages(save_messages);\n                                }\n                            }\n\n                            messages.messages.push(flg!(\"cache_properly_cleared\"));\n                            text_view_errors.buffer().set_text(messages.create_messages_text(MessageLimit::NoLimit).as_str());\n                        }\n                    }\n                    dialog.close();\n                });\n            });\n        }\n        {\n            let button_settings_similar_images_clear_cache = gui_data.settings.button_settings_similar_images_clear_cache.clone();\n            let settings_window = gui_data.settings.window_settings.clone();\n            let text_view_errors = gui_data.text_view_errors.clone();\n\n            button_settings_similar_images_clear_cache.connect_clicked(move |_| {\n                let dialog = create_clear_cache_dialog(&flg!(\"cache_clear_similar_images_title\"), &settings_window);\n                dialog.set_visible(true);\n\n                let text_view_errors = text_view_errors.clone();\n\n                dialog.connect_response(move |dialog, response_type| {\n                    if response_type == ResponseType::Ok {\n                        let mut messages: Messages = Messages::new();\n                        for hash_size in [8, 16, 32, 64] {\n                            for image_filter in [\n                                FilterType::Lanczos3,\n                                FilterType::CatmullRom,\n                                FilterType::Gaussian,\n                                FilterType::Nearest,\n                                FilterType::Triangle,\n                            ] {\n                                for hash_alg in [\n                                    HashAlg::Blockhash,\n                                    HashAlg::Gradient,\n                                    HashAlg::DoubleGradient,\n                                    HashAlg::VertGradient,\n                                    HashAlg::Mean,\n                                    HashAlg::Median,\n                                ] {\n                                    let file_name = get_similar_images_cache_file(hash_size, hash_alg, image_filter);\n                                    let (mut messages, loaded_items) =\n                                        load_cache_from_file_generalized_by_path::<czkawka_core::tools::similar_images::ImagesEntry>(&file_name, true, &Default::default());\n\n                                    if let Some(cache_entries) = loaded_items {\n                                        let save_messages = save_cache_to_file_generalized(&file_name, &cache_entries, false, 0);\n                                        messages.extend_with_another_messages(save_messages);\n                                    }\n                                }\n                            }\n                        }\n\n                        messages.messages.push(flg!(\"cache_properly_cleared\"));\n                        text_view_errors.buffer().set_text(messages.create_messages_text(MessageLimit::NoLimit).as_str());\n                    }\n                    dialog.close();\n                });\n            });\n        }\n        {\n            let button_settings_similar_videos_clear_cache = gui_data.settings.button_settings_similar_videos_clear_cache.clone();\n            let settings_window = gui_data.settings.window_settings.clone();\n            let text_view_errors = gui_data.text_view_errors.clone();\n\n            button_settings_similar_videos_clear_cache.connect_clicked(move |_| {\n                let dialog = create_clear_cache_dialog(&flg!(\"cache_clear_similar_videos_title\"), &settings_window);\n                dialog.set_visible(true);\n\n                let text_view_errors = text_view_errors.clone();\n\n                dialog.connect_response(move |dialog, response_type| {\n                    if response_type == ResponseType::Ok {\n                        let file_name = get_similar_videos_cache_file(DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION, DEFAULT_CROP_DETECT);\n                        let (mut messages, loaded_items) =\n                            load_cache_from_file_generalized_by_path::<czkawka_core::tools::similar_videos::VideosEntry>(&file_name, true, &Default::default());\n\n                        if let Some(cache_entries) = loaded_items {\n                            let save_messages = save_cache_to_file_generalized(&file_name, &cache_entries, false, 0);\n                            messages.extend_with_another_messages(save_messages);\n                        }\n\n                        messages.messages.push(flg!(\"cache_properly_cleared\"));\n                        text_view_errors.buffer().set_text(messages.create_messages_text(MessageLimit::NoLimit).as_str());\n                    }\n                    dialog.close();\n                });\n            });\n        }\n    }\n}\n\nfn create_clear_cache_dialog(title_str: &str, window_settings: &Window) -> gtk4::Dialog {\n    let dialog = gtk4::Dialog::builder().title(title_str).modal(true).transient_for(window_settings).build();\n    dialog.add_button(&flg!(\"general_ok_button\"), ResponseType::Ok);\n    dialog.add_button(&flg!(\"general_close_button\"), ResponseType::Cancel);\n\n    let label = Label::builder().label(flg!(\"cache_clear_message_label_1\")).build();\n    let label2 = Label::builder().label(flg!(\"cache_clear_message_label_2\")).build();\n    let label3 = Label::builder().label(flg!(\"cache_clear_message_label_3\")).build();\n    let label4 = Label::builder().label(flg!(\"cache_clear_message_label_4\")).build();\n\n    let internal_box = dialog.get_box_child();\n    internal_box.append(&label);\n    internal_box.append(&label2);\n    internal_box.append(&label3);\n    internal_box.append(&label4);\n    dialog\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_show_hide_ui.rs",
    "content": "use gtk4::prelude::*;\n\nuse crate::gui_structs::gui_data::GuiData;\n\npub(crate) fn connect_show_hide_ui(gui_data: &GuiData) {\n    let check_button_settings_show_text_view = gui_data.settings.check_button_settings_show_text_view.clone();\n    let buttons_show_errors = gui_data.bottom_buttons.buttons_show_errors.clone();\n    let scrolled_window_errors = gui_data.scrolled_window_errors.clone();\n\n    buttons_show_errors.connect_clicked(move |_| {\n        if scrolled_window_errors.is_visible() {\n            scrolled_window_errors.set_visible(false);\n            check_button_settings_show_text_view.set_active(false);\n        } else {\n            scrolled_window_errors.set_visible(true);\n            check_button_settings_show_text_view.set_active(true);\n        }\n    });\n\n    let buttons_show_upper_notebook = gui_data.bottom_buttons.buttons_show_upper_notebook.clone();\n    let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();\n\n    buttons_show_upper_notebook.connect_clicked(move |_| {\n        if notebook_upper.is_visible() {\n            notebook_upper.set_visible(false);\n        } else {\n            notebook_upper.set_visible(true);\n        }\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/connect_similar_image_size_change.rs",
    "content": "use czkawka_core::tools::similar_images::SIMILAR_VALUES;\nuse czkawka_core::tools::similar_images::core::get_string_from_similarity;\nuse gtk4::prelude::*;\n\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_combo_box::IMAGES_HASH_SIZE_COMBO_BOX;\n\npub(crate) fn connect_similar_image_size_change(gui_data: &GuiData) {\n    let label_similar_images_minimal_similarity = gui_data.main_notebook.label_similar_images_minimal_similarity.clone();\n    label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[0][5], 8));\n\n    let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone();\n    let label_similar_images_minimal_similarity = gui_data.main_notebook.label_similar_images_minimal_similarity.clone();\n    let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();\n    combo_box_image_hash_size.connect_changed(move |combo_box_image_hash_size| {\n        let hash_size_index = combo_box_image_hash_size.active().expect(\"Failed to get active item\") as usize;\n        let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index];\n\n        let index = match hash_size {\n            8 => 0,\n            16 => 1,\n            32 => 2,\n            64 => 3,\n            _ => panic!(),\n        };\n\n        scale_similarity_similar_images.set_range(0_f64, SIMILAR_VALUES[index][5] as f64);\n        scale_similarity_similar_images.set_fill_level(SIMILAR_VALUES[index][5] as f64);\n        label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[index][5], hash_size as u8));\n    });\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/file_chooser_helpers.rs",
    "content": "use std::path::PathBuf;\n\nuse gtk4::prelude::*;\n\npub fn extract_paths_from_file_chooser(file_chooser: &gtk4::FileChooserNative) -> Vec<PathBuf> {\n    let mut folders: Vec<PathBuf> = Vec::new();\n    let g_files = file_chooser.files();\n    for index in 0..g_files.n_items() {\n        if let Some(file) = g_files.item(index) {\n            let ss = file.clone().downcast::<gtk4::gio::File>().expect(\"Failed to downcast to File\");\n            if let Some(path_buf) = ss.path() {\n                folders.push(path_buf);\n            }\n        }\n    }\n    folders\n}\n"
  },
  {
    "path": "czkawka_gui/src/connect_things/mod.rs",
    "content": "pub mod connect_about_buttons;\npub mod connect_button_compare;\npub mod connect_button_delete;\npub mod connect_button_hardlink;\npub mod connect_button_move;\npub mod connect_button_save;\npub mod connect_button_search;\npub mod connect_button_select;\npub mod connect_button_sort;\npub mod connect_button_stop;\npub mod connect_change_language;\npub mod connect_duplicate_buttons;\npub mod connect_header_buttons;\npub mod connect_krokiet_info_dialog;\npub mod connect_notebook_tabs;\npub mod connect_popovers_select;\npub mod connect_popovers_sort;\npub mod connect_progress_window;\npub mod connect_same_music_mode_changed;\npub mod connect_selection_of_directories;\npub mod connect_settings;\npub mod connect_show_hide_ui;\npub mod connect_similar_image_size_change;\npub mod file_chooser_helpers;\n"
  },
  {
    "path": "czkawka_gui/src/gtk_traits.rs",
    "content": "use std::collections::VecDeque;\nuse std::vec::Vec;\n\nuse gtk4::prelude::{ComboBoxExtManual, *};\nuse gtk4::{Box as GtkBox, ComboBoxText, Dialog, Widget};\n\npub trait ComboBoxTraits {\n    fn set_model_and_first<I, S>(&self, models: I)\n    where\n        I: IntoIterator<Item = S>,\n        S: AsRef<str>;\n}\n\nimpl ComboBoxTraits for ComboBoxText {\n    fn set_model_and_first<I, S>(&self, models: I)\n    where\n        I: IntoIterator<Item = S>,\n        S: AsRef<str>,\n    {\n        for item in models {\n            self.append_text(item.as_ref());\n        }\n        self.set_active(Some(0));\n    }\n}\n\npub trait DialogTraits {\n    fn get_box_child(&self) -> GtkBox;\n}\n\nimpl DialogTraits for Dialog {\n    fn get_box_child(&self) -> GtkBox {\n        self.child().expect(\"Dialog has no child\").downcast::<GtkBox>().expect(\"Dialog child is not Box\")\n    }\n}\n\n#[allow(clippy::allow_attributes)]\n#[allow(dead_code)]\npub trait WidgetTraits {\n    fn get_all_direct_children(&self) -> Vec<Widget>;\n    fn get_all_widgets_of_type<T: IsA<Widget>>(&self, recursive: bool) -> Vec<T>;\n    fn get_widget_of_type<T: IsA<Widget>>(&self, recursive: bool) -> T;\n    fn get_all_boxes(&self) -> Vec<GtkBox>;\n    fn debug_print_widget(&self, print_only_direct_children: bool);\n}\n\nimpl<P: IsA<Widget>> WidgetTraits for P {\n    fn get_all_direct_children(&self) -> Vec<Widget> {\n        let mut vector = Vec::new();\n        if let Some(mut child) = self.first_child() {\n            vector.push(child.clone());\n            loop {\n                child = match child.next_sibling() {\n                    Some(t) => t,\n                    None => break,\n                };\n                vector.push(child.clone());\n            }\n        }\n\n        vector\n    }\n\n    fn get_all_widgets_of_type<T: IsA<Widget>>(&self, recursive: bool) -> Vec<T> {\n        let mut widgets_to_check = VecDeque::from([self.clone().upcast::<Widget>()]);\n        let mut found_widgets = Vec::new();\n        let mut is_root = true;\n\n        while let Some(widget) = widgets_to_check.pop_front() {\n            if (recursive || !is_root)\n                && let Ok(specific_widget) = widget.clone().downcast::<T>()\n            {\n                found_widgets.push(specific_widget);\n            }\n\n            if recursive || is_root {\n                widgets_to_check.extend(widget.get_all_direct_children());\n            }\n\n            is_root = false;\n        }\n        found_widgets\n    }\n\n    fn get_widget_of_type<T: IsA<Widget>>(&self, recursive: bool) -> T {\n        let mut widgets_to_check = VecDeque::from([self.clone().upcast::<Widget>()]);\n        let mut is_root = true;\n\n        while let Some(widget) = widgets_to_check.pop_front() {\n            if (recursive || !is_root)\n                && let Ok(specific_widget) = widget.clone().downcast::<T>()\n            {\n                return specific_widget;\n            }\n\n            if recursive || is_root {\n                widgets_to_check.extend(widget.get_all_direct_children());\n            }\n\n            is_root = false;\n        }\n        panic!(\"Widget doesn't have proper child of specified type\");\n    }\n\n    fn get_all_boxes(&self) -> Vec<GtkBox> {\n        let mut widgets_to_check = VecDeque::from([self.clone().upcast::<Widget>()]);\n        let mut boxes = Vec::new();\n\n        while let Some(widget) = widgets_to_check.pop_front() {\n            if let Ok(bbox) = widget.clone().downcast::<GtkBox>() {\n                boxes.push(bbox);\n            }\n            widgets_to_check.extend(widget.get_all_direct_children());\n        }\n        boxes\n    }\n\n    #[expect(clippy::print_stdout)]\n    fn debug_print_widget(&self, print_only_direct_children: bool) {\n        struct WidgetInfo {\n            depth: usize,\n            widget: Widget,\n        }\n\n        fn collect_widgets(widget: &Widget, depth: usize, print_only_direct_children: bool) -> Vec<WidgetInfo> {\n            let mut result = vec![WidgetInfo { depth, widget: widget.clone() }];\n\n            if !print_only_direct_children || depth == 0 {\n                for child in widget.get_all_direct_children() {\n                    result.extend(collect_widgets(&child, depth + 1, print_only_direct_children));\n                }\n            }\n\n            result\n        }\n\n        let widget_infos = collect_widgets(&self.clone().upcast::<Widget>(), 0, print_only_direct_children);\n\n        println!(\"Widget hierarchy:\");\n\n        for widget_info in widget_infos {\n            let indent = \"  \".repeat(widget_info.depth);\n            println!(\"{}{:?}\", indent, widget_info.widget);\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use gtk4::prelude::BoxExt;\n    use gtk4::{Image, Label, Orientation};\n\n    use super::*;\n\n    #[gtk4::test]\n    fn test_get_all_direct_children() {\n        let obj = gtk4::Box::new(Orientation::Horizontal, 0);\n        let obj2 = gtk4::Box::new(Orientation::Horizontal, 0);\n        let obj3 = gtk4::Image::new();\n        let obj4 = gtk4::Image::new();\n        let obj5 = gtk4::Image::new();\n        obj.append(&obj2);\n        obj.append(&obj3);\n        obj2.append(&obj4);\n        obj2.append(&obj5);\n        assert_eq!(obj.get_all_direct_children().len(), 2);\n    }\n\n    #[gtk4::test]\n    fn test_get_all_boxes() {\n        let obj = gtk4::Box::new(Orientation::Horizontal, 0);\n        let obj2 = gtk4::Box::new(Orientation::Horizontal, 0);\n        let obj3 = gtk4::Image::new();\n        let obj4 = gtk4::Image::new();\n        let obj5 = gtk4::Image::new();\n        obj.append(&obj2);\n        obj.append(&obj3);\n        obj2.append(&obj4);\n        obj2.append(&obj5);\n        assert_eq!(obj.get_all_boxes().len(), 2);\n    }\n\n    #[gtk4::test]\n    fn test_get_all_direct_children_empty() {\n        let obj = gtk4::Box::new(Orientation::Horizontal, 0);\n        assert_eq!(obj.get_all_direct_children().len(), 0);\n    }\n\n    #[gtk4::test]\n    fn test_get_all_boxes_nested() {\n        let root = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box1 = gtk4::Box::new(Orientation::Vertical, 0);\n        let box2 = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box3 = gtk4::Box::new(Orientation::Vertical, 0);\n\n        root.append(&box1);\n        box1.append(&box2);\n        box2.append(&box3);\n\n        assert_eq!(root.get_all_boxes().len(), 4);\n    }\n\n    #[gtk4::test]\n    fn test_get_all_boxes_with_mixed_widgets() {\n        let root = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box1 = gtk4::Box::new(Orientation::Vertical, 0);\n        let label = gtk4::Label::new(Some(\"Test\"));\n        let image = gtk4::Image::new();\n        let box2 = gtk4::Box::new(Orientation::Horizontal, 0);\n\n        root.append(&box1);\n        root.append(&label);\n        root.append(&image);\n        box1.append(&box2);\n\n        assert_eq!(root.get_all_boxes().len(), 3);\n    }\n\n    #[gtk4::test]\n    fn test_combo_box_set_model_and_first() {\n        let combo = gtk4::ComboBoxText::new();\n        combo.set_model_and_first([\"Option 1\", \"Option 2\", \"Option 3\"]);\n\n        assert_eq!(combo.active(), Some(0));\n        assert_eq!(combo.active_text().unwrap(), \"Option 1\");\n    }\n\n    #[gtk4::test]\n    fn test_dialog_get_box_child() {\n        let dialog = gtk4::Dialog::new();\n\n        let result = dialog.get_box_child();\n        assert_eq!(result.spacing(), 0);\n    }\n\n    #[gtk4::test]\n    #[should_panic(expected = \"Widget doesn't have proper child of specified type\")]\n    fn test_get_custom_label_panic() {\n        let container = gtk4::Box::new(Orientation::Horizontal, 0);\n        let image = gtk4::Image::new();\n        container.append(&image);\n\n        container.get_widget_of_type::<Label>(true);\n    }\n\n    #[gtk4::test]\n    #[should_panic(expected = \"Widget doesn't have proper child of specified type\")]\n    fn test_get_custom_image_panic() {\n        let container = gtk4::Box::new(Orientation::Horizontal, 0);\n        let label = gtk4::Label::new(Some(\"Test\"));\n        container.append(&label);\n\n        container.get_widget_of_type::<Image>(true);\n    }\n\n    #[gtk4::test]\n    fn test_get_all_widgets_of_type() {\n        // Test finding labels recursively\n        let root = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box1 = gtk4::Box::new(Orientation::Vertical, 0);\n        let label1 = gtk4::Label::new(Some(\"Label 1\"));\n        let label2 = gtk4::Label::new(Some(\"Label 2\"));\n        let image = gtk4::Image::new();\n        let label3 = gtk4::Label::new(Some(\"Label 3\"));\n\n        root.append(&box1);\n        root.append(&label1);\n        box1.append(&label2);\n        box1.append(&image);\n        box1.append(&label3);\n\n        // Recursive search - finds all labels\n        let labels = root.get_all_widgets_of_type::<Label>(true);\n        assert_eq!(labels.len(), 3);\n        assert_eq!(labels[0].text(), \"Label 1\");\n        assert_eq!(labels[1].text(), \"Label 2\");\n        assert_eq!(labels[2].text(), \"Label 3\");\n\n        // Non-recursive search - finds only direct children (not root itself)\n        let labels_direct = root.get_all_widgets_of_type::<Label>(false);\n        assert_eq!(labels_direct.len(), 1);\n        assert_eq!(labels_direct[0].text(), \"Label 1\");\n\n        // Test finding images recursively\n        let images = root.get_all_widgets_of_type::<Image>(true);\n        assert_eq!(images.len(), 1);\n\n        // Test finding boxes recursively (includes root)\n        let boxes = root.get_all_widgets_of_type::<GtkBox>(true);\n        assert_eq!(boxes.len(), 2); // root + box1\n\n        // Test finding boxes non-recursively (only direct children, not root)\n        let boxes_direct = root.get_all_widgets_of_type::<GtkBox>(false);\n        assert_eq!(boxes_direct.len(), 1); // box1 only (direct child)\n\n        // Test empty result\n        let root2 = gtk4::Box::new(Orientation::Horizontal, 0);\n        root2.append(&gtk4::Image::new());\n        let labels2 = root2.get_all_widgets_of_type::<Label>(true);\n        assert_eq!(labels2.len(), 0);\n    }\n\n    #[gtk4::test]\n    fn test_get_widget_of_type() {\n        // Test finding first label (breadth-first search) - recursive\n        let root = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box1 = gtk4::Box::new(Orientation::Vertical, 0);\n        let label1 = gtk4::Label::new(Some(\"First Label\"));\n        let label2 = gtk4::Label::new(Some(\"Second Label\"));\n\n        root.append(&box1);\n        root.append(&label1);\n        box1.append(&label2);\n\n        let found_label = root.get_widget_of_type::<Label>(true);\n        assert_eq!(found_label.text(), \"First Label\");\n\n        // Test non-recursive - finds only in direct children\n        let found_label_direct = root.get_widget_of_type::<Label>(false);\n        assert_eq!(found_label_direct.text(), \"First Label\");\n\n        // Test finding image recursively\n        let root2 = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box2 = gtk4::Box::new(Orientation::Vertical, 0);\n        let label = gtk4::Label::new(Some(\"Test\"));\n        let image = gtk4::Image::new();\n        image.set_icon_name(Some(\"test-icon\"));\n\n        root2.append(&box2);\n        box2.append(&label);\n        box2.append(&image);\n\n        let found_image = root2.get_widget_of_type::<Image>(true);\n        assert_eq!(found_image.icon_name(), Some(\"test-icon\".into()));\n\n        // Test finding nested widget recursively\n        let root3 = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box3 = gtk4::Box::new(Orientation::Vertical, 0);\n        let box4 = gtk4::Box::new(Orientation::Horizontal, 0);\n        let label3 = gtk4::Label::new(Some(\"Nested Label\"));\n\n        root3.append(&box3);\n        box3.append(&box4);\n        box4.append(&label3);\n\n        let found_label3 = root3.get_widget_of_type::<Label>(true);\n        assert_eq!(found_label3.text(), \"Nested Label\");\n\n        // Test non-recursive on nested - should find box3 (direct child of root3)\n        let found_box = root3.get_widget_of_type::<GtkBox>(false);\n        assert_eq!(found_box.orientation(), Orientation::Vertical);\n    }\n\n    #[gtk4::test]\n    #[should_panic(expected = \"Widget doesn't have proper child of specified type\")]\n    fn test_get_widget_of_type_panic() {\n        let root = gtk4::Box::new(Orientation::Horizontal, 0);\n        let image = gtk4::Image::new();\n        root.append(&image);\n\n        root.get_widget_of_type::<Label>(true);\n    }\n\n    #[gtk4::test]\n    #[should_panic(expected = \"Widget doesn't have proper child of specified type\")]\n    fn test_get_widget_of_type_panic_non_recursive() {\n        let root = gtk4::Box::new(Orientation::Horizontal, 0);\n        let box1 = gtk4::Box::new(Orientation::Vertical, 0);\n        let label = gtk4::Label::new(Some(\"Nested\"));\n\n        root.append(&box1);\n        box1.append(&label);\n\n        root.get_widget_of_type::<Label>(false);\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/common_tree_view.rs",
    "content": "use std::cell::RefCell;\nuse std::rc::Rc;\n\nuse czkawka_core::common::image::{ImgResizeOptions, check_if_can_display_image, get_dynamic_image_from_path};\nuse czkawka_core::common::traits::PrintResults;\nuse czkawka_core::re_exported::FirFilterType;\nuse czkawka_core::tools::bad_extensions::BadExtensions;\nuse czkawka_core::tools::big_file::BigFile;\nuse czkawka_core::tools::broken_files::BrokenFiles;\nuse czkawka_core::tools::duplicate::DuplicateFinder;\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse czkawka_core::tools::empty_folder::EmptyFolder;\nuse czkawka_core::tools::invalid_symlinks::InvalidSymlinks;\nuse czkawka_core::tools::same_music::SameMusic;\nuse czkawka_core::tools::similar_images::SimilarImages;\nuse czkawka_core::tools::similar_videos::SimilarVideos;\nuse czkawka_core::tools::temporary::Temporary;\nuse gdk4::gdk_pixbuf::{InterpType, Pixbuf};\nuse gtk4::prelude::*;\nuse gtk4::{\n    Builder, CellRendererText, CellRendererToggle, CheckButton, EventControllerKey, GestureClick, ListStore, Notebook, Picture, ScrolledWindow, SelectionMode, TextView, TreeModel,\n    TreeSelection, TreeView, TreeViewColumn,\n};\n\nuse crate::connect_things::connect_button_delete::delete_things;\nuse crate::flg;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_functions::{KEY_DELETE, SharedState, add_text_to_text_view, get_full_name_from_path_name};\nuse crate::helpers::enums::{\n    ColumnsBadExtensions, ColumnsBigFiles, ColumnsBrokenFiles, ColumnsDuplicates, ColumnsEmptyFiles, ColumnsEmptyFolders, ColumnsInvalidSymlinks, ColumnsSameMusic,\n    ColumnsSimilarImages, ColumnsSimilarVideos, ColumnsTemporaryFiles,\n};\nuse crate::helpers::image_operations::{get_pixbuf_from_dynamic_image, resize_pixbuf_dimension};\nuse crate::notebook_enums::NotebookMainEnum;\nuse crate::notebook_info::{NOTEBOOKS_INFO, NotebookObject};\nuse crate::opening_selecting_records::{opening_double_click_function, opening_enter_function_ported, opening_middle_mouse_function, select_function_header};\n\n#[derive(Clone)]\npub struct CommonTreeViews {\n    pub subviews: Vec<SubView>,\n    pub notebook_main: Notebook,\n    pub preview_path: Rc<RefCell<String>>,\n}\nimpl CommonTreeViews {\n    pub fn get_subview(&self, item: NotebookMainEnum) -> &SubView {\n        self.subviews.iter().find(|s| s.enum_value == item).expect(\"Cannot find subview\")\n    }\n    pub fn get_current_page(&self) -> NotebookMainEnum {\n        let current_page = self.notebook_main.current_page().expect(\"Cannot get current page from notebook\");\n        NOTEBOOKS_INFO[current_page as usize].notebook_type\n    }\n    pub fn get_current_subview(&self) -> &SubView {\n        let current_page = self.notebook_main.current_page().expect(\"Cannot get current page from notebook\");\n        let enum_value = NOTEBOOKS_INFO[current_page as usize].notebook_type;\n        self.get_subview(enum_value)\n    }\n    pub fn hide_preview(&self) {\n        let current_subview = self.get_current_subview();\n        if let Some(preview_struct) = &current_subview.preview_struct {\n            preview_struct.image_preview.set_visible(false);\n        }\n        *self.preview_path.borrow_mut() = String::new();\n    }\n    // pub fn get_tree_view_from_its_name(&self, name: &str) -> TreeView {\n    //     for subview in &self.subviews {\n    //         if subview.tree_view_name == name {\n    //             return subview.tree_view.clone();\n    //         }\n    //     }\n    //     panic!(\"Cannot find tree view with name {name}\");\n    // }\n    pub fn setup(&self, gui_data: &GuiData) {\n        for subview in &self.subviews {\n            subview.setup(&self.preview_path, gui_data);\n        }\n    }\n}\n\npub trait TreeViewListStoreTrait {\n    fn get_model(&self) -> ListStore;\n}\nimpl TreeViewListStoreTrait for TreeView {\n    fn get_model(&self) -> ListStore {\n        self.model()\n            .expect(\"TreeView has no model\")\n            .downcast_ref::<ListStore>()\n            .expect(\"TreeView model is not ListStore\")\n            .clone()\n    }\n}\npub trait GetTreeViewTrait {\n    fn get_tree_view(&self) -> TreeView;\n}\nimpl GetTreeViewTrait for &EventControllerKey {\n    fn get_tree_view(&self) -> TreeView {\n        self.widget()\n            .expect(\"EventControllerKey has no widget\")\n            .downcast_ref::<TreeView>()\n            .expect(\"EventControllerKey widget is not TreeView\")\n            .clone()\n    }\n}\nimpl GetTreeViewTrait for &GestureClick {\n    fn get_tree_view(&self) -> TreeView {\n        self.widget()\n            .expect(\"GestureClick has no widget\")\n            .downcast_ref::<TreeView>()\n            .expect(\"GestureClick widget is not TreeView\")\n            .clone()\n    }\n}\n\n#[derive(Clone)]\npub struct SubView {\n    pub scrolled_window: ScrolledWindow,\n    pub tree_view: TreeView,\n    pub gesture_click: GestureClick,\n    pub event_controller_key: EventControllerKey,\n    pub nb_object: NotebookObject,\n    pub enum_value: NotebookMainEnum,\n    pub preview_struct: Option<PreviewStruct>,\n    pub shared_model_enum: SharedModelEnum,\n}\n\n#[derive(Clone)]\npub struct PreviewStruct {\n    pub image_preview: Picture,\n    pub settings_show_preview: CheckButton,\n}\n\nimpl SubView {\n    pub fn get_model(&self) -> ListStore {\n        self.tree_view.get_model()\n    }\n    pub fn get_tree_model(&self) -> TreeModel {\n        self.tree_view.model().expect(\"TreeView has no model\")\n    }\n    pub fn get_tree_selection(&self) -> TreeSelection {\n        self.tree_view.selection()\n    }\n    pub fn new(\n        builder: &Builder,\n        scrolled_name: &str,\n        enum_value: NotebookMainEnum,\n        preview_str: Option<&str>,\n        settings_show_preview: Option<CheckButton>,\n        shared_model_enum: SharedModelEnum,\n    ) -> Self {\n        let tree_view: TreeView = TreeView::new();\n        let event_controller_key: EventControllerKey = EventControllerKey::new();\n        tree_view.add_controller(event_controller_key.clone());\n        let gesture_click: GestureClick = GestureClick::new();\n        tree_view.add_controller(gesture_click.clone());\n\n        let image_preview = preview_str.map(|name| builder.object(name).unwrap_or_else(|| panic!(\"Cannot find preview image {name}\")));\n\n        let nb_object = NOTEBOOKS_INFO[enum_value as usize].clone();\n        assert_eq!(nb_object.notebook_type, enum_value);\n\n        let preview_struct = if let (Some(image_preview), Some(settings_show_preview)) = (image_preview, settings_show_preview) {\n            Some(PreviewStruct {\n                image_preview,\n                settings_show_preview,\n            })\n        } else {\n            None\n        };\n\n        Self {\n            scrolled_window: builder.object(scrolled_name).unwrap_or_else(|| panic!(\"Cannot find scrolled window {scrolled_name}\")),\n            tree_view,\n            gesture_click,\n            event_controller_key,\n            nb_object,\n            enum_value,\n            preview_struct,\n            shared_model_enum,\n        }\n    }\n\n    fn _setup_tree_view(&self) {\n        self.tree_view.set_model(Some(&ListStore::new(self.nb_object.columns_types)));\n        self.tree_view.selection().set_mode(SelectionMode::Multiple);\n\n        if let Some(column_header) = self.nb_object.column_header {\n            self.tree_view.selection().set_select_function(select_function_header(column_header));\n        }\n\n        self.tree_view.set_vexpand(true);\n\n        self._setup_tree_view_config();\n\n        self.tree_view.set_widget_name(self.nb_object.tree_view_name);\n        self.scrolled_window.set_child(Some(&self.tree_view));\n        self.scrolled_window.set_visible(true);\n    }\n    fn _setup_gesture_click(&self) {\n        self.gesture_click.set_button(0);\n        self.gesture_click.connect_pressed(opening_double_click_function);\n        self.gesture_click.connect_released(opening_middle_mouse_function); // TODO GTK 4 - https://github.com/gtk-rs/gtk4-rs/issues/1043\n    }\n\n    fn _setup_evk(&self, gui_data: &GuiData) {\n        let gui_data_clone = gui_data.clone();\n        self.event_controller_key.connect_key_pressed(opening_enter_function_ported);\n\n        self.event_controller_key\n            .connect_key_released(move |_event_controller_key, _key_value, key_code, _modifier_type| {\n                if key_code == KEY_DELETE {\n                    glib::MainContext::default().spawn_local(delete_things(gui_data_clone.clone()));\n                }\n            });\n    }\n\n    fn _connect_show_mouse_preview(&self, gui_data: &GuiData, preview_path: &Rc<RefCell<String>>) {\n        // TODO GTK 4, currently not works, connect_pressed shows previous thing - https://gitlab.gnome.org/GNOME/gtk/-/issues/4939\n        // Use connect_released when it will be fixed, currently using connect_row_activated workaround\n        let use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone();\n        let text_view_errors = gui_data.text_view_errors.clone();\n        if let Some(preview_struct) = self.preview_struct.clone() {\n            self.tree_view.set_property(\"activate-on-single-click\", true);\n            let preview_path = preview_path.clone();\n            let nb_object = self.nb_object.clone();\n\n            self.tree_view.clone().connect_row_activated(move |tree_view, _b, _c| {\n                show_preview(\n                    tree_view,\n                    &text_view_errors,\n                    &preview_struct.settings_show_preview,\n                    &preview_struct.image_preview,\n                    &preview_path,\n                    nb_object.column_path,\n                    nb_object.column_name,\n                    use_rust_preview.is_active(),\n                );\n            });\n        }\n    }\n    fn _connect_show_keyboard_preview(&self, gui_data: &GuiData, preview_path: &Rc<RefCell<String>>, preview_struct: &PreviewStruct) {\n        let use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone();\n        let text_view_errors = gui_data.text_view_errors.clone();\n        let check_button_settings_show_preview = preview_struct.settings_show_preview.clone();\n        let image_preview = preview_struct.image_preview.clone();\n        let gui_data_clone = gui_data.clone();\n\n        self.event_controller_key.connect_key_pressed(opening_enter_function_ported);\n        let preview_path = preview_path.clone();\n        let nb_object = self.nb_object.clone();\n\n        self.event_controller_key\n            .clone()\n            .connect_key_released(move |event_controller_key, _key_value, key_code, _modifier_type| {\n                if key_code == KEY_DELETE {\n                    glib::MainContext::default().spawn_local(delete_things(gui_data_clone.clone()));\n                }\n                show_preview(\n                    &event_controller_key.get_tree_view(),\n                    &text_view_errors,\n                    &check_button_settings_show_preview,\n                    &image_preview,\n                    &preview_path,\n                    nb_object.column_path,\n                    nb_object.column_name,\n                    use_rust_preview.is_active(),\n                );\n            });\n    }\n\n    fn setup(&self, preview_path: &Rc<RefCell<String>>, gui_data: &GuiData) {\n        if let Some(preview_struct) = &self.preview_struct {\n            preview_struct.image_preview.set_visible(false);\n        }\n        self._setup_tree_view();\n        self._setup_gesture_click();\n        self._connect_show_mouse_preview(gui_data, preview_path);\n\n        // Items with image preview, are differently handled\n        if let Some(preview_struct) = &self.preview_struct {\n            self._connect_show_keyboard_preview(gui_data, preview_path, preview_struct);\n        } else {\n            self._setup_evk(gui_data);\n        }\n    }\n    fn _setup_tree_view_config(&self) {\n        let tree_view = &self.tree_view;\n        let model = self.get_model();\n        match self.enum_value {\n            NotebookMainEnum::Duplicate => {\n                let columns_colors = (ColumnsDuplicates::Color as i32, ColumnsDuplicates::TextColor as i32);\n                let activatable_colors = (ColumnsDuplicates::ActivatableSelectButton as i32, ColumnsDuplicates::Color as i32);\n                create_default_selection_button_column(tree_view, ColumnsDuplicates::SelectionButton as i32, model, Some(activatable_colors));\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsDuplicates::Size as i32, ColumnSort::None),\n                        (ColumnsDuplicates::Name as i32, ColumnSort::None),\n                        (ColumnsDuplicates::Path as i32, ColumnSort::None),\n                        (ColumnsDuplicates::Modification as i32, ColumnSort::None),\n                    ],\n                    Some(columns_colors),\n                );\n                assert_eq!(tree_view.columns().len(), 5);\n            }\n            NotebookMainEnum::EmptyDirectories => {\n                create_default_selection_button_column(tree_view, ColumnsEmptyFolders::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsEmptyFolders::Name as i32, ColumnSort::Default),\n                        (ColumnsEmptyFolders::Path as i32, ColumnSort::Default),\n                        (ColumnsEmptyFolders::Modification as i32, ColumnSort::Custom(ColumnsEmptyFolders::ModificationAsSecs as i32)),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 4);\n            }\n            NotebookMainEnum::BigFiles => {\n                create_default_selection_button_column(tree_view, ColumnsBigFiles::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsBigFiles::Size as i32, ColumnSort::Custom(ColumnsBigFiles::SizeAsBytes as i32)),\n                        (ColumnsBigFiles::Name as i32, ColumnSort::Default),\n                        (ColumnsBigFiles::Path as i32, ColumnSort::Default),\n                        (ColumnsBigFiles::Modification as i32, ColumnSort::Custom(ColumnsBigFiles::ModificationAsSecs as i32)),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 5);\n            }\n            NotebookMainEnum::EmptyFiles => {\n                create_default_selection_button_column(tree_view, ColumnsEmptyFiles::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsEmptyFiles::Name as i32, ColumnSort::Default),\n                        (ColumnsEmptyFiles::Path as i32, ColumnSort::Default),\n                        (ColumnsEmptyFiles::Modification as i32, ColumnSort::Custom(ColumnsEmptyFiles::ModificationAsSecs as i32)),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 4);\n            }\n            NotebookMainEnum::Temporary => {\n                create_default_selection_button_column(tree_view, ColumnsTemporaryFiles::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsTemporaryFiles::Name as i32, ColumnSort::Default),\n                        (ColumnsTemporaryFiles::Path as i32, ColumnSort::Default),\n                        (\n                            ColumnsTemporaryFiles::Modification as i32,\n                            ColumnSort::Custom(ColumnsTemporaryFiles::ModificationAsSecs as i32),\n                        ),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 4);\n            }\n            NotebookMainEnum::SimilarImages => {\n                let columns_colors = (ColumnsSimilarImages::Color as i32, ColumnsSimilarImages::TextColor as i32);\n                let activatable_colors = (ColumnsSimilarImages::ActivatableSelectButton as i32, ColumnsSimilarImages::Color as i32);\n                create_default_selection_button_column(tree_view, ColumnsSimilarImages::SelectionButton as i32, model, Some(activatable_colors));\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsSimilarImages::Similarity as i32, ColumnSort::None),\n                        (ColumnsSimilarImages::Size as i32, ColumnSort::None),\n                        (ColumnsSimilarImages::Dimensions as i32, ColumnSort::None),\n                        (ColumnsSimilarImages::Name as i32, ColumnSort::None),\n                        (ColumnsSimilarImages::Path as i32, ColumnSort::None),\n                        (ColumnsSimilarImages::Modification as i32, ColumnSort::None),\n                    ],\n                    Some(columns_colors),\n                );\n                assert_eq!(tree_view.columns().len(), 7);\n            }\n            NotebookMainEnum::SimilarVideos => {\n                let columns_colors = (ColumnsSimilarVideos::Color as i32, ColumnsSimilarVideos::TextColor as i32);\n                let activatable_colors = (ColumnsSimilarVideos::ActivatableSelectButton as i32, ColumnsSimilarVideos::Color as i32);\n                create_default_selection_button_column(tree_view, ColumnsSimilarVideos::SelectionButton as i32, model, Some(activatable_colors));\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsSimilarVideos::Size as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Name as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Path as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Modification as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Fps as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Codec as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Bitrate as i32, ColumnSort::None),\n                        (ColumnsSimilarVideos::Dimensions as i32, ColumnSort::None),\n                    ],\n                    Some(columns_colors),\n                );\n                assert_eq!(tree_view.columns().len(), 9);\n            }\n            NotebookMainEnum::SameMusic => {\n                let columns_colors = (ColumnsSameMusic::Color as i32, ColumnsSameMusic::TextColor as i32);\n                let activatable_colors = (ColumnsSameMusic::ActivatableSelectButton as i32, ColumnsSameMusic::Color as i32);\n                create_default_selection_button_column(tree_view, ColumnsSameMusic::SelectionButton as i32, model, Some(activatable_colors));\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsSameMusic::Size as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Name as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Title as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Artist as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Year as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Bitrate as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Length as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Genre as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Path as i32, ColumnSort::None),\n                        (ColumnsSameMusic::Modification as i32, ColumnSort::None),\n                    ],\n                    Some(columns_colors),\n                );\n                assert_eq!(tree_view.columns().len(), 11);\n            }\n            NotebookMainEnum::Symlinks => {\n                create_default_selection_button_column(tree_view, ColumnsInvalidSymlinks::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsInvalidSymlinks::Name as i32, ColumnSort::Default),\n                        (ColumnsInvalidSymlinks::Path as i32, ColumnSort::Default),\n                        (ColumnsInvalidSymlinks::DestinationPath as i32, ColumnSort::Default),\n                        (ColumnsInvalidSymlinks::TypeOfError as i32, ColumnSort::Default),\n                        (\n                            ColumnsInvalidSymlinks::Modification as i32,\n                            ColumnSort::Custom(ColumnsInvalidSymlinks::ModificationAsSecs as i32),\n                        ),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 6);\n            }\n            NotebookMainEnum::BrokenFiles => {\n                create_default_selection_button_column(tree_view, ColumnsBrokenFiles::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsBrokenFiles::Name as i32, ColumnSort::Default),\n                        (ColumnsBrokenFiles::Path as i32, ColumnSort::Default),\n                        (ColumnsBrokenFiles::ErrorType as i32, ColumnSort::Default),\n                        (ColumnsBrokenFiles::Modification as i32, ColumnSort::Custom(ColumnsBrokenFiles::ModificationAsSecs as i32)),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 5);\n            }\n            NotebookMainEnum::BadExtensions => {\n                create_default_selection_button_column(tree_view, ColumnsBadExtensions::SelectionButton as i32, model, None);\n                create_default_columns(\n                    tree_view,\n                    &[\n                        (ColumnsBadExtensions::Name as i32, ColumnSort::Default),\n                        (ColumnsBadExtensions::Path as i32, ColumnSort::Default),\n                        (ColumnsBadExtensions::CurrentExtension as i32, ColumnSort::Default),\n                        (ColumnsBadExtensions::ValidExtensions as i32, ColumnSort::Default),\n                    ],\n                    None,\n                );\n                assert_eq!(tree_view.columns().len(), 5);\n            }\n        }\n    }\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnSort {\n    None,\n    Default,\n    Custom(i32),\n}\n\npub(crate) fn create_default_selection_button_column(\n    tree_view: &TreeView,\n    column_id: i32,\n    model: ListStore,\n    activatable_color_columns: Option<(i32, i32)>,\n) -> (CellRendererToggle, TreeViewColumn) {\n    let renderer = CellRendererToggle::new();\n    renderer.connect_toggled(move |_r, path| {\n        let iter = model.iter(&path).expect(\"Failed to get iter from tree_path\");\n        let mut fixed = model.get::<bool>(&iter, column_id);\n        fixed = !fixed;\n        model.set_value(&iter, column_id as u32, &fixed.to_value());\n    });\n    let column = TreeViewColumn::new();\n    column.pack_start(&renderer, true);\n    column.set_resizable(false);\n    column.set_fixed_width(30);\n    column.add_attribute(&renderer, \"active\", column_id);\n    if let Some(activatable_color_columns) = activatable_color_columns {\n        column.add_attribute(&renderer, \"activatable\", activatable_color_columns.0);\n        column.add_attribute(&renderer, \"cell-background\", activatable_color_columns.1);\n    }\n    tree_view.append_column(&column);\n    (renderer, column)\n}\n\npub(crate) fn create_default_columns(tree_view: &TreeView, columns: &[(i32, ColumnSort)], colors_columns_id: Option<(i32, i32)>) {\n    for (col_id, sort_method) in columns {\n        let renderer = CellRendererText::new();\n        let column: TreeViewColumn = TreeViewColumn::new();\n        column.pack_start(&renderer, true);\n        column.set_resizable(true);\n        column.set_min_width(50);\n        column.add_attribute(&renderer, \"text\", *col_id);\n        match sort_method {\n            ColumnSort::None => {}\n            ColumnSort::Default => column.set_sort_column_id(*col_id),\n            ColumnSort::Custom(val) => column.set_sort_column_id(*val),\n        }\n        if let Some(colors_columns_id) = colors_columns_id {\n            column.add_attribute(&renderer, \"background\", colors_columns_id.0);\n            column.add_attribute(&renderer, \"foreground\", colors_columns_id.1);\n        }\n        tree_view.append_column(&column);\n    }\n}\n\npub(crate) fn show_preview(\n    tree_view: &TreeView,\n    text_view_errors: &TextView,\n    check_button_settings_show_preview: &CheckButton,\n    image_preview: &Picture,\n    preview_path: &Rc<RefCell<String>>,\n    column_path: i32,\n    column_name: i32,\n    use_rust_preview: bool,\n) {\n    let (selected_rows, tree_model) = tree_view.selection().selected_rows();\n\n    let mut created_image = false;\n\n    // Only show preview when selected is only one item, because there is no method to recognize current clicked item in multiselection\n    if selected_rows.len() == 1 && check_button_settings_show_preview.is_active() {\n        let tree_path = selected_rows[0].clone();\n        // TODO labels on {} are in testing stage, so we just ignore for now this warning until found better idea how to fix this\n        #[expect(clippy::never_loop)]\n        'dir: loop {\n            let path = tree_model.get::<String>(&tree_model.iter(&tree_path).expect(\"Invalid tree_path\"), column_path);\n            let name = tree_model.get::<String>(&tree_model.iter(&tree_path).expect(\"Invalid tree_path\"), column_name);\n\n            let file_name = get_full_name_from_path_name(&path, &name);\n\n            if !check_if_can_display_image(&file_name) {\n                break 'dir;\n            }\n\n            if file_name == preview_path.borrow().as_str() {\n                return; // Preview is already created, no need to recreate it\n            }\n\n            let mut pixbuf = if use_rust_preview {\n                let image = match get_dynamic_image_from_path(\n                    &file_name,\n                    Some(ImgResizeOptions {\n                        max_width: 1024,\n                        max_height: 1024,\n                        filter: FirFilterType::Bilinear,\n                    }),\n                ) {\n                    Ok(t) => t.image,\n                    Err(e) => {\n                        add_text_to_text_view(text_view_errors, &flg!(\"preview_image_opening_failure\", name = file_name, reason = e));\n                        break 'dir;\n                    }\n                };\n\n                match get_pixbuf_from_dynamic_image(image) {\n                    Ok(t) => t,\n                    Err(e) => {\n                        add_text_to_text_view(text_view_errors, &flg!(\"preview_image_opening_failure\", name = file_name, reason = e));\n                        break 'dir;\n                    }\n                }\n            } else {\n                match Pixbuf::from_file(&file_name) {\n                    Ok(pixbuf) => pixbuf,\n                    Err(e) => {\n                        add_text_to_text_view(text_view_errors, &flg!(\"preview_image_opening_failure\", name = file_name, reason = e.to_string()));\n                        break 'dir;\n                    }\n                }\n            };\n            pixbuf = match resize_pixbuf_dimension(&pixbuf, (800, 800), InterpType::Bilinear) {\n                None => {\n                    add_text_to_text_view(text_view_errors, &flg!(\"preview_image_resize_failure\", name = file_name));\n                    break 'dir;\n                }\n                Some(pixbuf) => pixbuf,\n            };\n\n            image_preview.set_pixbuf(Some(&pixbuf));\n            {\n                let mut preview_path = preview_path.borrow_mut();\n                *preview_path = file_name;\n            }\n\n            created_image = true;\n\n            break 'dir;\n        }\n    }\n    if created_image {\n        image_preview.set_visible(true);\n    } else {\n        image_preview.set_visible(false);\n        {\n            let mut preview_path = preview_path.borrow_mut();\n            *preview_path = String::new();\n        }\n    }\n}\n#[derive(Clone)]\npub enum SharedModelEnum {\n    Duplicates(SharedState<DuplicateFinder>),\n    EmptyFolder(SharedState<EmptyFolder>),\n    EmptyFiles(SharedState<EmptyFiles>),\n    Temporary(SharedState<Temporary>),\n    BigFile(SharedState<BigFile>),\n    SimilarImages(SharedState<SimilarImages>),\n    SimilarVideos(SharedState<SimilarVideos>),\n    SameMusic(SharedState<SameMusic>),\n    Symlinks(SharedState<InvalidSymlinks>),\n    BrokenFiles(SharedState<BrokenFiles>),\n    BadExtensions(SharedState<BadExtensions>),\n}\n\nimpl SharedModelEnum {\n    pub(crate) fn save_all_in_one(&self, path: &str) -> Result<(), String> {\n        match self {\n            Self::Duplicates(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_duplicates\")),\n            Self::EmptyFolder(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_empty_directories\")),\n            Self::EmptyFiles(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_empty_files\")),\n            Self::Temporary(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_temporary_files\")),\n            Self::BigFile(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_big_files\")),\n            Self::SimilarImages(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_similar_images\")),\n            Self::SimilarVideos(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_similar_videos\")),\n            Self::SameMusic(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_same_music\")),\n            Self::Symlinks(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_invalid_symlinks\")),\n            Self::BrokenFiles(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_broken_files\")),\n            Self::BadExtensions(state) => state.borrow().as_ref().map(|x| x.save_all_in_one(path, \"results_bad_extensions\")),\n        }\n        .transpose()\n        .map_err(|e| e.to_string())?;\n\n        Ok(())\n    }\n    pub(crate) fn replace(&self, new_item: Self) {\n        match (self, new_item) {\n            (Self::Duplicates(old), Self::Duplicates(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::EmptyFolder(old), Self::EmptyFolder(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::EmptyFiles(old), Self::EmptyFiles(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::Temporary(old), Self::Temporary(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::BigFile(old), Self::BigFile(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::SimilarImages(old), Self::SimilarImages(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::SimilarVideos(old), Self::SimilarVideos(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::SameMusic(old), Self::SameMusic(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::Symlinks(old), Self::Symlinks(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::BrokenFiles(old), Self::BrokenFiles(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            (Self::BadExtensions(old), Self::BadExtensions(new)) => {\n                old.borrow_mut().replace(new.take().expect(\"TEST\"));\n            }\n            _ => panic!(\"Mismatched SharedModelEnum variants\"),\n        }\n    }\n}\nimpl From<DuplicateFinder> for SharedModelEnum {\n    fn from(value: DuplicateFinder) -> Self {\n        Self::Duplicates(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<EmptyFolder> for SharedModelEnum {\n    fn from(value: EmptyFolder) -> Self {\n        Self::EmptyFolder(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<EmptyFiles> for SharedModelEnum {\n    fn from(value: EmptyFiles) -> Self {\n        Self::EmptyFiles(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<Temporary> for SharedModelEnum {\n    fn from(value: Temporary) -> Self {\n        Self::Temporary(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<BigFile> for SharedModelEnum {\n    fn from(value: BigFile) -> Self {\n        Self::BigFile(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<SimilarImages> for SharedModelEnum {\n    fn from(value: SimilarImages) -> Self {\n        Self::SimilarImages(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<SimilarVideos> for SharedModelEnum {\n    fn from(value: SimilarVideos) -> Self {\n        Self::SimilarVideos(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<SameMusic> for SharedModelEnum {\n    fn from(value: SameMusic) -> Self {\n        Self::SameMusic(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<InvalidSymlinks> for SharedModelEnum {\n    fn from(value: InvalidSymlinks) -> Self {\n        Self::Symlinks(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<BrokenFiles> for SharedModelEnum {\n    fn from(value: BrokenFiles) -> Self {\n        Self::BrokenFiles(Rc::new(RefCell::new(Some(value))))\n    }\n}\nimpl From<BadExtensions> for SharedModelEnum {\n    fn from(value: BadExtensions) -> Self {\n        Self::BadExtensions(Rc::new(RefCell::new(Some(value))))\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/common_upper_tree_view.rs",
    "content": "use glib::Type;\nuse gtk4::prelude::*;\nuse gtk4::{Builder, EventControllerKey, GestureClick, ScrolledWindow, SelectionMode, TreeView};\n\nuse crate::gui_structs::common_tree_view::{ColumnSort, TreeViewListStoreTrait, create_default_columns, create_default_selection_button_column};\nuse crate::help_functions::KEY_DELETE;\nuse crate::helpers::enums::{ColumnsExcludedDirectory, ColumnsIncludedDirectory};\nuse crate::notebook_enums::NotebookUpperEnum;\nuse crate::opening_selecting_records::{opening_double_click_function_directories, opening_enter_function_ported_upper_directories};\n\n#[derive(Clone, Copy, PartialEq, Eq)]\npub enum UpperTreeViewEnum {\n    IncludedDirectories,\n    ExcludedDirectories,\n}\n\n#[derive(Clone)]\npub struct CommonUpperTreeViews {\n    pub subviews: Vec<UpperSubView>,\n}\n\n#[derive(Clone)]\npub struct UpperSubView {\n    pub scrolled_window: ScrolledWindow,\n    pub tree_view: TreeView,\n    pub gesture_click: GestureClick,\n    pub event_controller_key: EventControllerKey,\n    #[expect(dead_code)]\n    pub enum_value: NotebookUpperEnum,\n    pub upper_tree_view_enum: UpperTreeViewEnum,\n    pub tree_view_name: &'static str,\n}\n\nimpl CommonUpperTreeViews {\n    pub fn get_subview(&self, item: UpperTreeViewEnum) -> &UpperSubView {\n        self.subviews.iter().find(|s| s.upper_tree_view_enum == item).expect(\"Cannot find subview\")\n    }\n    pub fn get_tree_view(&self, item: UpperTreeViewEnum) -> &TreeView {\n        &self.get_subview(item).tree_view\n    }\n    // pub fn get_current_page(&self) -> Option<NotebookMainEnum {\n    //     let current_page = self.notebook_main.current_page().expect(\"Cannot get current page from notebook\");\n    //     NOTEBOOKS_INFO[current_page as usize].notebook_type\n    // }\n    pub fn setup(&self) {\n        for subview in &self.subviews {\n            subview.setup();\n        }\n    }\n}\n\nimpl UpperSubView {\n    // pub fn get_model(&self) -> ListStore {\n    //     self.tree_view.get_model()\n    // }\n    // pub fn get_tree_model(&self) -> TreeModel {\n    //     self.tree_view.model().expect(\"TreeView has no model\")\n    // }\n    // pub fn get_tree_selection(&self) -> TreeSelection {\n    //     self.tree_view.selection()\n    // }\n    pub fn new(builder: &Builder, scrolled_name: &str, enum_value: NotebookUpperEnum, upper_tree_view_enum: UpperTreeViewEnum, tree_view_name: &'static str) -> Self {\n        let tree_view: TreeView = TreeView::new();\n        let event_controller_key: EventControllerKey = EventControllerKey::new();\n        tree_view.add_controller(event_controller_key.clone());\n        let gesture_click: GestureClick = GestureClick::new();\n        tree_view.add_controller(gesture_click.clone());\n\n        Self {\n            scrolled_window: builder.object(scrolled_name).unwrap_or_else(|| panic!(\"Cannot find scrolled window {scrolled_name}\")),\n            tree_view,\n            gesture_click,\n            event_controller_key,\n            enum_value,\n            tree_view_name,\n            upper_tree_view_enum,\n        }\n    }\n\n    fn _setup_tree_view(&self) {\n        self._setup_tree_view_config();\n        self.tree_view.selection().set_mode(SelectionMode::Multiple);\n\n        self.tree_view.set_vexpand(true);\n\n        self.tree_view.set_widget_name(self.tree_view_name);\n        self.scrolled_window.set_child(Some(&self.tree_view));\n        self.scrolled_window.set_visible(true);\n    }\n    fn _setup_gesture_click(&self) {\n        self.gesture_click.connect_pressed(opening_double_click_function_directories);\n    }\n\n    fn _setup_evk(&self) {\n        let tree_view = self.tree_view.clone();\n        self.event_controller_key.connect_key_pressed(opening_enter_function_ported_upper_directories);\n        self.event_controller_key\n            .connect_key_released(move |_event_controller_key, _key_value, key_code, _modifier_type| {\n                if key_code == KEY_DELETE {\n                    let list_store = tree_view.get_model();\n                    let selection = tree_view.selection();\n\n                    let (vec_tree_path, _tree_model) = selection.selected_rows();\n\n                    for tree_path in vec_tree_path.iter().rev() {\n                        list_store.remove(&list_store.iter(tree_path).expect(\"Using invalid tree_path\"));\n                    }\n                }\n            });\n    }\n\n    fn setup(&self) {\n        self._setup_tree_view();\n        self._setup_gesture_click();\n        self._setup_evk();\n    }\n    fn _setup_tree_view_config(&self) {\n        let tree_view = &self.tree_view;\n        match self.upper_tree_view_enum {\n            UpperTreeViewEnum::IncludedDirectories => {\n                let col_types: [Type; 2] = [\n                    Type::STRING, // Path\n                    Type::BOOL,   // ReferenceButton\n                ];\n                let model: gtk4::ListStore = gtk4::ListStore::new(&col_types);\n                tree_view.set_model(Some(&model));\n\n                create_default_columns(tree_view, &[(ColumnsIncludedDirectory::Path as i32, ColumnSort::Default)], None);\n                create_default_selection_button_column(tree_view, ColumnsIncludedDirectory::ReferenceButton as i32, model, None);\n            }\n            UpperTreeViewEnum::ExcludedDirectories => {\n                let col_types: [Type; 1] = [Type::STRING];\n                let list_store: gtk4::ListStore = gtk4::ListStore::new(&col_types);\n                tree_view.set_model(Some(&list_store));\n\n                tree_view.set_headers_visible(false);\n                create_default_columns(tree_view, &[(ColumnsExcludedDirectory::Path as i32, ColumnSort::Default)], None);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_about.rs",
    "content": "use gdk4::gdk_pixbuf::Pixbuf;\nuse gtk4::prelude::*;\nuse gtk4::{Builder, Button, Orientation, Picture, Window};\n\nuse crate::flg;\nuse crate::gtk_traits::WidgetTraits;\n\n#[derive(Clone)]\npub struct GuiAbout {\n    pub about_dialog: gtk4::AboutDialog,\n\n    pub button_repository: Button,\n    pub button_donation: Button,\n    pub button_krokiet: Button,\n    pub button_instruction: Button,\n    pub button_translation: Button,\n}\n\nimpl GuiAbout {\n    pub(crate) fn create_from_builder(window_main: &Window, logo: &Pixbuf) -> Self {\n        let glade_src = include_str!(\"../../ui/about_dialog.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let about_dialog: gtk4::AboutDialog = builder.object(\"about_dialog\").expect(\"Cambalache\");\n        about_dialog.set_modal(true);\n        about_dialog.set_transient_for(Some(window_main));\n\n        about_dialog.set_logo(Picture::for_pixbuf(logo).paintable().as_ref());\n\n        // Taken from command - \"git shortlog -s -n -e > a.txt\" - remember to remove duplicates\n        // First clean it with regex \" \\<[^\\n]+\" and next with \" +[0-9]+\\t\" and at end replace \"([^\\n]+)\" with \"\"$1\",\" (or \"\"\\0\",\")\n        // This should be updated only before releasing new version\n        about_dialog.set_authors(&[\n            \"Rafał Mikrut\",\n            \"Alexis Lefebvre\",\n            \"Thomas Andreas Jung\",\n            \"Peter Blackson\",\n            \"TheEvilSkeleton\",\n            \"Ben Bodenmiller\",\n            \"ChihWei Wang\",\n            \"Dan Dascalescu\",\n            \"Dominik Piątkowski\",\n            \"Igor\",\n            \"Jocelyn Le Sage\",\n            \"Kerfuffle\",\n            \"Shriraj Hegde\",\n            \"krzysdz\",\n            \"0x4A6F\",\n            \"0xflotus\",\n            \"Aarni Koskela\",\n            \"Adam Boguszewski\",\n            \"Alex\",\n            \"Andreas Gerstmayr\",\n            \"AshesOfEther\",\n            \"Caduser2020\",\n            \"CalunVier\",\n            \"Danny Kirkham\",\n            \"Dariusz Niedoba\",\n            \"Douman\",\n            \"Elazar Fine\",\n            \"Farmadupe\",\n            \"Fr_Dae\",\n            \"Gitoffthelawn\",\n            \"Integral\",\n            \"Ivan Habernal\",\n            \"Jan Jurec\",\n            \"Joey Babcock\",\n            \"Jona\",\n            \"Jonathan Hult\",\n            \"Kian-Meng Ang\",\n            \"Liru Wilkowski\",\n            \"Meir Klemfner\",\n            \"Mek101\",\n            \"Michael Grigoryan\",\n            \"Mitchel Stewart\",\n            \"Nick Gallimore\",\n            \"Nikita Karamov\",\n            \"OMEGA_RAZER\",\n            \"Renner0E\",\n            \"Rodrigo Torres\",\n            \"Samuel\",\n            \"Sbgodin\",\n            \"Spirit\",\n            \"Stefan Seering\",\n            \"Syfaro\",\n            \"Sébastien\",\n            \"Tom Paine\",\n            \"Tom Praschan\",\n            \"Torsten Homberger\",\n            \"Yuri Slobodyanyuk\",\n            \"alexdraconian\",\n            \"bakeromso\",\n            \"bellrise\",\n            \"codingnewcode\",\n            \"cyqsimon\",\n            \"endolith\",\n            \"freeducks-debug\",\n            \"jann\",\n            \"kamilek96\",\n            \"kuskov\",\n            \"leapwill\",\n            \"rugk\",\n            \"santiago fn\",\n            \"tecome\",\n            \"tenninjas\",\n            \"undefined-landmark\",\n        ]);\n\n        let custom_box = about_dialog.get_all_boxes()[2].clone(); // TODO may not be stable enough between GTK versions\n        let new_box = gtk4::Box::new(Orientation::Horizontal, 5);\n\n        let button_repository = Button::builder().label(\"Repository\").build();\n        let button_donation = Button::builder().label(\"Donation\").build();\n        let button_krokiet = Button::builder().label(\"Krokiet\").build();\n        let button_instruction = Button::builder().label(\"Instruction\").build();\n        let button_translation = Button::builder().label(\"Translation\").build();\n\n        new_box.append(&button_repository);\n        new_box.append(&button_donation);\n        new_box.append(&button_krokiet);\n        new_box.append(&button_instruction);\n        new_box.append(&button_translation);\n\n        custom_box.append(&new_box);\n\n        Self {\n            about_dialog,\n            button_repository,\n            button_donation,\n            button_krokiet,\n            button_instruction,\n            button_translation,\n        }\n    }\n\n    pub(crate) fn update_language(&self) {\n        let mut comment_text: String = \"2020 - 2026  Rafał Mikrut(qarmin)\\n\\n\".to_string();\n        comment_text += &flg!(\"about_window_motto\");\n        comment_text += \"\\n\\n\";\n        comment_text += &flg!(\"krokiet_new_app\");\n        self.about_dialog.set_comments(Some(&comment_text));\n\n        self.button_repository.set_tooltip_text(Some(&flg!(\"about_repository_button_tooltip\")));\n        self.button_donation.set_tooltip_text(Some(&flg!(\"about_donation_button_tooltip\")));\n        self.button_instruction.set_tooltip_text(Some(&flg!(\"about_instruction_button_tooltip\")));\n        self.button_translation.set_tooltip_text(Some(&flg!(\"about_translation_button_tooltip\")));\n\n        self.button_repository.set_label(&flg!(\"about_repository_button\"));\n        self.button_donation.set_label(&flg!(\"about_donation_button\"));\n        self.button_krokiet.set_label(\"Krokiet\");\n        self.button_instruction.set_label(&flg!(\"about_instruction_button\"));\n        self.button_translation.set_label(&flg!(\"about_translation_button\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_bottom_buttons.rs",
    "content": "use gtk4::prelude::*;\nuse gtk4::{GestureClick, Label, Widget};\n\nuse crate::gtk_traits::WidgetTraits;\nuse crate::gui_structs::gui_data::CZK_ICON_SORT;\nuse crate::helpers::enums::BottomButtonsEnum;\nuse crate::helpers::image_operations::set_icon_of_button;\nuse crate::{\n    CZK_ICON_COMPARE, CZK_ICON_HARDLINK, CZK_ICON_HIDE_DOWN, CZK_ICON_HIDE_UP, CZK_ICON_MOVE, CZK_ICON_SAVE, CZK_ICON_SEARCH, CZK_ICON_SELECT, CZK_ICON_SYMLINK, CZK_ICON_TRASH,\n    flg,\n};\n\n#[derive(Clone)]\npub struct GuiBottomButtons {\n    pub buttons_search: gtk4::Button,\n    pub buttons_select: gtk4::MenuButton,\n    pub buttons_delete: gtk4::Button,\n    pub buttons_save: gtk4::Button,\n    pub buttons_symlink: gtk4::Button,\n    pub buttons_hardlink: gtk4::Button,\n    pub buttons_move: gtk4::Button,\n    pub buttons_compare: gtk4::Button,\n    pub buttons_sort: gtk4::MenuButton,\n    pub buttons_show_errors: gtk4::Button,\n    pub buttons_show_upper_notebook: gtk4::Button,\n\n    pub label_buttons_select: gtk4::Label,\n    pub label_buttons_sort: gtk4::Label,\n\n    pub buttons_names: [BottomButtonsEnum; 9],\n    pub buttons_array: [Widget; 9],\n\n    pub gc_buttons_select: GestureClick,\n    pub gc_buttons_sort: GestureClick,\n}\n\nimpl GuiBottomButtons {\n    pub(crate) fn create_from_builder(builder: &gtk4::Builder, popover_select: &gtk4::Popover, popover_sort: &gtk4::Popover) -> Self {\n        let buttons_search: gtk4::Button = builder.object(\"buttons_search\").expect(\"Cambalache\");\n        let buttons_select: gtk4::MenuButton = builder.object(\"buttons_select\").expect(\"Cambalache\");\n        let buttons_delete: gtk4::Button = builder.object(\"buttons_delete\").expect(\"Cambalache\");\n        let buttons_save: gtk4::Button = builder.object(\"buttons_save\").expect(\"Cambalache\");\n        let buttons_symlink: gtk4::Button = builder.object(\"buttons_symlink\").expect(\"Cambalache\");\n        let buttons_hardlink: gtk4::Button = builder.object(\"buttons_hardlink\").expect(\"Cambalache\");\n        let buttons_move: gtk4::Button = builder.object(\"buttons_move\").expect(\"Cambalache\");\n        let buttons_compare: gtk4::Button = builder.object(\"buttons_compare\").expect(\"Cambalache\");\n        let buttons_sort: gtk4::MenuButton = builder.object(\"buttons_sort\").expect(\"Cambalache\");\n\n        let buttons_show_errors: gtk4::Button = builder.object(\"buttons_show_errors\").expect(\"Cambalache\");\n        let buttons_show_upper_notebook: gtk4::Button = builder.object(\"buttons_show_upper_notebook\").expect(\"Cambalache\");\n\n        let label_buttons_select: gtk4::Label = builder.object(\"label_buttons_select\").expect(\"Cambalache\");\n        let label_buttons_sort: gtk4::Label = builder.object(\"label_buttons_sort\").expect(\"Cambalache\");\n\n        let gc_buttons_select: GestureClick = GestureClick::new();\n        let gc_buttons_sort: GestureClick = GestureClick::new();\n\n        buttons_select.add_controller(gc_buttons_select.clone());\n        buttons_sort.add_controller(gc_buttons_sort.clone());\n\n        set_icon_of_button(&buttons_search, CZK_ICON_SEARCH);\n        set_icon_of_button(&buttons_select, CZK_ICON_SELECT);\n        set_icon_of_button(&buttons_delete, CZK_ICON_TRASH);\n        set_icon_of_button(&buttons_save, CZK_ICON_SAVE);\n        set_icon_of_button(&buttons_symlink, CZK_ICON_SYMLINK);\n        set_icon_of_button(&buttons_hardlink, CZK_ICON_HARDLINK);\n        set_icon_of_button(&buttons_move, CZK_ICON_MOVE);\n        set_icon_of_button(&buttons_compare, CZK_ICON_COMPARE);\n        set_icon_of_button(&buttons_sort, CZK_ICON_SORT);\n        set_icon_of_button(&buttons_show_errors, CZK_ICON_HIDE_DOWN);\n        set_icon_of_button(&buttons_show_upper_notebook, CZK_ICON_HIDE_UP);\n\n        let buttons_names = [\n            BottomButtonsEnum::Search,\n            BottomButtonsEnum::Select,\n            BottomButtonsEnum::Delete,\n            BottomButtonsEnum::Save,\n            BottomButtonsEnum::Symlink,\n            BottomButtonsEnum::Hardlink,\n            BottomButtonsEnum::Move,\n            BottomButtonsEnum::Compare,\n            BottomButtonsEnum::Sort,\n        ];\n        let buttons_array = [\n            buttons_search.clone().upcast::<Widget>(),\n            buttons_select.clone().upcast::<Widget>(),\n            buttons_delete.clone().upcast::<Widget>(),\n            buttons_save.clone().upcast::<Widget>(),\n            buttons_symlink.clone().upcast::<Widget>(),\n            buttons_hardlink.clone().upcast::<Widget>(),\n            buttons_move.clone().upcast::<Widget>(),\n            buttons_compare.clone().upcast::<Widget>(),\n            buttons_sort.clone().upcast::<Widget>(),\n        ];\n\n        buttons_select.set_popover(Some(popover_select));\n        buttons_sort.set_popover(Some(popover_sort));\n\n        #[cfg(target_family = \"windows\")]\n        buttons_hardlink.set_sensitive(test_hardlinks());\n\n        Self {\n            buttons_search,\n            buttons_select,\n            buttons_delete,\n            buttons_save,\n            buttons_symlink,\n            buttons_hardlink,\n            buttons_move,\n            buttons_compare,\n            buttons_sort,\n            buttons_show_errors,\n            buttons_show_upper_notebook,\n            label_buttons_select,\n            label_buttons_sort,\n            buttons_names,\n            buttons_array,\n            gc_buttons_select,\n            gc_buttons_sort,\n        }\n    }\n    pub(crate) fn update_language(&self) {\n        self.buttons_search.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_search_button\"));\n        self.label_buttons_select.set_text(&flg!(\"bottom_select_button\"));\n        self.buttons_delete.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_delete_button\"));\n        self.buttons_save.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_save_button\"));\n        self.buttons_symlink.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_symlink_button\"));\n        self.buttons_move.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_move_button\"));\n        self.buttons_hardlink.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_hardlink_button\"));\n        self.buttons_compare.get_widget_of_type::<Label>(true).set_text(&flg!(\"bottom_compare_button\"));\n        self.label_buttons_sort.set_text(&flg!(\"bottom_sort_button\"));\n\n        self.buttons_search.set_tooltip_text(Some(&flg!(\"bottom_search_button_tooltip\")));\n        self.buttons_select.set_tooltip_text(Some(&flg!(\"bottom_select_button_tooltip\")));\n        self.buttons_delete.set_tooltip_text(Some(&flg!(\"bottom_delete_button_tooltip\")));\n        self.buttons_save.set_tooltip_text(Some(&flg!(\"bottom_save_button_tooltip\")));\n        self.buttons_symlink.set_tooltip_text(Some(&flg!(\"bottom_symlink_button_tooltip\")));\n        self.buttons_move.set_tooltip_text(Some(&flg!(\"bottom_move_button_tooltip\")));\n        self.buttons_sort.set_tooltip_text(Some(&flg!(\"bottom_sort_button_tooltip\")));\n        self.buttons_compare.set_tooltip_text(Some(&flg!(\"bottom_compare_button_tooltip\")));\n        if self.buttons_hardlink.is_sensitive() {\n            self.buttons_hardlink.set_tooltip_text(Some(&flg!(\"bottom_hardlink_button_tooltip\")));\n        } else {\n            self.buttons_hardlink.set_tooltip_text(Some(&flg!(\"bottom_hardlink_button_not_available_tooltip\")));\n        }\n\n        self.buttons_show_errors.set_tooltip_text(Some(&flg!(\"bottom_show_errors_tooltip\")));\n        self.buttons_show_upper_notebook.set_tooltip_text(Some(&flg!(\"bottom_show_upper_notebook_tooltip\")));\n    }\n}\n\n#[cfg(target_family = \"windows\")]\nfn test_hardlinks() -> bool {\n    use std::io::Write;\n    use std::{env, fs};\n\n    use rand::Rng;\n\n    fn try_create_hardlink(dir: &std::path::Path) -> bool {\n        use rand::RngExt;\n        let random_suffix: u32 = rand::rng().random();\n        let cache_file = dir.join(format!(\"czkawka_test_{}.czkawka_tmp\", random_suffix));\n        let cache_file_second = dir.join(format!(\"czkawka_test_{}_link.czkawka_tmp\", random_suffix));\n\n        let _ = fs::remove_file(&cache_file);\n        let _ = fs::remove_file(&cache_file_second);\n\n        let result = (|| {\n            let mut file_handler = fs::File::create(&cache_file).ok()?;\n            writeln!(file_handler, \"test\").ok()?;\n            drop(file_handler);\n\n            czkawka_core::common::make_hard_link(&cache_file, &cache_file_second).ok()?;\n\n            if cache_file_second.exists() { Some(true) } else { None }\n        })();\n\n        let _ = fs::remove_file(&cache_file);\n        let _ = fs::remove_file(&cache_file_second);\n\n        result.unwrap_or(false)\n    }\n\n    // Try home directory first\n    if let Ok(home_dir) = env::var(\"USERPROFILE\").or_else(|_| env::var(\"HOME\")) {\n        if let Ok(home_path) = std::path::PathBuf::from(home_dir).canonicalize() {\n            if try_create_hardlink(&home_path) {\n                return true;\n            }\n        }\n    }\n\n    // Fallback to current directory\n    if let Ok(current_dir) = env::current_dir() {\n        if try_create_hardlink(&current_dir) {\n            return true;\n        }\n    }\n\n    false\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_compare_images.rs",
    "content": "use std::cell::RefCell;\nuse std::rc::Rc;\n\nuse gtk4::prelude::*;\nuse gtk4::{Builder, TreePath};\n\nuse crate::gui_structs::gui_data::CZK_ICON_REPLACE;\nuse crate::helpers::image_operations::set_icon_of_button;\nuse crate::{CZK_ICON_LEFT, CZK_ICON_RIGHT, flg};\n\n#[derive(Clone)]\npub struct GuiCompareImages {\n    pub window_compare: gtk4::Window,\n\n    pub label_group_info: gtk4::Label,\n\n    pub button_go_previous_compare_group: gtk4::Button,\n    pub button_go_next_compare_group: gtk4::Button,\n    pub button_replace_group: gtk4::Button,\n\n    pub check_button_left_preview_text: gtk4::CheckButton,\n    pub check_button_right_preview_text: gtk4::CheckButton,\n\n    pub image_compare_left: gtk4::Picture,\n    pub image_compare_right: gtk4::Picture,\n\n    pub scrolled_window_compare_choose_images: gtk4::ScrolledWindow,\n\n    pub shared_numbers_of_groups: Rc<RefCell<u32>>,\n    pub shared_current_of_groups: Rc<RefCell<u32>>,\n    pub shared_current_path: Rc<RefCell<Option<TreePath>>>,\n    pub shared_image_cache: Rc<RefCell<Vec<(String, String, gtk4::Picture, gtk4::Picture, TreePath)>>>,\n    pub shared_using_for_preview: Rc<RefCell<(Option<TreePath>, Option<TreePath>)>>,\n}\n\nimpl GuiCompareImages {\n    pub(crate) fn create_from_builder(window_main: &gtk4::Window) -> Self {\n        let glade_src = include_str!(\"../../ui/compare_images.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let window_compare: gtk4::Window = builder.object(\"window_compare\").expect(\"Cambalache\");\n        window_compare.set_title(Some(&flg!(\"window_compare_images\")));\n        window_compare.set_modal(true);\n        window_compare.set_transient_for(Some(window_main));\n\n        let label_group_info: gtk4::Label = builder.object(\"label_group_info\").expect(\"Cambalache\");\n\n        let button_go_previous_compare_group: gtk4::Button = builder.object(\"button_go_previous_compare_group\").expect(\"Cambalache\");\n        let button_go_next_compare_group: gtk4::Button = builder.object(\"button_go_next_compare_group\").expect(\"Cambalache\");\n        let button_replace_group: gtk4::Button = builder.object(\"button_replace_group\").expect(\"Cambalache\");\n\n        let check_button_left_preview_text: gtk4::CheckButton = builder.object(\"check_button_left_preview_text\").expect(\"Cambalache\");\n        let check_button_right_preview_text: gtk4::CheckButton = builder.object(\"check_button_right_preview_text\").expect(\"Cambalache\");\n\n        let image_compare_left: gtk4::Picture = builder.object(\"image_compare_left\").expect(\"Cambalache\");\n        let image_compare_right: gtk4::Picture = builder.object(\"image_compare_right\").expect(\"Cambalache\");\n\n        let scrolled_window_compare_choose_images: gtk4::ScrolledWindow = builder.object(\"scrolled_window_compare_choose_images\").expect(\"Cambalache\");\n\n        let shared_numbers_of_groups = Rc::new(RefCell::new(0));\n        let shared_current_of_groups = Rc::new(RefCell::new(0));\n        let shared_current_path = Rc::new(RefCell::new(None));\n        let shared_image_cache = Rc::new(RefCell::new(Vec::new()));\n        let shared_using_for_preview = Rc::new(RefCell::new((None, None)));\n\n        set_icon_of_button(&button_go_previous_compare_group, CZK_ICON_LEFT);\n        set_icon_of_button(&button_go_next_compare_group, CZK_ICON_RIGHT);\n        set_icon_of_button(&button_replace_group, CZK_ICON_REPLACE);\n\n        Self {\n            window_compare,\n            label_group_info,\n            button_go_previous_compare_group,\n            button_go_next_compare_group,\n            button_replace_group,\n            check_button_left_preview_text,\n            check_button_right_preview_text,\n            image_compare_left,\n            image_compare_right,\n            scrolled_window_compare_choose_images,\n            shared_numbers_of_groups,\n            shared_current_of_groups,\n            shared_current_path,\n            shared_image_cache,\n            shared_using_for_preview,\n        }\n    }\n    pub(crate) fn update_language(&self) {\n        self.window_compare.set_title(Some(&flg!(\"window_compare_images\")));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_data.rs",
    "content": "use std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::io::BufReader;\nuse std::rc::Rc;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse gdk4::gdk_pixbuf::Pixbuf;\nuse gtk4::prelude::*;\nuse gtk4::{Builder, FileChooserNative};\n\nuse crate::flg;\nuse crate::gui_structs::gui_about::GuiAbout;\nuse crate::gui_structs::gui_bottom_buttons::GuiBottomButtons;\nuse crate::gui_structs::gui_compare_images::GuiCompareImages;\nuse crate::gui_structs::gui_header::GuiHeader;\nuse crate::gui_structs::gui_main_notebook::GuiMainNotebook;\nuse crate::gui_structs::gui_popovers_select::GuiSelectPopovers;\nuse crate::gui_structs::gui_popovers_sort::GuiSortPopovers;\nuse crate::gui_structs::gui_progress_dialog::GuiProgressDialog;\nuse crate::gui_structs::gui_settings::GuiSettings;\nuse crate::gui_structs::gui_upper_notebook::GuiUpperNotebook;\nuse crate::helpers::enums::BottomButtonsEnum;\nuse crate::notebook_enums::{NotebookMainEnum, get_all_main_tabs};\nuse crate::taskbar_progress::TaskbarProgress;\n\npub const ICON_ABOUT: &[u8] = include_bytes!(\"../../icons/icon_about.png\");\npub const CZK_ICON_ADD: &[u8] = include_bytes!(\"../../icons/czk_add.svg\");\npub const CZK_ICON_COMPARE: &[u8] = include_bytes!(\"../../icons/czk_compare.svg\");\npub const CZK_ICON_DELETE: &[u8] = include_bytes!(\"../../icons/czk_delete.svg\");\npub const CZK_ICON_HARDLINK: &[u8] = include_bytes!(\"../../icons/czk_hardlink.svg\");\npub const CZK_ICON_HIDE_DOWN: &[u8] = include_bytes!(\"../../icons/czk_hide_down.svg\");\npub const CZK_ICON_HIDE_UP: &[u8] = include_bytes!(\"../../icons/czk_hide_up.svg\");\npub const CZK_ICON_INFO: &[u8] = include_bytes!(\"../../icons/czk_info.svg\");\npub const CZK_ICON_LEFT: &[u8] = include_bytes!(\"../../icons/czk_left.svg\");\npub const CZK_ICON_MANUAL_ADD: &[u8] = include_bytes!(\"../../icons/czk_manual_add.svg\");\npub const CZK_ICON_MOVE: &[u8] = include_bytes!(\"../../icons/czk_move.svg\");\npub const CZK_ICON_RIGHT: &[u8] = include_bytes!(\"../../icons/czk_right.svg\");\npub const CZK_ICON_SAVE: &[u8] = include_bytes!(\"../../icons/czk_save.svg\");\npub const CZK_ICON_SEARCH: &[u8] = include_bytes!(\"../../icons/czk_search.svg\");\npub const CZK_ICON_SELECT: &[u8] = include_bytes!(\"../../icons/czk_select.svg\");\npub const CZK_ICON_SETTINGS: &[u8] = include_bytes!(\"../../icons/czk_settings.svg\");\npub const CZK_ICON_SORT: &[u8] = include_bytes!(\"../../icons/czk_sort.svg\");\npub const CZK_ICON_STOP: &[u8] = include_bytes!(\"../../icons/czk_stop.svg\");\npub const CZK_ICON_SYMLINK: &[u8] = include_bytes!(\"../../icons/czk_symlink.svg\");\npub const CZK_ICON_TRASH: &[u8] = include_bytes!(\"../../icons/czk_trash.svg\");\npub const CZK_ICON_REPLACE: &[u8] = include_bytes!(\"../../icons/czk_replace.svg\");\n\n#[derive(Clone)]\npub struct GuiData {\n    // Windows\n    pub window_main: gtk4::Window,\n\n    pub main_notebook: GuiMainNotebook,\n    pub upper_notebook: GuiUpperNotebook,\n    pub popovers_select: GuiSelectPopovers,\n    pub popovers_sort: GuiSortPopovers,\n    pub bottom_buttons: GuiBottomButtons,\n    pub progress_window: GuiProgressDialog,\n    pub about: GuiAbout,\n    pub settings: GuiSettings,\n    pub header: GuiHeader,\n    pub compare_images: GuiCompareImages,\n\n    pub file_dialog_include_exclude_folder_selection: FileChooserNative,\n    pub file_dialog_move_to_folder: FileChooserNative,\n\n    // Taskbar state\n    pub taskbar_state: Rc<RefCell<TaskbarProgress>>,\n\n    // Buttons state\n    pub shared_buttons: Rc<RefCell<HashMap<NotebookMainEnum, HashMap<BottomButtonsEnum, bool>>>>,\n\n    //// Entry\n    pub entry_info: gtk4::Entry,\n\n    //// Bottom\n    pub text_view_errors: gtk4::TextView,\n    pub scrolled_window_errors: gtk4::ScrolledWindow,\n\n    // Used for sending stop signal to thread\n    pub stop_flag: Arc<AtomicBool>,\n}\n\nimpl GuiData {\n    pub fn new_with_application(application: &gtk4::Application) -> Self {\n        //// Loading glade file content and build with it help UI\n        let glade_src = include_str!(\"../../ui/main_window.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        //// Windows\n        let window_main: gtk4::Window = builder.object(\"window_main\").expect(\"Cambalache\");\n        window_main.set_title(Some(&flg!(\"window_main_title\")));\n        window_main.set_visible(true);\n\n        let pixbuf = Pixbuf::from_read(BufReader::new(ICON_ABOUT))\n            .unwrap_or(Pixbuf::new(gdk4::gdk_pixbuf::Colorspace::Rgb, false, 8, 1, 1).expect(\"Crash is a lot of less likely than loading png file\"));\n\n        window_main.set_application(Some(application));\n\n        let upper_notebook = GuiUpperNotebook::create_from_builder(&builder);\n        let popovers_select = GuiSelectPopovers::create_from_builder();\n        let popovers_sort = GuiSortPopovers::create_from_builder();\n        let bottom_buttons = GuiBottomButtons::create_from_builder(&builder, &popovers_select.popover_select, &popovers_sort.popover_sort);\n        let progress_window = GuiProgressDialog::create_from_builder(&window_main);\n        let about = GuiAbout::create_from_builder(&window_main, &pixbuf);\n        let header = GuiHeader::create_from_builder(&builder);\n        let settings = GuiSettings::create_from_builder(&window_main);\n        let compare_images = GuiCompareImages::create_from_builder(&window_main);\n        let main_notebook = GuiMainNotebook::create_from_builder(&builder, &settings);\n\n        ////////////////////////////////////////////////////////////////////////////////////////////////\n\n        // Taskbar state\n        let taskbar_state = Rc::new(RefCell::new(TaskbarProgress::new()));\n\n        // Buttons State - to remember existence of different buttons on pages\n        let shared_buttons: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::<NotebookMainEnum, HashMap<BottomButtonsEnum, bool>>::new()));\n\n        // Show by default only search button\n        for i in &get_all_main_tabs() {\n            let mut temp_hashmap: HashMap<BottomButtonsEnum, bool> = Default::default();\n            for button_name in &bottom_buttons.buttons_names {\n                if *button_name == BottomButtonsEnum::Search {\n                    temp_hashmap.insert(*button_name, true);\n                } else {\n                    temp_hashmap.insert(*button_name, false);\n                }\n            }\n            shared_buttons.borrow_mut().insert(*i, temp_hashmap);\n        }\n\n        // File Dialogs - Native file dialogs must exist all the time in opposite to normal dialog\n\n        let file_dialog_include_exclude_folder_selection = FileChooserNative::builder()\n            .action(gtk4::FileChooserAction::SelectFolder)\n            .transient_for(&window_main)\n            .select_multiple(true)\n            .modal(true)\n            .build();\n        let file_dialog_move_to_folder = FileChooserNative::builder()\n            .title(flg!(\"move_files_title_dialog\"))\n            .action(gtk4::FileChooserAction::SelectFolder)\n            .transient_for(&window_main)\n            .select_multiple(false)\n            .modal(true)\n            .build();\n\n        //// Entry\n        let entry_info: gtk4::Entry = builder.object(\"entry_info\").expect(\"Cambalache\");\n\n        //// Bottom\n        let text_view_errors: gtk4::TextView = builder.object(\"text_view_errors\").expect(\"Cambalache\");\n        let scrolled_window_errors: gtk4::ScrolledWindow = builder.object(\"scrolled_window_errors\").expect(\"Cambalache\");\n        scrolled_window_errors.set_visible(true); // Not sure why needed, but without it text view errors sometimes hide itself\n\n        // Used for sending stop signal to thread\n        let stop_flag = Arc::default();\n\n        Self {\n            window_main,\n            main_notebook,\n            upper_notebook,\n            popovers_select,\n            popovers_sort,\n            bottom_buttons,\n            progress_window,\n            about,\n            settings,\n            header,\n            compare_images,\n            file_dialog_include_exclude_folder_selection,\n            file_dialog_move_to_folder,\n            taskbar_state,\n            shared_buttons,\n            entry_info,\n            text_view_errors,\n            scrolled_window_errors,\n            stop_flag,\n        }\n    }\n\n    pub(crate) fn setup(&self) {\n        self.main_notebook.setup(self);\n        self.upper_notebook.setup();\n    }\n\n    pub(crate) fn update_language(&self) {\n        self.window_main.set_title(Some(&flg!(\"window_main_title\")));\n\n        self.main_notebook.update_language();\n        self.upper_notebook.update_language();\n        self.popovers_select.update_language();\n        self.popovers_sort.update_language();\n        self.bottom_buttons.update_language();\n        self.progress_window.update_language();\n        self.about.update_language();\n        self.header.update_language();\n        self.settings.update_language();\n        self.compare_images.update_language();\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_header.rs",
    "content": "use gtk4::prelude::*;\n\nuse crate::helpers::image_operations::set_icon_of_button;\nuse crate::{CZK_ICON_INFO, CZK_ICON_SETTINGS, flg};\n\n#[derive(Clone)]\npub struct GuiHeader {\n    pub button_settings: gtk4::Button,\n    pub button_app_info: gtk4::Button,\n}\n\nimpl GuiHeader {\n    pub(crate) fn create_from_builder(builder: &gtk4::Builder) -> Self {\n        let button_settings: gtk4::Button = builder.object(\"button_settings\").expect(\"Cambalache\");\n        let button_app_info: gtk4::Button = builder.object(\"button_app_info\").expect(\"Cambalache\");\n\n        set_icon_of_button(&button_settings, CZK_ICON_SETTINGS);\n        set_icon_of_button(&button_app_info, CZK_ICON_INFO);\n\n        Self { button_settings, button_app_info }\n    }\n\n    pub(crate) fn update_language(&self) {\n        self.button_settings.set_tooltip_text(Some(&flg!(\"header_setting_button_tooltip\")));\n        self.button_app_info.set_tooltip_text(Some(&flg!(\"header_about_button_tooltip\")));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_main_notebook.rs",
    "content": "use std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::rc::Rc;\n\nuse czkawka_core::common::model::CheckingMethod;\nuse czkawka_core::localizer_core::{fnc_get_similarity_minimal, fnc_get_similarity_very_high};\nuse czkawka_core::tools::big_file::SearchMode;\nuse czkawka_core::tools::similar_images::SIMILAR_VALUES;\nuse czkawka_core::tools::similar_images::core::get_string_from_similarity;\nuse gtk4::prelude::*;\nuse gtk4::{Builder, CheckButton, ComboBoxText, Entry, Label, Notebook, Picture, Scale, Widget};\nuse log::error;\n\nuse crate::flg;\nuse crate::gtk_traits::WidgetTraits;\nuse crate::gui_structs::common_tree_view::{CommonTreeViews, SharedModelEnum, SubView};\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::gui_structs::gui_settings::GuiSettings;\nuse crate::help_combo_box::{AUDIO_TYPE_CHECK_METHOD_COMBO_BOX, BIG_FILES_CHECK_METHOD_COMBO_BOX, DUPLICATES_CHECK_METHOD_COMBO_BOX, IMAGES_HASH_SIZE_COMBO_BOX};\nuse crate::notebook_enums::NotebookMainEnum;\n\n#[derive(Clone)]\npub struct GuiMainNotebook {\n    pub notebook_main: Notebook,\n\n    // General\n\n    // Duplicate\n    pub combo_box_duplicate_check_method: ComboBoxText,\n    pub combo_box_duplicate_hash_type: ComboBoxText,\n    pub label_duplicate_check_method: Label,\n    pub label_duplicate_hash_type: Label,\n    pub check_button_duplicate_case_sensitive_name: CheckButton,\n\n    pub image_preview_duplicates: Picture,\n\n    // Big file\n    pub label_big_shown_files: Label,\n    pub entry_big_files_number: Entry,\n    pub label_big_files_mode: Label,\n    pub combo_box_big_files_mode: ComboBoxText,\n\n    // Similar Images\n    pub scale_similarity_similar_images: Scale,\n\n    pub label_image_resize_algorithm: Label,\n    pub label_image_hash_type: Label,\n    pub label_image_hash_size: Label,\n\n    pub combo_box_image_resize_algorithm: ComboBoxText,\n    pub combo_box_image_hash_algorithm: ComboBoxText,\n    pub combo_box_image_hash_size: ComboBoxText,\n\n    pub check_button_image_ignore_same_size: CheckButton,\n    pub check_button_video_ignore_same_size: CheckButton,\n\n    pub label_image_similarity: Label,\n    pub label_image_similarity_max: Label,\n\n    pub image_preview_similar_images: Picture,\n    pub label_similar_images_minimal_similarity: Label,\n\n    // Video\n    pub label_video_similarity: Label,\n    pub label_video_similarity_min: Label,\n    pub label_video_similarity_max: Label,\n\n    pub scale_similarity_similar_videos: Scale,\n\n    // Broken Files\n    pub check_button_broken_files_audio: CheckButton,\n    pub check_button_broken_files_pdf: CheckButton,\n    pub check_button_broken_files_archive: CheckButton,\n    pub check_button_broken_files_image: CheckButton,\n    pub check_button_broken_files_video: CheckButton,\n\n    // Music\n    pub check_button_music_title: CheckButton,\n    pub check_button_music_artist: CheckButton,\n    pub check_button_music_year: CheckButton,\n    pub check_button_music_bitrate: CheckButton,\n    pub check_button_music_genre: CheckButton,\n    pub check_button_music_length: CheckButton,\n    pub check_button_music_approximate_comparison: CheckButton,\n    pub check_button_music_compare_only_in_title_group: CheckButton,\n    #[expect(unused)]\n    pub label_audio_check_type: Label,\n    pub combo_box_audio_check_type: ComboBoxText,\n    pub label_same_music_seconds: Label,\n    pub label_same_music_similarity: Label,\n    pub scale_seconds_same_music: Scale,\n    pub scale_similarity_same_music: Scale,\n\n    pub common_tree_views: CommonTreeViews,\n}\n\nimpl GuiMainNotebook {\n    pub(crate) fn create_from_builder(builder: &Builder, settings: &GuiSettings) -> Self {\n        let notebook_main: Notebook = builder.object(\"notebook_main\").expect(\"Cambalache\");\n\n        let combo_box_duplicate_check_method: ComboBoxText = builder.object(\"combo_box_duplicate_check_method\").expect(\"Cambalache\");\n        let combo_box_duplicate_hash_type: ComboBoxText = builder.object(\"combo_box_duplicate_hash_type\").expect(\"Cambalache\");\n\n        let entry_big_files_number: Entry = builder.object(\"entry_big_files_number\").expect(\"Cambalache\");\n\n        //// Check Buttons\n        let check_button_duplicate_case_sensitive_name: CheckButton = builder.object(\"check_button_duplicate_case_sensitive_name\").expect(\"Cambalache\");\n        let check_button_music_title: CheckButton = builder.object(\"check_button_music_title\").expect(\"Cambalache\");\n        let check_button_music_artist: CheckButton = builder.object(\"check_button_music_artist\").expect(\"Cambalache\");\n        let check_button_music_year: CheckButton = builder.object(\"check_button_music_year\").expect(\"Cambalache\");\n        let check_button_music_bitrate: CheckButton = builder.object(\"check_button_music_bitrate\").expect(\"Cambalache\");\n        let check_button_music_genre: CheckButton = builder.object(\"check_button_music_genre\").expect(\"Cambalache\");\n        let check_button_music_length: CheckButton = builder.object(\"check_button_music_length\").expect(\"Cambalache\");\n        let check_button_music_approximate_comparison: CheckButton = builder.object(\"check_button_music_approximate_comparison\").expect(\"Cambalache\");\n        let check_button_music_compare_only_in_title_group: CheckButton = builder.object(\"check_button_music_compare_only_in_title_group\").expect(\"Cambalache\");\n\n        let check_button_broken_files_audio: CheckButton = builder.object(\"check_button_broken_files_audio\").expect(\"Cambalache\");\n        let check_button_broken_files_pdf: CheckButton = builder.object(\"check_button_broken_files_pdf\").expect(\"Cambalache\");\n        let check_button_broken_files_archive: CheckButton = builder.object(\"check_button_broken_files_archive\").expect(\"Cambalache\");\n        let check_button_broken_files_image: CheckButton = builder.object(\"check_button_broken_files_image\").expect(\"Cambalache\");\n        let check_button_broken_files_video: CheckButton = builder.object(\"check_button_broken_files_video\").expect(\"Cambalache\");\n\n        let scale_similarity_similar_images: Scale = builder.object(\"scale_similarity_similar_images\").expect(\"Cambalache\");\n        let scale_similarity_similar_videos: Scale = builder.object(\"scale_similarity_similar_videos\").expect(\"Cambalache\");\n\n        let combo_box_image_resize_algorithm: ComboBoxText = builder.object(\"combo_box_image_resize_algorithm\").expect(\"Cambalache\");\n        let combo_box_image_hash_algorithm: ComboBoxText = builder.object(\"combo_box_image_hash_algorithm\").expect(\"Cambalache\");\n        let combo_box_image_hash_size: ComboBoxText = builder.object(\"combo_box_image_hash_size\").expect(\"Cambalache\");\n        let combo_box_big_files_mode: ComboBoxText = builder.object(\"combo_box_big_files_mode\").expect(\"Cambalache\");\n\n        let check_button_image_ignore_same_size: CheckButton = builder.object(\"check_button_image_ignore_same_size\").expect(\"Cambalache\");\n        let check_button_video_ignore_same_size: CheckButton = builder.object(\"check_button_video_ignore_same_size\").expect(\"Cambalache\");\n\n        let label_similar_images_minimal_similarity: Label = builder.object(\"label_similar_images_minimal_similarity\").expect(\"Cambalache\");\n\n        let label_duplicate_check_method: Label = builder.object(\"label_duplicate_check_method\").expect(\"Cambalache\");\n        let label_duplicate_hash_type: Label = builder.object(\"label_duplicate_hash_type\").expect(\"Cambalache\");\n        let label_big_shown_files: Label = builder.object(\"label_big_shown_files\").expect(\"Cambalache\");\n        let label_image_resize_algorithm: Label = builder.object(\"label_image_resize_algorithm\").expect(\"Cambalache\");\n        let label_image_hash_type: Label = builder.object(\"label_image_hash_type\").expect(\"Cambalache\");\n        let label_image_hash_size: Label = builder.object(\"label_image_hash_size\").expect(\"Cambalache\");\n        let label_image_similarity: Label = builder.object(\"label_image_similarity\").expect(\"Cambalache\");\n        let label_image_similarity_max: Label = builder.object(\"label_image_similarity_max\").expect(\"Cambalache\");\n        let label_video_similarity: Label = builder.object(\"label_video_similarity\").expect(\"Cambalache\");\n        let label_video_similarity_min: Label = builder.object(\"label_video_similarity_min\").expect(\"Cambalache\");\n        let label_video_similarity_max: Label = builder.object(\"label_video_similarity_max\").expect(\"Cambalache\");\n        let label_big_files_mode: Label = builder.object(\"label_big_files_mode\").expect(\"Cambalache\");\n\n        let image_preview_similar_images: Picture = builder.object(\"image_preview_similar_images\").expect(\"Cambalache\");\n        let image_preview_duplicates: Picture = builder.object(\"image_preview_duplicates\").expect(\"Cambalache\");\n\n        let label_audio_check_type: Label = builder.object(\"label_audio_check_type\").expect(\"Cambalache\");\n        let combo_box_audio_check_type: ComboBoxText = builder.object(\"combo_box_audio_check_type\").expect(\"Cambalache\");\n        let label_same_music_seconds: Label = builder.object(\"label_same_music_seconds\").expect(\"Cambalache\");\n        let label_same_music_similarity: Label = builder.object(\"label_same_music_similarity\").expect(\"Cambalache\");\n        let scale_seconds_same_music: Scale = builder.object(\"scale_seconds_same_music\").expect(\"Cambalache\");\n        let scale_similarity_same_music: Scale = builder.object(\"scale_similarity_same_music\").expect(\"Cambalache\");\n\n        #[rustfmt::skip]\n        let subviews: Vec<_> = [\n            SubView::new(builder, \"scrolled_window_duplicate_finder\", NotebookMainEnum::Duplicate, Some(\"image_preview_duplicates\"), Some(settings.check_button_settings_show_preview_duplicates.clone()), SharedModelEnum::Duplicates(Rc::default())),\n            SubView::new(builder, \"scrolled_window_empty_folder_finder\", NotebookMainEnum::EmptyDirectories, None, None, SharedModelEnum::EmptyFolder(Rc::default())),\n            SubView::new(builder, \"scrolled_window_empty_files_finder\", NotebookMainEnum::EmptyFiles, None, None, SharedModelEnum::EmptyFiles(Rc::default())),\n            SubView::new(builder, \"scrolled_window_temporary_files_finder\", NotebookMainEnum::Temporary, None, None, SharedModelEnum::Temporary(Rc::default())),\n            SubView::new(builder, \"scrolled_window_big_files_finder\", NotebookMainEnum::BigFiles, None, None, SharedModelEnum::BigFile(Rc::default())),\n            SubView::new(builder, \"scrolled_window_similar_images_finder\", NotebookMainEnum::SimilarImages, Some(\"image_preview_similar_images\"), Some(settings.check_button_settings_show_preview_similar_images.clone()), SharedModelEnum::SimilarImages(Rc::default())),\n            SubView::new(builder, \"scrolled_window_similar_videos_finder\", NotebookMainEnum::SimilarVideos, None, None, SharedModelEnum::SimilarVideos(Rc::default())),\n            SubView::new(builder, \"scrolled_window_same_music_finder\", NotebookMainEnum::SameMusic, None, None, SharedModelEnum::SameMusic(Rc::default())),\n            SubView::new(builder, \"scrolled_window_invalid_symlinks\", NotebookMainEnum::Symlinks, None, None, SharedModelEnum::Symlinks(Rc::default())),\n            SubView::new(builder, \"scrolled_window_broken_files\", NotebookMainEnum::BrokenFiles, None, None, SharedModelEnum::BrokenFiles(Rc::default())),\n            SubView::new(builder, \"scrolled_window_bad_extensions\", NotebookMainEnum::BadExtensions, None, None, SharedModelEnum::BadExtensions(Rc::default())),\n        ]\n        .into_iter()\n        .collect();\n\n        let common_tree_views = CommonTreeViews {\n            notebook_main: notebook_main.clone(),\n            subviews,\n            preview_path: Rc::new(RefCell::new(String::new())),\n        };\n\n        Self {\n            notebook_main,\n            combo_box_duplicate_check_method,\n            combo_box_duplicate_hash_type,\n            label_duplicate_check_method,\n            label_duplicate_hash_type,\n            check_button_duplicate_case_sensitive_name,\n            image_preview_duplicates,\n            label_big_shown_files,\n            entry_big_files_number,\n            label_big_files_mode,\n            combo_box_big_files_mode,\n            scale_similarity_similar_images,\n            label_image_resize_algorithm,\n            label_image_hash_type,\n            label_image_hash_size,\n            combo_box_image_resize_algorithm,\n            combo_box_image_hash_algorithm,\n            combo_box_image_hash_size,\n            check_button_image_ignore_same_size,\n            check_button_video_ignore_same_size,\n            label_image_similarity,\n            label_image_similarity_max,\n            image_preview_similar_images,\n            label_similar_images_minimal_similarity,\n            label_video_similarity,\n            label_video_similarity_min,\n            label_video_similarity_max,\n            scale_similarity_similar_videos,\n            check_button_broken_files_audio,\n            check_button_broken_files_pdf,\n            check_button_broken_files_archive,\n            check_button_broken_files_image,\n            check_button_broken_files_video,\n            check_button_music_title,\n            check_button_music_artist,\n            check_button_music_year,\n            check_button_music_bitrate,\n            check_button_music_genre,\n            check_button_music_length,\n            check_button_music_approximate_comparison,\n            check_button_music_compare_only_in_title_group,\n            label_audio_check_type,\n            combo_box_audio_check_type,\n            label_same_music_seconds,\n            label_same_music_similarity,\n            scale_seconds_same_music,\n            scale_similarity_same_music,\n            common_tree_views,\n        }\n    }\n\n    pub(crate) fn setup(&self, gui_data: &GuiData) {\n        self.common_tree_views.setup(gui_data);\n    }\n\n    pub(crate) fn update_language(&self) {\n        self.check_button_duplicate_case_sensitive_name.set_label(Some(&flg!(\"duplicate_case_sensitive_name\")));\n        self.check_button_music_title.set_label(Some(&flg!(\"music_title_checkbox\")));\n        self.check_button_music_artist.set_label(Some(&flg!(\"music_artist_checkbox\")));\n        self.check_button_music_year.set_label(Some(&flg!(\"music_year_checkbox\")));\n        self.check_button_music_bitrate.set_label(Some(&flg!(\"music_bitrate_checkbox\")));\n        self.check_button_music_genre.set_label(Some(&flg!(\"music_genre_checkbox\")));\n        self.check_button_music_length.set_label(Some(&flg!(\"music_length_checkbox\")));\n        self.check_button_music_approximate_comparison.set_label(Some(&flg!(\"music_comparison_checkbox\")));\n        self.check_button_music_compare_only_in_title_group\n            .set_label(Some(&flg!(\"music_compare_only_in_title_group\")));\n\n        self.check_button_music_approximate_comparison\n            .set_tooltip_text(Some(&flg!(\"music_comparison_checkbox_tooltip\")));\n\n        self.label_duplicate_check_method.set_label(&flg!(\"main_label_check_method\"));\n        self.label_duplicate_hash_type.set_label(&flg!(\"main_label_hash_type\"));\n        self.label_big_shown_files.set_label(&flg!(\"main_label_shown_files\"));\n        self.label_image_resize_algorithm.set_label(&flg!(\"main_label_resize_algorithm\"));\n        self.label_image_hash_type.set_label(&flg!(\"main_label_hash_type\"));\n        self.label_image_hash_size.set_label(&flg!(\"main_label_hash_size\"));\n        self.label_image_similarity.set_label(&flg!(\"main_label_similarity\"));\n        self.label_image_similarity_max.set_label(&fnc_get_similarity_very_high());\n        self.label_video_similarity.set_label(&flg!(\"main_label_similarity\"));\n        self.label_video_similarity_min.set_label(&fnc_get_similarity_minimal());\n        self.label_video_similarity_max.set_label(&fnc_get_similarity_very_high());\n\n        self.label_duplicate_check_method.set_tooltip_text(Some(&flg!(\"duplicate_check_method_tooltip\")));\n        self.combo_box_duplicate_check_method.set_tooltip_text(Some(&flg!(\"duplicate_check_method_tooltip\")));\n        self.label_duplicate_hash_type.set_tooltip_text(Some(&flg!(\"duplicate_hash_type_tooltip\")));\n        self.combo_box_duplicate_hash_type.set_tooltip_text(Some(&flg!(\"duplicate_hash_type_tooltip\")));\n        self.check_button_duplicate_case_sensitive_name\n            .set_tooltip_text(Some(&flg!(\"duplicate_case_sensitive_name_tooltip\")));\n        self.check_button_music_compare_only_in_title_group\n            .set_tooltip_text(Some(&flg!(\"music_compare_only_in_title_group_tooltip\")));\n\n        self.combo_box_image_hash_size.set_tooltip_text(Some(&flg!(\"image_hash_size_tooltip\")));\n        self.label_image_hash_size.set_tooltip_text(Some(&flg!(\"image_hash_size_tooltip\")));\n\n        self.combo_box_image_resize_algorithm.set_tooltip_text(Some(&flg!(\"image_resize_filter_tooltip\")));\n        self.label_image_resize_algorithm.set_tooltip_text(Some(&flg!(\"image_resize_filter_tooltip\")));\n\n        self.combo_box_image_hash_algorithm.set_tooltip_text(Some(&flg!(\"image_hash_alg_tooltip\")));\n        self.label_image_hash_type.set_tooltip_text(Some(&flg!(\"image_hash_alg_tooltip\")));\n\n        self.combo_box_big_files_mode.set_tooltip_text(Some(&flg!(\"big_files_mode_combobox_tooltip\")));\n        self.label_big_files_mode.set_tooltip_text(Some(&flg!(\"big_files_mode_combobox_tooltip\")));\n        self.label_big_files_mode.set_label(&flg!(\"big_files_mode_label\"));\n\n        self.check_button_image_ignore_same_size\n            .set_tooltip_text(Some(&flg!(\"check_button_general_same_size_tooltip\")));\n        self.check_button_video_ignore_same_size\n            .set_tooltip_text(Some(&flg!(\"check_button_general_same_size_tooltip\")));\n        self.check_button_image_ignore_same_size.set_label(Some(&flg!(\"check_button_general_same_size\")));\n        self.check_button_video_ignore_same_size.set_label(Some(&flg!(\"check_button_general_same_size\")));\n\n        self.check_button_broken_files_audio.set_label(Some(&flg!(\"main_check_box_broken_files_audio\")));\n        self.check_button_broken_files_archive.set_label(Some(&flg!(\"main_check_box_broken_files_archive\")));\n        self.check_button_broken_files_image.set_label(Some(&flg!(\"main_check_box_broken_files_image\")));\n        self.check_button_broken_files_pdf.set_label(Some(&flg!(\"main_check_box_broken_files_pdf\")));\n        self.check_button_broken_files_video.set_label(Some(&flg!(\"main_check_box_broken_files_video\")));\n        self.check_button_broken_files_video\n            .set_tooltip_text(Some(&flg!(\"main_check_box_broken_files_video_tooltip\")));\n\n        self.label_same_music_seconds.set_label(&flg!(\"same_music_seconds_label\"));\n        self.label_same_music_similarity.set_label(&flg!(\"same_music_similarity_label\"));\n        self.label_same_music_seconds.set_tooltip_text(Some(&flg!(\"same_music_tooltip\")));\n        self.label_same_music_similarity.set_tooltip_text(Some(&flg!(\"same_music_tooltip\")));\n        self.scale_seconds_same_music.set_tooltip_text(Some(&flg!(\"same_music_tooltip\")));\n        self.scale_similarity_similar_videos.set_tooltip_text(Some(&flg!(\"same_music_tooltip\")));\n\n        {\n            let hash_size_index = self.combo_box_image_hash_size.active().expect(\"Some hash size must be active\") as usize;\n            let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index];\n            match hash_size {\n                8 => {\n                    self.label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[0][5], 8));\n                }\n                16 => {\n                    self.label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[1][5], 16));\n                }\n                32 => {\n                    self.label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[2][5], 32));\n                }\n                64 => {\n                    self.label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[3][5], 64));\n                }\n                _ => panic!(),\n            }\n        }\n\n        let vec_children: Vec<Widget> = self.notebook_main.get_all_direct_children();\n        let vec_children: Vec<Widget> = vec_children[1].get_all_direct_children();\n\n        // Change name of main notebook tabs\n        for (main_enum, fl_thing) in [\n            (NotebookMainEnum::Duplicate as usize, flg!(\"main_notebook_duplicates\")),\n            (NotebookMainEnum::EmptyDirectories as usize, flg!(\"main_notebook_empty_directories\")),\n            (NotebookMainEnum::BigFiles as usize, flg!(\"main_notebook_big_files\")),\n            (NotebookMainEnum::EmptyFiles as usize, flg!(\"main_notebook_empty_files\")),\n            (NotebookMainEnum::Temporary as usize, flg!(\"main_notebook_temporary\")),\n            (NotebookMainEnum::SimilarImages as usize, flg!(\"main_notebook_similar_images\")),\n            (NotebookMainEnum::SimilarVideos as usize, flg!(\"main_notebook_similar_videos\")),\n            (NotebookMainEnum::SameMusic as usize, flg!(\"main_notebook_same_music\")),\n            (NotebookMainEnum::Symlinks as usize, flg!(\"main_notebook_symlinks\")),\n            (NotebookMainEnum::BrokenFiles as usize, flg!(\"main_notebook_broken_files\")),\n            (NotebookMainEnum::BadExtensions as usize, flg!(\"main_notebook_bad_extensions\")),\n        ] {\n            let tabel = self.notebook_main.tab_label(&vec_children[main_enum]);\n\n            if let Some(tabel) = tabel {\n                tabel.downcast::<Label>().expect(\"Tab label must be a label\").set_text(&fl_thing);\n            } else {\n                error!(\"Tab label for main notebook not found for enum {main_enum:?}, message {fl_thing:?}\");\n            }\n        }\n\n        // Change names of columns\n        let mut names_of_columns: HashMap<NotebookMainEnum, Vec<String>> = HashMap::new();\n\n        names_of_columns.insert(\n            NotebookMainEnum::Duplicate,\n            vec![\n                flg!(\"main_tree_view_column_size\"),\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::EmptyDirectories,\n            vec![\n                flg!(\"main_tree_view_column_folder_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::BigFiles,\n            vec![\n                flg!(\"main_tree_view_column_size\"),\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::EmptyFiles,\n            vec![\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::Temporary,\n            vec![\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::SimilarImages,\n            vec![\n                flg!(\"main_tree_view_column_similarity\"),\n                flg!(\"main_tree_view_column_size\"),\n                flg!(\"main_tree_view_column_dimensions\"),\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::SimilarVideos,\n            vec![\n                flg!(\"main_tree_view_column_size\"),\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n                flg!(\"main_tree_view_column_fps\"),\n                flg!(\"main_tree_view_column_codec\"),\n                flg!(\"main_tree_view_column_bitrate\"),\n                flg!(\"main_tree_view_column_dimensions\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::SameMusic,\n            vec![\n                flg!(\"main_tree_view_column_size\"),\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_title\"),\n                flg!(\"main_tree_view_column_artist\"),\n                flg!(\"main_tree_view_column_year\"),\n                flg!(\"main_tree_view_column_bitrate\"),\n                flg!(\"main_tree_view_column_length\"),\n                flg!(\"main_tree_view_column_genre\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::Symlinks,\n            vec![\n                flg!(\"main_tree_view_column_symlink_file_name\"),\n                flg!(\"main_tree_view_column_symlink_folder\"),\n                flg!(\"main_tree_view_column_destination_path\"),\n                flg!(\"main_tree_view_column_type_of_error\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::BrokenFiles,\n            vec![\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_type_of_error\"),\n                flg!(\"main_tree_view_column_modification\"),\n            ],\n        );\n        names_of_columns.insert(\n            NotebookMainEnum::BadExtensions,\n            vec![\n                flg!(\"main_tree_view_column_file_name\"),\n                flg!(\"main_tree_view_column_path\"),\n                flg!(\"main_tree_view_column_current_extension\"),\n                flg!(\"main_tree_view_column_proper_extensions\"),\n                // flg!(\"main_tree_view_column_modification\"), // TODO - too much data?\n            ],\n        );\n\n        for (key_enum, columns_names) in names_of_columns {\n            let s = &self.common_tree_views.get_subview(key_enum);\n\n            // Skipping first column because it is selection button\n            assert_eq!(\n                columns_names.len() + 1,\n                s.tree_view.columns().len(),\n                \"Number of columns in tree view and names do not match for {:?}, tree_view - {:?}\",\n                key_enum,\n                s.tree_view.widget_name()\n            );\n            for (column, name) in s.tree_view.columns().iter().skip(1).zip(columns_names.iter()) {\n                column.set_title(name);\n            }\n        }\n\n        {\n            let active = self.combo_box_audio_check_type.active().unwrap_or(0);\n            self.combo_box_audio_check_type.remove_all();\n            for i in &AUDIO_TYPE_CHECK_METHOD_COMBO_BOX {\n                let text = match i.check_method {\n                    CheckingMethod::AudioTags => flg!(\"music_checking_by_tags\"),\n                    CheckingMethod::AudioContent => flg!(\"music_checking_by_content\"),\n                    _ => panic!(),\n                };\n                self.combo_box_audio_check_type.append_text(&text);\n            }\n            self.combo_box_audio_check_type.set_active(Some(active));\n        }\n        {\n            let active = self.combo_box_duplicate_check_method.active().unwrap_or(0);\n            self.combo_box_duplicate_check_method.remove_all();\n            for i in &DUPLICATES_CHECK_METHOD_COMBO_BOX {\n                let text = match i.check_method {\n                    CheckingMethod::Hash => flg!(\"duplicate_mode_hash_combo_box\"),\n                    CheckingMethod::Size => flg!(\"duplicate_mode_size_combo_box\"),\n                    CheckingMethod::Name => flg!(\"duplicate_mode_name_combo_box\"),\n                    CheckingMethod::SizeName => flg!(\"duplicate_mode_size_name_combo_box\"),\n                    _ => panic!(),\n                };\n                self.combo_box_duplicate_check_method.append_text(&text);\n            }\n            self.combo_box_duplicate_check_method.set_active(Some(active));\n        }\n        {\n            let active = self.combo_box_big_files_mode.active().unwrap_or(0);\n            self.combo_box_big_files_mode.remove_all();\n            for i in &BIG_FILES_CHECK_METHOD_COMBO_BOX {\n                let text = match i.check_method {\n                    SearchMode::SmallestFiles => flg!(\"big_files_mode_smallest_combo_box\"),\n                    SearchMode::BiggestFiles => flg!(\"big_files_mode_biggest_combo_box\"),\n                };\n                self.combo_box_big_files_mode.append_text(&text);\n            }\n            self.combo_box_big_files_mode.set_active(Some(active));\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_popovers_select.rs",
    "content": "use gtk4::Builder;\nuse gtk4::prelude::*;\n\nuse crate::flg;\n\n#[derive(Clone)]\npub struct GuiSelectPopovers {\n    pub buttons_popover_select_all: gtk4::Button,\n    pub buttons_popover_unselect_all: gtk4::Button,\n    pub buttons_popover_reverse: gtk4::Button,\n    pub buttons_popover_select_all_except_shortest_path: gtk4::Button,\n    pub buttons_popover_select_all_except_longest_path: gtk4::Button,\n    pub buttons_popover_select_all_except_oldest: gtk4::Button,\n    pub buttons_popover_select_all_except_newest: gtk4::Button,\n    pub buttons_popover_select_one_oldest: gtk4::Button,\n    pub buttons_popover_select_one_newest: gtk4::Button,\n    pub buttons_popover_select_custom: gtk4::Button,\n    pub buttons_popover_unselect_custom: gtk4::Button,\n    pub buttons_popover_select_all_images_except_biggest: gtk4::Button,\n    pub buttons_popover_select_all_images_except_smallest: gtk4::Button,\n\n    pub separator_select_image_size: gtk4::Separator,\n    pub separator_select_reverse: gtk4::Separator,\n    pub separator_select_date: gtk4::Separator,\n    pub separator_select_custom: gtk4::Separator,\n    pub separator_select_shortest_path: gtk4::Separator,\n\n    #[expect(unused)]\n    pub buttons_popover_right_click_open_file: gtk4::Button,\n    #[expect(unused)]\n    pub buttons_popover_right_click_open_folder: gtk4::Button,\n\n    pub popover_select: gtk4::Popover,\n    #[expect(unused)]\n    pub popover_right_click: gtk4::Popover,\n}\n\nimpl GuiSelectPopovers {\n    pub(crate) fn create_from_builder() -> Self {\n        let glade_src = include_str!(\"../../ui/popover_select.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let buttons_popover_select_all: gtk4::Button = builder.object(\"buttons_popover_select_all\").expect(\"Cambalache\");\n        let buttons_popover_unselect_all: gtk4::Button = builder.object(\"buttons_popover_unselect_all\").expect(\"Cambalache\");\n        let buttons_popover_reverse: gtk4::Button = builder.object(\"buttons_popover_reverse\").expect(\"Cambalache\");\n        let buttons_popover_select_all_except_shortest_path: gtk4::Button = builder.object(\"buttons_popover_select_all_except_shortest_path\").expect(\"Cambalache\");\n        let buttons_popover_select_all_except_longest_path: gtk4::Button = builder.object(\"buttons_popover_select_all_except_longest_path\").expect(\"Cambalache\");\n        let buttons_popover_select_all_except_oldest: gtk4::Button = builder.object(\"buttons_popover_select_all_except_oldest\").expect(\"Cambalache\");\n        let buttons_popover_select_all_except_newest: gtk4::Button = builder.object(\"buttons_popover_select_all_except_newest\").expect(\"Cambalache\");\n        let buttons_popover_select_one_oldest: gtk4::Button = builder.object(\"buttons_popover_select_one_oldest\").expect(\"Cambalache\");\n        let buttons_popover_select_one_newest: gtk4::Button = builder.object(\"buttons_popover_select_one_newest\").expect(\"Cambalache\");\n        let buttons_popover_select_custom: gtk4::Button = builder.object(\"buttons_popover_select_custom\").expect(\"Cambalache\");\n        let buttons_popover_unselect_custom: gtk4::Button = builder.object(\"buttons_popover_unselect_custom\").expect(\"Cambalache\");\n        let buttons_popover_select_all_images_except_biggest: gtk4::Button = builder.object(\"buttons_popover_select_all_images_except_biggest\").expect(\"Cambalache\");\n        let buttons_popover_select_all_images_except_smallest: gtk4::Button = builder.object(\"buttons_popover_select_all_images_except_smallest\").expect(\"Cambalache\");\n\n        let separator_select_image_size: gtk4::Separator = builder.object(\"separator_select_image_size\").expect(\"Cambalache\");\n        let separator_select_reverse: gtk4::Separator = builder.object(\"separator_select_reverse\").expect(\"Cambalache\");\n        let separator_select_date: gtk4::Separator = builder.object(\"separator_select_date\").expect(\"Cambalache\");\n        let separator_select_custom: gtk4::Separator = builder.object(\"separator_select_custom\").expect(\"Cambalache\");\n        let separator_select_shortest_path: gtk4::Separator = builder.object(\"separator_select_shortest_path\").expect(\"Cambalache\");\n\n        let popover_select: gtk4::Popover = builder.object(\"popover_select\").expect(\"Cambalache\");\n\n        // Popover right click(not implemented for now)\n        let glade_src = include_str!(\"../../ui/popover_right_click.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let buttons_popover_right_click_open_file: gtk4::Button = builder.object(\"buttons_popover_right_click_open_file\").expect(\"Cambalache\");\n        let buttons_popover_right_click_open_folder: gtk4::Button = builder.object(\"buttons_popover_right_click_open_folder\").expect(\"Cambalache\");\n\n        let popover_right_click: gtk4::Popover = builder.object(\"popover_right_click\").expect(\"Cambalache\");\n\n        Self {\n            buttons_popover_select_all,\n            buttons_popover_unselect_all,\n            buttons_popover_reverse,\n            buttons_popover_select_all_except_shortest_path,\n            buttons_popover_select_all_except_longest_path,\n            buttons_popover_select_all_except_oldest,\n            buttons_popover_select_all_except_newest,\n            buttons_popover_select_one_oldest,\n            buttons_popover_select_one_newest,\n            buttons_popover_select_custom,\n            buttons_popover_unselect_custom,\n            buttons_popover_select_all_images_except_biggest,\n            buttons_popover_select_all_images_except_smallest,\n            separator_select_image_size,\n            separator_select_reverse,\n            separator_select_date,\n            separator_select_custom,\n            separator_select_shortest_path,\n            buttons_popover_right_click_open_file,\n            buttons_popover_right_click_open_folder,\n            popover_select,\n            popover_right_click,\n        }\n    }\n    pub(crate) fn update_language(&self) {\n        self.buttons_popover_select_all.set_label(&flg!(\"popover_select_all\"));\n        self.buttons_popover_unselect_all.set_label(&flg!(\"popover_unselect_all\"));\n        self.buttons_popover_reverse.set_label(&flg!(\"popover_reverse\"));\n        self.buttons_popover_select_all_except_shortest_path\n            .set_label(&flg!(\"popover_select_all_except_shortest_path\"));\n        self.buttons_popover_select_all_except_longest_path\n            .set_label(&flg!(\"popover_select_all_except_longest_path\"));\n        self.buttons_popover_select_all_except_oldest.set_label(&flg!(\"popover_select_all_except_oldest\"));\n        self.buttons_popover_select_all_except_newest.set_label(&flg!(\"popover_select_all_except_newest\"));\n        self.buttons_popover_select_one_oldest.set_label(&flg!(\"popover_select_one_oldest\"));\n        self.buttons_popover_select_one_newest.set_label(&flg!(\"popover_select_one_newest\"));\n        self.buttons_popover_select_custom.set_label(&flg!(\"popover_select_custom\"));\n        self.buttons_popover_unselect_custom.set_label(&flg!(\"popover_unselect_custom\"));\n        self.buttons_popover_select_all_images_except_biggest\n            .set_label(&flg!(\"popover_select_all_images_except_biggest\"));\n        self.buttons_popover_select_all_images_except_smallest\n            .set_label(&flg!(\"popover_select_all_images_except_smallest\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_popovers_sort.rs",
    "content": "use gtk4::Builder;\nuse gtk4::prelude::*;\n\nuse crate::flg;\n\n#[derive(Clone)]\npub struct GuiSortPopovers {\n    pub buttons_popover_sort_file_name: gtk4::Button,\n    pub buttons_popover_sort_folder_name: gtk4::Button,\n    pub buttons_popover_sort_full_name: gtk4::Button,\n    pub buttons_popover_sort_size: gtk4::Button,\n    pub buttons_popover_sort_selection: gtk4::Button,\n\n    pub popover_sort: gtk4::Popover,\n}\n\nimpl GuiSortPopovers {\n    pub(crate) fn create_from_builder() -> Self {\n        let glade_src = include_str!(\"../../ui/popover_sort.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let buttons_popover_sort_file_name: gtk4::Button = builder.object(\"buttons_popover_sort_file_name\").expect(\"Cambalache\");\n        let buttons_popover_sort_folder_name: gtk4::Button = builder.object(\"buttons_popover_sort_folder_name\").expect(\"Cambalache\");\n        let buttons_popover_sort_full_name: gtk4::Button = builder.object(\"buttons_popover_sort_full_name\").expect(\"Cambalache\");\n        let buttons_popover_sort_size: gtk4::Button = builder.object(\"buttons_popover_sort_size\").expect(\"Cambalache\");\n        let buttons_popover_sort_selection: gtk4::Button = builder.object(\"buttons_popover_sort_selection\").expect(\"Cambalache\");\n\n        let popover_sort: gtk4::Popover = builder.object(\"popover_sort\").expect(\"Cambalache\");\n\n        Self {\n            buttons_popover_sort_file_name,\n            buttons_popover_sort_folder_name,\n            buttons_popover_sort_full_name,\n            buttons_popover_sort_size,\n            buttons_popover_sort_selection,\n            popover_sort,\n        }\n    }\n    pub(crate) fn update_language(&self) {\n        self.buttons_popover_sort_file_name.set_label(&flg!(\"popover_sort_file_name\"));\n        self.buttons_popover_sort_folder_name.set_label(&flg!(\"popover_sort_folder_name\"));\n        self.buttons_popover_sort_full_name.set_label(&flg!(\"popover_sort_full_name\"));\n        self.buttons_popover_sort_size.set_label(&flg!(\"popover_sort_size\"));\n        self.buttons_popover_sort_selection.set_label(&flg!(\"popover_sort_selection\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_progress_dialog.rs",
    "content": "use gtk4::prelude::*;\nuse gtk4::{Builder, EventControllerKey, Label, Window};\n\nuse crate::gtk_traits::WidgetTraits;\nuse crate::helpers::image_operations::set_icon_of_button;\nuse crate::{CZK_ICON_STOP, flg};\n\n#[derive(Clone)]\npub struct GuiProgressDialog {\n    pub window_progress: gtk4::Dialog,\n\n    pub progress_bar_current_stage: gtk4::ProgressBar,\n    pub progress_bar_all_stages: gtk4::ProgressBar,\n\n    pub label_stage: gtk4::Label,\n    pub label_progress_current_stage: gtk4::Label,\n    pub label_progress_all_stages: gtk4::Label,\n\n    pub grid_progress: gtk4::Grid,\n\n    pub button_stop_in_dialog: gtk4::Button,\n    pub evk_button_stop_in_dialog: EventControllerKey,\n}\n\nimpl GuiProgressDialog {\n    pub(crate) fn create_from_builder(window_main: &Window) -> Self {\n        let glade_src = include_str!(\"../../ui/progress.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let window_progress: gtk4::Dialog = builder.object(\"window_progress\").expect(\"Cambalache\");\n        window_progress.set_title(Some(&flg!(\"window_progress_title\")));\n        window_progress.set_transient_for(Some(window_main));\n        window_progress.set_modal(true);\n\n        let progress_bar_current_stage: gtk4::ProgressBar = builder.object(\"progress_bar_current_stage\").expect(\"Cambalache\");\n        let progress_bar_all_stages: gtk4::ProgressBar = builder.object(\"progress_bar_all_stages\").expect(\"Cambalache\");\n\n        let label_stage: gtk4::Label = builder.object(\"label_stage\").expect(\"Cambalache\");\n        let label_progress_current_stage: gtk4::Label = builder.object(\"label_progress_current_stage\").expect(\"Cambalache\");\n        let label_progress_all_stages: gtk4::Label = builder.object(\"label_progress_all_stages\").expect(\"Cambalache\");\n\n        let grid_progress: gtk4::Grid = builder.object(\"grid_progress\").expect(\"Cambalache\");\n\n        let button_stop_in_dialog: gtk4::Button = builder.object(\"button_stop_in_dialog\").expect(\"Cambalache\");\n        let evk_button_stop_in_dialog = EventControllerKey::new();\n        button_stop_in_dialog.add_controller(evk_button_stop_in_dialog.clone());\n\n        set_icon_of_button(&button_stop_in_dialog, CZK_ICON_STOP);\n\n        Self {\n            window_progress,\n            progress_bar_current_stage,\n            progress_bar_all_stages,\n            label_stage,\n            label_progress_current_stage,\n            label_progress_all_stages,\n            grid_progress,\n            button_stop_in_dialog,\n            evk_button_stop_in_dialog,\n        }\n    }\n    pub(crate) fn update_language(&self) {\n        self.window_progress.set_title(Some(&flg!(\"window_progress_title\")));\n\n        self.button_stop_in_dialog.get_widget_of_type::<Label>(true).set_text(&flg!(\"progress_stop_button\"));\n\n        self.label_progress_current_stage.set_label(&flg!(\"progress_current_stage\"));\n        self.label_progress_all_stages.set_label(&flg!(\"progress_all_stages\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_settings.rs",
    "content": "use gtk4::prelude::*;\nuse gtk4::{Builder, Window};\n\nuse crate::flg;\nuse crate::gtk_traits::WidgetTraits;\n\n#[derive(Clone)]\npub struct GuiSettings {\n    pub window_settings: Window,\n\n    pub notebook_settings: gtk4::Notebook,\n\n    // General\n    pub check_button_settings_save_at_exit: gtk4::CheckButton,\n    pub check_button_settings_load_at_start: gtk4::CheckButton,\n    pub check_button_settings_confirm_deletion: gtk4::CheckButton,\n    pub check_button_settings_confirm_link: gtk4::CheckButton,\n    pub check_button_settings_confirm_group_deletion: gtk4::CheckButton,\n    pub check_button_settings_show_text_view: gtk4::CheckButton,\n    pub check_button_settings_use_cache: gtk4::CheckButton,\n    pub check_button_settings_save_also_json: gtk4::CheckButton,\n    pub check_button_settings_use_trash: gtk4::CheckButton,\n    pub label_settings_general_language: gtk4::Label,\n    pub combo_box_settings_language: gtk4::ComboBoxText,\n    pub check_button_settings_one_filesystem: gtk4::CheckButton,\n    pub label_settings_number_of_threads: gtk4::Label,\n    pub scale_settings_number_of_threads: gtk4::Scale,\n    pub label_restart_needed: gtk4::Label,\n    pub check_button_settings_use_rust_preview: gtk4::CheckButton,\n\n    // Duplicates\n    pub check_button_settings_hide_hard_links: gtk4::CheckButton,\n    pub entry_settings_cache_file_minimal_size: gtk4::Entry,\n    pub entry_settings_prehash_cache_file_minimal_size: gtk4::Entry,\n    pub check_button_duplicates_use_prehash_cache: gtk4::CheckButton,\n    pub check_button_settings_show_preview_duplicates: gtk4::CheckButton,\n    pub check_button_settings_duplicates_delete_outdated_cache: gtk4::CheckButton,\n    pub button_settings_duplicates_clear_cache: gtk4::Button,\n    pub label_settings_duplicate_minimal_size_cache: gtk4::Label,\n    pub label_settings_duplicate_minimal_size_cache_prehash: gtk4::Label,\n\n    // Similar Images\n    pub check_button_settings_show_preview_similar_images: gtk4::CheckButton,\n    pub check_button_settings_similar_images_delete_outdated_cache: gtk4::CheckButton,\n    pub button_settings_similar_images_clear_cache: gtk4::Button,\n\n    // Similar Videos\n    pub check_button_settings_similar_videos_delete_outdated_cache: gtk4::CheckButton,\n    pub button_settings_similar_videos_clear_cache: gtk4::Button,\n\n    // Buttons\n    pub button_settings_save_configuration: gtk4::Button,\n    pub button_settings_load_configuration: gtk4::Button,\n    pub button_settings_reset_configuration: gtk4::Button,\n\n    pub button_settings_open_cache_folder: gtk4::Button,\n    pub button_settings_open_settings_folder: gtk4::Button,\n}\n\nimpl GuiSettings {\n    pub(crate) fn create_from_builder(window_main: &Window) -> Self {\n        let glade_src = include_str!(\"../../ui/settings.ui\").to_string();\n        let builder = Builder::from_string(glade_src.as_str());\n\n        let window_settings: Window = builder.object(\"window_settings\").expect(\"Cambalache\");\n        window_settings.set_title(Some(&flg!(\"window_settings_title\")));\n        window_settings.set_modal(true);\n        window_settings.set_transient_for(Some(window_main));\n\n        let notebook_settings: gtk4::Notebook = builder.object(\"notebook_settings\").expect(\"Cambalache\");\n\n        // General\n        let check_button_settings_one_filesystem: gtk4::CheckButton = builder.object(\"check_button_settings_one_filesystem\").expect(\"Cambalache\");\n        let check_button_settings_save_at_exit: gtk4::CheckButton = builder.object(\"check_button_settings_save_at_exit\").expect(\"Cambalache\");\n        let check_button_settings_load_at_start: gtk4::CheckButton = builder.object(\"check_button_settings_load_at_start\").expect(\"Cambalache\");\n        let check_button_settings_confirm_deletion: gtk4::CheckButton = builder.object(\"check_button_settings_confirm_deletion\").expect(\"Cambalache\");\n        let check_button_settings_confirm_link: gtk4::CheckButton = builder.object(\"check_button_settings_confirm_link\").expect(\"Cambalache\");\n        let check_button_settings_confirm_group_deletion: gtk4::CheckButton = builder.object(\"check_button_settings_confirm_group_deletion\").expect(\"Cambalache\");\n        let check_button_settings_show_text_view: gtk4::CheckButton = builder.object(\"check_button_settings_show_text_view\").expect(\"Cambalache\");\n        let check_button_settings_use_cache: gtk4::CheckButton = builder.object(\"check_button_settings_use_cache\").expect(\"Cambalache\");\n        let check_button_settings_save_also_json: gtk4::CheckButton = builder.object(\"check_button_settings_save_also_json\").expect(\"Cambalache\");\n        let check_button_settings_use_trash: gtk4::CheckButton = builder.object(\"check_button_settings_use_trash\").expect(\"Cambalache\");\n        let label_settings_general_language: gtk4::Label = builder.object(\"label_settings_general_language\").expect(\"Cambalache\");\n        let combo_box_settings_language: gtk4::ComboBoxText = builder.object(\"combo_box_settings_language\").expect(\"Cambalache\");\n        let label_settings_number_of_threads: gtk4::Label = builder.object(\"label_settings_number_of_threads\").expect(\"Cambalache\");\n        let scale_settings_number_of_threads: gtk4::Scale = builder.object(\"scale_settings_number_of_threads\").expect(\"Cambalache\");\n        let label_restart_needed: gtk4::Label = builder.object(\"label_restart_needed\").expect(\"Cambalache\");\n        let check_button_settings_use_rust_preview: gtk4::CheckButton = builder.object(\"check_button_settings_use_rust_preview\").expect(\"Cambalache\");\n\n        // Duplicates\n        let check_button_settings_hide_hard_links: gtk4::CheckButton = builder.object(\"check_button_settings_hide_hard_links\").expect(\"Cambalache\");\n        let entry_settings_cache_file_minimal_size: gtk4::Entry = builder.object(\"entry_settings_cache_file_minimal_size\").expect(\"Cambalache\");\n        let check_button_settings_show_preview_duplicates: gtk4::CheckButton = builder.object(\"check_button_settings_show_preview_duplicates\").expect(\"Cambalache\");\n        let check_button_settings_duplicates_delete_outdated_cache: gtk4::CheckButton =\n            builder.object(\"check_button_settings_duplicates_delete_outdated_cache\").expect(\"Cambalache\");\n        let button_settings_duplicates_clear_cache: gtk4::Button = builder.object(\"button_settings_duplicates_clear_cache\").expect(\"Cambalache\");\n        let check_button_duplicates_use_prehash_cache: gtk4::CheckButton = builder.object(\"check_button_duplicates_use_prehash_cache\").expect(\"Cambalache\");\n        let entry_settings_prehash_cache_file_minimal_size: gtk4::Entry = builder.object(\"entry_settings_prehash_cache_file_minimal_size\").expect(\"Cambalache\");\n        let label_settings_duplicate_minimal_size_cache: gtk4::Label = builder.object(\"label_settings_duplicate_minimal_size_cache\").expect(\"Cambalache\");\n        let label_settings_duplicate_minimal_size_cache_prehash: gtk4::Label = builder.object(\"label_settings_duplicate_minimal_size_cache_prehash\").expect(\"Cambalache\");\n\n        // Similar Images\n        let check_button_settings_show_preview_similar_images: gtk4::CheckButton = builder.object(\"check_button_settings_show_preview_similar_images\").expect(\"Cambalache\");\n        let check_button_settings_similar_images_delete_outdated_cache: gtk4::CheckButton =\n            builder.object(\"check_button_settings_similar_images_delete_outdated_cache\").expect(\"Cambalache\");\n        let button_settings_similar_images_clear_cache: gtk4::Button = builder.object(\"button_settings_similar_images_clear_cache\").expect(\"Cambalache\");\n\n        // Similar Videos\n        let check_button_settings_similar_videos_delete_outdated_cache: gtk4::CheckButton =\n            builder.object(\"check_button_settings_similar_videos_delete_outdated_cache\").expect(\"Cambalache\");\n        let button_settings_similar_videos_clear_cache: gtk4::Button = builder.object(\"button_settings_similar_videos_clear_cache\").expect(\"Cambalache\");\n\n        // Saving/Loading/Resetting configuration\n        let button_settings_save_configuration: gtk4::Button = builder.object(\"button_settings_save_configuration\").expect(\"Cambalache\");\n        let button_settings_load_configuration: gtk4::Button = builder.object(\"button_settings_load_configuration\").expect(\"Cambalache\");\n        let button_settings_reset_configuration: gtk4::Button = builder.object(\"button_settings_reset_configuration\").expect(\"Cambalache\");\n\n        let button_settings_open_cache_folder: gtk4::Button = builder.object(\"button_settings_open_cache_folder\").expect(\"Cambalache\");\n        let button_settings_open_settings_folder: gtk4::Button = builder.object(\"button_settings_open_settings_folder\").expect(\"Cambalache\");\n\n        Self {\n            window_settings,\n            notebook_settings,\n            check_button_settings_save_at_exit,\n            check_button_settings_load_at_start,\n            check_button_settings_confirm_deletion,\n            check_button_settings_confirm_link,\n            check_button_settings_confirm_group_deletion,\n            check_button_settings_show_text_view,\n            check_button_settings_use_cache,\n            check_button_settings_save_also_json,\n            check_button_settings_use_trash,\n            label_settings_general_language,\n            combo_box_settings_language,\n            check_button_settings_one_filesystem,\n            label_settings_number_of_threads,\n            scale_settings_number_of_threads,\n            label_restart_needed,\n            check_button_settings_use_rust_preview,\n            check_button_settings_hide_hard_links,\n            entry_settings_cache_file_minimal_size,\n            entry_settings_prehash_cache_file_minimal_size,\n            check_button_duplicates_use_prehash_cache,\n            check_button_settings_show_preview_duplicates,\n            check_button_settings_duplicates_delete_outdated_cache,\n            button_settings_duplicates_clear_cache,\n            label_settings_duplicate_minimal_size_cache,\n            label_settings_duplicate_minimal_size_cache_prehash,\n            check_button_settings_show_preview_similar_images,\n            check_button_settings_similar_images_delete_outdated_cache,\n            button_settings_similar_images_clear_cache,\n            check_button_settings_similar_videos_delete_outdated_cache,\n            button_settings_similar_videos_clear_cache,\n            button_settings_save_configuration,\n            button_settings_load_configuration,\n            button_settings_reset_configuration,\n            button_settings_open_cache_folder,\n            button_settings_open_settings_folder,\n        }\n    }\n\n    pub(crate) fn update_language(&self) {\n        self.window_settings.set_title(Some(&flg!(\"window_settings_title\")));\n\n        if !self.label_restart_needed.label().is_empty() {\n            self.label_restart_needed.set_label(&flg!(\"settings_label_restart\"));\n        }\n\n        self.check_button_settings_save_at_exit.set_label(Some(&flg!(\"settings_save_at_exit_button\")));\n        self.check_button_settings_load_at_start.set_label(Some(&flg!(\"settings_load_at_start_button\")));\n        self.check_button_settings_confirm_deletion.set_label(Some(&flg!(\"settings_confirm_deletion_button\")));\n        self.check_button_settings_confirm_link.set_label(Some(&flg!(\"settings_confirm_link_button\")));\n        self.check_button_settings_confirm_group_deletion\n            .set_label(Some(&flg!(\"settings_confirm_group_deletion_button\")));\n        self.check_button_settings_show_text_view.set_label(Some(&flg!(\"settings_show_text_view_button\")));\n        self.check_button_settings_use_cache.set_label(Some(&flg!(\"settings_use_cache_button\")));\n        self.check_button_settings_save_also_json.set_label(Some(&flg!(\"settings_save_also_as_json_button\")));\n        self.check_button_settings_use_trash.set_label(Some(&flg!(\"settings_use_trash_button\")));\n        self.label_settings_general_language.set_label(&flg!(\"settings_language_label\"));\n        self.check_button_settings_one_filesystem.set_label(Some(&flg!(\"settings_ignore_other_filesystems\")));\n        self.label_settings_number_of_threads.set_label(&flg!(\"settings_number_of_threads\"));\n        self.check_button_settings_use_rust_preview.set_label(Some(&flg!(\"settings_use_rust_preview\")));\n\n        self.check_button_settings_save_at_exit\n            .set_tooltip_text(Some(&flg!(\"settings_save_at_exit_button_tooltip\")));\n        self.check_button_settings_load_at_start\n            .set_tooltip_text(Some(&flg!(\"settings_load_at_start_button_tooltip\")));\n        self.check_button_settings_confirm_deletion\n            .set_tooltip_text(Some(&flg!(\"settings_confirm_deletion_button_tooltip\")));\n        self.check_button_settings_confirm_link\n            .set_tooltip_text(Some(&flg!(\"settings_confirm_link_button_tooltip\")));\n        self.check_button_settings_confirm_group_deletion\n            .set_tooltip_text(Some(&flg!(\"settings_confirm_group_deletion_button_tooltip\")));\n        self.check_button_settings_show_text_view\n            .set_tooltip_text(Some(&flg!(\"settings_show_text_view_button_tooltip\")));\n        self.check_button_settings_save_also_json\n            .set_tooltip_text(Some(&flg!(\"settings_save_also_as_json_button_tooltip\")));\n        self.check_button_settings_use_cache.set_tooltip_text(Some(&flg!(\"settings_use_cache_button_tooltip\")));\n        self.check_button_settings_use_trash.set_tooltip_text(Some(&flg!(\"settings_use_trash_button_tooltip\")));\n        self.label_settings_general_language.set_tooltip_text(Some(&flg!(\"settings_language_label_tooltip\")));\n        self.check_button_settings_one_filesystem\n            .set_tooltip_text(Some(&flg!(\"settings_ignore_other_filesystems_tooltip\")));\n        self.scale_settings_number_of_threads.set_tooltip_text(Some(&flg!(\"settings_number_of_threads_tooltip\")));\n        self.check_button_settings_use_rust_preview\n            .set_tooltip_text(Some(&flg!(\"settings_use_rust_preview_tooltip\")));\n\n        self.check_button_settings_hide_hard_links\n            .set_label(Some(&flg!(\"settings_duplicates_hide_hard_link_button\")));\n        self.check_button_settings_show_preview_duplicates\n            .set_label(Some(&flg!(\"settings_multiple_image_preview_checkbutton\")));\n        self.check_button_settings_duplicates_delete_outdated_cache\n            .set_label(Some(&flg!(\"settings_multiple_delete_outdated_cache_checkbutton\")));\n        self.button_settings_duplicates_clear_cache.set_label(&flg!(\"settings_multiple_clear_cache_button\"));\n        self.check_button_duplicates_use_prehash_cache\n            .set_label(Some(&flg!(\"settings_duplicates_prehash_checkbutton\")));\n        self.label_settings_duplicate_minimal_size_cache\n            .set_label(&flg!(\"settings_duplicates_minimal_size_cache_label\"));\n        self.label_settings_duplicate_minimal_size_cache_prehash\n            .set_label(&flg!(\"settings_duplicates_minimal_size_cache_prehash_label\"));\n\n        self.check_button_settings_hide_hard_links\n            .set_tooltip_text(Some(&flg!(\"settings_duplicates_hide_hard_link_button_tooltip\")));\n        self.entry_settings_cache_file_minimal_size\n            .set_tooltip_text(Some(&flg!(\"settings_duplicates_minimal_size_entry_tooltip\")));\n        self.check_button_settings_show_preview_duplicates\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_image_preview_checkbutton_tooltip\")));\n        self.check_button_settings_duplicates_delete_outdated_cache\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_delete_outdated_cache_checkbutton_tooltip\")));\n        self.button_settings_duplicates_clear_cache\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_clear_cache_button_tooltip\")));\n        self.check_button_duplicates_use_prehash_cache\n            .set_tooltip_text(Some(&flg!(\"settings_duplicates_prehash_checkbutton_tooltip\")));\n        self.entry_settings_prehash_cache_file_minimal_size\n            .set_tooltip_text(Some(&flg!(\"settings_duplicates_prehash_minimal_entry_tooltip\")));\n\n        self.check_button_settings_show_preview_similar_images\n            .set_label(Some(&flg!(\"settings_multiple_image_preview_checkbutton\")));\n        self.check_button_settings_similar_images_delete_outdated_cache\n            .set_label(Some(&flg!(\"settings_multiple_delete_outdated_cache_checkbutton\")));\n        self.button_settings_similar_images_clear_cache.set_label(&flg!(\"settings_multiple_clear_cache_button\"));\n\n        self.check_button_settings_show_preview_similar_images\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_image_preview_checkbutton_tooltip\")));\n        self.check_button_settings_similar_images_delete_outdated_cache\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_delete_outdated_cache_checkbutton_tooltip\")));\n        self.button_settings_similar_images_clear_cache\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_clear_cache_button_tooltip\")));\n\n        self.check_button_settings_similar_videos_delete_outdated_cache\n            .set_label(Some(&flg!(\"settings_multiple_delete_outdated_cache_checkbutton\")));\n        self.button_settings_similar_videos_clear_cache.set_label(&flg!(\"settings_multiple_clear_cache_button\"));\n\n        self.check_button_settings_similar_videos_delete_outdated_cache\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_delete_outdated_cache_checkbutton_tooltip\")));\n        self.button_settings_similar_videos_clear_cache\n            .set_tooltip_text(Some(&flg!(\"settings_multiple_clear_cache_button_tooltip\")));\n\n        self.button_settings_save_configuration.set_label(&flg!(\"settings_saving_button\"));\n        self.button_settings_load_configuration.set_label(&flg!(\"settings_loading_button\"));\n        self.button_settings_reset_configuration.set_label(&flg!(\"settings_reset_button\"));\n\n        self.button_settings_save_configuration.set_tooltip_text(Some(&flg!(\"settings_saving_button_tooltip\")));\n        self.button_settings_load_configuration.set_tooltip_text(Some(&flg!(\"settings_loading_button_tooltip\")));\n        self.button_settings_reset_configuration.set_tooltip_text(Some(&flg!(\"settings_reset_button_tooltip\")));\n\n        self.button_settings_open_cache_folder.set_label(&flg!(\"settings_folder_cache_open\"));\n        self.button_settings_open_settings_folder.set_label(&flg!(\"settings_folder_settings_open\"));\n\n        self.button_settings_open_cache_folder.set_tooltip_text(Some(&flg!(\"settings_folder_cache_open_tooltip\")));\n        self.button_settings_open_settings_folder\n            .set_tooltip_text(Some(&flg!(\"settings_folder_settings_open_tooltip\")));\n\n        let vec_children: Vec<gtk4::Widget> = self.notebook_settings.get_all_direct_children();\n        let vec_children: Vec<gtk4::Widget> = vec_children[1].get_all_direct_children();\n\n        // Change name of main notebook tabs\n        let names: [String; 4] = [\n            flg!(\"settings_notebook_general\"),\n            flg!(\"settings_notebook_duplicates\"),\n            flg!(\"settings_notebook_images\"),\n            flg!(\"settings_notebook_videos\"),\n        ];\n        for (index, fl_thing) in names.iter().enumerate() {\n            self.notebook_settings\n                .tab_label(&vec_children[index])\n                .expect(\"Couldn't get tab label\")\n                .downcast::<gtk4::Label>()\n                .expect(\"Couldn't downcast to Label\")\n                .set_text(fl_thing);\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/gui_upper_notebook.rs",
    "content": "use gtk4::Label;\nuse gtk4::prelude::*;\n\nuse crate::gtk_traits::WidgetTraits;\nuse crate::gui_structs::common_upper_tree_view::{CommonUpperTreeViews, UpperSubView, UpperTreeViewEnum};\nuse crate::helpers::image_operations::set_icon_of_button;\nuse crate::notebook_enums::NotebookUpperEnum;\nuse crate::{CZK_ICON_ADD, CZK_ICON_DELETE, CZK_ICON_MANUAL_ADD, flg};\n\n#[derive(Clone)]\npub struct GuiUpperNotebook {\n    pub notebook_upper: gtk4::Notebook,\n\n    pub entry_excluded_items: gtk4::Entry,\n    pub entry_allowed_extensions: gtk4::Entry,\n    pub entry_excluded_extensions: gtk4::Entry,\n\n    pub check_button_recursive: gtk4::CheckButton,\n\n    pub buttons_manual_add_included_directory: gtk4::Button,\n    pub buttons_add_included_directory: gtk4::Button,\n    pub buttons_remove_included_directory: gtk4::Button,\n    pub buttons_manual_add_excluded_directory: gtk4::Button,\n    pub buttons_add_excluded_directory: gtk4::Button,\n    pub buttons_remove_excluded_directory: gtk4::Button,\n\n    pub label_excluded_items: gtk4::Label,\n    pub label_allowed_extensions: gtk4::Label,\n    pub label_excluded_extensions: gtk4::Label,\n\n    pub entry_general_minimal_size: gtk4::Entry,\n    pub entry_general_maximal_size: gtk4::Entry,\n    pub label_general_size_bytes: gtk4::Label,\n    pub label_general_min_size: gtk4::Label,\n    pub label_general_max_size: gtk4::Label,\n\n    pub common_upper_tree_views: CommonUpperTreeViews,\n}\n\nimpl GuiUpperNotebook {\n    pub(crate) fn create_from_builder(builder: &gtk4::Builder) -> Self {\n        let notebook_upper: gtk4::Notebook = builder.object(\"notebook_upper\").expect(\"Cambalache\");\n\n        let entry_allowed_extensions: gtk4::Entry = builder.object(\"entry_allowed_extensions\").expect(\"Cambalache\");\n        let entry_excluded_extensions: gtk4::Entry = builder.object(\"entry_excluded_extensions\").expect(\"Cambalache\");\n        let entry_excluded_items: gtk4::Entry = builder.object(\"entry_excluded_items\").expect(\"Cambalache\");\n\n        let check_button_recursive: gtk4::CheckButton = builder.object(\"check_button_recursive\").expect(\"Cambalache\");\n\n        let buttons_manual_add_included_directory: gtk4::Button = builder.object(\"buttons_manual_add_included_directory\").expect(\"Cambalache\");\n        let buttons_add_included_directory: gtk4::Button = builder.object(\"buttons_add_included_directory\").expect(\"Cambalache\");\n        let buttons_remove_included_directory: gtk4::Button = builder.object(\"buttons_remove_included_directory\").expect(\"Cambalache\");\n        let buttons_manual_add_excluded_directory: gtk4::Button = builder.object(\"buttons_manual_add_excluded_directory\").expect(\"Cambalache\");\n        let buttons_add_excluded_directory: gtk4::Button = builder.object(\"buttons_add_excluded_directory\").expect(\"Cambalache\");\n        let buttons_remove_excluded_directory: gtk4::Button = builder.object(\"buttons_remove_excluded_directory\").expect(\"Cambalache\");\n\n        let label_excluded_items: gtk4::Label = builder.object(\"label_excluded_items\").expect(\"Cambalache\");\n        let label_allowed_extensions: gtk4::Label = builder.object(\"label_allowed_extensions\").expect(\"Cambalache\");\n        let label_excluded_extensions: gtk4::Label = builder.object(\"label_excluded_extensions\").expect(\"Cambalache\");\n\n        let entry_general_minimal_size: gtk4::Entry = builder.object(\"entry_general_minimal_size\").expect(\"Cambalache\");\n        let entry_general_maximal_size: gtk4::Entry = builder.object(\"entry_general_maximal_size\").expect(\"Cambalache\");\n        let label_general_size_bytes: gtk4::Label = builder.object(\"label_general_size_bytes\").expect(\"Cambalache\");\n        let label_general_min_size: gtk4::Label = builder.object(\"label_general_min_size\").expect(\"Cambalache\");\n        let label_general_max_size: gtk4::Label = builder.object(\"label_general_max_size\").expect(\"Cambalache\");\n\n        set_icon_of_button(&buttons_add_included_directory, CZK_ICON_ADD);\n        set_icon_of_button(&buttons_manual_add_included_directory, CZK_ICON_MANUAL_ADD);\n        set_icon_of_button(&buttons_remove_included_directory, CZK_ICON_DELETE);\n        set_icon_of_button(&buttons_add_excluded_directory, CZK_ICON_ADD);\n        set_icon_of_button(&buttons_manual_add_excluded_directory, CZK_ICON_MANUAL_ADD);\n        set_icon_of_button(&buttons_remove_excluded_directory, CZK_ICON_DELETE);\n\n        let common_upper_tree_views = CommonUpperTreeViews {\n            subviews: vec![\n                UpperSubView::new(\n                    builder,\n                    \"scrolled_window_included_directories\",\n                    NotebookUpperEnum::IncludedDirectories,\n                    UpperTreeViewEnum::IncludedDirectories,\n                    \"tree_view_upper_included_directories\",\n                ),\n                UpperSubView::new(\n                    builder,\n                    \"scrolled_window_excluded_directories\",\n                    NotebookUpperEnum::ExcludedDirectories,\n                    UpperTreeViewEnum::ExcludedDirectories,\n                    \"tree_view_upper_excluded_directories\",\n                ),\n            ],\n        };\n\n        Self {\n            notebook_upper,\n            entry_excluded_items,\n            entry_allowed_extensions,\n            entry_excluded_extensions,\n            check_button_recursive,\n            buttons_manual_add_included_directory,\n            buttons_add_included_directory,\n            buttons_remove_included_directory,\n            buttons_manual_add_excluded_directory,\n            buttons_add_excluded_directory,\n            buttons_remove_excluded_directory,\n            label_excluded_items,\n            label_allowed_extensions,\n            label_excluded_extensions,\n            entry_general_minimal_size,\n            entry_general_maximal_size,\n            label_general_size_bytes,\n            label_general_min_size,\n            label_general_max_size,\n            common_upper_tree_views,\n        }\n    }\n\n    pub(crate) fn setup(&self) {\n        self.common_upper_tree_views.setup();\n    }\n\n    pub(crate) fn update_language(&self) {\n        self.check_button_recursive.set_label(Some(&flg!(\"upper_recursive_button\")));\n        self.check_button_recursive.set_tooltip_text(Some(&flg!(\"upper_recursive_button_tooltip\")));\n\n        self.buttons_manual_add_included_directory\n            .get_widget_of_type::<Label>(true)\n            .set_text(&flg!(\"upper_manual_add_included_button\"));\n        self.buttons_add_included_directory\n            .get_widget_of_type::<Label>(true)\n            .set_text(&flg!(\"upper_add_included_button\"));\n        self.buttons_remove_included_directory\n            .get_widget_of_type::<Label>(true)\n            .set_text(&flg!(\"upper_remove_included_button\"));\n        self.buttons_manual_add_excluded_directory\n            .get_widget_of_type::<Label>(true)\n            .set_text(&flg!(\"upper_manual_add_excluded_button\"));\n        self.buttons_add_excluded_directory\n            .get_widget_of_type::<Label>(true)\n            .set_text(&flg!(\"upper_add_excluded_button\"));\n        self.buttons_remove_excluded_directory\n            .get_widget_of_type::<Label>(true)\n            .set_text(&flg!(\"upper_remove_excluded_button\"));\n\n        self.buttons_manual_add_included_directory\n            .set_tooltip_text(Some(&flg!(\"upper_manual_add_included_button_tooltip\")));\n        self.buttons_add_included_directory.set_tooltip_text(Some(&flg!(\"upper_add_included_button_tooltip\")));\n        self.buttons_remove_included_directory.set_tooltip_text(Some(&flg!(\"upper_remove_included_button_tooltip\")));\n        self.buttons_manual_add_excluded_directory\n            .set_tooltip_text(Some(&flg!(\"upper_manual_add_excluded_button_tooltip\")));\n        self.buttons_add_excluded_directory.set_tooltip_text(Some(&flg!(\"upper_add_excluded_button_tooltip\")));\n        self.buttons_remove_excluded_directory.set_tooltip_text(Some(&flg!(\"upper_remove_excluded_button_tooltip\")));\n\n        self.label_allowed_extensions.set_tooltip_text(Some(&flg!(\"upper_allowed_extensions_tooltip\")));\n        self.entry_allowed_extensions.set_tooltip_text(Some(&flg!(\"upper_allowed_extensions_tooltip\")));\n        self.label_excluded_extensions.set_tooltip_text(Some(&flg!(\"upper_excluded_extensions_tooltip\")));\n        self.entry_excluded_extensions.set_tooltip_text(Some(&flg!(\"upper_excluded_extensions_tooltip\")));\n        self.label_excluded_items.set_tooltip_text(Some(&flg!(\"upper_excluded_items_tooltip\")));\n        self.entry_excluded_items.set_tooltip_text(Some(&flg!(\"upper_excluded_items_tooltip\")));\n\n        self.label_excluded_items.set_label(&flg!(\"upper_excluded_items\"));\n        self.label_allowed_extensions.set_label(&flg!(\"upper_allowed_extensions\"));\n        self.label_excluded_extensions.set_label(&flg!(\"upper_excluded_extensions\"));\n\n        self.label_general_size_bytes.set_label(&flg!(\"main_label_size_bytes\"));\n        self.label_general_min_size.set_label(&flg!(\"main_label_min_size\"));\n        self.label_general_max_size.set_label(&flg!(\"main_label_max_size\"));\n\n        self.label_general_size_bytes.set_tooltip_text(Some(&flg!(\"main_label_size_bytes_tooltip\")));\n        self.label_general_min_size.set_tooltip_text(Some(&flg!(\"main_label_size_bytes_tooltip\")));\n        self.label_general_max_size.set_tooltip_text(Some(&flg!(\"main_label_size_bytes_tooltip\")));\n        self.entry_general_minimal_size.set_tooltip_text(Some(&flg!(\"main_label_size_bytes_tooltip\")));\n        self.entry_general_maximal_size.set_tooltip_text(Some(&flg!(\"main_label_size_bytes_tooltip\")));\n\n        let vec_children: Vec<gtk4::Widget> = self.notebook_upper.get_all_direct_children();\n        let vec_children: Vec<gtk4::Widget> = vec_children[1].get_all_direct_children(); // This is quite safe in GTK 4, because tab label is always second child\n\n        // Change name of upper notebook tabs\n        for (upper_enum, fl_thing) in [\n            (NotebookUpperEnum::ItemsConfiguration as usize, flg!(\"upper_notebook_items_configuration\")),\n            (NotebookUpperEnum::ExcludedDirectories as usize, flg!(\"upper_notebook_excluded_directories\")),\n            (NotebookUpperEnum::IncludedDirectories as usize, flg!(\"upper_notebook_included_directories\")),\n        ] {\n            self.notebook_upper\n                .tab_label(&vec_children[upper_enum])\n                .expect(\"Failed to get tab label\")\n                .downcast::<gtk4::Label>()\n                .expect(\"Failed to downcast to label\")\n                .set_text(&fl_thing);\n        }\n\n        let names_of_columns = [\n            vec![\n                flg!(\"upper_tree_view_included_folder_column_title\"),\n                flg!(\"upper_tree_view_included_reference_column_title\"),\n            ], // Included folders\n               // TODO - missing Excluded folders?\n        ];\n\n        for (notebook_index, tree_view) in std::iter::once(self.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::IncludedDirectories)).enumerate() {\n            for (column_index, column) in tree_view.columns().iter().enumerate() {\n                column.set_title(&names_of_columns[notebook_index][column_index]);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/gui_structs/mod.rs",
    "content": "pub mod common_tree_view;\npub mod common_upper_tree_view;\nmod gui_about;\nmod gui_bottom_buttons;\nmod gui_compare_images;\npub mod gui_data;\nmod gui_header;\npub mod gui_main_notebook;\npub mod gui_popovers_select;\npub mod gui_popovers_sort;\nmod gui_progress_dialog;\npub mod gui_settings;\npub mod gui_upper_notebook;\n"
  },
  {
    "path": "czkawka_gui/src/help_combo_box.rs",
    "content": "use czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::re_exported::{FilterType, HashAlg};\nuse czkawka_core::tools::big_file::SearchMode;\n\npub struct HashTypeStruct {\n    pub eng_name: &'static str,\n    pub hash_type: HashType,\n}\n\npub const DUPLICATES_HASH_TYPE_COMBO_BOX: [HashTypeStruct; 3] = [\n    HashTypeStruct {\n        eng_name: \"Blake3\",\n        hash_type: HashType::Blake3,\n    },\n    HashTypeStruct {\n        eng_name: \"CRC32\",\n        hash_type: HashType::Crc32,\n    },\n    HashTypeStruct {\n        eng_name: \"XXH3\",\n        hash_type: HashType::Xxh3,\n    },\n];\n\npub struct CheckMethodStruct {\n    pub eng_name: &'static str,\n    pub check_method: CheckingMethod,\n}\n\npub const DUPLICATES_CHECK_METHOD_COMBO_BOX: [CheckMethodStruct; 4] = [\n    CheckMethodStruct {\n        eng_name: \"Hash\",\n        check_method: CheckingMethod::Hash,\n    },\n    CheckMethodStruct {\n        eng_name: \"Size\",\n        check_method: CheckingMethod::Size,\n    },\n    CheckMethodStruct {\n        eng_name: \"Name\",\n        check_method: CheckingMethod::Name,\n    },\n    CheckMethodStruct {\n        eng_name: \"Size and Name\",\n        check_method: CheckingMethod::SizeName,\n    },\n];\n\n#[derive(Copy, Clone)]\npub struct AudioTypeStruct {\n    #[expect(unused)]\n    pub eng_name: &'static str,\n    pub check_method: CheckingMethod,\n}\n\npub const AUDIO_TYPE_CHECK_METHOD_COMBO_BOX: [AudioTypeStruct; 2] = [\n    AudioTypeStruct {\n        eng_name: \"Tags\",\n        check_method: CheckingMethod::AudioTags,\n    },\n    AudioTypeStruct {\n        eng_name: \"Content\",\n        check_method: CheckingMethod::AudioContent,\n    },\n];\n\n#[derive(Copy, Clone)]\npub struct SearchModeStruct {\n    #[expect(unused)]\n    pub eng_name: &'static str,\n    pub check_method: SearchMode,\n}\n\npub const BIG_FILES_CHECK_METHOD_COMBO_BOX: [SearchModeStruct; 2] = [\n    SearchModeStruct {\n        eng_name: \"Biggest\",\n        check_method: SearchMode::BiggestFiles,\n    },\n    SearchModeStruct {\n        eng_name: \"Smallest\",\n        check_method: SearchMode::SmallestFiles,\n    },\n];\n\npub struct ImageResizeAlgStruct {\n    pub eng_name: &'static str,\n    pub filter: FilterType,\n}\n\npub const IMAGES_RESIZE_ALGORITHM_COMBO_BOX: [ImageResizeAlgStruct; 5] = [\n    ImageResizeAlgStruct {\n        eng_name: \"Lanczos3\",\n        filter: FilterType::Lanczos3,\n    },\n    ImageResizeAlgStruct {\n        eng_name: \"Nearest\",\n        filter: FilterType::Nearest,\n    },\n    ImageResizeAlgStruct {\n        eng_name: \"Triangle\",\n        filter: FilterType::Triangle,\n    },\n    ImageResizeAlgStruct {\n        eng_name: \"Gaussian\",\n        filter: FilterType::Gaussian,\n    },\n    ImageResizeAlgStruct {\n        eng_name: \"CatmullRom\",\n        filter: FilterType::CatmullRom,\n    },\n];\n\npub struct ImageHashTypeStruct {\n    pub eng_name: &'static str,\n    pub hash_alg: HashAlg,\n}\n\npub const IMAGES_HASH_TYPE_COMBO_BOX: &[ImageHashTypeStruct] = &[\n    ImageHashTypeStruct {\n        eng_name: \"Gradient\",\n        hash_alg: HashAlg::Gradient,\n    },\n    ImageHashTypeStruct {\n        eng_name: \"Mean\",\n        hash_alg: HashAlg::Mean,\n    },\n    ImageHashTypeStruct {\n        eng_name: \"VertGradient\",\n        hash_alg: HashAlg::VertGradient,\n    },\n    ImageHashTypeStruct {\n        eng_name: \"Blockhash\",\n        hash_alg: HashAlg::Blockhash,\n    },\n    ImageHashTypeStruct {\n        eng_name: \"DoubleGradient\",\n        hash_alg: HashAlg::DoubleGradient,\n    },\n    ImageHashTypeStruct {\n        eng_name: \"Median\",\n        hash_alg: HashAlg::Median,\n    },\n];\n\npub const IMAGES_HASH_SIZE_COMBO_BOX: [i32; 4] = [8, 16, 32, 64];\n"
  },
  {
    "path": "czkawka_gui/src/help_functions.rs",
    "content": "use std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::path::{MAIN_SEPARATOR, PathBuf};\nuse std::rc::Rc;\n\nuse czkawka_core::helpers::messages::Messages;\nuse gtk4::prelude::*;\nuse gtk4::{Scale, ScrollType, TextView, TreeView, Widget};\n\nuse crate::flg;\nuse crate::helpers::enums::BottomButtonsEnum;\nuse crate::notebook_enums::NotebookUpperEnum;\nuse crate::notebook_info::{NOTEBOOKS_INFO, NotebookObject};\n\npub const KEY_DELETE: u32 = 119;\npub const KEY_ENTER: u32 = 36;\npub const KEY_SPACE: u32 = 65;\n\npub type SharedState<T> = Rc<RefCell<Option<T>>>;\n\npub const MAIN_ROW_COLOR: &str = \"#222222\";\npub const HEADER_ROW_COLOR: &str = \"#111111\";\npub const TEXT_COLOR: &str = \"#ffffff\";\n\npub(crate) fn get_path_buf_from_vector_of_strings(vec_string: &[String]) -> Vec<PathBuf> {\n    vec_string.iter().map(PathBuf::from).collect()\n}\n\npub(crate) fn print_text_messages_to_text_view(text_messages: &Messages, text_view: &TextView) {\n    let mut messages: String = String::new();\n    if !text_messages.messages.is_empty() {\n        messages += format!(\"############### {}({}) ###############\\n\", flg!(\"text_view_messages\"), text_messages.messages.len()).as_str();\n    }\n    for text in &text_messages.messages {\n        messages += text.as_str();\n        messages += \"\\n\";\n    }\n    // if !text_messages.messages.is_empty() {\n    //     messages += \"\\n\";\n    // }\n    if !text_messages.warnings.is_empty() {\n        messages += format!(\"############### {}({}) ###############\\n\", flg!(\"text_view_warnings\"), text_messages.warnings.len()).as_str();\n    }\n    for text in &text_messages.warnings {\n        messages += text.as_str();\n        messages += \"\\n\";\n    }\n    // if !text_messages.warnings.is_empty() {\n    //     messages += \"\\n\";\n    // }\n    if !text_messages.errors.is_empty() {\n        messages += format!(\"############### {}({}) ###############\\n\", flg!(\"text_view_errors\"), text_messages.errors.len()).as_str();\n    }\n    for text in &text_messages.errors {\n        messages += text.as_str();\n        messages += \"\\n\";\n    }\n    // if !text_messages.errors.is_empty() {\n    //     messages += \"\\n\";\n    // }\n\n    text_view.buffer().set_text(messages.as_str());\n}\n\npub(crate) fn reset_text_view(text_view: &TextView) {\n    text_view.buffer().set_text(\"\");\n}\n\npub(crate) fn add_text_to_text_view(text_view: &TextView, string_to_append: &str) {\n    let buffer = text_view.buffer();\n    let current_text = buffer.text(&buffer.start_iter(), &buffer.end_iter(), true).to_string();\n    if current_text.is_empty() {\n        buffer.set_text(string_to_append);\n    } else {\n        buffer.set_text(format!(\"{current_text}\\n{string_to_append}\").as_str());\n    }\n}\n\npub(crate) fn set_buttons(hashmap: &mut HashMap<BottomButtonsEnum, bool>, buttons_array: &[Widget], button_names: &[BottomButtonsEnum]) {\n    for (index, button) in buttons_array.iter().enumerate() {\n        if *hashmap.get_mut(&button_names[index]).expect(\"Invalid button name\") {\n            button.set_visible(true);\n        } else {\n            button.set_visible(false);\n        }\n    }\n}\n\npub(crate) fn hide_all_buttons(buttons_array: &[Widget]) {\n    for button in buttons_array {\n        button.set_visible(false);\n    }\n}\n\npub(crate) fn change_dimension_to_krotka(dimensions: &str) -> (u64, u64) {\n    #[expect(clippy::single_char_pattern)]\n    let vec = dimensions.split::<&str>(\"x\").collect::<Vec<_>>();\n    assert_eq!(vec.len(), 2); // 400x400 - should only have two elements, if have more, then something is not good\n    let number1 = vec[0].parse::<u64>().expect(\"Invalid data in image dimension in position 0\");\n    let number2 = vec[1].parse::<u64>().expect(\"Invalid data in image dimension in position 1\");\n    (number1, number2)\n}\n\npub(crate) fn get_notebook_upper_enum_from_tree_view(tree_view: &TreeView) -> NotebookUpperEnum {\n    match (*tree_view).widget_name().to_string().as_str() {\n        \"tree_view_upper_included_directories\" => NotebookUpperEnum::IncludedDirectories,\n        \"tree_view_upper_excluded_directories\" => NotebookUpperEnum::ExcludedDirectories,\n        e => panic!(\"{}\", e),\n    }\n}\n\npub(crate) fn get_notebook_object_from_tree_view(tree_view: &TreeView) -> &NotebookObject {\n    let tree_view_name = (*tree_view).widget_name().to_string();\n\n    NOTEBOOKS_INFO\n        .iter()\n        .find(|nb_object| nb_object.tree_view_name == tree_view_name)\n        .unwrap_or_else(|| panic!(\"Tree view name '{tree_view_name}' not found in NOTEBOOKS_INFO\"))\n}\n\npub(crate) fn get_full_name_from_path_name(path: &str, name: &str) -> String {\n    let mut string = String::with_capacity(path.len() + name.len() + 1);\n    string.push_str(path);\n    string.push(MAIN_SEPARATOR);\n    string.push_str(name);\n    string\n}\n\npub(crate) fn get_max_file_name(file_name: &str, max_length: usize) -> String {\n    assert!(max_length > 10); // Maybe in future will be supported lower values\n    let characters_in_filename = file_name.chars().count();\n    if characters_in_filename > max_length {\n        let start_characters = 10;\n        let difference = characters_in_filename - max_length;\n        let second_part_start = start_characters + difference;\n        let mut string_pre = String::new();\n        let mut string_after = String::new();\n\n        for (index, character) in file_name.chars().enumerate() {\n            if index < start_characters {\n                string_pre.push(character);\n            } else if index >= second_part_start {\n                string_after.push(character);\n            }\n        }\n\n        format!(\"{string_pre} ... {string_after}\")\n    } else {\n        file_name.to_string()\n    }\n}\n\npub(crate) fn scale_set_min_max_values(scale: &Scale, minimum: f64, maximum: f64, current_value: f64, step: Option<f64>) {\n    scale.set_range(minimum, maximum);\n    scale.set_fill_level(maximum);\n    scale.set_value(current_value);\n    if let Some(step) = step {\n        scale.adjustment().set_step_increment(step);\n    }\n}\n\npub(crate) fn scale_step_function(scale: &Scale, _scroll_type: ScrollType, value: f64) -> glib::Propagation {\n    scale.set_increments(1_f64, 1_f64);\n    scale.set_round_digits(0);\n    scale.set_fill_level(value.round());\n    glib::Propagation::Proceed\n}\n\n#[cfg(test)]\nmod test {\n    use std::path::PathBuf;\n\n    use gtk4::prelude::*;\n\n    use super::*;\n\n    #[test]\n    fn test_file_name_shortener() {\n        let name_to_check = \"/home/rafal/czkawek/romek/atomek.txt\";\n        assert_eq!(get_max_file_name(name_to_check, 20), \"/home/rafa ... atomek.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 21), \"/home/rafa ... /atomek.txt\");\n        let name_to_check = \"/home/rafal/czkawek/romek/czekistan/atomek.txt\";\n        assert_eq!(get_max_file_name(name_to_check, 21), \"/home/rafa ... /atomek.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 80), name_to_check);\n        let name_to_check = \"/home/rafal/‍🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈.txt\";\n        assert_eq!(get_max_file_name(name_to_check, 21), \"/home/rafa ... 🌈🌈🌈🌈🌈🌈🌈.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 20), \"/home/rafa ... 🌈🌈🌈🌈🌈🌈.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 19), \"/home/rafa ... 🌈🌈🌈🌈🌈.txt\");\n        let name_to_check = \"/home/rafal/‍🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️🏳️‍🌈️.txt\";\n        assert_eq!(get_max_file_name(name_to_check, 21), \"/home/rafa ... 🌈\\u{fe0f}🏳\\u{fe0f}\\u{200d}🌈\\u{fe0f}.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 20), \"/home/rafa ... \\u{fe0f}🏳\\u{fe0f}\\u{200d}🌈\\u{fe0f}.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 19), \"/home/rafa ... 🏳\\u{fe0f}\\u{200d}🌈\\u{fe0f}.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 18), \"/home/rafa ... \\u{fe0f}\\u{200d}🌈\\u{fe0f}.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 17), \"/home/rafa ... \\u{200d}🌈\\u{fe0f}.txt\");\n        assert_eq!(get_max_file_name(name_to_check, 16), \"/home/rafa ... 🌈\\u{fe0f}.txt\");\n    }\n\n    #[test]\n    fn test_get_path_buf_from_vector_of_strings() {\n        let input = vec![\"/tmp/test1\".to_string(), \"relative/path\".to_string()];\n        let result = get_path_buf_from_vector_of_strings(&input);\n        assert_eq!(result, vec![PathBuf::from(\"/tmp/test1\"), PathBuf::from(\"relative/path\")]);\n    }\n\n    #[test]\n    fn test_get_full_name_from_path_name() {\n        let path = \"/home/user\";\n        let name = \"file.txt\";\n        let expected = format!(\"{}{}{}\", path, std::path::MAIN_SEPARATOR, name);\n        assert_eq!(get_full_name_from_path_name(path, name), expected);\n    }\n\n    #[gtk4::test]\n    fn test_set_and_hide_buttons() {\n        use std::collections::HashMap;\n        let btn1 = gtk4::Button::new();\n        let btn2 = gtk4::Button::new();\n        let w1: Widget = btn1.upcast();\n        let w2: Widget = btn2.upcast();\n        let buttons = vec![w1, w2];\n\n        let mut map: HashMap<BottomButtonsEnum, bool> = HashMap::new();\n        map.insert(BottomButtonsEnum::Save, true);\n        map.insert(BottomButtonsEnum::Delete, false);\n        let names = [BottomButtonsEnum::Save, BottomButtonsEnum::Delete];\n\n        set_buttons(&mut map, &buttons, &names);\n        assert!(buttons[0].is_visible());\n        assert!(!buttons[1].is_visible());\n\n        hide_all_buttons(&buttons);\n        assert!(!buttons[0].is_visible());\n        assert!(!buttons[1].is_visible());\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::MAIN_SEPARATOR;\n\n    use super::*;\n\n    #[test]\n    fn test_get_full_name_from_path_name() {\n        let path = \"some_dir\";\n        let name = \"file.txt\";\n        let expected = format!(\"{path}{MAIN_SEPARATOR}{name}\");\n        assert_eq!(get_full_name_from_path_name(path, name), expected);\n    }\n\n    #[test]\n    fn test_change_dimension_to_krotka() {\n        let dim = \"1024x768\";\n        let (w, h) = change_dimension_to_krotka(dim);\n        assert_eq!((w, h), (1024, 768));\n    }\n\n    #[test]\n    fn test_get_max_file_name_truncation() {\n        let name = \"very_long_filename_example.txt\";\n        // use max_length smaller than name length to trigger truncation\n        let out = get_max_file_name(name, 20);\n        // Should contain ellipsis and keep the first 10 chars\n        assert!(out.contains(\" ... \"));\n        assert!(out.starts_with(&name.chars().take(10).collect::<String>()));\n    }\n\n    #[test]\n    fn test_get_path_buf_from_vector_of_strings() {\n        let v = vec![\"a\".to_string(), \"b\".to_string()];\n        let res = get_path_buf_from_vector_of_strings(&v);\n        assert_eq!(res.len(), 2);\n        assert_eq!(res[0], PathBuf::from(\"a\"));\n        assert_eq!(res[1], PathBuf::from(\"b\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/helpers/enums.rs",
    "content": "// ...new file: enums.rs...\nuse czkawka_core::tools::bad_extensions::BadExtensions;\nuse czkawka_core::tools::big_file::BigFile;\nuse czkawka_core::tools::broken_files::BrokenFiles;\nuse czkawka_core::tools::duplicate::DuplicateFinder;\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse czkawka_core::tools::empty_folder::EmptyFolder;\nuse czkawka_core::tools::invalid_symlinks::InvalidSymlinks;\nuse czkawka_core::tools::same_music::SameMusic;\nuse czkawka_core::tools::similar_images::SimilarImages;\nuse czkawka_core::tools::similar_videos::SimilarVideos;\nuse czkawka_core::tools::temporary::Temporary;\n\n#[derive(Eq, PartialEq, Copy, Clone, Debug)]\npub enum PopoverTypes {\n    All,\n    Size,\n    Reverse,\n    Custom,\n    Date,\n    PathLength,\n}\n\n#[derive(Eq, PartialEq, Copy, Clone, Hash, Debug)]\npub enum BottomButtonsEnum {\n    Search,\n    Select,\n    Delete,\n    Save,\n    Symlink,\n    Hardlink,\n    Move,\n    Compare,\n    Sort,\n}\n\npub enum Message {\n    Duplicates(DuplicateFinder),\n    EmptyFolders(EmptyFolder),\n    EmptyFiles(EmptyFiles),\n    BigFiles(BigFile),\n    Temporary(Temporary),\n    SimilarImages(SimilarImages),\n    SimilarVideos(SimilarVideos),\n    SameMusic(SameMusic),\n    InvalidSymlinks(InvalidSymlinks),\n    BrokenFiles(BrokenFiles),\n    BadExtensions(BadExtensions),\n}\n\nimpl Message {\n    pub(crate) fn get_message_type(&self) -> crate::notebook_enums::NotebookMainEnum {\n        match self {\n            Self::Duplicates(_) => crate::notebook_enums::NotebookMainEnum::Duplicate,\n            Self::EmptyFolders(_) => crate::notebook_enums::NotebookMainEnum::EmptyDirectories,\n            Self::EmptyFiles(_) => crate::notebook_enums::NotebookMainEnum::EmptyFiles,\n            Self::BigFiles(_) => crate::notebook_enums::NotebookMainEnum::BigFiles,\n            Self::Temporary(_) => crate::notebook_enums::NotebookMainEnum::Temporary,\n            Self::SimilarImages(_) => crate::notebook_enums::NotebookMainEnum::SimilarImages,\n            Self::SimilarVideos(_) => crate::notebook_enums::NotebookMainEnum::SimilarVideos,\n            Self::SameMusic(_) => crate::notebook_enums::NotebookMainEnum::SameMusic,\n            Self::InvalidSymlinks(_) => crate::notebook_enums::NotebookMainEnum::Symlinks,\n            Self::BrokenFiles(_) => crate::notebook_enums::NotebookMainEnum::BrokenFiles,\n            Self::BadExtensions(_) => crate::notebook_enums::NotebookMainEnum::BadExtensions,\n        }\n    }\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsDuplicates {\n    // Columns for duplicate treeview\n    ActivatableSelectButton = 0,\n    SelectionButton,\n    Size,\n    SizeAsBytes,\n    Name,\n    Path,\n    Modification,\n    ModificationAsSecs,\n    Color,\n    IsHeader,\n    TextColor,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsEmptyFolders {\n    // Columns for empty folder treeview\n    SelectionButton = 0,\n    Name,\n    Path,\n    Modification,\n    ModificationAsSecs,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsIncludedDirectory {\n    // Columns for Included Paths in upper Notebook\n    Path = 0,\n    ReferenceButton,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsExcludedDirectory {\n    // Columns for Excluded Paths in upper Notebook\n    Path = 0,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsBigFiles {\n    SelectionButton = 0,\n    Size,\n    Name,\n    Path,\n    Modification,\n    SizeAsBytes,\n    ModificationAsSecs,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsEmptyFiles {\n    SelectionButton = 0,\n    Name,\n    Path,\n    Modification,\n    ModificationAsSecs,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsTemporaryFiles {\n    SelectionButton = 0,\n    Name,\n    Path,\n    Modification,\n    ModificationAsSecs,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsSimilarImages {\n    ActivatableSelectButton = 0,\n    SelectionButton,\n    Similarity,\n    Size,\n    SizeAsBytes,\n    Dimensions,\n    Name,\n    Path,\n    Modification,\n    ModificationAsSecs,\n    Color,\n    IsHeader,\n    TextColor,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsSimilarVideos {\n    ActivatableSelectButton = 0,\n    SelectionButton,\n    Size,\n    SizeAsBytes,\n    Fps,\n    Codec,\n    Bitrate,\n    Dimensions,\n    Duration,\n    Name,\n    Path,\n    Modification,\n    ModificationAsSecs,\n    Color,\n    IsHeader,\n    TextColor,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsSameMusic {\n    ActivatableSelectButton = 0,\n    SelectionButton,\n    Size,\n    SizeAsBytes,\n    Name,\n    Path,\n    Title,\n    Artist,\n    Year,\n    Bitrate,\n    BitrateAsNumber,\n    Length,\n    Genre,\n    Modification,\n    ModificationAsSecs,\n    Color,\n    IsHeader,\n    TextColor,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsInvalidSymlinks {\n    SelectionButton = 0,\n    Name,\n    Path,\n    DestinationPath,\n    TypeOfError,\n    Modification,\n    ModificationAsSecs,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsBrokenFiles {\n    SelectionButton = 0,\n    Name,\n    Path,\n    ErrorType,\n    Modification,\n    ModificationAsSecs,\n}\n\n#[derive(Clone, Copy)]\npub enum ColumnsBadExtensions {\n    SelectionButton = 0,\n    Name,\n    Path,\n    CurrentExtension,\n    ValidExtensions,\n    Modification,\n    ModificationAsSecs,\n}\n"
  },
  {
    "path": "czkawka_gui/src/helpers/image_operations.rs",
    "content": "use std::cmp::Ordering;\nuse std::io::{BufReader, Cursor};\n\nuse gdk4::gdk_pixbuf::{InterpType, Pixbuf};\nuse glib::Bytes;\nuse gtk4::gdk_pixbuf::Colorspace;\nuse gtk4::prelude::*;\nuse gtk4::{Image, Widget};\nuse image::codecs::png::PngEncoder;\nuse image::{DynamicImage, GenericImageView, ImageEncoder, RgbaImage};\nuse resvg::tiny_skia;\nuse resvg::usvg::{Options, Tree};\n\nuse crate::gtk_traits::WidgetTraits;\n\nconst SIZE_OF_ICON: i32 = 18;\nconst TYPE_OF_INTERPOLATION: InterpType = InterpType::Tiles;\n\n// TODO - verify if this needs to be executed for smaller items than requested size\npub(crate) fn resize_pixbuf_dimension(pixbuf: &Pixbuf, requested_size: (i32, i32), interp_type: InterpType) -> Option<Pixbuf> {\n    let current_ratio = pixbuf.width() as f32 / pixbuf.height() as f32;\n    let mut new_size;\n    match current_ratio.total_cmp(&(requested_size.0 as f32 / requested_size.1 as f32)) {\n        Ordering::Greater => {\n            new_size = (requested_size.0, (pixbuf.height() * requested_size.0) / pixbuf.width());\n            new_size = (std::cmp::max(new_size.0, 1), std::cmp::max(new_size.1, 1));\n        }\n\n        Ordering::Less | Ordering::Equal => {\n            return Some(pixbuf.clone());\n        }\n    }\n    pixbuf.scale_simple(new_size.0, new_size.1, interp_type)\n}\n\nfn svg_to_dynamic_image(svg_data: &[u8]) -> Option<DynamicImage> {\n    let opt = Options::default();\n    let tree = Tree::from_data(svg_data, &opt).ok()?;\n\n    let mut pixmap = tiny_skia::Pixmap::new(tree.size().width() as u32, tree.size().height() as u32)?;\n    resvg::render(&tree, tiny_skia::Transform::default(), &mut (pixmap.as_mut()));\n\n    let rgba = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())?;\n\n    Some(DynamicImage::ImageRgba8(rgba))\n}\n\nfn dynamic_image_to_pixbuf(img: DynamicImage) -> Pixbuf {\n    let (width, height) = img.dimensions();\n    let rgba = img.into_rgba8();\n    let bytes = Bytes::from(&rgba.into_raw());\n\n    let pixbuf = Pixbuf::from_bytes(&bytes, Colorspace::Rgb, true, 8, width as i32, height as i32, (4 * width) as i32);\n    pixbuf.scale_simple(SIZE_OF_ICON, SIZE_OF_ICON, TYPE_OF_INTERPOLATION).expect(\"Failed to scale pixbuf\")\n}\n\npub(crate) fn set_icon_of_button<P: IsA<Widget>>(button: &P, data: &'static [u8]) {\n    let image = button.get_widget_of_type::<Image>(true);\n    let dynamic_image = svg_to_dynamic_image(data).expect(\"Failed to convert SVG data to DynamicImage\");\n    let pixbuf = dynamic_image_to_pixbuf(dynamic_image);\n    image.set_from_pixbuf(Some(&pixbuf));\n}\n\npub(crate) fn get_pixbuf_from_dynamic_image(dynamic_image: DynamicImage) -> Result<Pixbuf, String> {\n    let mut output = Vec::new();\n    let width = dynamic_image.width();\n    let height = dynamic_image.height();\n    let rgba = dynamic_image.into_rgba8();\n    let encoder = PngEncoder::new(&mut output);\n    encoder\n        .write_image(&rgba, width, height, image::ExtendedColorType::Rgba8)\n        .map_err(|e| format!(\"Failed to encode image: {e}\"))?;\n    Pixbuf::from_read(BufReader::new(Cursor::new(output))).map_err(|e| format!(\"Failed to create Pixbuf from DynamicImage: {e}\"))\n}\n\n#[cfg(test)]\nmod test {\n    use image::DynamicImage;\n\n    use super::*;\n\n    #[test]\n    fn test_pixbuf_from_dynamic_image() {\n        let dynamic_image = DynamicImage::new_rgb8(1, 1);\n        get_pixbuf_from_dynamic_image(dynamic_image.clone()).expect(\"Failed to get pixbuf from dynamic image\");\n        get_pixbuf_from_dynamic_image(dynamic_image.clone()).expect(\"Failed to get pixbuf from dynamic image\");\n        get_pixbuf_from_dynamic_image(dynamic_image.clone()).expect(\"Failed to get pixbuf from dynamic image\");\n        get_pixbuf_from_dynamic_image(dynamic_image).expect(\"Failed to get pixbuf from dynamic image\");\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/helpers/list_store_operations.rs",
    "content": "use fun_time::fun_time;\nuse gtk4::prelude::*;\nuse gtk4::{ListStore, TreeView};\n\nuse crate::gui_structs::common_tree_view::{SubView, TreeViewListStoreTrait};\nuse crate::helpers::model_iter::{iter_list, iter_list_with_break, iter_list_with_break_init};\n\npub(crate) fn get_string_from_list_store(tree_view: &TreeView, column_full_path: i32, column_selection: Option<i32>) -> Vec<String> {\n    let list_store: ListStore = tree_view.get_model();\n\n    let mut string_vector: Vec<String> = Vec::new();\n\n    match column_selection {\n        Some(column_selection) => {\n            iter_list(&list_store, |m, i| {\n                if m.get::<bool>(i, column_selection) {\n                    string_vector.push(m.get::<String>(i, column_full_path));\n                }\n            });\n        }\n        None => {\n            iter_list(&list_store, |m, i| {\n                string_vector.push(m.get::<String>(i, column_full_path));\n            });\n        }\n    }\n\n    string_vector\n}\n\npub(crate) fn get_from_list_store_fnc<T>(tree_view: &TreeView, fnc: &dyn Fn(&ListStore, &gtk4::TreeIter, &mut Vec<T>)) -> Vec<T> {\n    let list_store: ListStore = tree_view.get_model();\n\n    let mut result_vector: Vec<T> = Vec::new();\n\n    iter_list(&list_store, |m, i| {\n        fnc(m, i, &mut result_vector);\n    });\n\n    result_vector\n}\n\n// After e.g. deleting files, header may become orphan or have one child, so should be deleted in this case\n#[fun_time(message = \"clean_invalid_headers\", level = \"debug\")]\npub(crate) fn clean_invalid_headers(model: &ListStore, column_header: i32, column_path: i32) {\n    // Remove only child from header\n    if let Some(first_iter) = model.iter_first() {\n        let mut vec_tree_path_to_delete: Vec<gtk4::TreePath> = Vec::new();\n        let mut current_iter = first_iter;\n        // First element should be header\n        assert!(model.get::<bool>(&current_iter, column_header), \"First deleted element, should be a header\");\n\n        let mut next_iter;\n        let mut next_next_iter;\n\n        // Empty means default check type\n        if model.get::<String>(&current_iter, column_path).is_empty() {\n            'main: loop {\n                // First element should be header\n                assert!(model.get::<bool>(&current_iter, column_header), \"First deleted element, should be a header\");\n\n                next_iter = current_iter;\n                if !model.iter_next(&mut next_iter) {\n                    // There is only single header left (H1 -> END) -> (NOTHING)\n                    vec_tree_path_to_delete.push(model.path(&current_iter));\n                    break 'main;\n                }\n\n                if model.get::<bool>(&next_iter, column_header) {\n                    // There are two headers each others(we remove just first) -> (H1 -> H2) -> (H2)\n                    vec_tree_path_to_delete.push(model.path(&current_iter));\n                    current_iter = next_iter;\n                    continue 'main;\n                }\n\n                next_next_iter = next_iter;\n                if !model.iter_next(&mut next_next_iter) {\n                    // There is only one child of header left, so we remove it with header (H1 -> C1 -> END) -> (NOTHING)\n                    vec_tree_path_to_delete.push(model.path(&current_iter));\n                    vec_tree_path_to_delete.push(model.path(&next_iter));\n                    break 'main;\n                }\n\n                if model.get::<bool>(&next_next_iter, column_header) {\n                    // One child between two headers, we can remove them  (H1 -> C1 -> H2) -> (H2)\n                    vec_tree_path_to_delete.push(model.path(&current_iter));\n                    vec_tree_path_to_delete.push(model.path(&next_iter));\n                    current_iter = next_next_iter;\n                    continue 'main;\n                }\n\n                loop {\n                    // (H1 -> C1 -> C2 -> Cn -> END) -> (NO CHANGE, BECAUSE IS GOOD)\n                    if !model.iter_next(&mut next_next_iter) {\n                        break 'main;\n                    }\n                    // Move to next header\n                    if model.get::<bool>(&next_next_iter, column_header) {\n                        current_iter = next_next_iter;\n                        continue 'main;\n                    }\n                }\n            }\n        }\n        // Non empty means that header points at reference folder\n        else {\n            'reference: loop {\n                // First element should be header\n                assert!(model.get::<bool>(&current_iter, column_header), \"First deleted element, should be a header\");\n\n                next_iter = current_iter;\n                if !model.iter_next(&mut next_iter) {\n                    // There is only single header left (H1 -> END) -> (NOTHING)\n                    vec_tree_path_to_delete.push(model.path(&current_iter));\n                    break 'reference;\n                }\n\n                if model.get::<bool>(&next_iter, column_header) {\n                    // There are two headers each others(we remove just first) -> (H1 -> H2) -> (H2)\n                    vec_tree_path_to_delete.push(model.path(&current_iter));\n                    current_iter = next_iter;\n                    continue 'reference;\n                }\n\n                next_next_iter = next_iter;\n                if !model.iter_next(&mut next_next_iter) {\n                    // There is only one child of header left, so we remove it with header (H1 -> C1 -> END) -> (NOTHING)\n                    break 'reference;\n                }\n\n                if model.get::<bool>(&next_next_iter, column_header) {\n                    // One child between two headers, we can remove them  (H1 -> C1 -> H2) -> (H2)\n                    current_iter = next_next_iter;\n                    continue 'reference;\n                }\n\n                loop {\n                    // (H1 -> C1 -> C2 -> Cn -> END) -> (NO CHANGE, BECAUSE IS GOOD)\n                    if !model.iter_next(&mut next_next_iter) {\n                        break 'reference;\n                    }\n                    // Move to next header\n                    if model.get::<bool>(&next_next_iter, column_header) {\n                        current_iter = next_next_iter;\n                        continue 'reference;\n                    }\n                }\n            }\n        }\n        for tree_path in vec_tree_path_to_delete.iter().rev() {\n            model.remove(&model.iter(tree_path).expect(\"Using invalid tree_path\"));\n        }\n    }\n\n    // Last step, remove orphan header if exists\n    if let Some(mut iter) = model.iter_first()\n        && !model.iter_next(&mut iter)\n    {\n        model.clear();\n    }\n}\n\npub(crate) fn check_how_much_elements_is_selected(sv: &SubView) -> (u64, u64) {\n    let mut number_of_selected_items: u64 = 0;\n    let mut number_of_selected_groups: u64 = 0;\n\n    let model = sv.get_model();\n\n    let mut is_item_currently_selected_in_group: bool = false;\n\n    if let Some(column_header) = sv.nb_object.column_header {\n        iter_list_with_break_init(\n            &model,\n            |m, i| {\n                assert!(m.get::<bool>(i, column_header)); // First element should be header\n                m.iter_next(i)\n            },\n            |m, i| {\n                if m.get::<bool>(i, column_header) {\n                    is_item_currently_selected_in_group = false;\n                } else if m.get::<bool>(i, sv.nb_object.column_selection) {\n                    number_of_selected_items += 1;\n\n                    if !is_item_currently_selected_in_group {\n                        number_of_selected_groups += 1;\n                    }\n                    is_item_currently_selected_in_group = true;\n                }\n            },\n        );\n    } else {\n        iter_list(&model, |m, i| {\n            if m.get::<bool>(i, sv.nb_object.column_selection) {\n                number_of_selected_items += 1;\n            }\n        });\n    }\n\n    (number_of_selected_items, number_of_selected_groups)\n}\n\npub(crate) fn count_number_of_groups(sv: &SubView) -> u32 {\n    let mut number_of_selected_groups = 0;\n    let column_header = sv.nb_object.column_header.expect(\"Column header should be present to count number of groups\");\n\n    let model = sv.get_model();\n\n    iter_list_with_break_init(\n        &model,\n        |_m, i| {\n            assert!(model.get::<bool>(i, column_header)); // First element should be header\n            true\n        },\n        |m, i| {\n            if m.get::<bool>(i, column_header) {\n                number_of_selected_groups += 1;\n            }\n        },\n    );\n    number_of_selected_groups\n}\n\npub(crate) fn check_if_value_is_in_list_store(model: &ListStore, column: i32, value: &str) -> bool {\n    let mut is_in_store = false;\n    iter_list_with_break(model, |m, i| {\n        if m.get::<String>(i, column) == value {\n            is_in_store = true;\n            return false;\n        }\n        true\n    });\n\n    is_in_store\n}\n\npub(crate) fn check_if_list_store_column_have_all_same_values(model: &ListStore, column: i32, value: bool) -> bool {\n    let mut all_are_same = false;\n    iter_list_with_break(model, |m, i| {\n        all_are_same = true;\n        if m.get::<bool>(i, column) != value {\n            all_are_same = false;\n            return false;\n        }\n\n        true\n    });\n\n    all_are_same\n}\n\npub(crate) fn append_row_to_list_store(list_store: &ListStore, values: &[(u32, &dyn ToValue)]) {\n    list_store.set(&list_store.append(), values);\n}\n\n#[cfg(test)]\nmod test {\n    use glib::Value;\n    use glib::types::Type;\n    use gtk4::TreeView;\n    use gtk4::prelude::*;\n\n    use super::*;\n    use crate::notebook_enums::NotebookMainEnum;\n    use crate::notebook_info::NOTEBOOKS_INFO;\n\n    // Helper to create a minimal SubView for Duplicate notebook along with its ListStore\n    fn get_test_sv_duplicate() -> (crate::gui_structs::common_tree_view::SubView, gtk4::ListStore) {\n        use std::cell::RefCell;\n        use std::rc::Rc;\n\n        use czkawka_core::tools::duplicate::DuplicateFinder;\n\n        use crate::gui_structs::common_tree_view::SharedModelEnum;\n\n        let nb_object = NOTEBOOKS_INFO[NotebookMainEnum::Duplicate as usize].clone();\n\n        let list_store = gtk4::ListStore::new(nb_object.columns_types);\n        let tree_view = gtk4::TreeView::new();\n        tree_view.set_model(Some(&list_store));\n\n        let scrolled_window = gtk4::ScrolledWindow::new();\n        let gesture_click = gtk4::GestureClick::new();\n        let event_controller_key = gtk4::EventControllerKey::new();\n        tree_view.add_controller(event_controller_key.clone());\n        tree_view.add_controller(gesture_click.clone());\n\n        let sv = crate::gui_structs::common_tree_view::SubView {\n            scrolled_window,\n            tree_view,\n            gesture_click,\n            event_controller_key,\n            nb_object,\n            enum_value: NotebookMainEnum::Duplicate,\n            preview_struct: None,\n            shared_model_enum: SharedModelEnum::Duplicates(Rc::new(RefCell::new(None::<DuplicateFinder>))),\n        };\n\n        (sv, list_store)\n    }\n\n    #[gtk4::test]\n    fn returns_empty_vector_when_list_store_is_empty() {\n        let columns_types: &[Type] = &[Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n        let tree_view = TreeView::with_model(&list_store);\n\n        assert_eq!(get_string_from_list_store(&tree_view, 0, None), Vec::<String>::new());\n    }\n\n    #[gtk4::test]\n    fn filters_by_boolean_column_when_selection_specified() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n        let tree_view = TreeView::with_model(&list_store);\n\n        let values_to_add: &[&[(u32, &dyn ToValue)]] = &[\n            &[(0, &Into::<Value>::into(true)), (1, &Into::<Value>::into(\"selected1\"))],\n            &[(0, &Into::<Value>::into(false)), (1, &Into::<Value>::into(\"not_selected\"))],\n            &[(0, &Into::<Value>::into(true)), (1, &Into::<Value>::into(\"selected2\"))],\n        ];\n        for i in values_to_add {\n            append_row_to_list_store(&list_store, i);\n        }\n\n        assert_eq!(get_string_from_list_store(&tree_view, 1, Some(0)), vec![\"selected1\".to_string(), \"selected2\".to_string()]);\n    }\n\n    #[gtk4::test]\n    fn applies_custom_function_to_all_rows() {\n        let columns_types: &[Type] = &[Type::STRING, Type::I32];\n        let list_store = gtk4::ListStore::new(columns_types);\n        let tree_view = TreeView::with_model(&list_store);\n\n        append_row_to_list_store(&list_store, &[(0, &\"row1\"), (1, &10)]);\n        append_row_to_list_store(&list_store, &[(0, &\"row2\"), (1, &20)]);\n        append_row_to_list_store(&list_store, &[(0, &\"row3\"), (1, &30)]);\n\n        let collected: Vec<(String, i32)> = get_from_list_store_fnc(&tree_view, &|m, i, vec: &mut Vec<(String, i32)>| {\n            vec.push((m.get::<String>(i, 0), m.get::<i32>(i, 1)));\n        });\n\n        assert_eq!(collected, vec![(\"row1\".to_string(), 10), (\"row2\".to_string(), 20), (\"row3\".to_string(), 30)]);\n    }\n\n    #[gtk4::test]\n    fn removes_single_orphan_header_with_empty_path() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        append_row_to_list_store(&list_store, &[(0, &true), (1, &\"\")]);\n\n        clean_invalid_headers(&list_store, 0, 1);\n\n        assert!(list_store.iter_first().is_none());\n    }\n\n    #[gtk4::test]\n    fn cleans_invalid_headers_properly() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        // Scenario: H1 -> C1 -> H2 -> C2 -> C3\n        // After cleaning: H2 -> C2 -> C3 (H1 and C1 removed as H1 has only one child)\n        append_row_to_list_store(&list_store, &[(0, &true), (1, &\"\")]);\n        append_row_to_list_store(&list_store, &[(0, &false), (1, &\"/path/file1.txt\")]);\n        append_row_to_list_store(&list_store, &[(0, &true), (1, &\"\")]);\n        append_row_to_list_store(&list_store, &[(0, &false), (1, &\"/path/file2.txt\")]);\n        append_row_to_list_store(&list_store, &[(0, &false), (1, &\"/path/file3.txt\")]);\n\n        clean_invalid_headers(&list_store, 0, 1);\n\n        let count = list_store.iter_n_children(None);\n        assert_eq!(count, 3, \"Should keep header with 2 children\");\n    }\n\n    #[gtk4::test]\n    fn keeps_header_with_multiple_children_when_path_empty() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        append_row_to_list_store(&list_store, &[(0, &true), (1, &\"\")]);\n        append_row_to_list_store(&list_store, &[(0, &false), (1, &\"/path/file1.txt\")]);\n        append_row_to_list_store(&list_store, &[(0, &false), (1, &\"/path/file2.txt\")]);\n\n        clean_invalid_headers(&list_store, 0, 1);\n\n        let mut count = 0;\n        iter_list(&list_store, |_, _| count += 1);\n        assert_eq!(count, 3);\n    }\n\n    #[gtk4::test]\n    fn keeps_reference_folder_header_with_single_child() {\n        let columns_types: &[Type] = &[Type::BOOL, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        append_row_to_list_store(&list_store, &[(0, &true), (1, &\"/reference/path\")]);\n        append_row_to_list_store(&list_store, &[(0, &false), (1, &\"/some/child\")]);\n\n        clean_invalid_headers(&list_store, 0, 1);\n\n        let mut count = 0;\n        iter_list(&list_store, |_, _| count += 1);\n        assert_eq!(count, 2);\n    }\n\n    #[gtk4::test]\n    fn counts_items_and_groups_correctly() {\n        let (sv, list_store) = get_test_sv_duplicate();\n\n        let column_header = sv.nb_object.column_header.expect(\"Duplicate NB must have header column\");\n        let column_selection = sv.nb_object.column_selection;\n\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(true)), (column_selection as u32, &Into::<Value>::into(false))],\n        );\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(false)), (column_selection as u32, &Into::<Value>::into(true))],\n        );\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(false)), (column_selection as u32, &Into::<Value>::into(true))],\n        );\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(true)), (column_selection as u32, &Into::<Value>::into(false))],\n        );\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(false)), (column_selection as u32, &Into::<Value>::into(false))],\n        );\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(false)), (column_selection as u32, &Into::<Value>::into(true))],\n        );\n\n        let (items, groups) = check_how_much_elements_is_selected(&sv);\n        assert_eq!(items, 3);\n        assert_eq!(groups, 2);\n    }\n\n    #[gtk4::test]\n    fn returns_zero_when_nothing_selected() {\n        let (sv, list_store) = get_test_sv_duplicate();\n\n        let column_header = sv.nb_object.column_header.expect(\"Duplicate NB must have header column\");\n        let column_selection = sv.nb_object.column_selection;\n\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(true)), (column_selection as u32, &Into::<Value>::into(false))],\n        );\n        append_row_to_list_store(\n            &list_store,\n            &[(column_header as u32, &Into::<Value>::into(false)), (column_selection as u32, &Into::<Value>::into(false))],\n        );\n\n        let (items, groups) = check_how_much_elements_is_selected(&sv);\n        assert_eq!(items, 0);\n        assert_eq!(groups, 0);\n    }\n\n    #[gtk4::test]\n    fn returns_correct_count_of_groups() {\n        let (sv, list_store) = get_test_sv_duplicate();\n\n        let column_header = sv.nb_object.column_header.expect(\"Duplicate NB must have header column\");\n\n        append_row_to_list_store(&list_store, &[(column_header as u32, &Into::<Value>::into(true))]);\n        append_row_to_list_store(&list_store, &[(column_header as u32, &Into::<Value>::into(false))]);\n        append_row_to_list_store(&list_store, &[(column_header as u32, &Into::<Value>::into(true))]);\n        append_row_to_list_store(&list_store, &[(column_header as u32, &Into::<Value>::into(false))]);\n        append_row_to_list_store(&list_store, &[(column_header as u32, &Into::<Value>::into(true))]);\n\n        assert_eq!(count_number_of_groups(&sv), 3);\n    }\n\n    #[gtk4::test]\n    fn finds_existing_values_in_different_columns() {\n        let columns_types: &[Type] = &[Type::STRING, Type::STRING];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        let values_to_add: &[&[(u32, &dyn ToValue)]] = &[&[(0, &\"key1\"), (1, &\"value1\")], &[(0, &\"key2\"), (1, &\"value2\")], &[(0, &\"key3\"), (1, &\"value3\")]];\n        for i in values_to_add {\n            append_row_to_list_store(&list_store, i);\n        }\n\n        assert!(check_if_value_is_in_list_store(&list_store, 0, \"key2\"));\n        assert!(check_if_value_is_in_list_store(&list_store, 1, \"value3\"));\n        assert!(!check_if_value_is_in_list_store(&list_store, 0, \"nonexistent\"));\n        assert!(!check_if_value_is_in_list_store(&list_store, 1, \"key1\"));\n    }\n\n    #[gtk4::test]\n    fn detects_uniform_values_in_column() {\n        let columns_types: &[Type] = &[Type::BOOL];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        append_row_to_list_store(&list_store, &[(0, &true)]);\n        append_row_to_list_store(&list_store, &[(0, &true)]);\n        append_row_to_list_store(&list_store, &[(0, &true)]);\n\n        assert!(check_if_list_store_column_have_all_same_values(&list_store, 0, true));\n        assert!(!check_if_list_store_column_have_all_same_values(&list_store, 0, false));\n    }\n\n    #[gtk4::test]\n    fn detects_mixed_values_in_column() {\n        let columns_types: &[Type] = &[Type::BOOL];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        append_row_to_list_store(&list_store, &[(0, &true)]);\n        append_row_to_list_store(&list_store, &[(0, &false)]);\n        append_row_to_list_store(&list_store, &[(0, &true)]);\n\n        assert!(!check_if_list_store_column_have_all_same_values(&list_store, 0, true));\n        assert!(!check_if_list_store_column_have_all_same_values(&list_store, 0, false));\n    }\n\n    #[gtk4::test]\n    fn adds_row_with_multiple_columns() {\n        let columns_types: &[Type] = &[Type::STRING, Type::I32, Type::BOOL];\n        let list_store = gtk4::ListStore::new(columns_types);\n\n        append_row_to_list_store(&list_store, &[(0, &\"test\"), (1, &42), (2, &true)]);\n\n        let iter = list_store.iter_first().expect(\"Should have a row\");\n        assert_eq!(list_store.get::<String>(&iter, 0), \"test\");\n        assert_eq!(list_store.get::<i32>(&iter, 1), 42);\n        assert!(list_store.get::<bool>(&iter, 2));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/helpers/mod.rs",
    "content": "pub mod enums;\npub mod image_operations;\npub mod list_store_operations;\npub mod model_iter;\n"
  },
  {
    "path": "czkawka_gui/src/helpers/model_iter.rs",
    "content": "use gtk4::prelude::*;\nuse gtk4::{ListStore, TreeIter};\n\npub fn iter_list_with_break_init<G, F>(model: &ListStore, init: G, mut f: F)\nwhere\n    G: Fn(&ListStore, &mut TreeIter) -> bool,\n    F: FnMut(&ListStore, &TreeIter),\n{\n    if let Some(mut iter) = model.iter_first() {\n        if !init(model, &mut iter) {\n            return;\n        }\n        loop {\n            f(model, &iter);\n\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n        }\n    }\n}\npub fn iter_list_break_with_init<G, F>(model: &ListStore, init: G, mut f: F)\nwhere\n    G: Fn(&ListStore, &mut TreeIter),\n    F: FnMut(&ListStore, &TreeIter) -> bool,\n{\n    if let Some(mut iter) = model.iter_first() {\n        init(model, &mut iter);\n        loop {\n            if !f(model, &iter) {\n                break;\n            }\n\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n        }\n    }\n}\n\npub fn iter_list<F>(model: &ListStore, mut f: F)\nwhere\n    F: FnMut(&ListStore, &TreeIter),\n{\n    if let Some(mut iter) = model.iter_first() {\n        loop {\n            f(model, &iter);\n\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n        }\n    }\n}\npub fn iter_list_with_break<F>(model: &ListStore, mut f: F)\nwhere\n    F: FnMut(&ListStore, &TreeIter) -> bool,\n{\n    if let Some(mut iter) = model.iter_first() {\n        loop {\n            if !f(model, &iter) {\n                break;\n            }\n\n            if !model.iter_next(&mut iter) {\n                break;\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use glib::Value;\n    use glib::types::Type;\n\n    use super::*;\n\n    #[gtk4::test]\n    fn test_iter_list_collects_items() {\n        let types: &[Type] = &[Type::STRING];\n        let list_store = ListStore::new(types);\n\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"a\"))]);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"b\"))]);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"c\"))]);\n\n        let mut collected = Vec::new();\n        iter_list(&list_store, |m, i| {\n            collected.push(m.get::<String>(i, 0));\n        });\n\n        assert_eq!(collected, vec![\"a\".to_string(), \"b\".to_string(), \"c\".to_string()]);\n    }\n\n    #[gtk4::test]\n    fn test_iter_list_with_break_stops() {\n        let types: &[Type] = &[Type::STRING];\n        let list_store = ListStore::new(types);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"a\"))]);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"b\"))]);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"c\"))]);\n\n        let mut collected = Vec::new();\n        iter_list_with_break(&list_store, |m, i| {\n            collected.push(m.get::<String>(i, 0));\n            false\n        });\n\n        assert_eq!(collected, vec![\"a\".to_string()]);\n    }\n\n    #[gtk4::test]\n    fn test_iter_list_with_break_init_runs_init_and_iterates() {\n        let types: &[Type] = &[Type::STRING];\n        let list_store = ListStore::new(types);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"a\"))]);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"b\"))]);\n\n        let mut collected = Vec::new();\n        iter_list_with_break_init(\n            &list_store,\n            |m, i| m.iter_next(i),\n            |m, i| {\n                collected.push(m.get::<String>(i, 0));\n            },\n        );\n\n        assert_eq!(collected, vec![\"b\".to_string()]);\n    }\n\n    #[gtk4::test]\n    fn test_iter_list_break_with_init_runs_init_and_can_break() {\n        let types: &[Type] = &[Type::STRING];\n        let list_store = ListStore::new(types);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"a\"))]);\n        list_store.set(&list_store.append(), &[(0u32, &Into::<Value>::into(\"b\"))]);\n\n        let mut collected = Vec::new();\n        iter_list_break_with_init(\n            &list_store,\n            |_m, _i| {},\n            |m, i| {\n                collected.push(m.get::<String>(i, 0));\n                false\n            },\n        );\n\n        assert_eq!(collected, vec![\"a\".to_string()]);\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/initialize_gui.rs",
    "content": "use czkawka_core::tools::similar_images::SIMILAR_VALUES;\nuse czkawka_core::tools::similar_videos::MAX_TOLERANCE;\nuse gtk4::prelude::*;\n\nuse crate::gtk_traits::ComboBoxTraits;\nuse crate::gui_structs::gui_data::GuiData;\nuse crate::help_combo_box::{\n    DUPLICATES_CHECK_METHOD_COMBO_BOX, DUPLICATES_HASH_TYPE_COMBO_BOX, IMAGES_HASH_SIZE_COMBO_BOX, IMAGES_HASH_TYPE_COMBO_BOX, IMAGES_RESIZE_ALGORITHM_COMBO_BOX,\n};\nuse crate::help_functions::scale_set_min_max_values;\nuse crate::language_functions::LANGUAGES_ALL;\n\npub(crate) fn initialize_gui(gui_data: &GuiData) {\n    //// Initialize button\n    {\n        let buttons = &gui_data.bottom_buttons.buttons_array;\n        for button in buttons {\n            button.set_visible(false);\n        }\n        gui_data.bottom_buttons.buttons_search.set_visible(true);\n    }\n    //// Initialize language combo box\n    gui_data\n        .settings\n        .combo_box_settings_language\n        .set_model_and_first(LANGUAGES_ALL.iter().map(|e| &e.combo_box_text));\n\n    gui_data\n        .main_notebook\n        .combo_box_duplicate_check_method\n        .set_model_and_first(DUPLICATES_CHECK_METHOD_COMBO_BOX.iter().map(|e| &e.eng_name));\n    gui_data\n        .main_notebook\n        .combo_box_duplicate_hash_type\n        .set_model_and_first(DUPLICATES_HASH_TYPE_COMBO_BOX.iter().map(|e| &e.eng_name));\n\n    gui_data\n        .main_notebook\n        .combo_box_image_hash_algorithm\n        .set_model_and_first(IMAGES_HASH_TYPE_COMBO_BOX.iter().map(|e| &e.eng_name));\n    gui_data\n        .main_notebook\n        .combo_box_image_hash_size\n        .set_model_and_first(IMAGES_HASH_SIZE_COMBO_BOX.iter().map(|e| e.to_string()));\n    gui_data\n        .main_notebook\n        .combo_box_image_resize_algorithm\n        .set_model_and_first(IMAGES_RESIZE_ALGORITHM_COMBO_BOX.iter().map(|e| &e.eng_name));\n\n    //// Initialize main scrolled view with notebook\n    {\n        // Set step increment\n        let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();\n        scale_set_min_max_values(&scale_similarity_similar_images, 0_f64, SIMILAR_VALUES[0][5] as f64, 15_f64, Some(1_f64));\n\n        // Set step increment\n        let scale_similarity_similar_videos = gui_data.main_notebook.scale_similarity_similar_videos.clone();\n        scale_set_min_max_values(&scale_similarity_similar_videos, 0_f64, MAX_TOLERANCE as f64, 15_f64, Some(1_f64));\n    }\n\n    //// Window progress\n    {\n        let window_progress = gui_data.progress_window.window_progress.clone();\n        let stop_flag = gui_data.stop_flag.clone();\n\n        window_progress.connect_close_request(move |_| {\n            stop_flag.store(true, std::sync::atomic::Ordering::Relaxed);\n            glib::Propagation::Stop\n        });\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/language_functions.rs",
    "content": "#[derive(Clone)]\npub struct Language {\n    pub combo_box_text: &'static str,\n    pub short_text: &'static str,\n}\n\npub const LANGUAGES_ALL: &[Language] = &[\n    Language {\n        combo_box_text: \"English\",\n        short_text: \"en\",\n    },\n    Language {\n        combo_box_text: \"Français (French)\",\n        short_text: \"fr\",\n    },\n    Language {\n        combo_box_text: \"Italiano (Italian)\",\n        short_text: \"it\",\n    },\n    Language {\n        combo_box_text: \"Polski (Polish)\",\n        short_text: \"pl\",\n    },\n    Language {\n        combo_box_text: \"Русский (Russian)\",\n        short_text: \"ru\",\n    },\n    Language {\n        combo_box_text: \"український (Ukrainian)\",\n        short_text: \"uk\",\n    },\n    Language {\n        combo_box_text: \"한국어 (Korean)\",\n        short_text: \"ko\",\n    },\n    Language {\n        combo_box_text: \"Česky (Czech)\",\n        short_text: \"cs\",\n    },\n    Language {\n        combo_box_text: \"Deutsch (German)\",\n        short_text: \"de\",\n    },\n    Language {\n        combo_box_text: \"日本語 (Japanese)\",\n        short_text: \"ja\",\n    },\n    Language {\n        combo_box_text: \"Português (Portuguese)\",\n        short_text: \"pt-PT\",\n    },\n    Language {\n        combo_box_text: \"Português Brasileiro (Brazilian Portuguese)\",\n        short_text: \"pt-BR\",\n    },\n    Language {\n        combo_box_text: \"简体中文 (Simplified Chinese)\",\n        short_text: \"zh-CN\",\n    },\n    Language {\n        combo_box_text: \"繁體中文 (Traditional Chinese)\",\n        short_text: \"zh-TW\",\n    },\n    Language {\n        combo_box_text: \"Español (Spanish)\",\n        short_text: \"es-ES\",\n    },\n    Language {\n        combo_box_text: \"Norsk (Norwegian)\",\n        short_text: \"no\",\n    },\n    Language {\n        combo_box_text: \"Svenska (Swedish)\",\n        short_text: \"sv-SE\",\n    },\n    Language {\n        combo_box_text: \"العربية (Arabic)\",\n        short_text: \"ar\",\n    },\n    Language {\n        combo_box_text: \"Български (Bulgarian)\",\n        short_text: \"bg\",\n    },\n    Language {\n        combo_box_text: \"Ελληνικά (Greek)\",\n        short_text: \"el\",\n    },\n    Language {\n        combo_box_text: \"Nederlands (Dutch)\",\n        short_text: \"nl\",\n    },\n    Language {\n        combo_box_text: \"Română (Romanian)\",\n        short_text: \"ro\",\n    },\n    Language {\n        combo_box_text: \"Türkçe (Turkish)\",\n        short_text: \"tr\",\n    },\n];\n\npub(crate) fn get_language_from_combo_box_text(combo_box_text: &str) -> Language {\n    for lang in LANGUAGES_ALL {\n        if lang.combo_box_text == combo_box_text {\n            return lang.clone();\n        }\n    }\n\n    panic!(\"Not found proper text\"); // Must be valid, because it is loaded from gui, not from untrusted source\n}\n"
  },
  {
    "path": "czkawka_gui/src/localizer_gui.rs",
    "content": "use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};\nuse i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};\nuse rust_embed::RustEmbed;\n\n#[derive(RustEmbed)]\n#[folder = \"i18n/\"]\nstruct Localizations;\n\npub static LANGUAGE_LOADER_GUI: std::sync::LazyLock<FluentLanguageLoader> = std::sync::LazyLock::new(|| {\n    let loader: FluentLanguageLoader = fluent_language_loader!();\n\n    loader.load_fallback_language(&Localizations).expect(\"Error while loading fallback language\");\n\n    loader\n});\n\n#[macro_export]\nmacro_rules! flg {\n    ( $($tt:tt)* ) => {{\n        i18n_embed_fl::fl!($crate::localizer_gui::LANGUAGE_LOADER_GUI, $($tt)*)\n    }};\n}\n\n// Get the `Localizer` to be used for localizing this library.\npub(crate) fn localizer_gui() -> Box<dyn Localizer> {\n    Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_GUI, &Localizations))\n}\n"
  },
  {
    "path": "czkawka_gui/src/main.rs",
    "content": "// Remove console window in Windows OS\n#![windows_subsystem = \"windows\"]\n#![allow(clippy::indexing_slicing)] // Too much used, to be able to ignore it in every place\n\nuse std::env;\n\nuse connect_things::connect_about_buttons::connect_about_buttons;\nuse connect_things::connect_button_compare::connect_button_compare;\nuse connect_things::connect_button_delete::connect_button_delete;\nuse connect_things::connect_button_hardlink::connect_button_hardlink_symlink;\nuse connect_things::connect_button_move::connect_button_move;\nuse connect_things::connect_button_save::connect_button_save;\nuse connect_things::connect_button_search::connect_button_search;\nuse connect_things::connect_button_select::connect_button_select;\nuse connect_things::connect_button_stop::connect_button_stop;\nuse connect_things::connect_change_language::{connect_change_language, load_system_language};\nuse connect_things::connect_duplicate_buttons::connect_duplicate_combo_box;\nuse connect_things::connect_header_buttons::connect_button_about;\nuse connect_things::connect_krokiet_info_dialog::show_krokiet_info_dialog;\nuse connect_things::connect_notebook_tabs::connect_notebook_tabs;\nuse connect_things::connect_progress_window::connect_progress_window;\nuse connect_things::connect_selection_of_directories::connect_selection_of_directories;\nuse connect_things::connect_settings::connect_settings;\nuse connect_things::connect_show_hide_ui::connect_show_hide_ui;\nuse connect_things::connect_similar_image_size_change::connect_similar_image_size_change;\nuse crossbeam_channel::{Receiver, Sender, unbounded};\nuse czkawka_core::common::basic_gui_cli::{CliResult, process_cli_args};\nuse czkawka_core::common::config_cache_path::{get_config_cache_path, print_infos_and_warnings, set_config_cache_path};\nuse czkawka_core::common::image::register_image_decoding_hooks;\nuse czkawka_core::common::logger::{filtering_messages, print_version_mode, setup_logger};\nuse czkawka_core::common::progress_data::ProgressData;\nuse czkawka_core::common::{get_number_of_threads, set_number_of_threads};\nuse czkawka_core::{TOOLS_NUMBER, localizer_core};\nuse glib::ExitCode;\nuse gtk4::Application;\nuse gtk4::gio::ApplicationFlags;\nuse gtk4::prelude::*;\nuse gui_structs::gui_data::{\n    CZK_ICON_ADD, CZK_ICON_COMPARE, CZK_ICON_DELETE, CZK_ICON_HARDLINK, CZK_ICON_HIDE_DOWN, CZK_ICON_HIDE_UP, CZK_ICON_INFO, CZK_ICON_LEFT, CZK_ICON_MANUAL_ADD, CZK_ICON_MOVE,\n    CZK_ICON_RIGHT, CZK_ICON_SAVE, CZK_ICON_SEARCH, CZK_ICON_SELECT, CZK_ICON_SETTINGS, CZK_ICON_STOP, CZK_ICON_SYMLINK, CZK_ICON_TRASH, GuiData,\n};\nuse log::info;\n\nuse crate::compute_results::connect_compute_results;\nuse crate::connect_things::connect_button_sort::connect_button_sort;\nuse crate::connect_things::connect_popovers_select::connect_popover_select;\nuse crate::connect_things::connect_popovers_sort::connect_popover_sort;\nuse crate::connect_things::connect_same_music_mode_changed::connect_same_music_change_mode;\nuse crate::initialize_gui::initialize_gui;\nuse crate::language_functions::LANGUAGES_ALL;\nuse crate::saving_loading::{DEFAULT_MAXIMAL_FILE_SIZE, DEFAULT_MINIMAL_CACHE_SIZE, DEFAULT_MINIMAL_FILE_SIZE, load_configuration, reset_configuration, save_configuration};\n\nmod compute_results;\nmod connect_things;\nmod gtk_traits;\nmod gui_structs;\nmod help_combo_box;\nmod help_functions;\nmod helpers;\nmod initialize_gui;\nmod language_functions;\nmod localizer_gui;\nmod notebook_enums;\nmod notebook_info;\nmod opening_selecting_records;\nmod saving_loading;\nmod taskbar_progress;\n#[cfg(not(target_os = \"windows\"))]\nmod taskbar_progress_dummy;\n#[cfg(target_os = \"windows\")]\nmod taskbar_progress_win;\n\npub const CZKAWKA_GTK_TOOL_NUMBER: usize = TOOLS_NUMBER - 3; // Missing exif, video optimizer, bad names tools\n\nfn main() {\n    register_image_decoding_hooks();\n    let config_cache_path_set_result = set_config_cache_path(\"Czkawka\", \"Czkawka\");\n    let exists_krokiet_info_file = get_config_cache_path().is_some_and(|cache_config| {\n        let file_path = cache_config.cache_folder.join(\"krokiet_info_dialog_seen.txt\");\n        let exists = file_path.exists();\n        if !exists {\n            let _ = std::fs::write(\n                file_path,\n                \"This file indicates that user has seen Krokiet info dialog. You can delete this file to see the dialog again.\",\n            );\n        }\n        exists\n    });\n\n    let needs_to_open_dialog_about_krokiet = !(config_cache_path_set_result.config_env_set\n        || config_cache_path_set_result.cache_env_set\n        || exists_krokiet_info_file\n        || option_env!(\"CZKAWKA_DONT_ANNOY_ME\").as_ref().is_some_and(|x| !x.is_empty()));\n\n    let application = Application::new(None::<String>, ApplicationFlags::HANDLES_OPEN | ApplicationFlags::HANDLES_COMMAND_LINE);\n\n    #[cfg(target_os = \"linux\")]\n    glib::set_prgname(Some(\"com.github.qarmin.czkawka\"));\n\n    application.connect_command_line(move |app, cmdline| {\n        let cli_args = process_cli_args(\n            \"Czkawka Gui\",\n            \"czkawka_gui\",\n            cmdline.arguments().into_iter().skip(1).map(|x| x.to_string_lossy().to_string()).collect(),\n        );\n        setup_logger(false, \"czkawka_gui\", filtering_messages);\n        print_version_mode(\"Czkawka gtk\");\n        print_infos_and_warnings(config_cache_path_set_result.infos.clone(), config_cache_path_set_result.warnings.clone());\n        build_ui(app, cli_args.as_ref(), needs_to_open_dialog_about_krokiet);\n        ExitCode::new(0)\n    });\n    application.run_with_args(&env::args().collect::<Vec<_>>());\n}\n\nfn build_ui(application: &Application, cli_args: Option<&CliResult>, needs_to_open_dialog_about_krokiet: bool) {\n    let gui_data: GuiData = GuiData::new_with_application(application);\n    gui_data.setup();\n\n    let (result_sender, result_receiver) = unbounded();\n\n    // Futures progress report\n    let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n\n    initialize_gui(&gui_data);\n    reset_configuration(false, &gui_data.upper_notebook, &gui_data.main_notebook, &gui_data.settings, &gui_data.text_view_errors); // Fallback for invalid loading setting project\n    load_system_language(&gui_data); // Check for default system language, must be loaded after initializing GUI and before loading settings from file\n    load_configuration(\n        false,\n        &gui_data.upper_notebook,\n        &gui_data.main_notebook,\n        &gui_data.settings,\n        &gui_data.text_view_errors,\n        &gui_data.scrolled_window_errors,\n        cli_args,\n    );\n    set_number_of_threads(gui_data.settings.scale_settings_number_of_threads.value().round() as usize);\n\n    print_czkawka_gui_info(get_number_of_threads());\n\n    // Needs to run when entire GUI is initialized\n    connect_change_language(&gui_data);\n\n    connect_button_delete(&gui_data);\n    connect_button_save(&gui_data);\n    connect_button_search(&gui_data, result_sender, progress_sender);\n    connect_button_select(&gui_data);\n    connect_button_sort(&gui_data);\n    connect_button_stop(&gui_data);\n    connect_button_hardlink_symlink(&gui_data);\n    connect_button_move(&gui_data);\n    connect_button_compare(&gui_data);\n\n    connect_duplicate_combo_box(&gui_data);\n    connect_notebook_tabs(&gui_data);\n    connect_selection_of_directories(&gui_data);\n    connect_popover_select(&gui_data);\n    connect_popover_sort(&gui_data);\n    connect_compute_results(&gui_data, result_receiver);\n    connect_progress_window(&gui_data, progress_receiver);\n    connect_show_hide_ui(&gui_data);\n    connect_settings(&gui_data);\n    connect_button_about(&gui_data);\n    connect_about_buttons(&gui_data);\n    connect_similar_image_size_change(&gui_data);\n    connect_same_music_change_mode(&gui_data);\n\n    let window_main = gui_data.window_main.clone();\n    let taskbar_state = gui_data.taskbar_state.clone();\n    let used_additional_arguments = cli_args.is_some();\n\n    // Show Krokiet info dialog if needed\n    if needs_to_open_dialog_about_krokiet {\n        show_krokiet_info_dialog(&window_main);\n    }\n\n    window_main.connect_close_request(move |_| {\n        // Not save configuration when using non default arguments\n        if !used_additional_arguments {\n            save_configuration(false, &gui_data.upper_notebook, &gui_data.main_notebook, &gui_data.settings, &gui_data.text_view_errors);\n            // Save configuration at exit\n        }\n        taskbar_state.borrow_mut().release();\n        glib::Propagation::Proceed\n    });\n}\n\npub(crate) fn print_czkawka_gui_info(thread_number: usize) {\n    let gtk_version = format!(\"{}.{}.{}\", gtk4::major_version(), gtk4::minor_version(), gtk4::micro_version());\n\n    info!(\"Czkawka Gui - used thread number: {thread_number}, gtk version {gtk_version}\");\n}\n"
  },
  {
    "path": "czkawka_gui/src/notebook_enums.rs",
    "content": "use crate::CZKAWKA_GTK_TOOL_NUMBER;\n\n// Needs to be updated when changed order of notebook tabs\n#[derive(Eq, PartialEq, Hash, Clone, Debug, Copy)]\npub enum NotebookMainEnum {\n    Duplicate = 0,\n    EmptyDirectories,\n    BigFiles,\n    EmptyFiles,\n    Temporary,\n    SimilarImages,\n    SimilarVideos,\n    SameMusic,\n    Symlinks,\n    BrokenFiles,\n    BadExtensions,\n}\n\npub(crate) fn to_notebook_main_enum(notebook_number: u32) -> NotebookMainEnum {\n    match notebook_number {\n        0 => NotebookMainEnum::Duplicate,\n        1 => NotebookMainEnum::EmptyDirectories,\n        2 => NotebookMainEnum::BigFiles,\n        3 => NotebookMainEnum::EmptyFiles,\n        4 => NotebookMainEnum::Temporary,\n        5 => NotebookMainEnum::SimilarImages,\n        6 => NotebookMainEnum::SimilarVideos,\n        7 => NotebookMainEnum::SameMusic,\n        8 => NotebookMainEnum::Symlinks,\n        9 => NotebookMainEnum::BrokenFiles,\n        10 => NotebookMainEnum::BadExtensions,\n        _ => panic!(\"Invalid Notebook Tab\"),\n    }\n}\n\npub(crate) fn get_all_main_tabs() -> [NotebookMainEnum; CZKAWKA_GTK_TOOL_NUMBER] {\n    [\n        to_notebook_main_enum(0),\n        to_notebook_main_enum(1),\n        to_notebook_main_enum(2),\n        to_notebook_main_enum(3),\n        to_notebook_main_enum(4),\n        to_notebook_main_enum(5),\n        to_notebook_main_enum(6),\n        to_notebook_main_enum(7),\n        to_notebook_main_enum(8),\n        to_notebook_main_enum(9),\n        to_notebook_main_enum(10),\n    ]\n}\n\n#[derive(Eq, PartialEq, Hash, Clone, Debug, Copy)]\npub enum NotebookUpperEnum {\n    IncludedDirectories = 0,\n    ExcludedDirectories,\n    ItemsConfiguration,\n}\n\npub(crate) fn to_notebook_upper_enum(notebook_number: u32) -> NotebookUpperEnum {\n    match notebook_number {\n        0 => NotebookUpperEnum::IncludedDirectories,\n        1 => NotebookUpperEnum::ExcludedDirectories,\n        2 => NotebookUpperEnum::ItemsConfiguration,\n        _ => panic!(\"Invalid Upper Notebook Tab\"),\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/notebook_info.rs",
    "content": "use glib::types::Type;\n\nuse crate::CZKAWKA_GTK_TOOL_NUMBER;\nuse crate::helpers::enums::{\n    BottomButtonsEnum, ColumnsBadExtensions, ColumnsBigFiles, ColumnsBrokenFiles, ColumnsDuplicates, ColumnsEmptyFiles, ColumnsEmptyFolders, ColumnsInvalidSymlinks,\n    ColumnsSameMusic, ColumnsSimilarImages, ColumnsSimilarVideos, ColumnsTemporaryFiles, PopoverTypes,\n};\nuse crate::notebook_enums::NotebookMainEnum;\n\n#[derive(Debug, Clone)]\npub struct NotebookObject {\n    pub name: &'static str,\n    pub notebook_type: NotebookMainEnum,\n    pub available_modes: &'static [PopoverTypes],\n    #[expect(unused)]\n    pub column_activatable_button: Option<i32>,\n    pub column_path: i32,\n    pub column_name: i32,\n    pub column_selection: i32,\n    pub column_header: Option<i32>,\n    pub column_dimensions: Option<i32>,\n    #[expect(unused)]\n    pub column_size: Option<i32>,\n    pub column_size_as_bytes: Option<i32>,\n    pub column_modification_as_secs: Option<i32>,\n    pub columns_types: &'static [Type],\n    pub bottom_buttons: &'static [BottomButtonsEnum],\n    pub tree_view_name: &'static str,\n}\n\npub static NOTEBOOKS_INFO: [NotebookObject; CZKAWKA_GTK_TOOL_NUMBER] = [\n    NotebookObject {\n        name: \"Duplicates\",\n        notebook_type: NotebookMainEnum::Duplicate,\n        available_modes: &[\n            PopoverTypes::All,\n            PopoverTypes::Reverse,\n            PopoverTypes::Custom,\n            PopoverTypes::Date,\n            PopoverTypes::Size,\n            PopoverTypes::All,\n            PopoverTypes::PathLength,\n        ],\n        column_activatable_button: Some(ColumnsDuplicates::ActivatableSelectButton as i32),\n        column_path: ColumnsDuplicates::Path as i32,\n        column_name: ColumnsDuplicates::Name as i32,\n        column_selection: ColumnsDuplicates::SelectionButton as i32,\n        column_header: Some(ColumnsDuplicates::IsHeader as i32),\n        column_dimensions: None,\n        column_size: Some(ColumnsDuplicates::Size as i32), // Useless with duplicates by hash or size, but needed by sorting by name\n        column_size_as_bytes: Some(ColumnsDuplicates::SizeAsBytes as i32),\n        column_modification_as_secs: Some(ColumnsDuplicates::ModificationAsSecs as i32),\n        columns_types: &[\n            Type::BOOL,   // ActivatableSelectButton\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Size\n            Type::U64,    // SizeAsBytes\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n            Type::STRING, // Color\n            Type::BOOL,   // IsHeader\n            Type::STRING, // TextColor\n        ],\n        bottom_buttons: &[\n            BottomButtonsEnum::Save,\n            BottomButtonsEnum::Delete,\n            BottomButtonsEnum::Select,\n            BottomButtonsEnum::Sort,\n            BottomButtonsEnum::Symlink,\n            BottomButtonsEnum::Hardlink,\n            BottomButtonsEnum::Move,\n        ],\n        tree_view_name: \"tree_view_duplicate_finder\",\n    },\n    NotebookObject {\n        name: \"Empty Folders\",\n        notebook_type: NotebookMainEnum::EmptyDirectories,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsEmptyFolders::Path as i32,\n        column_name: ColumnsEmptyFolders::Name as i32,\n        column_selection: ColumnsEmptyFolders::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_empty_folder_finder\",\n    },\n    NotebookObject {\n        name: \"Big Files\",\n        notebook_type: NotebookMainEnum::BigFiles,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsBigFiles::Path as i32,\n        column_name: ColumnsBigFiles::Name as i32,\n        column_selection: ColumnsBigFiles::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Size\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // SizeAsBytes\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_big_files_finder\",\n    },\n    NotebookObject {\n        name: \"Big Files\",\n        notebook_type: NotebookMainEnum::EmptyFiles,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsEmptyFiles::Path as i32,\n        column_name: ColumnsEmptyFiles::Name as i32,\n        column_selection: ColumnsEmptyFiles::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_empty_files_finder\",\n    },\n    NotebookObject {\n        name: \"Temporary Files\",\n        notebook_type: NotebookMainEnum::Temporary,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsTemporaryFiles::Path as i32,\n        column_name: ColumnsTemporaryFiles::Name as i32,\n        column_selection: ColumnsTemporaryFiles::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_temporary_files_finder\",\n    },\n    NotebookObject {\n        name: \"Similar Images\",\n        notebook_type: NotebookMainEnum::SimilarImages,\n        available_modes: &[\n            PopoverTypes::All,\n            PopoverTypes::Reverse,\n            PopoverTypes::Custom,\n            PopoverTypes::Date,\n            PopoverTypes::Size,\n            PopoverTypes::PathLength,\n        ],\n        column_activatable_button: Some(ColumnsSimilarImages::ActivatableSelectButton as i32),\n        column_path: ColumnsSimilarImages::Path as i32,\n        column_name: ColumnsSimilarImages::Name as i32,\n        column_selection: ColumnsSimilarImages::SelectionButton as i32,\n        column_header: Some(ColumnsSimilarImages::IsHeader as i32),\n        column_dimensions: Some(ColumnsSimilarImages::Dimensions as i32),\n        column_size: Some(ColumnsSimilarImages::Size as i32),\n        column_size_as_bytes: Some(ColumnsSimilarImages::SizeAsBytes as i32),\n        column_modification_as_secs: Some(ColumnsSimilarImages::ModificationAsSecs as i32),\n        columns_types: &[\n            Type::BOOL,   // ActivatableSelectButton\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Similarity\n            Type::STRING, // Size\n            Type::U64,    // SizeAsBytes\n            Type::STRING, // Dimensions\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n            Type::STRING, // Color\n            Type::BOOL,   // IsHeader\n            Type::STRING, // TextColor\n        ],\n        bottom_buttons: &[\n            BottomButtonsEnum::Save,\n            BottomButtonsEnum::Delete,\n            BottomButtonsEnum::Select,\n            BottomButtonsEnum::Sort,\n            BottomButtonsEnum::Symlink,\n            BottomButtonsEnum::Hardlink,\n            BottomButtonsEnum::Move,\n            BottomButtonsEnum::Compare,\n        ],\n        tree_view_name: \"tree_view_similar_images_finder\",\n    },\n    NotebookObject {\n        name: \"Similar Videos\",\n        notebook_type: NotebookMainEnum::SimilarVideos,\n        available_modes: &[\n            PopoverTypes::All,\n            PopoverTypes::Reverse,\n            PopoverTypes::Custom,\n            PopoverTypes::Date,\n            PopoverTypes::Size,\n            PopoverTypes::PathLength,\n        ],\n        column_activatable_button: Some(ColumnsSimilarVideos::ActivatableSelectButton as i32),\n        column_path: ColumnsSimilarVideos::Path as i32,\n        column_name: ColumnsSimilarVideos::Name as i32,\n        column_selection: ColumnsSimilarVideos::SelectionButton as i32,\n        column_header: Some(ColumnsSimilarVideos::IsHeader as i32),\n        column_dimensions: Some(ColumnsSimilarVideos::Dimensions as i32),\n        column_size: Some(ColumnsSimilarVideos::Size as i32),\n        column_size_as_bytes: Some(ColumnsSimilarVideos::SizeAsBytes as i32),\n        column_modification_as_secs: Some(ColumnsSimilarVideos::ModificationAsSecs as i32),\n        columns_types: &[\n            Type::BOOL,   // ActivatableSelectButton\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Size\n            Type::U64,    // SizeAsBytes\n            Type::STRING, // Fps\n            Type::STRING, // Codec\n            Type::STRING, // Bitrate\n            Type::STRING, // Dimensions\n            Type::STRING, // Duration\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n            Type::STRING, // Color\n            Type::BOOL,   // IsHeader\n            Type::STRING, // TextColor\n        ],\n        bottom_buttons: &[\n            BottomButtonsEnum::Save,\n            BottomButtonsEnum::Delete,\n            BottomButtonsEnum::Select,\n            BottomButtonsEnum::Sort,\n            BottomButtonsEnum::Symlink,\n            BottomButtonsEnum::Hardlink,\n            BottomButtonsEnum::Move,\n        ],\n        tree_view_name: \"tree_view_similar_videos_finder\",\n    },\n    NotebookObject {\n        name: \"Same Music\",\n        notebook_type: NotebookMainEnum::SameMusic,\n        available_modes: &[\n            PopoverTypes::All,\n            PopoverTypes::Reverse,\n            PopoverTypes::Custom,\n            PopoverTypes::Date,\n            PopoverTypes::Size,\n            PopoverTypes::PathLength,\n        ],\n        column_activatable_button: Some(ColumnsSameMusic::ActivatableSelectButton as i32),\n        column_path: ColumnsSameMusic::Path as i32,\n        column_name: ColumnsSameMusic::Name as i32,\n        column_selection: ColumnsSameMusic::SelectionButton as i32,\n        column_header: Some(ColumnsSameMusic::IsHeader as i32),\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: Some(ColumnsSameMusic::SizeAsBytes as i32),\n        column_modification_as_secs: Some(ColumnsSameMusic::ModificationAsSecs as i32),\n        columns_types: &[\n            Type::BOOL,   // ActivatableSelectButton\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Size\n            Type::U64,    // SizeAsBytes\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // Title\n            Type::STRING, // Artist\n            Type::STRING, // Year\n            Type::STRING, // Bitrate\n            Type::U64,    // BitrateAsNumber\n            Type::STRING, // Length\n            Type::STRING, // Genre\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n            Type::STRING, // Color\n            Type::BOOL,   // IsHeader\n            Type::STRING, // TextColor\n        ],\n        bottom_buttons: &[\n            BottomButtonsEnum::Save,\n            BottomButtonsEnum::Delete,\n            BottomButtonsEnum::Select,\n            BottomButtonsEnum::Sort,\n            BottomButtonsEnum::Symlink,\n            BottomButtonsEnum::Hardlink,\n            BottomButtonsEnum::Move,\n        ],\n        tree_view_name: \"tree_view_same_music_finder\",\n    },\n    NotebookObject {\n        name: \"Invalid Symlinks\",\n        notebook_type: NotebookMainEnum::Symlinks,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsInvalidSymlinks::Path as i32,\n        column_name: ColumnsInvalidSymlinks::Name as i32,\n        column_selection: ColumnsInvalidSymlinks::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // DestinationPath\n            Type::STRING, // TypeOfError\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_invalid_symlinks\",\n    },\n    NotebookObject {\n        name: \"Broken Files\",\n        notebook_type: NotebookMainEnum::BrokenFiles,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsBrokenFiles::Path as i32,\n        column_name: ColumnsBrokenFiles::Name as i32,\n        column_selection: ColumnsBrokenFiles::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // ErrorType\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_broken_files\",\n    },\n    NotebookObject {\n        name: \"Bad Extensions\",\n        notebook_type: NotebookMainEnum::BadExtensions,\n        available_modes: &[PopoverTypes::All, PopoverTypes::Reverse, PopoverTypes::Custom, PopoverTypes::PathLength],\n        column_activatable_button: None,\n        column_path: ColumnsBadExtensions::Path as i32,\n        column_name: ColumnsBadExtensions::Name as i32,\n        column_selection: ColumnsBadExtensions::SelectionButton as i32,\n        column_header: None,\n        column_dimensions: None,\n        column_size: None,\n        column_size_as_bytes: None,\n        column_modification_as_secs: None,\n        columns_types: &[\n            Type::BOOL,   // SelectionButton\n            Type::STRING, // Name\n            Type::STRING, // Path\n            Type::STRING, // CurrentExtension\n            Type::STRING, // ProperExtensions\n            Type::STRING, // Modification\n            Type::U64,    // ModificationAsSecs\n        ],\n        bottom_buttons: &[BottomButtonsEnum::Save, BottomButtonsEnum::Delete, BottomButtonsEnum::Select, BottomButtonsEnum::Move],\n        tree_view_name: \"tree_view_bad_extensions\",\n    },\n];\n"
  },
  {
    "path": "czkawka_gui/src/opening_selecting_records.rs",
    "content": "use gdk4::{Key, ModifierType};\nuse gtk4::prelude::*;\nuse gtk4::{GestureClick, TreeModel, TreePath, TreeSelection};\nuse log::{debug, error};\n\nuse crate::gui_structs::common_tree_view::{GetTreeViewTrait, TreeViewListStoreTrait};\nuse crate::help_functions::{KEY_ENTER, KEY_SPACE, get_full_name_from_path_name, get_notebook_object_from_tree_view, get_notebook_upper_enum_from_tree_view};\nuse crate::helpers::enums::{ColumnsDuplicates, ColumnsExcludedDirectory, ColumnsIncludedDirectory, ColumnsSameMusic, ColumnsSimilarImages, ColumnsSimilarVideos};\nuse crate::notebook_enums::NotebookUpperEnum;\n\n// TODO add option to open files and folders from context menu activated by pressing ONCE with right mouse button\n\npub(crate) fn opening_enter_function_ported_upper_directories(\n    event_controller: &gtk4::EventControllerKey,\n    _key_value: Key,\n    key_code: u32,\n    _modifier_type: ModifierType,\n) -> glib::Propagation {\n    let tree_view = event_controller.get_tree_view();\n\n    if cfg!(debug_assertions) {\n        debug!(\"Clicked at key: {key_code}\");\n    }\n\n    match get_notebook_upper_enum_from_tree_view(&tree_view) {\n        NotebookUpperEnum::IncludedDirectories => {\n            handle_tree_keypress_upper_directories(\n                &tree_view,\n                key_code,\n                ColumnsIncludedDirectory::Path as i32,\n                Some(ColumnsIncludedDirectory::ReferenceButton as i32),\n            );\n        }\n        NotebookUpperEnum::ExcludedDirectories => {\n            handle_tree_keypress_upper_directories(&tree_view, key_code, ColumnsExcludedDirectory::Path as i32, None);\n        }\n        NotebookUpperEnum::ItemsConfiguration => {\n            panic!()\n        }\n    }\n    // false // True catches signal, and don't send it to function, e.g. up button is caught and don't move selection\n    glib::Propagation::Proceed\n}\n\npub(crate) fn opening_middle_mouse_function(gesture_click: &GestureClick, _number_of_clicks: i32, _b: f64, _c: f64) {\n    let tree_view = gesture_click.get_tree_view();\n\n    let nt_object = get_notebook_object_from_tree_view(&tree_view);\n    if let Some(column_header) = nt_object.column_header\n        && gesture_click.current_button() == 2\n    {\n        reverse_selection(&tree_view, column_header, nt_object.column_selection);\n    }\n}\n\npub(crate) fn opening_double_click_function_directories(gesture_click: &GestureClick, number_of_clicks: i32, _b: f64, _c: f64) {\n    let tree_view = gesture_click.get_tree_view();\n\n    if number_of_clicks == 2 && (gesture_click.current_button() == 1 || gesture_click.current_button() == 3) {\n        match get_notebook_upper_enum_from_tree_view(&tree_view) {\n            NotebookUpperEnum::IncludedDirectories => {\n                common_open_function_upper_directories(&tree_view, ColumnsIncludedDirectory::Path as i32);\n            }\n            NotebookUpperEnum::ExcludedDirectories => {\n                common_open_function_upper_directories(&tree_view, ColumnsExcludedDirectory::Path as i32);\n            }\n            NotebookUpperEnum::ItemsConfiguration => {\n                panic!()\n            }\n        }\n    }\n}\n\npub(crate) fn opening_enter_function_ported(event_controller: &gtk4::EventControllerKey, _key: Key, key_code: u32, _modifier_type: ModifierType) -> glib::Propagation {\n    let tree_view = event_controller.get_tree_view();\n    if cfg!(debug_assertions) {\n        debug!(\"Clicked {key_code}\");\n    }\n\n    let nt_object = get_notebook_object_from_tree_view(&tree_view);\n    handle_tree_keypress(\n        &tree_view,\n        key_code,\n        nt_object.column_name,\n        nt_object.column_path,\n        nt_object.column_selection,\n        nt_object.column_header,\n    );\n    glib::Propagation::Proceed // True catches signal, and don't send it to function, e.g. up button is caught and don't move selection\n}\n\npub(crate) fn opening_double_click_function(gesture_click: &GestureClick, number_of_clicks: i32, _b: f64, _c: f64) {\n    let tree_view = gesture_click.get_tree_view();\n\n    let nt_object = get_notebook_object_from_tree_view(&tree_view);\n    if number_of_clicks == 2 {\n        if gesture_click.current_button() == 1 {\n            common_open_function(&tree_view, nt_object.column_name, nt_object.column_path, &OpenMode::PathAndName);\n        } else if gesture_click.current_button() == 3 {\n            common_open_function(&tree_view, nt_object.column_name, nt_object.column_path, &OpenMode::OnlyPath);\n        }\n    }\n}\n\nenum OpenMode {\n    OnlyPath,\n    PathAndName,\n}\n\nfn common_mark_function(tree_view: &gtk4::TreeView, column_selection: i32, column_header: Option<i32>) {\n    let selection = tree_view.selection();\n    let (selected_rows, tree_model) = selection.selected_rows();\n\n    let model = tree_view.get_model();\n\n    for tree_path in selected_rows.iter().rev() {\n        if let Some(column_header) = column_header\n            && model.get::<bool>(&model.iter(tree_path).expect(\"Using invalid tree_path\"), column_header)\n        {\n            continue;\n        }\n        let value = !tree_model.get::<bool>(&tree_model.iter(tree_path).expect(\"Invalid tree_path\"), column_selection);\n        model.set_value(&tree_model.iter(tree_path).expect(\"Invalid tree_path\"), column_selection as u32, &value.to_value());\n    }\n}\n\nfn common_open_function(tree_view: &gtk4::TreeView, column_name: i32, column_path: i32, opening_mode: &OpenMode) {\n    let selection = tree_view.selection();\n    let (selected_rows, tree_model) = selection.selected_rows();\n\n    for tree_path in selected_rows.iter().rev() {\n        let name = tree_model.get::<String>(&tree_model.iter(tree_path).expect(\"Invalid tree_path\"), column_name);\n        let path = tree_model.get::<String>(&tree_model.iter(tree_path).expect(\"Invalid tree_path\"), column_path);\n\n        let end_path = match opening_mode {\n            OpenMode::OnlyPath => path,\n            OpenMode::PathAndName => get_full_name_from_path_name(&path, &name),\n        };\n\n        if let Err(e) = open::that(&end_path) {\n            error!(\"Failed to open file {end_path}, reason {e}\");\n        }\n    }\n}\n\nfn reverse_selection(tree_view: &gtk4::TreeView, column_header: i32, column_selection: i32) {\n    let (selected_rows, model) = tree_view.selection().selected_rows();\n    let model = model.downcast::<gtk4::ListStore>().expect(\"Invalid list store model\");\n\n    if selected_rows.len() != 1 {\n        return; // Multiple selection is not supported because it is a lot of harder to do it properly\n    }\n    let tree_path = selected_rows[0].clone();\n    let current_iter = model.iter(&tree_path).expect(\"Invalid tree_path\");\n\n    if model.get::<bool>(&current_iter, column_header) {\n        return; // Selecting header is not supported(this is available by using reference)\n    }\n\n    // This will revert selection of current selected item, but I don't think that this is needed\n    // let current_value = model.get::<bool>(&current_iter, column_selection);\n    // model.set_value(&current_iter, column_selection as u32, &(!current_value).to_value());\n\n    let mut to_upper_iter = current_iter;\n    loop {\n        if !model.iter_previous(&mut to_upper_iter) {\n            break;\n        }\n        if model.get::<bool>(&to_upper_iter, column_header) {\n            break;\n        }\n\n        let current_value = model.get::<bool>(&to_upper_iter, column_selection);\n        model.set_value(&to_upper_iter, column_selection as u32, &(!current_value).to_value());\n    }\n\n    let mut to_lower_iter = current_iter;\n    loop {\n        if !model.iter_next(&mut to_lower_iter) {\n            break;\n        }\n        if model.get::<bool>(&to_lower_iter, column_header) {\n            break;\n        }\n\n        let current_value = model.get::<bool>(&to_lower_iter, column_selection);\n        model.set_value(&to_lower_iter, column_selection as u32, &(!current_value).to_value());\n    }\n}\n\nfn common_open_function_upper_directories(tree_view: &gtk4::TreeView, column_full_path: i32) {\n    let selection = tree_view.selection();\n    let (selected_rows, tree_model) = selection.selected_rows();\n\n    for tree_path in selected_rows.iter().rev() {\n        let full_path = tree_model.get::<String>(&tree_model.iter(tree_path).expect(\"Invalid tree_path\"), column_full_path);\n\n        if let Err(e) = open::that(&full_path) {\n            error!(\"Failed to open file {full_path}, reason {e}\");\n        }\n    }\n}\n\nfn handle_tree_keypress_upper_directories(tree_view: &gtk4::TreeView, key_code: u32, full_path_column: i32, mark_column: Option<i32>) {\n    match key_code {\n        KEY_ENTER => {\n            common_open_function_upper_directories(tree_view, full_path_column);\n        }\n        KEY_SPACE => {\n            if let Some(mark_column) = mark_column {\n                common_mark_function(tree_view, mark_column, None);\n            }\n        }\n        _ => {}\n    }\n}\n\nfn handle_tree_keypress(tree_view: &gtk4::TreeView, key_code: u32, name_column: i32, path_column: i32, mark_column: i32, column_header: Option<i32>) {\n    match key_code {\n        KEY_ENTER => {\n            common_open_function(tree_view, name_column, path_column, &OpenMode::PathAndName);\n        }\n        KEY_SPACE => {\n            common_mark_function(tree_view, mark_column, column_header);\n        }\n        _ => {}\n    }\n}\n\npub(crate) fn select_function_duplicates(tree_selection: &gtk4::TreeSelection, tree_model: &gtk4::TreeModel, tree_path: &gtk4::TreePath, is_path_currently_selected: bool) -> bool {\n    select_function_header(ColumnsDuplicates::IsHeader as i32)(tree_selection, tree_model, tree_path, is_path_currently_selected)\n}\n\npub(crate) fn select_function_same_music(tree_selection: &gtk4::TreeSelection, tree_model: &gtk4::TreeModel, tree_path: &gtk4::TreePath, is_path_currently_selected: bool) -> bool {\n    select_function_header(ColumnsSameMusic::IsHeader as i32)(tree_selection, tree_model, tree_path, is_path_currently_selected)\n}\n\npub(crate) fn select_function_similar_images(\n    tree_selection: &gtk4::TreeSelection,\n    tree_model: &gtk4::TreeModel,\n    tree_path: &gtk4::TreePath,\n    is_path_currently_selected: bool,\n) -> bool {\n    select_function_header(ColumnsSimilarImages::IsHeader as i32)(tree_selection, tree_model, tree_path, is_path_currently_selected)\n}\n\npub(crate) fn select_function_similar_videos(\n    tree_selection: &gtk4::TreeSelection,\n    tree_model: &gtk4::TreeModel,\n    tree_path: &gtk4::TreePath,\n    is_path_currently_selected: bool,\n) -> bool {\n    select_function_header(ColumnsSimilarVideos::IsHeader as i32)(tree_selection, tree_model, tree_path, is_path_currently_selected)\n}\n\npub(crate) fn select_function_header(header_id: i32) -> Box<dyn Fn(&TreeSelection, &TreeModel, &TreePath, bool) -> bool> {\n    Box::new(\n        move |_tree_selection: &gtk4::TreeSelection, tree_model: &gtk4::TreeModel, tree_path: &gtk4::TreePath, _is_path_currently_selected: bool| {\n            !tree_model.get::<bool>(&tree_model.iter(tree_path).expect(\"Invalid tree_path\"), header_id)\n        },\n    )\n}\n\n// pub(crate) fn select_function_always_true_no_args() -> Box<dyn Fn(&TreeSelection, &TreeModel, &TreePath, bool) -> bool> {\n//     Box::new(|_tree_selection: &gtk4::TreeSelection, _tree_model: &gtk4::TreeModel, _tree_path: &gtk4::TreePath, _is_path_currently_selected: bool| true)\n// }\n\npub(crate) fn select_function_always_true(\n    _tree_selection: &gtk4::TreeSelection,\n    _tree_model: &gtk4::TreeModel,\n    _tree_path: &gtk4::TreePath,\n    _is_path_currently_selected: bool,\n) -> bool {\n    true\n}\n"
  },
  {
    "path": "czkawka_gui/src/saving_loading.rs",
    "content": "use std::env;\nuse std::path::{Path, PathBuf};\n\nuse czkawka_core::common::basic_gui_cli::CliResult;\nuse czkawka_core::common::config_cache_path::get_config_cache_path;\nuse czkawka_core::common::get_all_available_threads;\nuse czkawka_core::common::items::DEFAULT_EXCLUDED_ITEMS;\nuse czkawka_core::common::model::CheckingMethod;\nuse czkawka_core::tools::similar_images::SIMILAR_VALUES;\nuse gtk4::prelude::*;\nuse gtk4::{ListStore, ScrolledWindow, TextView, TreeView};\nuse serde::{Deserialize, Serialize};\n\nuse crate::flg;\nuse crate::gui_structs::common_tree_view::TreeViewListStoreTrait;\nuse crate::gui_structs::common_upper_tree_view::UpperTreeViewEnum;\nuse crate::gui_structs::gui_main_notebook::GuiMainNotebook;\nuse crate::gui_structs::gui_settings::GuiSettings;\nuse crate::gui_structs::gui_upper_notebook::GuiUpperNotebook;\nuse crate::help_combo_box::DUPLICATES_CHECK_METHOD_COMBO_BOX;\nuse crate::help_functions::{add_text_to_text_view, reset_text_view, scale_step_function};\nuse crate::helpers::enums::{ColumnsExcludedDirectory, ColumnsIncludedDirectory};\nuse crate::helpers::list_store_operations::{append_row_to_list_store, get_from_list_store_fnc, get_string_from_list_store};\nuse crate::language_functions::{LANGUAGES_ALL, get_language_from_combo_box_text};\n\nconst SAVE_FILE_NAME_JSON: &str = \"czkawka_gui_config.json\";\n\nconst DEFAULT_SAVE_ON_EXIT: bool = true;\nconst DEFAULT_LOAD_AT_START: bool = true;\nconst DEFAULT_CONFIRM_DELETION: bool = true;\nconst DEFAULT_CONFIRM_LINK_DELETION: bool = true;\nconst DEFAULT_CONFIRM_GROUP_DELETION: bool = true;\nconst DEFAULT_SHOW_IMAGE_PREVIEW: bool = true;\nconst DEFAULT_SHOW_DUPLICATE_IMAGE_PREVIEW: bool = true;\nconst DEFAULT_BOTTOM_TEXT_VIEW: bool = true;\nconst DEFAULT_USE_CACHE: bool = true;\nconst DEFAULT_SAVE_ALSO_AS_JSON: bool = false;\nconst DEFAULT_HIDE_HARD_LINKS: bool = true;\nconst DEFAULT_USE_PRECACHE: bool = false;\nconst DEFAULT_USE_TRASH: bool = false;\npub const DEFAULT_MINIMAL_CACHE_SIZE: &str = \"257144\";\nconst DEFAULT_PREHASH_MINIMAL_CACHE_SIZE: &str = \"0\";\nconst DEFAULT_VIDEO_REMOVE_AUTO_OUTDATED_CACHE: bool = false;\nconst DEFAULT_IMAGE_REMOVE_AUTO_OUTDATED_CACHE: bool = true;\nconst DEFAULT_DUPLICATE_REMOVE_AUTO_OUTDATED_CACHE: bool = true;\nconst DEFAULT_DUPLICATE_CASE_SENSITIVE_NAME_CHECKING: bool = false;\nconst DEFAULT_GENERAL_IGNORE_OTHER_FILESYSTEMS: bool = false;\nconst DEFAULT_USING_RUST_LIBRARIES_TO_SHOW_PREVIEW: bool = true;\n\nconst DEFAULT_MUSIC_APPROXIMATE_COMPARISON: bool = false;\nconst DEFAULT_MUSIC_GROUP_CONTENT_BY_TITLE: bool = false;\n\nconst DEFAULT_BROKEN_FILES_PDF: bool = true;\nconst DEFAULT_BROKEN_FILES_AUDIO: bool = true;\nconst DEFAULT_BROKEN_FILES_ARCHIVE: bool = true;\nconst DEFAULT_BROKEN_FILES_IMAGE: bool = true;\nconst DEFAULT_BROKEN_FILES_VIDEO: bool = false;\n\nconst DEFAULT_THREAD_NUMBER: u32 = 0;\n\nconst DEFAULT_NUMBER_OF_BIGGEST_FILES: &str = \"50\";\nconst DEFAULT_SIMILAR_IMAGES_SIMILARITY: f32 = 0.0;\nconst DEFAULT_SIMILAR_IMAGES_IGNORE_SAME_SIZE: bool = false;\nconst DEFAULT_SIMILAR_VIDEOS_SIMILARITY: f32 = 15.0;\nconst DEFAULT_SIMILAR_VIDEOS_IGNORE_SAME_SIZE: bool = false;\n\npub const DEFAULT_MINIMAL_FILE_SIZE: &str = \"16384\";\npub const DEFAULT_MAXIMAL_FILE_SIZE: &str = \"999999999999\";\n\n#[cfg(target_family = \"unix\")]\nconst DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[\"/proc\", \"/dev\", \"/sys\", \"/snap\"];\n#[cfg(not(target_family = \"unix\"))]\nconst DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[\"C:\\\\Windows\"];\n\nstruct LoadSaveStruct {\n    settings: SettingsJson,\n}\n\nimpl LoadSaveStruct {\n    pub(crate) fn with_text_view() -> Self {\n        Self {\n            settings: SettingsJson {\n                included_directories: vec![get_current_directory()],\n                ..Default::default()\n            },\n        }\n    }\n\n    fn open_save_file_path() -> Option<PathBuf> {\n        let config_dir = get_config_cache_path()?.config_folder;\n        Some(config_dir.join(Path::new(SAVE_FILE_NAME_JSON)))\n    }\n\n    pub(crate) fn open_and_read_content(&mut self, text_view_errors: &TextView, manual_execution: bool) {\n        let json_file = match get_config_cache_path() {\n            Some(cfg) => cfg.config_folder.join(Path::new(SAVE_FILE_NAME_JSON)),\n            None => {\n                if manual_execution {\n                    add_text_to_text_view(text_view_errors, &flg!(\"saving_loading_failed_to_read_config_file\", path = SAVE_FILE_NAME_JSON));\n                }\n                return;\n            }\n        };\n\n        if !json_file.is_file() {\n            if manual_execution {\n                add_text_to_text_view(text_view_errors, &flg!(\"saving_loading_loading_success\"));\n            }\n            return;\n        }\n\n        match std::fs::read_to_string(&json_file) {\n            Ok(content) => match serde_json::from_str::<SettingsJson>(&content) {\n                Ok(cfg) => {\n                    self.settings = cfg;\n                    if manual_execution {\n                        add_text_to_text_view(text_view_errors, &flg!(\"saving_loading_loading_success\"));\n                    }\n                }\n                Err(e) => {\n                    add_text_to_text_view(\n                        text_view_errors,\n                        &flg!(\n                            \"saving_loading_failed_to_read_data_from_file\",\n                            path = json_file.to_string_lossy().to_string(),\n                            reason = e.to_string()\n                        ),\n                    );\n                }\n            },\n            Err(e) => {\n                add_text_to_text_view(\n                    text_view_errors,\n                    &flg!(\n                        \"saving_loading_failed_to_read_data_from_file\",\n                        path = json_file.to_string_lossy().to_string(),\n                        reason = e.to_string()\n                    ),\n                );\n            }\n        }\n    }\n\n    pub(crate) fn save_to_file(&self, text_view_errors: &TextView) {\n        let Some(json_file) = Self::open_save_file_path() else {\n            add_text_to_text_view(\n                text_view_errors,\n                &flg!(\n                    \"saving_loading_failed_to_create_config_file\",\n                    path = SAVE_FILE_NAME_JSON,\n                    reason = \"config directory not found\"\n                ),\n            );\n            return;\n        };\n\n        match serde_json::to_string_pretty(&self.settings) {\n            Ok(json_string) => match std::fs::write(&json_file, json_string) {\n                Ok(()) => {\n                    add_text_to_text_view(text_view_errors, &flg!(\"saving_loading_saving_success\", name = json_file.to_string_lossy().to_string()));\n                }\n                Err(e) => {\n                    add_text_to_text_view(\n                        text_view_errors,\n                        &flg!(\n                            \"saving_loading_failed_to_create_config_file\",\n                            path = json_file.to_string_lossy().to_string(),\n                            reason = e.to_string()\n                        ),\n                    );\n                }\n            },\n            Err(e) => {\n                add_text_to_text_view(\n                    text_view_errors,\n                    &flg!(\"saving_loading_saving_failure\", name = json_file.to_string_lossy().to_string(), reason = e.to_string()),\n                );\n            }\n        }\n    }\n\n    pub(crate) fn settings_mut(&mut self) -> &mut SettingsJson {\n        &mut self.settings\n    }\n}\n\n// New JSON model mirroring available settings; per-field serde defaults so a bad/missing field won't break deserialization\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct SettingsJson {\n    #[serde(default = \"default_included_directories\")]\n    pub included_directories: Vec<String>,\n\n    #[serde(default)]\n    pub reference_directories: Vec<String>,\n\n    #[serde(default = \"default_excluded_directories\")]\n    pub excluded_directories: Vec<String>,\n\n    #[serde(default = \"default_excluded_items\")]\n    pub excluded_items: String,\n\n    #[serde(default)]\n    pub allowed_extensions: String,\n\n    #[serde(default = \"default_minimal_file_size\")]\n    pub minimal_file_size: String,\n\n    #[serde(default = \"default_maximal_file_size\")]\n    pub maximal_file_size: String,\n\n    #[serde(default = \"default_save_at_exit\")]\n    pub save_at_exit: bool,\n\n    #[serde(default = \"default_load_at_start\")]\n    pub load_at_start: bool,\n\n    #[serde(default = \"default_confirm_deletion\")]\n    pub confirm_deletion_files: bool,\n\n    #[serde(default = \"default_confirm_group_deletion\")]\n    pub confirm_deletion_all_files_in_group: bool,\n\n    #[serde(default = \"default_confirm_link_deletion\")]\n    pub confirm_deletion_links: bool,\n\n    #[serde(default = \"default_show_bottom_text_panel\")]\n    pub show_bottom_text_panel: bool,\n\n    #[serde(default = \"default_hide_hard_links\")]\n    pub hide_hard_links: bool,\n\n    #[serde(default = \"default_use_cache\")]\n    pub use_cache: bool,\n\n    #[serde(default = \"default_save_also_as_json\")]\n    pub use_json_cache_file: bool,\n\n    #[serde(default = \"default_delete_to_trash\")]\n    pub delete_to_trash: bool,\n\n    #[serde(default = \"default_minimal_cache_size\")]\n    pub minimal_cache_size: String,\n\n    #[serde(default = \"default_image_preview_image\")]\n    pub image_preview_image: bool,\n\n    #[serde(default = \"default_duplicate_preview_image\")]\n    pub duplicate_preview_image: bool,\n\n    #[serde(default = \"default_duplicate_delete_outdated_cache_entries\")]\n    pub duplicate_delete_outdated_cache_entries: bool,\n\n    #[serde(default = \"default_image_delete_outdated_cache_entries\")]\n    pub image_delete_outdated_cache_entries: bool,\n\n    #[serde(default = \"default_video_delete_outdated_cache_entries\")]\n    pub video_delete_outdated_cache_entries: bool,\n\n    #[serde(default = \"default_use_prehash_cache\")]\n    pub use_prehash_cache: bool,\n\n    #[serde(default = \"default_minimal_prehash_cache_size\")]\n    pub minimal_prehash_cache_size: String,\n\n    #[serde(default)]\n    pub language: String,\n\n    #[serde(default)]\n    pub combo_box_duplicate_hash_type: u32,\n\n    #[serde(default)]\n    pub combo_box_duplicate_check_method: u32,\n\n    #[serde(default)]\n    pub combo_box_image_resize_algorithm: u32,\n\n    #[serde(default)]\n    pub combo_box_image_hash_type: u32,\n\n    #[serde(default = \"default_image_hash_size\")]\n    pub combo_box_image_hash_size: u32,\n\n    #[serde(default = \"default_number_of_biggest_files\")]\n    pub number_of_biggest_files: String,\n\n    #[serde(default = \"default_similar_images_similarity\")]\n    pub similar_images_similarity: f64,\n\n    #[serde(default = \"default_similar_images_ignore_same_size\")]\n    pub similar_images_ignore_same_size: bool,\n\n    #[serde(default = \"default_similar_videos_similarity\")]\n    pub similar_videos_similarity: f64,\n\n    #[serde(default = \"default_similar_videos_ignore_same_size\")]\n    pub similar_videos_ignore_same_size: bool,\n\n    #[serde(default = \"default_music_approximate_comparison\")]\n    pub music_approximate_comparison: bool,\n\n    #[serde(default = \"default_duplicate_name_case_sensitive\")]\n    pub duplicate_name_case_sensitive: bool,\n\n    #[serde(default)]\n    pub combo_box_big_files_mode: u32,\n\n    #[serde(default = \"default_broken_files_pdf\")]\n    pub broken_files_pdf: bool,\n\n    #[serde(default = \"default_broken_files_audio\")]\n    pub broken_files_audio: bool,\n\n    #[serde(default = \"default_broken_files_image\")]\n    pub broken_files_image: bool,\n\n    #[serde(default = \"default_broken_files_archive\")]\n    pub broken_files_archive: bool,\n\n    #[serde(default = \"default_broken_files_video\")]\n    pub broken_files_video: bool,\n\n    #[serde(default = \"default_ignore_other_filesystems\")]\n    pub ignore_other_filesystems: bool,\n\n    #[serde(default = \"default_thread_number\")]\n    pub thread_number: u32,\n\n    #[serde(default = \"default_music_compare_by_title\")]\n    pub music_compare_by_title: bool,\n\n    #[serde(default = \"default_use_rust_libraries_to_preview\")]\n    pub use_rust_libraries_to_preview: bool,\n}\n\n// Use serde to build defaults from empty object; this uses per-field defaults above\nimpl Default for SettingsJson {\n    fn default() -> Self {\n        serde_json::from_str(\"{}\").expect(\"Cannot fail creating SettingsJson from empty object\")\n    }\n}\n\n// Per-field default helper functions (kept small and explicit)\nfn default_included_directories() -> Vec<String> {\n    Vec::new()\n}\nfn default_excluded_directories() -> Vec<String> {\n    DEFAULT_EXCLUDED_DIRECTORIES.iter().map(|s| s.to_string()).collect()\n}\nfn default_excluded_items() -> String {\n    DEFAULT_EXCLUDED_ITEMS.to_string()\n}\nfn default_minimal_file_size() -> String {\n    DEFAULT_MINIMAL_FILE_SIZE.to_string()\n}\nfn default_maximal_file_size() -> String {\n    DEFAULT_MAXIMAL_FILE_SIZE.to_string()\n}\nfn default_save_at_exit() -> bool {\n    DEFAULT_SAVE_ON_EXIT\n}\nfn default_load_at_start() -> bool {\n    DEFAULT_LOAD_AT_START\n}\nfn default_confirm_deletion() -> bool {\n    DEFAULT_CONFIRM_DELETION\n}\nfn default_confirm_group_deletion() -> bool {\n    DEFAULT_CONFIRM_GROUP_DELETION\n}\nfn default_confirm_link_deletion() -> bool {\n    DEFAULT_CONFIRM_LINK_DELETION\n}\nfn default_show_bottom_text_panel() -> bool {\n    DEFAULT_BOTTOM_TEXT_VIEW\n}\nfn default_hide_hard_links() -> bool {\n    DEFAULT_HIDE_HARD_LINKS\n}\nfn default_use_cache() -> bool {\n    DEFAULT_USE_CACHE\n}\nfn default_save_also_as_json() -> bool {\n    DEFAULT_SAVE_ALSO_AS_JSON\n}\nfn default_delete_to_trash() -> bool {\n    DEFAULT_USE_TRASH\n}\nfn default_minimal_cache_size() -> String {\n    DEFAULT_MINIMAL_CACHE_SIZE.to_string()\n}\nfn default_image_preview_image() -> bool {\n    DEFAULT_SHOW_IMAGE_PREVIEW\n}\nfn default_duplicate_preview_image() -> bool {\n    DEFAULT_SHOW_DUPLICATE_IMAGE_PREVIEW\n}\nfn default_duplicate_delete_outdated_cache_entries() -> bool {\n    DEFAULT_DUPLICATE_REMOVE_AUTO_OUTDATED_CACHE\n}\nfn default_image_delete_outdated_cache_entries() -> bool {\n    DEFAULT_IMAGE_REMOVE_AUTO_OUTDATED_CACHE\n}\nfn default_video_delete_outdated_cache_entries() -> bool {\n    DEFAULT_VIDEO_REMOVE_AUTO_OUTDATED_CACHE\n}\nfn default_use_prehash_cache() -> bool {\n    DEFAULT_USE_PRECACHE\n}\nfn default_minimal_prehash_cache_size() -> String {\n    DEFAULT_PREHASH_MINIMAL_CACHE_SIZE.to_string()\n}\nfn default_image_hash_size() -> u32 {\n    1\n}\nfn default_number_of_biggest_files() -> String {\n    DEFAULT_NUMBER_OF_BIGGEST_FILES.to_string()\n}\nfn default_similar_images_similarity() -> f64 {\n    DEFAULT_SIMILAR_IMAGES_SIMILARITY as f64\n}\nfn default_similar_images_ignore_same_size() -> bool {\n    DEFAULT_SIMILAR_IMAGES_IGNORE_SAME_SIZE\n}\nfn default_similar_videos_similarity() -> f64 {\n    DEFAULT_SIMILAR_VIDEOS_SIMILARITY as f64\n}\nfn default_similar_videos_ignore_same_size() -> bool {\n    DEFAULT_SIMILAR_VIDEOS_IGNORE_SAME_SIZE\n}\nfn default_music_approximate_comparison() -> bool {\n    DEFAULT_MUSIC_APPROXIMATE_COMPARISON\n}\nfn default_duplicate_name_case_sensitive() -> bool {\n    DEFAULT_DUPLICATE_CASE_SENSITIVE_NAME_CHECKING\n}\nfn default_broken_files_pdf() -> bool {\n    DEFAULT_BROKEN_FILES_PDF\n}\nfn default_broken_files_audio() -> bool {\n    DEFAULT_BROKEN_FILES_AUDIO\n}\nfn default_broken_files_image() -> bool {\n    DEFAULT_BROKEN_FILES_IMAGE\n}\nfn default_broken_files_archive() -> bool {\n    DEFAULT_BROKEN_FILES_ARCHIVE\n}\nfn default_broken_files_video() -> bool {\n    DEFAULT_BROKEN_FILES_VIDEO\n}\nfn default_ignore_other_filesystems() -> bool {\n    DEFAULT_GENERAL_IGNORE_OTHER_FILESYSTEMS\n}\nfn default_thread_number() -> u32 {\n    DEFAULT_THREAD_NUMBER\n}\nfn default_music_compare_by_title() -> bool {\n    DEFAULT_MUSIC_GROUP_CONTENT_BY_TITLE\n}\nfn default_use_rust_libraries_to_preview() -> bool {\n    DEFAULT_USING_RUST_LIBRARIES_TO_SHOW_PREVIEW\n}\n\nfn set_included_reference_folders(tree_view_included_directories: &TreeView, included_directories: &[String], referenced_directories: &[String]) {\n    let list_store = tree_view_included_directories.get_model();\n    list_store.clear();\n\n    // Referenced directories must be also in included directories\n    let referenced_directories: Vec<String> = referenced_directories.iter().filter(|s| included_directories.contains(s)).cloned().collect();\n\n    let only_included_directories: Vec<String> = included_directories.iter().filter(|s| !referenced_directories.contains(s)).cloned().collect();\n\n    for (directories, is_referenced) in [(only_included_directories, false), (referenced_directories, true)] {\n        for directory in directories {\n            let values: [(u32, &dyn ToValue); 2] = [\n                (ColumnsIncludedDirectory::Path as u32, &directory),\n                (ColumnsIncludedDirectory::ReferenceButton as u32, &is_referenced),\n            ];\n            append_row_to_list_store(&list_store, &values);\n        }\n    }\n}\n\nfn set_configuration_to_gui_internal(upper_notebook: &GuiUpperNotebook, main_notebook: &GuiMainNotebook, settings: &GuiSettings, default_config: &SettingsJson) {\n    let tree_view_included_directories = upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::IncludedDirectories);\n    let tree_view_excluded_directories = upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::ExcludedDirectories);\n\n    // Resetting included directories\n    {\n        set_included_reference_folders(tree_view_included_directories, &default_config.included_directories, &default_config.reference_directories);\n    }\n    // Resetting excluded directories\n    {\n        let list_store = tree_view_excluded_directories.get_model();\n        list_store.clear();\n        for i in default_config.excluded_directories.clone() {\n            let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &i)];\n            append_row_to_list_store(&list_store, &values);\n        }\n    }\n    // Resetting excluded items\n    {\n        upper_notebook.entry_excluded_items.set_text(&default_config.excluded_items);\n        upper_notebook.entry_allowed_extensions.set_text(&default_config.allowed_extensions);\n        upper_notebook.entry_general_minimal_size.set_text(&default_config.minimal_file_size);\n        upper_notebook.entry_general_maximal_size.set_text(&default_config.maximal_file_size);\n    }\n\n    // Set default settings\n    {\n        settings.check_button_settings_save_at_exit.set_active(default_config.save_at_exit);\n        settings.check_button_settings_load_at_start.set_active(default_config.load_at_start);\n        settings.check_button_settings_confirm_deletion.set_active(default_config.confirm_deletion_files);\n        settings\n            .check_button_settings_confirm_group_deletion\n            .set_active(default_config.confirm_deletion_all_files_in_group);\n        settings.check_button_settings_confirm_link.set_active(default_config.confirm_deletion_links);\n        settings.check_button_settings_show_preview_similar_images.set_active(default_config.image_preview_image);\n        settings.check_button_settings_show_preview_duplicates.set_active(default_config.duplicate_preview_image);\n        settings.check_button_settings_show_text_view.set_active(default_config.show_bottom_text_panel);\n        settings.check_button_settings_hide_hard_links.set_active(default_config.hide_hard_links);\n        settings.check_button_settings_use_cache.set_active(default_config.use_cache);\n        settings.check_button_settings_save_also_json.set_active(default_config.use_json_cache_file);\n        settings.check_button_settings_use_trash.set_active(default_config.delete_to_trash);\n        settings.entry_settings_cache_file_minimal_size.set_text(&default_config.minimal_cache_size);\n        settings\n            .check_button_settings_similar_videos_delete_outdated_cache\n            .set_active(default_config.video_delete_outdated_cache_entries);\n        settings\n            .check_button_settings_similar_images_delete_outdated_cache\n            .set_active(default_config.image_delete_outdated_cache_entries);\n        settings\n            .check_button_settings_duplicates_delete_outdated_cache\n            .set_active(default_config.duplicate_delete_outdated_cache_entries);\n        settings.check_button_duplicates_use_prehash_cache.set_active(default_config.use_prehash_cache);\n        settings.entry_settings_prehash_cache_file_minimal_size.set_text(&default_config.minimal_prehash_cache_size);\n\n        let lang_idx = LANGUAGES_ALL.iter().position(|l| l.short_text == default_config.language).unwrap_or(0);\n        settings.combo_box_settings_language.set_active(Some(lang_idx as u32));\n\n        settings.check_button_settings_one_filesystem.set_active(default_config.ignore_other_filesystems);\n        settings.check_button_settings_use_rust_preview.set_active(default_config.use_rust_libraries_to_preview);\n\n        // Set combo boxes and check buttons as before\n        main_notebook.combo_box_duplicate_hash_type.set_active(Some(default_config.combo_box_duplicate_hash_type));\n        main_notebook\n            .combo_box_duplicate_check_method\n            .set_active(Some(default_config.combo_box_duplicate_check_method));\n        main_notebook.combo_box_image_hash_algorithm.set_active(Some(default_config.combo_box_image_hash_type));\n        main_notebook\n            .combo_box_image_resize_algorithm\n            .set_active(Some(default_config.combo_box_image_resize_algorithm));\n        main_notebook.combo_box_image_hash_size.set_active(Some(default_config.combo_box_image_hash_size));\n        main_notebook.combo_box_big_files_mode.set_active(Some(default_config.combo_box_big_files_mode));\n\n        main_notebook.check_button_broken_files_audio.set_active(default_config.broken_files_audio);\n        main_notebook.check_button_broken_files_pdf.set_active(default_config.broken_files_pdf);\n        main_notebook.check_button_broken_files_archive.set_active(default_config.broken_files_archive);\n        main_notebook.check_button_broken_files_image.set_active(default_config.broken_files_image);\n        main_notebook.check_button_broken_files_video.set_active(default_config.broken_files_video);\n\n        // Set similarity scale range/value based on chosen image hash size index\n        let index = default_config.combo_box_image_hash_size as usize;\n        let max_similar = SIMILAR_VALUES[index][5] as f64;\n        main_notebook.scale_similarity_similar_images.set_range(0_f64, max_similar);\n        main_notebook.scale_similarity_similar_images.set_fill_level(max_similar);\n        main_notebook.scale_similarity_similar_images.connect_change_value(scale_step_function);\n        main_notebook.scale_similarity_similar_images.set_value(default_config.similar_images_similarity);\n\n        // Set similar videos scale value\n        main_notebook.scale_similarity_similar_videos.set_value(default_config.similar_videos_similarity);\n\n        // Update duplicate-related widget visibility according to chosen method\n        {\n            let combo_chosen_index = main_notebook.combo_box_duplicate_check_method.active().unwrap_or(0) as usize;\n            if DUPLICATES_CHECK_METHOD_COMBO_BOX[combo_chosen_index].check_method == CheckingMethod::Hash {\n                main_notebook.combo_box_duplicate_hash_type.set_visible(true);\n                main_notebook.label_duplicate_hash_type.set_visible(true);\n            } else {\n                main_notebook.combo_box_duplicate_hash_type.set_visible(false);\n                main_notebook.label_duplicate_hash_type.set_visible(false);\n            }\n\n            if [CheckingMethod::Name, CheckingMethod::SizeName].contains(&DUPLICATES_CHECK_METHOD_COMBO_BOX[combo_chosen_index].check_method) {\n                main_notebook.check_button_duplicate_case_sensitive_name.set_visible(true);\n            } else {\n                main_notebook.check_button_duplicate_case_sensitive_name.set_visible(false);\n            }\n        }\n\n        // Threads slider\n        settings.scale_settings_number_of_threads.set_range(0_f64, get_all_available_threads() as f64);\n        settings.scale_settings_number_of_threads.set_fill_level(get_all_available_threads() as f64);\n        settings.scale_settings_number_of_threads.connect_change_value(scale_step_function);\n        settings.scale_settings_number_of_threads.set_value(default_config.thread_number as f64);\n    }\n}\n\nfn get_current_directory() -> String {\n    match env::current_dir() {\n        Ok(t) => t.to_string_lossy().to_string(),\n        Err(_inspected) => {\n            if cfg!(target_family = \"unix\") {\n                \"/home\".to_string()\n            } else if cfg!(target_family = \"windows\") {\n                \"C:\\\\\".to_string()\n            } else {\n                String::new()\n            }\n        }\n    }\n}\n\nfn gui_to_settings(upper_notebook: &GuiUpperNotebook, main_notebook: &GuiMainNotebook, settings: &GuiSettings) -> SettingsJson {\n    let tree_view_included_directories = &upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::IncludedDirectories);\n    let tree_view_excluded_directories = &upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::ExcludedDirectories);\n\n    // Gather directories\n    let included_directories = get_string_from_list_store(tree_view_included_directories, ColumnsIncludedDirectory::Path as i32, None);\n    let excluded_directories = get_string_from_list_store(tree_view_excluded_directories, ColumnsExcludedDirectory::Path as i32, None);\n\n    let ref_fnc: &dyn Fn(&ListStore, &gtk4::TreeIter, &mut Vec<String>) = &|list_store, tree_iter, vec| {\n        if list_store.get::<bool>(tree_iter, ColumnsIncludedDirectory::ReferenceButton as i32) {\n            vec.push(list_store.get::<String>(tree_iter, ColumnsIncludedDirectory::Path as i32));\n        }\n    };\n\n    let reference_directories = get_from_list_store_fnc(tree_view_included_directories, ref_fnc);\n\n    // Language short text\n    let language_text = match settings.combo_box_settings_language.active_text() {\n        Some(t) => get_language_from_combo_box_text(&t).short_text.to_string(),\n        None => \"en\".to_string(),\n    };\n\n    SettingsJson {\n        included_directories,\n        reference_directories,\n        excluded_directories,\n        excluded_items: upper_notebook.entry_excluded_items.text().to_string(),\n        allowed_extensions: upper_notebook.entry_allowed_extensions.text().to_string(),\n        minimal_file_size: upper_notebook.entry_general_minimal_size.text().to_string(),\n        maximal_file_size: upper_notebook.entry_general_maximal_size.text().to_string(),\n        save_at_exit: settings.check_button_settings_save_at_exit.is_active(),\n        load_at_start: settings.check_button_settings_load_at_start.is_active(),\n        confirm_deletion_files: settings.check_button_settings_confirm_deletion.is_active(),\n        confirm_deletion_all_files_in_group: settings.check_button_settings_confirm_group_deletion.is_active(),\n        confirm_deletion_links: settings.check_button_settings_confirm_link.is_active(),\n        show_bottom_text_panel: settings.check_button_settings_show_text_view.is_active(),\n        hide_hard_links: settings.check_button_settings_hide_hard_links.is_active(),\n        use_cache: settings.check_button_settings_use_cache.is_active(),\n        use_json_cache_file: settings.check_button_settings_save_also_json.is_active(),\n        delete_to_trash: settings.check_button_settings_use_trash.is_active(),\n        minimal_cache_size: settings.entry_settings_cache_file_minimal_size.text().to_string(),\n        image_preview_image: settings.check_button_settings_show_preview_similar_images.is_active(),\n        duplicate_preview_image: settings.check_button_settings_show_preview_duplicates.is_active(),\n        duplicate_delete_outdated_cache_entries: settings.check_button_settings_duplicates_delete_outdated_cache.is_active(),\n        image_delete_outdated_cache_entries: settings.check_button_settings_similar_images_delete_outdated_cache.is_active(),\n        video_delete_outdated_cache_entries: settings.check_button_settings_similar_videos_delete_outdated_cache.is_active(),\n        use_prehash_cache: settings.check_button_duplicates_use_prehash_cache.is_active(),\n        minimal_prehash_cache_size: settings.entry_settings_prehash_cache_file_minimal_size.text().to_string(),\n        language: language_text,\n        combo_box_duplicate_hash_type: main_notebook.combo_box_duplicate_hash_type.active().unwrap_or(0),\n        combo_box_duplicate_check_method: main_notebook.combo_box_duplicate_check_method.active().unwrap_or(0),\n        combo_box_image_resize_algorithm: main_notebook.combo_box_image_resize_algorithm.active().unwrap_or(0),\n        combo_box_image_hash_type: main_notebook.combo_box_image_hash_algorithm.active().unwrap_or(0),\n        combo_box_image_hash_size: main_notebook.combo_box_image_hash_size.active().unwrap_or(1),\n        number_of_biggest_files: main_notebook.entry_big_files_number.text().to_string(),\n        similar_images_similarity: main_notebook.scale_similarity_similar_images.value(),\n        similar_images_ignore_same_size: main_notebook.check_button_image_ignore_same_size.is_active(),\n        similar_videos_similarity: main_notebook.scale_similarity_similar_videos.value(),\n        similar_videos_ignore_same_size: main_notebook.check_button_video_ignore_same_size.is_active(),\n        music_approximate_comparison: main_notebook.check_button_music_approximate_comparison.is_active(),\n        duplicate_name_case_sensitive: main_notebook.check_button_duplicate_case_sensitive_name.is_active(),\n        combo_box_big_files_mode: main_notebook.combo_box_big_files_mode.active().unwrap_or(0),\n        broken_files_pdf: main_notebook.check_button_broken_files_pdf.is_active(),\n        broken_files_audio: main_notebook.check_button_broken_files_audio.is_active(),\n        broken_files_image: main_notebook.check_button_broken_files_image.is_active(),\n        broken_files_archive: main_notebook.check_button_broken_files_archive.is_active(),\n        broken_files_video: main_notebook.check_button_broken_files_video.is_active(),\n        ignore_other_filesystems: settings.check_button_settings_one_filesystem.is_active(),\n        thread_number: settings.scale_settings_number_of_threads.value() as u32,\n        music_compare_by_title: main_notebook.check_button_music_compare_only_in_title_group.is_active(),\n        use_rust_libraries_to_preview: settings.check_button_settings_use_rust_preview.is_active(),\n    }\n}\n\n#[allow(clippy::allow_attributes)]\n#[allow(clippy::useless_let_if_seq)] // TODO - rust with some version shows this\npub fn load_configuration(\n    manual_execution: bool,\n    upper_notebook: &GuiUpperNotebook,\n    main_notebook: &GuiMainNotebook,\n    settings: &GuiSettings,\n    text_view_errors: &TextView,\n    scrolled_window_errors: &ScrolledWindow,\n    cli_result: Option<&CliResult>,\n) {\n    let mut loader = LoadSaveStruct::with_text_view();\n    loader.open_and_read_content(text_view_errors, manual_execution);\n\n    // Determine folders from CLI args (if any)\n    let set_start_folders = cli_result.is_some();\n\n    // Loaded settings (from file or defaults)\n    let loaded_settings = loader.settings_mut();\n\n    // Show/hide bottom text panel\n    if !loaded_settings.show_bottom_text_panel {\n        scrolled_window_errors.set_visible(false);\n    } else {\n        scrolled_window_errors.set_visible(true);\n    }\n\n    reset_text_view(text_view_errors);\n\n    let included_directories;\n    let excluded_directories;\n    let referenced_directories;\n    if let Some(cli) = cli_result {\n        included_directories = cli.included_items.clone();\n        excluded_directories = cli.excluded_items.clone();\n        referenced_directories = cli.referenced_items.clone();\n    } else {\n        included_directories = if !loaded_settings.included_directories.is_empty() {\n            loaded_settings.included_directories.clone()\n        } else {\n            vec![get_current_directory()]\n        };\n        excluded_directories = if !loaded_settings.excluded_directories.is_empty() {\n            loaded_settings.excluded_directories.clone()\n        } else {\n            DEFAULT_EXCLUDED_DIRECTORIES.iter().map(|s| s.to_string()).collect()\n        };\n        referenced_directories = loaded_settings\n            .reference_directories\n            .clone()\n            .into_iter()\n            .filter(|s| included_directories.contains(s))\n            .collect();\n    }\n\n    // When we manually load configuration, then we want them to be set, so allow it\n    // When we start app with load_at_start option, then we want to load them too\n    if manual_execution || loaded_settings.load_at_start {\n        set_configuration_to_gui_internal(upper_notebook, main_notebook, settings, loaded_settings);\n    }\n\n    // When starting app wtih arguments, we want to set folders\n    if set_start_folders {\n        set_directories(\n            upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::IncludedDirectories),\n            upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::ExcludedDirectories),\n            &included_directories,\n            &referenced_directories,\n            &excluded_directories,\n        );\n        // When using CLI args, disable saving at exit by default\n        // User still may enable it manually\n        settings.check_button_settings_save_at_exit.set_active(false);\n    }\n}\n\nfn set_directories(\n    tree_view_included_directories: &TreeView,\n    tree_view_excluded_directories: &TreeView,\n    included_directories: &[String],\n    referenced_directories: &[String],\n    excluded_directories: &[String],\n) {\n    set_included_reference_folders(tree_view_included_directories, included_directories, referenced_directories);\n\n    //// Exclude Directories\n    let list_store = tree_view_excluded_directories.get_model();\n    list_store.clear();\n\n    for directory in excluded_directories {\n        let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &directory)];\n        append_row_to_list_store(&list_store, &values);\n    }\n}\n\npub fn save_configuration(manual_execution: bool, upper_notebook: &GuiUpperNotebook, main_notebook: &GuiMainNotebook, settings: &GuiSettings, text_view_errors: &TextView) {\n    let check_button_settings_save_at_exit = settings.check_button_settings_save_at_exit.clone();\n    let text_view_errors = text_view_errors.clone();\n\n    reset_text_view(&text_view_errors);\n\n    if !manual_execution && !check_button_settings_save_at_exit.is_active() {\n        // When check button is deselected, not save configuration at exit\n        return;\n    }\n\n    let mut saver = LoadSaveStruct::with_text_view();\n    saver.settings = gui_to_settings(upper_notebook, main_notebook, settings);\n    saver.save_to_file(&text_view_errors);\n}\npub(crate) fn reset_configuration(manual_clearing: bool, upper_notebook: &GuiUpperNotebook, main_notebook: &GuiMainNotebook, settings: &GuiSettings, text_view_errors: &TextView) {\n    // TODO Maybe add popup dialog to confirm resetting\n    let text_view_errors = text_view_errors.clone();\n\n    let default_config = SettingsJson {\n        included_directories: vec![get_current_directory()],\n        ..Default::default()\n    };\n\n    reset_text_view(&text_view_errors);\n\n    set_configuration_to_gui_internal(upper_notebook, main_notebook, settings, &default_config);\n\n    if manual_clearing {\n        add_text_to_text_view(&text_view_errors, &flg!(\"saving_loading_reset_configuration\"));\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/src/taskbar_progress.rs",
    "content": "#[cfg(not(target_os = \"windows\"))]\npub use crate::taskbar_progress_dummy::{TaskbarProgress, tbp_flags};\n#[cfg(target_os = \"windows\")]\npub use crate::taskbar_progress_win::{TaskbarProgress, tbp_flags};\n"
  },
  {
    "path": "czkawka_gui/src/taskbar_progress_dummy.rs",
    "content": "#![allow(clippy::upper_case_acronyms)]\n#![allow(clippy::needless_pass_by_value)]\n#![allow(clippy::pedantic)]\n#![cfg(not(target_os = \"windows\"))]\n\nuse std::convert::From;\n\nenum HWND__ {}\n\ntype HWND = *mut HWND__;\n\n#[expect(non_camel_case_types, dead_code)]\npub enum TBPFLAG {\n    TBPF_NOPROGRESS = 0,\n    TBPF_INDETERMINATE = 0x1,\n    TBPF_NORMAL = 0x2,\n    TBPF_ERROR = 0x4,\n    TBPF_PAUSED = 0x8,\n}\n\npub mod tbp_flags {\n    pub use super::TBPFLAG::*;\n}\n\npub struct TaskbarProgress {}\n\nimpl TaskbarProgress {\n    pub fn new() -> Self {\n        Self {}\n    }\n\n    pub(crate) fn set_progress_state(&self, _tbp_flags: TBPFLAG) {}\n\n    pub(crate) fn set_progress_value(&self, _completed: u64, _total: u64) {}\n\n    pub(crate) fn hide(&self) {}\n\n    pub(crate) fn show(&self) {}\n\n    #[expect(clippy::needless_pass_by_ref_mut)]\n    pub(crate) fn release(&mut self) {}\n}\n\nimpl From<HWND> for TaskbarProgress {\n    fn from(_hwnd: HWND) -> Self {\n        Self {}\n    }\n}\n\nimpl Drop for TaskbarProgress {\n    fn drop(&mut self) {}\n}\n"
  },
  {
    "path": "czkawka_gui/src/taskbar_progress_win.rs",
    "content": "#![cfg(target_os = \"windows\")]\nextern crate winapi;\n\nuse std::cell::RefCell;\nuse std::convert::From;\nuse std::ptr;\n\nuse winapi::Interface;\nuse winapi::ctypes::c_void;\nuse winapi::shared::windef::HWND;\nuse winapi::shared::winerror::{E_POINTER, S_OK};\nuse winapi::shared::wtypesbase::CLSCTX_INPROC_SERVER;\nuse winapi::um::shobjidl_core::{CLSID_TaskbarList, ITaskbarList3, TBPFLAG};\nuse winapi::um::{combaseapi, objbase, winuser};\n\npub mod tbp_flags {\n    pub use winapi::um::shobjidl_core::{TBPF_ERROR, TBPF_INDETERMINATE, TBPF_NOPROGRESS, TBPF_NORMAL, TBPF_PAUSED};\n}\n\npub struct TaskbarProgress {\n    hwnd: HWND,\n    taskbar_list: *mut ITaskbarList3,\n    current_state: RefCell<TBPFLAG>,\n    current_progress: RefCell<(u64, u64)>,\n    must_uninit_com: bool,\n    is_active: RefCell<bool>,\n}\n\nimpl TaskbarProgress {\n    pub fn new() -> TaskbarProgress {\n        let hwnd = unsafe { winuser::GetActiveWindow() };\n        TaskbarProgress::from(hwnd)\n    }\n\n    pub(crate) fn set_progress_state(&self, tbp_flags: TBPFLAG) {\n        if tbp_flags == *self.current_state.borrow() || !*self.is_active.borrow() {\n            return ();\n        }\n        let result = unsafe {\n            if let Some(list) = self.taskbar_list.as_ref() {\n                list.SetProgressState(self.hwnd, tbp_flags)\n            } else {\n                E_POINTER\n            }\n        };\n        if result == S_OK {\n            self.current_state.replace(tbp_flags);\n        }\n    }\n\n    pub(crate) fn set_progress_value(&self, completed: u64, total: u64) {\n        // Don't change the value if the is_active flag is false or the value has not changed.\n        // If is_active is true and the value has not changed, but the progress indicator was in NOPROGRESS or INDETERMINATE state, set the value (and NORMAL state).\n        if ((completed, total) == *self.current_progress.borrow()\n            && *self.current_state.borrow() != tbp_flags::TBPF_NOPROGRESS\n            && *self.current_state.borrow() != tbp_flags::TBPF_INDETERMINATE)\n            || !*self.is_active.borrow()\n        {\n            return ();\n        }\n        let result = unsafe {\n            if let Some(list) = self.taskbar_list.as_ref() {\n                list.SetProgressValue(self.hwnd, completed, total)\n            } else {\n                E_POINTER\n            }\n        };\n        if result == S_OK {\n            self.current_progress.replace((completed, total));\n            if *self.current_state.borrow() == tbp_flags::TBPF_NOPROGRESS || *self.current_state.borrow() == tbp_flags::TBPF_INDETERMINATE {\n                self.current_state.replace(tbp_flags::TBPF_NORMAL);\n            }\n        }\n    }\n\n    pub(crate) fn hide(&self) {\n        self.set_progress_state(tbp_flags::TBPF_NOPROGRESS);\n        *self.is_active.borrow_mut() = false;\n    }\n\n    pub(crate) fn show(&self) {\n        *self.is_active.borrow_mut() = true;\n    }\n\n    /// Releases the ITaskbarList3 pointer, uninitialises the COM API and sets the struct to a valid \"empty\" state.\n    /// It's required for proper use of the COM API, because `drop` is never called (objects moved to GTK closures have `static` lifetime).\n    pub(crate) fn release(&mut self) {\n        unsafe {\n            if let Some(list) = self.taskbar_list.as_ref() {\n                list.Release();\n                self.taskbar_list = ptr::null_mut();\n                self.hwnd = ptr::null_mut();\n            }\n            // A thread must call CoUninitialize once for each successful call it has made to\n            // the CoInitialize or CoInitializeEx function, including any call that returns S_FALSE.\n            if self.must_uninit_com {\n                combaseapi::CoUninitialize();\n                self.must_uninit_com = false;\n            }\n        }\n    }\n}\n\nimpl From<HWND> for TaskbarProgress {\n    fn from(hwnd: HWND) -> Self {\n        if hwnd.is_null() {\n            return TaskbarProgress {\n                hwnd,\n                taskbar_list: ptr::null_mut(),\n                current_state: RefCell::new(tbp_flags::TBPF_NOPROGRESS),\n                current_progress: RefCell::new((0, 0)),\n                must_uninit_com: false,\n                is_active: RefCell::new(false),\n            };\n        }\n\n        let init_result = unsafe { combaseapi::CoInitializeEx(ptr::null_mut(), objbase::COINIT_APARTMENTTHREADED) };\n        // S_FALSE means that COM library is already initialised for this thread\n        // Success codes are not negative, RPC_E_CHANGED_MODE should not be possible and is treated as an error\n        if init_result < 0 {\n            return TaskbarProgress {\n                hwnd: ptr::null_mut(),\n                taskbar_list: ptr::null_mut(),\n                current_state: RefCell::new(tbp_flags::TBPF_NOPROGRESS),\n                current_progress: RefCell::new((0, 0)),\n                must_uninit_com: false,\n                is_active: RefCell::new(false),\n            };\n        }\n\n        let mut taskbar_list: *mut ITaskbarList3 = ptr::null_mut();\n        let taskbar_list_ptr: *mut *mut ITaskbarList3 = &mut taskbar_list;\n\n        unsafe {\n            combaseapi::CoCreateInstance(\n                &CLSID_TaskbarList,\n                ptr::null_mut(),\n                CLSCTX_INPROC_SERVER,\n                &ITaskbarList3::uuidof(),\n                taskbar_list_ptr as *mut *mut c_void,\n            )\n        };\n\n        TaskbarProgress {\n            hwnd: if taskbar_list.is_null() { ptr::null_mut() } else { hwnd },\n            taskbar_list,\n            current_state: RefCell::new(tbp_flags::TBPF_NOPROGRESS), // Assume no progress\n            current_progress: RefCell::new((0, 0)),\n            must_uninit_com: true,\n            is_active: RefCell::new(false),\n        }\n    }\n}\n\nimpl Drop for TaskbarProgress {\n    fn drop(&mut self) {\n        unsafe {\n            if let Some(list) = self.taskbar_list.as_ref() {\n                list.Release();\n            }\n            // A thread must call CoUninitialize once for each successful call it has made to\n            // the CoInitialize or CoInitializeEx function, including any call that returns S_FALSE.\n            if self.must_uninit_com {\n                combaseapi::CoUninitialize();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "czkawka_gui/ui/about_dialog.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name about_dialog.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkAboutDialog\" id=\"about_dialog\">\n    <property name=\"comments\" translatable=\"yes\">2020 - 2026\nRafał Mikrut (qarmin) and contributors\nThis program is free to use and will always be.\nApp is now in maintenance mode, so check Krokiet, the successor of Czkawka</property>\n    <property name=\"license-type\">mit-x11</property>\n    <property name=\"logo-icon-name\">help-about-symbolic</property>\n    <property name=\"program-name\">Czkawka</property>\n    <property name=\"version\">11.0.1</property>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/compare_images.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name compare_images.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkDialog\" id=\"window_compare\">\n    <child>\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <property name=\"vexpand\">1</property>\n        <child>\n          <object class=\"GtkBox\">\n            <child>\n              <object class=\"GtkButton\" id=\"button_go_previous_compare_group\">\n                <property name=\"focusable\">1</property>\n                <property name=\"receives-default\">1</property>\n                <child>\n                  <object class=\"GtkImage\">\n                    <property name=\"icon-name\">image-missing</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label_group_info\">\n                <property name=\"halign\">center</property>\n                <property name=\"hexpand\">1</property>\n                <property name=\"label\" translatable=\"yes\">Group XD/PER XD (99 images in current group)</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"button_go_next_compare_group\">\n                <property name=\"focusable\">1</property>\n                <property name=\"receives-default\">1</property>\n                <child>\n                  <object class=\"GtkImage\">\n                    <property name=\"icon-name\">image-missing</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkBox\">\n            <property name=\"hexpand\">True</property>\n            <property name=\"hexpand-set\">True</property>\n            <child>\n              <object class=\"GtkCheckButton\" id=\"check_button_left_preview_text\">\n                <property name=\"focusable\">1</property>\n                <property name=\"hexpand\">True</property>\n                <property name=\"hexpand-set\">True</property>\n                <property name=\"label\" translatable=\"yes\">First Game</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"button_replace_group\">\n                <property name=\"focusable\">1</property>\n                <property name=\"receives-default\">1</property>\n                <child>\n                  <object class=\"GtkImage\">\n                    <property name=\"icon-name\">image-missing</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkCheckButton\" id=\"check_button_right_preview_text\">\n                <property name=\"focusable\">1</property>\n                <property name=\"hexpand\">True</property>\n                <property name=\"hexpand-set\">True</property>\n                <property name=\"label\" translatable=\"yes\">Second Game</property>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkBox\">\n            <property name=\"homogeneous\">1</property>\n            <property name=\"vexpand\">1</property>\n            <child>\n              <object class=\"GtkPicture\" id=\"image_compare_left\">\n                <property name=\"height-request\">100</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkPicture\" id=\"image_compare_right\">\n                <property name=\"height-request\">100</property>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_compare_choose_images\">\n            <property name=\"focusable\">1</property>\n            <property name=\"max-content-height\">150</property>\n            <property name=\"min-content-height\">150</property>\n          </object>\n        </child>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/czkawka.cmb",
    "content": "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n<!DOCTYPE cambalache-project SYSTEM \"cambalache-project.dtd\">\n<!-- Created with Cambalache 0.96.1 -->\n<cambalache-project version=\"0.96.0\" target_tk=\"gtk-4.0\">\n  <ui filename=\"about_dialog.ui\" sha256=\"7f97a25703398a41685c8f1ae828be264074dfef8849e5edf3060da0e70122c6\"/>\n  <ui filename=\"compare_images.ui\" sha256=\"b625094a12cd8f3e34b07cef46fa46518b9d39101ff966cba0cd9721595e1a03\"/>\n  <ui filename=\"main_window.ui\" sha256=\"f15f0c56c020108438aa7d2f24a8f40748a755b16d877ef6f12ba842f06ef1bc\"/>\n  <ui filename=\"popover_right_click.ui\" sha256=\"79b76370c90b258d19a7e91db96324c74c00e2923c5fd7f390a41521c622f730\"/>\n  <ui filename=\"popover_select.ui\" sha256=\"e73afc190d745d39735147022ee01c47439f3261bcc5d07b82a016d92e47e083\"/>\n  <ui filename=\"progress.ui\" sha256=\"f1fd37dbf8ecc0a517598511dea23a0dc38fe62c0044fb3fdba6f3ee4e29f63a\"/>\n  <ui filename=\"settings.ui\" sha256=\"8356416558301563dfe82d70beee88e372ba5d177284f5f2f0ed6926439a81d1\"/>\n  <ui filename=\"popover_sort.ui\" sha256=\"4c4e22be924d17135373ce698fa816a26bcfb0206b567455a7e5779bece5d570\"/>\n</cambalache-project>\n"
  },
  {
    "path": "czkawka_gui/ui/main_window.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name main_window.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkAdjustment\" id=\"adjustment1\">\n    <property name=\"page-increment\">10</property>\n    <property name=\"step-increment\">1</property>\n    <property name=\"upper\">100</property>\n  </object>\n  <object class=\"GtkWindow\" id=\"window_main\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkPaned\">\n            <property name=\"focusable\">1</property>\n            <property name=\"orientation\">vertical</property>\n            <property name=\"resize-start-child\">0</property>\n            <property name=\"shrink-end-child\">0</property>\n            <property name=\"shrink-start-child\">0</property>\n            <property name=\"vexpand\">1</property>\n            <property name=\"wide-handle\">True</property>\n            <child>\n              <object class=\"GtkNotebook\" id=\"notebook_upper\">\n                <property name=\"focusable\">1</property>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\" id=\"notebook_upper_included_directories\">\n                        <property name=\"margin-end\">5</property>\n                        <property name=\"margin-start\">5</property>\n                        <property name=\"spacing\">5</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"orientation\">vertical</property>\n                            <property name=\"spacing\">1</property>\n                            <child>\n                              <object class=\"GtkButton\" id=\"buttons_add_included_directory\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"receives-default\">1</property>\n                                <child>\n                                  <object class=\"GtkBox\">\n                                    <property name=\"halign\">center</property>\n                                    <property name=\"spacing\">4</property>\n                                    <child>\n                                      <object class=\"GtkImage\">\n                                        <property name=\"icon-name\">image-missing</property>\n                                      </object>\n                                    </child>\n                                    <child>\n                                      <object class=\"GtkLabel\">\n                                        <property name=\"label\" translatable=\"yes\">Add </property>\n                                      </object>\n                                    </child>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkButton\" id=\"buttons_remove_included_directory\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"receives-default\">1</property>\n                                <child>\n                                  <object class=\"GtkBox\">\n                                    <property name=\"halign\">center</property>\n                                    <property name=\"spacing\">4</property>\n                                    <child>\n                                      <object class=\"GtkImage\">\n                                        <property name=\"icon-name\">image-missing</property>\n                                      </object>\n                                    </child>\n                                    <child>\n                                      <object class=\"GtkLabel\">\n                                        <property name=\"label\" translatable=\"yes\">Remove  </property>\n                                      </object>\n                                    </child>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkButton\" id=\"buttons_manual_add_included_directory\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"receives-default\">1</property>\n                                <property name=\"valign\">center</property>\n                                <child>\n                                  <object class=\"GtkBox\">\n                                    <property name=\"halign\">center</property>\n                                    <property name=\"spacing\">4</property>\n                                    <child>\n                                      <object class=\"GtkImage\">\n                                        <property name=\"icon-name\">image-missing</property>\n                                      </object>\n                                    </child>\n                                    <child>\n                                      <object class=\"GtkLabel\">\n                                        <property name=\"label\" translatable=\"yes\">Manual Add</property>\n                                      </object>\n                                    </child>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_included_directories\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"hexpand\">1</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_recursive\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"halign\">center</property>\n                            <property name=\"label\" translatable=\"yes\">Recursive</property>\n                            <property name=\"margin-end\">5</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Included Paths</property>\n                      </object>\n                    </property>\n                    <property name=\"tab-fill\">False</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\" id=\"notebook_upper_excluded_directories\">\n                        <property name=\"margin-end\">5</property>\n                        <property name=\"margin-start\">5</property>\n                        <property name=\"spacing\">6</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"orientation\">vertical</property>\n                            <property name=\"spacing\">1</property>\n                            <child>\n                              <object class=\"GtkButton\" id=\"buttons_add_excluded_directory\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"receives-default\">1</property>\n                                <child>\n                                  <object class=\"GtkBox\">\n                                    <property name=\"halign\">center</property>\n                                    <property name=\"spacing\">4</property>\n                                    <child>\n                                      <object class=\"GtkImage\">\n                                        <property name=\"icon-name\">image-missing</property>\n                                      </object>\n                                    </child>\n                                    <child>\n                                      <object class=\"GtkLabel\">\n                                        <property name=\"label\" translatable=\"yes\">Add </property>\n                                      </object>\n                                    </child>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkButton\" id=\"buttons_remove_excluded_directory\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"receives-default\">1</property>\n                                <child>\n                                  <object class=\"GtkBox\">\n                                    <property name=\"halign\">center</property>\n                                    <property name=\"spacing\">4</property>\n                                    <child>\n                                      <object class=\"GtkImage\">\n                                        <property name=\"icon-name\">image-missing</property>\n                                      </object>\n                                    </child>\n                                    <child>\n                                      <object class=\"GtkLabel\">\n                                        <property name=\"label\" translatable=\"yes\">Remove  </property>\n                                      </object>\n                                    </child>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkButton\" id=\"buttons_manual_add_excluded_directory\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"receives-default\">1</property>\n                                <property name=\"valign\">center</property>\n                                <child>\n                                  <object class=\"GtkBox\">\n                                    <property name=\"halign\">center</property>\n                                    <property name=\"spacing\">4</property>\n                                    <child>\n                                      <object class=\"GtkImage\">\n                                        <property name=\"icon-name\">image-missing</property>\n                                      </object>\n                                    </child>\n                                    <child>\n                                      <object class=\"GtkLabel\">\n                                        <property name=\"label\" translatable=\"yes\">Manual Add</property>\n                                      </object>\n                                    </child>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_excluded_directories\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"hexpand\">1</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">1</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Excluded Paths</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\" id=\"notebook_upper_excluded_items\">\n                        <property name=\"orientation\">vertical</property>\n                        <property name=\"valign\">center</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"spacing\">5</property>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_excluded_items\">\n                                <property name=\"label\" translatable=\"yes\">Excluded items</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkEntry\" id=\"entry_excluded_items\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"text\" translatable=\"yes\">*/.git,*/node_modules,*/lost+found</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"spacing\">5</property>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_allowed_extensions\">\n                                <property name=\"label\" translatable=\"yes\">Allowed Extensions</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkEntry\" id=\"entry_allowed_extensions\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_excluded_extensions\">\n                                <property name=\"label\" translatable=\"yes\">Disabled Extensions</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkEntry\" id=\"entry_excluded_extensions\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"spacing\">8</property>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_general_size_bytes\">\n                                <property name=\"label\" translatable=\"yes\">File Size(bytes)</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_general_min_size\">\n                                <property name=\"label\" translatable=\"yes\">Min:</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkEntry\" id=\"entry_general_minimal_size\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"input-purpose\">number</property>\n                                <property name=\"max-length\">15</property>\n                                <property name=\"text\" translatable=\"yes\">8192</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_general_max_size\">\n                                <property name=\"label\" translatable=\"yes\">Max:</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkEntry\" id=\"entry_general_maximal_size\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"input-purpose\">number</property>\n                                <property name=\"max-length\">15</property>\n                                <property name=\"text\" translatable=\"yes\">1099512000000</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">2</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Items Configuration</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkNotebook\" id=\"notebook_main\">\n                <property name=\"focusable\">1</property>\n                <property name=\"scrollable\">1</property>\n                <property name=\"tab-pos\">left</property>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkPaned\">\n                        <property name=\"wide-handle\">True</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"hexpand\">1</property>\n                            <property name=\"orientation\">vertical</property>\n                            <child>\n                              <object class=\"GtkBox\">\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"margin-start\">5</property>\n                                <property name=\"margin-top\">2</property>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_duplicate_check_method\">\n                                    <property name=\"label\" translatable=\"yes\">Check method</property>\n                                    <property name=\"margin-end\">3</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkComboBoxText\" id=\"combo_box_duplicate_check_method\"/>\n                                </child>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_duplicate_hash_type\">\n                                    <property name=\"label\" translatable=\"yes\">Hash type</property>\n                                    <property name=\"margin-end\">2</property>\n                                    <property name=\"margin-start\">5</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkComboBoxText\" id=\"combo_box_duplicate_hash_type\"/>\n                                </child>\n                                <child>\n                                  <object class=\"GtkCheckButton\" id=\"check_button_duplicate_case_sensitive_name\">\n                                    <property name=\"label\">Case sensitive</property>\n                                    <property name=\"visible\">0</property>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkScrolledWindow\" id=\"scrolled_window_duplicate_finder\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"vexpand\">1</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkPicture\" id=\"image_preview_duplicates\">\n                            <property name=\"height-request\">100</property>\n                            <property name=\"hexpand\">True</property>\n                            <property name=\"hexpand-set\">True</property>\n                            <property name=\"vexpand\">True</property>\n                            <property name=\"vexpand-set\">True</property>\n                            <property name=\"width-request\">100</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Duplicates files</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkScrolledWindow\" id=\"scrolled_window_empty_folder_finder\">\n                        <property name=\"focusable\">1</property>\n                      </object>\n                    </property>\n                    <property name=\"position\">1</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Empty Directories</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\">\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"margin-top\">2</property>\n                            <property name=\"spacing\">8</property>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_big_files_mode\">\n                                <property name=\"label\">VVV</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkComboBoxText\" id=\"combo_box_big_files_mode\"/>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_big_shown_files\">\n                                <property name=\"label\" translatable=\"yes\">Number of shown files</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkEntry\" id=\"entry_big_files_number\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"input-purpose\">number</property>\n                                <property name=\"max-length\">15</property>\n                                <property name=\"text\" translatable=\"yes\">50</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_big_files_finder\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"vexpand\">1</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">2</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Big Files</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkScrolledWindow\" id=\"scrolled_window_empty_files_finder\">\n                        <property name=\"focusable\">1</property>\n                      </object>\n                    </property>\n                    <property name=\"position\">3</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Empty Files</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkScrolledWindow\" id=\"scrolled_window_temporary_files_finder\">\n                        <property name=\"focusable\">1</property>\n                      </object>\n                    </property>\n                    <property name=\"position\">4</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Temporary Files</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkPaned\">\n                        <property name=\"wide-handle\">True</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"hexpand\">1</property>\n                            <property name=\"orientation\">vertical</property>\n                            <child>\n                              <object class=\"GtkBox\">\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"margin-start\">5</property>\n                                <property name=\"margin-top\">2</property>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_image_resize_algorithm\">\n                                    <property name=\"label\" translatable=\"yes\">Resize algorithm</property>\n                                    <property name=\"margin-end\">2</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkComboBoxText\" id=\"combo_box_image_resize_algorithm\"/>\n                                </child>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_image_hash_size\">\n                                    <property name=\"label\" translatable=\"yes\">Hash size:</property>\n                                    <property name=\"margin-end\">2</property>\n                                    <property name=\"margin-start\">5</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkComboBoxText\" id=\"combo_box_image_hash_size\"/>\n                                </child>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_image_hash_type\">\n                                    <property name=\"label\" translatable=\"yes\">Hash type:</property>\n                                    <property name=\"margin-end\">2</property>\n                                    <property name=\"margin-start\">5</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkComboBoxText\" id=\"combo_box_image_hash_algorithm\"/>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkBox\">\n                                <property name=\"margin-bottom\">2</property>\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"margin-start\">5</property>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_image_similarity\">\n                                    <property name=\"label\" translatable=\"yes\">Similarity </property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_image_similarity_max\">\n                                    <property name=\"label\" translatable=\"yes\">  Very High  </property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkScale\" id=\"scale_similarity_similar_images\">\n                                    <property name=\"digits\">0</property>\n                                    <property name=\"draw-value\">1</property>\n                                    <property name=\"fill-level\">100</property>\n                                    <property name=\"focusable\">1</property>\n                                    <property name=\"hexpand\">1</property>\n                                    <property name=\"round-digits\">1</property>\n                                    <property name=\"value-pos\">right</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkLabel\" id=\"label_similar_images_minimal_similarity\">\n                                    <property name=\"label\" translatable=\"yes\">Minimal</property>\n                                    <property name=\"margin-end\">5</property>\n                                    <property name=\"margin-start\">5</property>\n                                  </object>\n                                </child>\n                                <child>\n                                  <object class=\"GtkCheckButton\" id=\"check_button_image_ignore_same_size\">\n                                    <property name=\"focusable\">1</property>\n                                    <property name=\"label\" translatable=\"yes\">Ignore same size</property>\n                                    <property name=\"margin-start\">7</property>\n                                  </object>\n                                </child>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkScrolledWindow\" id=\"scrolled_window_similar_images_finder\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"vexpand\">1</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkPicture\" id=\"image_preview_similar_images\">\n                            <property name=\"height-request\">100</property>\n                            <property name=\"hexpand\">True</property>\n                            <property name=\"hexpand-set\">True</property>\n                            <property name=\"vexpand\">True</property>\n                            <property name=\"vexpand-set\">True</property>\n                            <property name=\"width-request\">100</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">5</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Similar Images</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\">\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-bottom\">2</property>\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_video_similarity\">\n                                <property name=\"label\" translatable=\"yes\">Similarity </property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_video_similarity_max\">\n                                <property name=\"label\" translatable=\"yes\">  Very High  </property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkScale\" id=\"scale_similarity_similar_videos\">\n                                <property name=\"digits\">0</property>\n                                <property name=\"draw-value\">1</property>\n                                <property name=\"fill-level\">100</property>\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"round-digits\">0</property>\n                                <property name=\"value-pos\">right</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_video_similarity_min\">\n                                <property name=\"label\" translatable=\"yes\">Minimal</property>\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"margin-start\">5</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_video_ignore_same_size\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\" translatable=\"yes\">Ignore same size</property>\n                                <property name=\"margin-start\">7</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_similar_videos_finder\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"vexpand\">1</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">6</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Similar Videos</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\">\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <property name=\"spacing\">8</property>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_title\">\n                                <property name=\"active\">1</property>\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\" translatable=\"yes\">Title</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_artist\">\n                                <property name=\"active\">1</property>\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\" translatable=\"yes\">Artist</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_year\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"halign\">start</property>\n                                <property name=\"label\">Year</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_bitrate\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\">Bitrate</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_genre\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\">Genre</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_length\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\">Length</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_same_music_seconds\">\n                                <property name=\"label\">Minimal  fragment second duration</property>\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"margin-start\">5</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkScale\" id=\"scale_seconds_same_music\">\n                                <property name=\"digits\">0</property>\n                                <property name=\"draw-value\">1</property>\n                                <property name=\"fill-level\">100</property>\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"round-digits\">1</property>\n                                <property name=\"value-pos\">right</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_same_music_similarity\">\n                                <property name=\"label\">Max difference</property>\n                                <property name=\"margin-end\">5</property>\n                                <property name=\"margin-start\">5</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkScale\" id=\"scale_similarity_same_music\">\n                                <property name=\"digits\">0</property>\n                                <property name=\"draw-value\">1</property>\n                                <property name=\"fill-level\">100</property>\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"round-digits\">1</property>\n                                <property name=\"value-pos\">right</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"margin-bottom\">2</property>\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"margin-start\">5</property>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_audio_check_type\">\n                                <property name=\"label\">Audio check type</property>\n                                <property name=\"margin-end\">2</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkComboBoxText\" id=\"combo_box_audio_check_type\"/>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_approximate_comparison\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\" translatable=\"yes\">Approximate Comparison</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_music_compare_only_in_title_group\">\n                                <property name=\"focusable\">1</property>\n                                <property name=\"label\">Comparison only in group</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_same_music_finder\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"margin-end\">5</property>\n                            <property name=\"vexpand\">1</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">7</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Music Duplicates</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkScrolledWindow\" id=\"scrolled_window_invalid_symlinks\">\n                        <property name=\"focusable\">1</property>\n                      </object>\n                    </property>\n                    <property name=\"position\">8</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Invalid Symlinks</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkBox\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"orientation\">vertical</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_broken_files_audio\">\n                                <property name=\"active\">True</property>\n                                <property name=\"label\">Audio</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_broken_files_pdf\">\n                                <property name=\"active\">True</property>\n                                <property name=\"label\">PDF</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_broken_files_archive\">\n                                <property name=\"active\">True</property>\n                                <property name=\"label\">Archive</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_broken_files_image\">\n                                <property name=\"active\">True</property>\n                                <property name=\"label\">Image</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkCheckButton\" id=\"check_button_broken_files_video\">\n                                <property name=\"active\">True</property>\n                                <property name=\"label\">Video</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_broken_files\">\n                            <property name=\"vexpand\">True</property>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                    <property name=\"position\">9</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Broken Files</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkNotebookPage\">\n                    <property name=\"child\">\n                      <object class=\"GtkScrolledWindow\" id=\"scrolled_window_bad_extensions\">\n                        <property name=\"focusable\">1</property>\n                      </object>\n                    </property>\n                    <property name=\"position\">10</property>\n                    <property name=\"tab\">\n                      <object class=\"GtkLabel\">\n                        <property name=\"label\" translatable=\"yes\">Bad Extensions</property>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"buttons\">\n            <property name=\"margin-bottom\">5</property>\n            <property name=\"margin-end\">5</property>\n            <property name=\"margin-start\">5</property>\n            <property name=\"margin-top\">2</property>\n            <child>\n              <object class=\"GtkBox\">\n                <property name=\"spacing\">2</property>\n                <child>\n                  <object class=\"GtkButton\" id=\"buttons_search\">\n                    <property name=\"focusable\">1</property>\n                    <property name=\"receives-default\">1</property>\n                    <child>\n                      <object class=\"GtkBox\">\n                        <property name=\"halign\">center</property>\n                        <property name=\"spacing\">2</property>\n                        <child>\n                          <object class=\"GtkImage\">\n                            <property name=\"icon-name\">image-missing</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkLabel\">\n                            <property name=\"label\" translatable=\"yes\">Search</property>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                </child>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkBox\">\n                <property name=\"halign\">end</property>\n                <property name=\"hexpand\">True</property>\n                <child>\n                  <object class=\"GtkBox\">\n                    <property name=\"halign\">end</property>\n                    <property name=\"spacing\">2</property>\n                    <child>\n                      <object class=\"GtkMenuButton\" id=\"buttons_select\">\n                        <property name=\"focus-on-click\">0</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_buttons_select\">\n                                <property name=\"label\">SelectMenu</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkMenuButton\" id=\"buttons_sort\">\n                        <property name=\"focus-on-click\">0</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_buttons_sort\">\n                                <property name=\"label\">SortMenu</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"buttons_compare\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\">\n                                <property name=\"label\" translatable=\"yes\">Compare</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"buttons_delete\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\">\n                                <property name=\"label\" translatable=\"yes\">Delete</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"buttons_move\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\">\n                                <property name=\"label\" translatable=\"yes\">Move</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"buttons_save\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\">\n                                <property name=\"label\" translatable=\"yes\">Save</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"buttons_symlink\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\">\n                                <property name=\"label\" translatable=\"yes\">Symlink</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"buttons_hardlink\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"receives-default\">1</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <property name=\"halign\">center</property>\n                            <property name=\"spacing\">2</property>\n                            <child>\n                              <object class=\"GtkImage\">\n                                <property name=\"icon-name\">image-missing</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkLabel\">\n                                <property name=\"label\" translatable=\"yes\">Hardlink</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"buttons_show_errors\">\n                    <property name=\"focusable\">1</property>\n                    <property name=\"receives-default\">1</property>\n                    <child>\n                      <object class=\"GtkImage\">\n                        <property name=\"halign\">center</property>\n                        <property name=\"icon-name\">image-missing</property>\n                      </object>\n                    </child>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"buttons_show_upper_notebook\">\n                    <property name=\"focusable\">1</property>\n                    <property name=\"receives-default\">1</property>\n                    <child>\n                      <object class=\"GtkImage\">\n                        <property name=\"halign\">center</property>\n                        <property name=\"icon-name\">image-missing</property>\n                      </object>\n                    </child>\n                  </object>\n                </child>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkBox\">\n            <property name=\"margin-bottom\">5</property>\n            <property name=\"margin-end\">5</property>\n            <property name=\"margin-start\">5</property>\n            <property name=\"margin-top\">5</property>\n            <child>\n              <object class=\"GtkEntry\" id=\"entry_info\">\n                <property name=\"editable\">0</property>\n                <property name=\"focusable\">1</property>\n                <property name=\"has-frame\">0</property>\n                <property name=\"hexpand\">1</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkEntry\">\n                <property name=\"editable\">0</property>\n                <property name=\"focusable\">1</property>\n                <property name=\"has-frame\">0</property>\n                <property name=\"text\" translatable=\"yes\">Czkawka 11.0.1</property>\n                <property name=\"xalign\">1</property>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkScrolledWindow\" id=\"scrolled_window_errors\">\n            <property name=\"child\">\n              <object class=\"GtkTextView\" id=\"text_view_errors\">\n                <property name=\"editable\">0</property>\n                <property name=\"focusable\">1</property>\n                <property name=\"monospace\">1</property>\n              </object>\n            </property>\n            <property name=\"focusable\">1</property>\n            <property name=\"min-content-height\">100</property>\n          </object>\n        </child>\n      </object>\n    </property>\n    <property name=\"default-height\">800</property>\n    <property name=\"default-width\">1100</property>\n    <child type=\"titlebar\">\n      <object class=\"GtkHeaderBar\">\n        <child type=\"end\">\n          <object class=\"GtkBox\">\n            <property name=\"spacing\">5</property>\n            <child>\n              <object class=\"GtkButton\" id=\"button_settings\">\n                <property name=\"focusable\">1</property>\n                <property name=\"receives-default\">1</property>\n                <child>\n                  <object class=\"GtkImage\">\n                    <property name=\"icon-name\">image-missing</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"button_app_info\">\n                <property name=\"focusable\">1</property>\n                <property name=\"receives-default\">1</property>\n                <child>\n                  <object class=\"GtkImage\">\n                    <property name=\"icon-name\">image-missing</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n          </object>\n        </child>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/popover_right_click.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name popover_right_click.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkPopover\" id=\"popover_right_click\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_right_click_open_file\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Open File</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_right_click_open_folder\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Open Folder</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n      </object>\n    </property>\n    <property name=\"position\">left</property>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/popover_select.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name popover_select.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkPopover\" id=\"popover_select\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_custom\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select custom</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_unselect_custom\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Unselect custom</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSeparator\" id=\"separator_select_shortest_path\"/>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all_except_shortest_path\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select all except shortest path</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all_except_longest_path\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select all except longest path</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSeparator\" id=\"separator_select_custom\"/>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all_images_except_biggest\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select all except biggest</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all_images_except_smallest\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select all except smallest</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSeparator\" id=\"separator_select_image_size\"/>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all_except_oldest\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select all except oldest</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all_except_newest\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select all except newest</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_one_oldest\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select one oldest</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_one_newest\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select one newest</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSeparator\" id=\"separator_select_date\"/>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_reverse\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Reverse Selection</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSeparator\" id=\"separator_select_reverse\"/>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_select_all\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Select All</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_unselect_all\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\" translatable=\"yes\">Unselect All</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n      </object>\n    </property>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/popover_sort.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name popover_sort.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkPopover\" id=\"popover_sort\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_sort_file_name\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\">File name</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_sort_folder_name\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\">Folder name</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_sort_full_name\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\">Full name</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_sort_size\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\">Size</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"buttons_popover_sort_selection\">\n            <property name=\"focusable\">1</property>\n            <property name=\"label\">Selection</property>\n            <property name=\"receives-default\">1</property>\n          </object>\n        </child>\n      </object>\n    </property>\n    <property name=\"position\">top</property>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/progress.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name progress.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkDialog\" id=\"window_progress\">\n    <child>\n      <object class=\"GtkBox\">\n        <property name=\"margin-bottom\">10</property>\n        <property name=\"margin-end\">10</property>\n        <property name=\"margin-start\">10</property>\n        <property name=\"margin-top\">10</property>\n        <property name=\"orientation\">vertical</property>\n        <property name=\"spacing\">10</property>\n        <child>\n          <object class=\"GtkGrid\" id=\"grid_progress\">\n            <property name=\"margin-end\">2</property>\n            <property name=\"margin-start\">2</property>\n            <property name=\"margin-top\">2</property>\n            <property name=\"valign\">center</property>\n            <property name=\"vexpand\">1</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"label_progress_all_stages\">\n                <property name=\"label\" translatable=\"yes\">All stages: </property>\n                <property name=\"name\">label_progress_all_stages</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">1</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkProgressBar\" id=\"progress_bar_all_stages\">\n                <property name=\"hexpand\">1</property>\n                <property name=\"pulse-step\">0.099999999776482579</property>\n                <property name=\"show-text\">1</property>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">1</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"label_progress_current_stage\">\n                <property name=\"label\" translatable=\"yes\">Current stage:  </property>\n                <property name=\"name\">label_progress_current_stage</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">0</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkProgressBar\" id=\"progress_bar_current_stage\">\n                <property name=\"show-text\">1</property>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">0</property>\n                </layout>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkLabel\" id=\"label_stage\">\n            <property name=\"label\" translatable=\"yes\">Stage 1/2</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"button_stop_in_dialog\">\n            <property name=\"focusable\">1</property>\n            <property name=\"halign\">end</property>\n            <property name=\"margin-end\">2</property>\n            <property name=\"receives-default\">1</property>\n            <property name=\"valign\">center</property>\n            <child>\n              <object class=\"GtkBox\">\n                <child>\n                  <object class=\"GtkImage\">\n                    <property name=\"icon-name\">image-missing</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkLabel\">\n                    <property name=\"hexpand\">1</property>\n                    <property name=\"label\" translatable=\"yes\">Stop</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n          </object>\n        </child>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "czkawka_gui/ui/settings.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.96.1 -->\n<interface>\n  <!-- interface-name settings.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkDialog\" id=\"window_settings\">\n    <property name=\"modal\">1</property>\n    <property name=\"title\" translatable=\"yes\">Czkawka Options</property>\n    <child>\n      <object class=\"GtkBox\" id=\"potatoo\">\n        <property name=\"orientation\">vertical</property>\n        <property name=\"vexpand\">1</property>\n        <child>\n          <object class=\"GtkNotebook\" id=\"notebook_settings\">\n            <property name=\"focusable\">1</property>\n            <property name=\"tab-pos\">left</property>\n            <property name=\"vexpand\">1</property>\n            <child>\n              <object class=\"GtkNotebookPage\">\n                <property name=\"child\">\n                  <object class=\"GtkBox\">\n                    <property name=\"margin-bottom\">5</property>\n                    <property name=\"orientation\">vertical</property>\n                    <child>\n                      <object class=\"GtkBox\">\n                        <property name=\"orientation\">vertical</property>\n                        <property name=\"valign\">center</property>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_settings_general_language\">\n                                <property name=\"label\" translatable=\"yes\">Language</property>\n                                <property name=\"margin-end\">10</property>\n                                <property name=\"margin-start\">5</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkComboBoxText\" id=\"combo_box_settings_language\">\n                                <property name=\"hexpand\">1</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_load_at_start\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Load configuration at start</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_save_at_exit\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Save configuration at exit</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_confirm_deletion\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Show confirm dialog when deleting any files</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_confirm_link\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Show confirm dialog when hard/symlinks any files</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_confirm_group_deletion\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Show confirm dialog when deleting all files in group</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_show_text_view\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Show bottom text panel</property>\n                            <property name=\"valign\">center</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_use_cache\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Use cache</property>\n                            <property name=\"vexpand\">1</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_save_also_json\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Save cache also to JSON file</property>\n                            <property name=\"vexpand\">1</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_use_trash\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Move deleted files to trash</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_one_filesystem\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\">Exclude other filesystems(Linux)</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkCheckButton\" id=\"check_button_settings_use_rust_preview\">\n                            <property name=\"active\">1</property>\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\">Use external libraries instead gtk to load previews</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkBox\">\n                            <child>\n                              <object class=\"GtkLabel\" id=\"label_settings_number_of_threads\">\n                                <property name=\"accessible-role\">presentation</property>\n                                <property name=\"label\">Number of used threads</property>\n                                <property name=\"margin-start\">5</property>\n                                <property name=\"wrap-mode\">word-char</property>\n                              </object>\n                            </child>\n                            <child>\n                              <object class=\"GtkScale\" id=\"scale_settings_number_of_threads\">\n                                <property name=\"digits\">0</property>\n                                <property name=\"draw-value\">True</property>\n                                <property name=\"fill-level\">100</property>\n                                <property name=\"focusable\">1</property>\n                                <property name=\"hexpand\">1</property>\n                                <property name=\"round-digits\">1</property>\n                                <property name=\"value-pos\">right</property>\n                              </object>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkLabel\" id=\"label_restart_needed\">\n                        <property name=\"accessible-role\">menu-item-checkbox</property>\n                        <property name=\"label\">Restart Required</property>\n                        <property name=\"margin-bottom\">4</property>\n                        <property name=\"margin-top\">5</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkBox\">\n                        <property name=\"valign\">center</property>\n                        <child>\n                          <object class=\"GtkButton\" id=\"button_settings_open_cache_folder\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Open cache folder</property>\n                            <property name=\"receives-default\">1</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkButton\" id=\"button_settings_open_settings_folder\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Open settings folder</property>\n                            <property name=\"receives-default\">1</property>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                </property>\n                <property name=\"tab\">\n                  <object class=\"GtkLabel\">\n                    <property name=\"label\" translatable=\"yes\">General</property>\n                  </object>\n                </property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkNotebookPage\">\n                <property name=\"child\">\n                  <object class=\"GtkBox\">\n                    <property name=\"orientation\">vertical</property>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_settings_hide_hard_links\">\n                        <property name=\"active\">1</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Hide hard links(only Linux and MacOS)</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_settings_show_preview_duplicates\">\n                        <property name=\"active\">1</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Show image preview</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkBox\">\n                        <property name=\"margin-end\">4</property>\n                        <property name=\"margin-start\">4</property>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label_settings_duplicate_minimal_size_cache\">\n                            <property name=\"hexpand\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Minimal size of files in bytes saved to cache</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkEntry\" id=\"entry_settings_cache_file_minimal_size\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"halign\">center</property>\n                            <property name=\"input-purpose\">number</property>\n                            <property name=\"max-length\">15</property>\n                            <property name=\"text\" translatable=\"yes\">257144</property>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_duplicates_use_prehash_cache\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Use prehash cache</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkBox\">\n                        <property name=\"margin-end\">4</property>\n                        <property name=\"margin-start\">4</property>\n                        <child>\n                          <object class=\"GtkLabel\" id=\"label_settings_duplicate_minimal_size_cache_prehash\">\n                            <property name=\"hexpand\">1</property>\n                            <property name=\"label\" translatable=\"yes\">Minimal size of files in bytes saved to prehash cache</property>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkEntry\" id=\"entry_settings_prehash_cache_file_minimal_size\">\n                            <property name=\"focusable\">1</property>\n                            <property name=\"halign\">center</property>\n                            <property name=\"input-purpose\">number</property>\n                            <property name=\"max-length\">15</property>\n                            <property name=\"text\" translatable=\"yes\">1</property>\n                          </object>\n                        </child>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_settings_duplicates_delete_outdated_cache\">\n                        <property name=\"active\">1</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Delete outdated cache entries automatically</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"button_settings_duplicates_clear_cache\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Remove outdated results from duplicates cache</property>\n                        <property name=\"receives-default\">1</property>\n                        <property name=\"valign\">center</property>\n                      </object>\n                    </child>\n                  </object>\n                </property>\n                <property name=\"position\">1</property>\n                <property name=\"tab\">\n                  <object class=\"GtkLabel\">\n                    <property name=\"label\" translatable=\"yes\">Duplicate Finder</property>\n                  </object>\n                </property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkNotebookPage\">\n                <property name=\"child\">\n                  <object class=\"GtkBox\">\n                    <property name=\"orientation\">vertical</property>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_settings_show_preview_similar_images\">\n                        <property name=\"active\">1</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Show image preview</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_settings_similar_images_delete_outdated_cache\">\n                        <property name=\"active\">1</property>\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Delete outdated cache entries automatically</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"button_settings_similar_images_clear_cache\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Remove outdated results from images cache</property>\n                        <property name=\"receives-default\">1</property>\n                        <property name=\"valign\">center</property>\n                      </object>\n                    </child>\n                  </object>\n                </property>\n                <property name=\"position\">2</property>\n                <property name=\"tab\">\n                  <object class=\"GtkLabel\">\n                    <property name=\"label\" translatable=\"yes\">Similar Images</property>\n                  </object>\n                </property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkNotebookPage\">\n                <property name=\"child\">\n                  <object class=\"GtkBox\">\n                    <property name=\"orientation\">vertical</property>\n                    <child>\n                      <object class=\"GtkCheckButton\" id=\"check_button_settings_similar_videos_delete_outdated_cache\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Delete outdated cache entries automatically</property>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"button_settings_similar_videos_clear_cache\">\n                        <property name=\"focusable\">1</property>\n                        <property name=\"label\" translatable=\"yes\">Remove outdated results from videos cache</property>\n                        <property name=\"receives-default\">1</property>\n                        <property name=\"valign\">center</property>\n                      </object>\n                    </child>\n                  </object>\n                </property>\n                <property name=\"position\">3</property>\n                <property name=\"tab\">\n                  <object class=\"GtkLabel\">\n                    <property name=\"label\" translatable=\"yes\">Similar Videos</property>\n                  </object>\n                </property>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkBox\">\n            <property name=\"margin-end\">3</property>\n            <property name=\"margin-start\">3</property>\n            <property name=\"spacing\">3</property>\n            <child>\n              <object class=\"GtkButton\" id=\"button_settings_load_configuration\">\n                <property name=\"focusable\">1</property>\n                <property name=\"halign\">center</property>\n                <property name=\"label\" translatable=\"yes\">Load configuration</property>\n                <property name=\"receives-default\">1</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"button_settings_reset_configuration\">\n                <property name=\"focusable\">1</property>\n                <property name=\"halign\">center</property>\n                <property name=\"hexpand\">1</property>\n                <property name=\"label\" translatable=\"yes\">Reset configuration</property>\n                <property name=\"receives-default\">1</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"button_settings_save_configuration\">\n                <property name=\"focusable\">1</property>\n                <property name=\"halign\">center</property>\n                <property name=\"label\" translatable=\"yes\">Save configuration</property>\n                <property name=\"receives-default\">1</property>\n              </object>\n            </child>\n          </object>\n        </child>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "data/com.github.qarmin.czkawka.desktop",
    "content": "[Desktop Entry]\nCategories=System;FileTools\nExec=czkawka_gui\nIcon=com.github.qarmin.czkawka\nStartupWMClass=czkawka_gui\nTerminal=false\nTryExec=czkawka_gui\nType=Application\n\nName=Czkawka\nName[it]=Singhiozzo\nName[pt_BR]=Comparador de Arquivos Duplicados Czkawka\n\nComment=Multi functional app to clean OS which allow to find duplicates, empty folders, similar files etc.\nComment[it]=Programma multifunzionale per pulire il sistema, che permette di trovare file duplicati, cartelle vuote, file simili, ecc...\nComment[pt_BR]=O ‘Czkawka’, que em idioma português significa ‘soluço’, é um programa que permite comparar e encontrar (buscar ou localizar ou pesquisar) arquivos duplicados, pastas ou diretórios vazios, arquivos equivalentes (semelhantes ou similares), criar arquivos de verificação da integridade (hash) de vários tipos de arquivos diferentes (por exemplo, arquivos de imagens, músicas, vídeos, etc.), mover para a lixeira os arquivos duplicados, excluir permanentemente os arquivos duplicados, etc., possibilita realizar a limpeza do sistema operacional\nComment[zh_CN]=可用于清理文件副本、空文件夹、相似文件等的系统清理工具\nComment[zh_TW]=可用於清理重複檔案、空資料夾、相似檔案等的系統清理工具\n\nKeywords=Hiccup;duplicate;same;similar;cleaner;copy;copies;compare;files;\nKeywords[pt_BR]=czkawka;hiccup;soluço;arquivos;ficheiros;duplicado;igual;iguais;equivalentes;similares;semelhantes;limpar;limpeza;mais limpo;cópia;cópias;comparar;pastas;\n"
  },
  {
    "path": "data/com.github.qarmin.czkawka.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop-application\">\n  <id>com.github.qarmin.czkawka</id>\n  <name>Czkawka</name>\n  <summary>Multi functional app to find duplicates, empty folders, similar images, broken files etc.</summary>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>MIT</project_license>\n  <description>\n    <p>\n      Czkawka is simple, fast and easy to use app to remove unnecessary files from your computer.\n    </p>\n  </description>\n  <launchable type=\"desktop-id\">com.github.qarmin.czkawka.desktop</launchable>\n  <screenshots>\n    <screenshot type=\"default\">\n      <image>https://user-images.githubusercontent.com/41945903/147875238-7f82fa27-c6dd-47e7-87ed-e253fb2cbc3e.png</image>\n    </screenshot>\n    <screenshot>\n      <image>https://user-images.githubusercontent.com/41945903/147875239-bcf9776c-885d-45ac-ba82-5a426d8e1647.png</image>\n    </screenshot>\n    <screenshot>\n      <image>https://user-images.githubusercontent.com/41945903/147875243-e654e683-37f7-46fa-8321-119a4c5775e7.png</image>\n    </screenshot>\n  </screenshots>\n  <releases>\n    <release version=\"11.0.1\" date=\"2026-02-14\"/>\n  </releases>\n  <content_rating type=\"oars-1.0\"/>\n  <developer_name>Rafał Mikrut</developer_name>\n    <developer id=\"com.github.qarmin\">\n      <name>Rafał Mikrut</name>\n  </developer>\n  <url type=\"homepage\">https://github.com/qarmin/czkawka</url>\n  <url type=\"bugtracker\">https://github.com/qarmin/czkawka/issues</url>\n  <url type=\"donation\">https://github.com/sponsors/qarmin</url>\n  <url type=\"translate\">https://crowdin.com/project/czkawka</url>\n</component>\n"
  },
  {
    "path": "data/io.github.qarmin.krokiet.desktop",
    "content": "[Desktop Entry]\nCategories=System;FileTools\nExec=krokiet\nIcon=io.github.qarmin.krokiet\nStartupWMClass=krokiet\nTerminal=false\nTryExec=krokiet\nType=Application\n\nName=Krokiet\n\nComment=Krokiet - multi-functional app to find duplicates, empty folders, similar files and many more.\n\nKeywords=Krokiet;duplicate;same;similar;cleaner;copy;copies;compare;files;krokiet\n"
  },
  {
    "path": "data/io.github.qarmin.krokiet.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop-application\">\n  <id>io.github.qarmin.krokiet</id>\n  <name>Krokiet</name>\n  <summary>Multi functional app to find duplicates, similar images and many more</summary>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>GPL-3.0-only</project_license>\n  <description>\n    <p>Krokiet is a multi functional app that finds:</p>\n    <ul>\n      <li>Duplicates</li>\n      <li>Similar images</li>\n      <li>Similar audio files</li>\n      <li>Similar videos</li>\n      <li>Empty folders</li>\n      <li>Empty files</li>\n      <li>Broken files</li>\n      <li>Temporary files</li>\n      <li>Big files</li>\n      <li>Invalid symlinks</li>\n      <li>Bad names</li>\n      <li>Videos that can be optimized/cropped</li>\n      <li>Exif tags</li>\n    </ul>\n  </description>\n  <launchable type=\"desktop-id\">io.github.qarmin.krokiet.desktop</launchable>\n  <screenshots>\n    <screenshot type=\"default\">\n      <image>https://github.com/user-attachments/assets/720e98c3-598a-41aa-a04b-0c0c1d8a28e6</image>\n    </screenshot>\n    <screenshot>\n      <image>https://github.com/user-attachments/assets/c95e51bf-1ae0-49ec-af92-0195efc98e5d</image>\n    </screenshot>\n    <screenshot>\n      <image>https://github.com/user-attachments/assets/4fe7bec3-4d67-48bb-91bc-91e7d3b82bdc</image>\n    </screenshot>\n  </screenshots>\n  <releases>\n    <release version=\"11.0.1\" date=\"2026-02-14\"/>\n  </releases>\n  <content_rating type=\"oars-1.0\"/>\n  <developer id=\"io.github.qarmin\">\n    <name>Rafał Mikrut</name>\n  </developer>\n  <url type=\"homepage\">https://github.com/qarmin/czkawka</url>\n  <url type=\"bugtracker\">https://github.com/qarmin/czkawka/issues</url>\n  <url type=\"donation\">https://github.com/sponsors/qarmin</url>\n  <url type=\"translate\">https://crowdin.com/project/czkawka</url>\n</component>\n"
  },
  {
    "path": "instructions/Instruction.md",
    "content": "# Instruction\n\n- [GUI Krokiet](#gui-krokiet)\n- [GUI GTK](#gui-gtk)\n- [CLI](#cli)\n- [Common Workflows](#common-workflows)\n- [Config / Cache files](#configcache-files)\n- [Tips, tricks and known bugs](#tips-tricks-and-known-bugs)\n- [Tools](#tools)\n\nCzkawka contains three independent frontends - the terminal app (CLI) and two graphical apps (Krokiet and GTK) which share the core module.\n\n**Krokiet** is the new primary GUI written in Slint, providing a consistent cross-platform experience with better performance and fewer bugs.\n\n**GTK** is the older GUI that is still maintained but will eventually be replaced by Krokiet.\n\n## GUI Krokiet\n<img src=\"https://github.com/user-attachments/assets/720e98c3-598a-41aa-a04b-0c0c1d8a28e6\" alt=\"Krokiet main window\" width=\"800\" />\n\nKrokiet is the new Czkawka frontend written in Slint. It provides a modern, consistent interface across all platforms (Linux, Windows, macOS) and is designed to be more performant and stable than the GTK version.\n\n### Main Interface Structure\n\nThe Krokiet interface consists of several key areas:\n\n1. **Left Side Panel** - Tool selector with tabs for each scanning mode (Duplicates, Empty Files, Similar Images, etc.)\n2. **Top Bar** - Contains scan button, settings button, and status information\n3. **Directory Selection Panel** - Area to add/remove included and excluded directories, set file filters\n4. **Results Area** - Displays scan results in a table/list format\n5. **Bottom Panel** - Action buttons for working with results (Select, Delete, Move, etc.)\n6. **Right Side Panel** - Preview area for images and additional information\n\n### Settings Screen\n\nAccessible via the settings button, contains multiple subsections:\n\n**General Settings**\n- Language selection\n- UI scale factor\n- Theme selection (Light/Dark)\n- Audio notification settings\n\n**Performance Settings**\n- Thread count configuration\n- Cache behavior settings\n- Memory limits\n\n**Tool-Specific Settings**\nEach tool has its own settings page with advanced options:\n- Cache management (enable/disable, clear old entries)\n- Scan depth limits\n- Excluded items lists\n- Algorithm-specific parameters\n\n### Translations\n\nKrokiet have full support for multiple languages in GUI. Language can be changed in Settings → General → Language.\n\n## GUI GTK\n<img src=\"https://user-images.githubusercontent.com/41945903/148281103-13c00d08-7881-43e8-b6e3-5178473bce85.png\" alt=\"Czkawka GTK main window\" width=\"800\" />\n\n**Note**: GTK GUI is the older interface that is still maintained but will eventually be replaced by Krokiet. For new users, we recommend using Krokiet.\n\n### GUI overview\nThe GUI is built from different pieces:\n- 1 - Image preview - it is used in duplicate files and similar images finder. Cannot be resized, but can be disabled.\n- 2 - Main Notebook to change used tool.\n- 3 - Main results window - allows to choose, delete, configure results.\n- 4 - Bottom image panels - contains buttons which do specific actions on data(like selecting them) or e.g. hide/show parts of GUI\n- 5 - Text panel - prints messages/warnings/errors about executed actions. User can hide it.\n- 6 - Panel with selecting specific directories to use or exclude. Also, here are specified allowed extensions and file sizes.\n- 7 - Buttons which opens About Window(shows info about app) and Settings in which scan can be customized\n\n## Terminology (shared across CLI / GTK / Krokiet)\n\nThis short glossary contains terms used consistently by all frontends (CLI, GTK and Krokiet).\n\n- Reference paths\n  - After adding directories or files, you can mark them as \"Reference paths\" (a path can be a file or a folder) by checking the checkbox next to them in the directory selection UI or using the CLI flag where available.\n  - Reference paths are used for comparison but are protected: they cannot be modified, moved or deleted by the automatic actions in the UI or by the CLI.\n  - Use cases: compare a working folder against a master backup, protect original files when removing duplicates, or use a dataset as read-only baseline.\n\n- Included / Excluded paths\n  - Included paths are scanned by the tools. Excluded paths are explicitly ignored during scans.\n\n- Configuration vs Cache\n  - Configuration files are frontend-specific (each frontend stores its own UI/settings files). Configuration is not shared between GTK, Krokiet and CLI by default(CLI doesn't even store config files).\n  - Cache files are shared across frontends and contain computed data (hashes, thumbnails, analysis results). Cache is placed in a shared cache directory so all frontends can reuse computed results.\n\n### Translations\nGTK GUI is fully translatable.  \nFor now at least 10 languages are supported(some was translated by automatic translation, so may not be perfect). \n\n### Opening/Manipulating files\nIt is possible to open selected files by double-clicking on them.\n\nTo open multiple file just select desired files with CTRL key pressed and still when clicking this key, double-click at selected items with left mouse button.\n\nTo open folder containing selected file, just click twice on it with right mouse button.\n\nTo invert a selection of files, click on a file with the middle mouse button, and it will invert the selection of the other files in the same group.\n\n### Adding directories \n\nBy default, current path is loaded to included directory and excluded directories are filled with default paths.\n\nIt is possible to override this, by adding arguments when opening app e.g. `czkawka_gui /home /usr --/home/rafal --/home/zaba` which means that `/home` and `/usr` directories will be checked and `/home/rafal` and `/home/zaba` will be excluded.\n\nWhen using additional command line arguments, saving at exit option become disabled, so this current info about directories will not be saved until user save it manually.\n\nBoth relative and absolute path are supported, so user can use both `../home` and `/home`.\n\nAfter adding a path it is possible to mark one or more paths as a _Reference path_. Reference paths (files or folders) cannot be acted upon, e.g. selected, moved or removed. This behaviour can be useful if you want to leave a path untouched, but still use it for comparison against others.\n\n## CLI\nCzkawka CLI frontend is great to automate some tasks like removing empty directories.\n\nTo get general info how to use it just try to open czkawka_cli in console `czkawka_cli`\n\n<img src=\"https://user-images.githubusercontent.com/41945903/103018271-3d64ac80-4545-11eb-975c-2132f2ccf66f.png\" alt=\"CLI help example\" width=\"800\" />\n\nYou should see a lot of examples how to use this app.\n\nIf you want to get more detailed info about certain tool, just add after its name  `-h` or `--help` to get more details.\n\n<img src=\"https://user-images.githubusercontent.com/41945903/103018151-0a221d80-4545-11eb-97b2-d7d77b49c735.png\" alt=\"CLI example 2\" width=\"800\" />\n\nBy default, all tools only write about results to console, but it is possible with specific arguments to delete some files/arguments or save it to file.\n\nApp returns exit code 0 when everything is ok, 1 when some error occurred and 11 when some files were found.\n\n## Common Workflows\n\nThis section describes typical workflows for common tasks using Czkawka.\n\n### Finding and Removing Duplicates\n\n**Scenario**: You want to find and remove duplicate files from your Downloads folder, but keep files from your Documents folder as reference.\n\n**Steps (Krokiet)**:\n1. Open Krokiet and select the **Duplicate Files** tab from the left panel\n2. Click **Add Directory** in the Directory Selection Panel\n3. Add your Downloads folder to included directories\n4. Add your Documents folder to included directories\n5. Right-click on Documents folder and select **Mark as Reference Path** - files here won't be deleted\n6. In tool settings, set:\n   - Check method: **Hash** (most reliable)\n   - Hash type: **Blake3** (fastest for most cases)\n   - Minimal file size: **1 KB** (skip very small files)\n7. Click **Scan** button in the top bar\n8. Wait for the scan to complete\n9. Review results in the Results Area\n10. Use **Select** button to choose files to remove:\n    - **All** - selects all but one file in each group\n    - **Custom** - allows advanced selection rules\n11. Click **Delete** to remove selected duplicates\n12. Confirm the deletion when prompted\n\n**Steps (GTK)**:\n1. Open Czkawka GTK and select **Duplicate Files** tab\n2. In the directories panel (6), add Downloads folder to included directories\n3. Add Documents folder and mark it as Reference Path (right-click → Mark as Reference)\n4. In settings (button 7), configure:\n   - Check Method: **Hash**\n   - Hash Type: **Blake3**\n5. Click **Search** button\n6. After scan completes, use bottom panel buttons (4) to select duplicates\n7. Click **Delete** button and confirm\n\n### Finding Similar Images\n\n**Scenario**: You have multiple folders with photos and want to find similar images (different sizes, minor edits).\n\n**Steps (Krokiet)**:\n1. Select **Similar Images** tab\n2. Add all photo directories to included directories\n3. Set similarity threshold (lower = more strict):\n   - **0-5**: Nearly identical images only\n   - **6-15**: Similar images with minor differences\n   - **16-30**: Images with noticeable differences\n4. Choose hash algorithm:\n   - **Gradient**: Good for most photos (recommended)\n   - **Mean**: Fast, less accurate\n   - **Blockhash**: Good for resized images\n5. Select hash size:\n   - **8x8**: Fastest, less precise\n   - **16x16**: Balanced (recommended)\n   - **32x32/64x64**: Most precise, slower\n6. Click **Scan**\n7. Use image preview panel to compare similar images visually\n8. Select images to delete or move\n9. Use **Delete** or **Move** action\n\n### Finding Large Files\n\n**Scenario**: Disk space is running low, you want to find the largest files.\n\n**Steps**:\n1. Select **Big Files** tab\n2. Add directories to scan\n3. Set how many files to display (e.g., 50 largest files)\n4. Choose mode: **Largest** files\n5. Click **Scan**\n6. Review results sorted by size\n7. Manually review and delete or move large files you don't need\n\n\n### Working with Reference Paths\n\n**Scenario**: You want to compare your working files against a master/backup collection without risking deletion of the master files.\n\n**Use Cases**:\n- Comparing current photos against backed-up originals\n- Checking if work files are already in archive\n- Finding duplicates without touching the reference collection\n\n**How to Use**:\n1. Add both your working directory and reference directory to included directories\n2. Right-click on the reference directory (or use CLI flag) and select **Mark as Reference Path**\n3. Files or folders marked as reference paths will:\n   - Appear in scan results for comparison\n   - Be protected from deletion or modification by automatic actions\n   - Be shown with a different indicator in the results if the UI supports it\n4. Proceed with scan and operations — reference paths are protected\n\n## Config/Cache files\n- **Configuration files (frontend-specific)**\n  - Configuration files are kept per frontend and are not automatically shared. Examples:\n    - `czkawka_gui_config.txt` — GTK GUI configuration stored under the user config directory\n    - Krokiet stores its own settings file (created under the user config directory) — do not rely on configuration being synchronized between frontends\n\n- **Cache files (shared across frontends)**\n  - Cache contains computed results (hashes, thumbnails, parsed metadata) and is shared by all frontends to avoid re-computation.\n  - Example cache files:\n    - `cache_similar_image_SIZE_HASH_FILTER.bin/json` — image hashes and cache\n    - `cache_broken_files.txt`\n    - `cache_duplicates_HASH.txt` — duplicates cache (only fully hashed files bigger than configured threshold are stored)\n    - `cache_similar_videos.bin/json`\n\nIt is possible to modify files with JSON extension (may be helpful when moving files to different disk or trying to use cache file on different computer). To do this, it is required to enable in settings option to generate also cache json file. By default, cache files with `bin` extension are loaded, but if it is missing (can be renamed or removed), then data from json file is loaded if exists.\n\nConfig files are located in this path:\n\nLinux - `/home/username/.config/czkawka`  \nLinux Flatpak - `/home/username/.var/app/com.github.qarmin.czkawka/config/czkawka`  \nMac - `/Users/username/Library/Application Support/pl.Qarmin.Czkawka`  \nWindows - `C:\\Users\\Username\\AppData\\Roaming\\Qarmin\\Czkawka\\config`\n\nCache should be here:\n\nLinux - `/home/username/.cache/czkawka`  \nLinux Flatpak - `/home/username/.var/app/com.github.qarmin.czkawka/cache/czkawka`  \nMac - `/Users/Username/Library/Caches/pl.Qarmin.Czkawka`  \nWindows - `C:\\Users\\Username\\AppData\\Local\\Qarmin\\Czkawka\\cache`\n\nit is possible to change cache/config location by using `CZKAWKA_CONFIG_PATH` and `CZKAWKA_CACHE_PATH` env\ne.g.\n```\nCZKAWKA_CONFIG_PATH=\"/media/rafal/Ventoy/config\" CZKAWKA_CACHE_PATH=\"/media/rafal/Ventoy/cache\" krokiet\n```\nIt is possible to create portable version of app, by using running czkawka/krokiet with such script from pendrive:\n\n`open_czkawka.sh` - on pendrive(along with czkawka/krokiet binary)\n```shell\n#!/bin/bash\n\nCZKAWKA_CONFIG_PATH=\"$(dirname \"$(realpath \"$0\")\")/config\"\nCZKAWKA_CACHE_PATH=\"$(dirname \"$(realpath \"$0\")\")/cache\"\n\n./czkawka_gui\n```\n\n## Tips, Tricks and Known Bugs\n- **Speedup of CPU bounds tasks with LTO**\n  You can easily compile app with lto, by adding/modyfing in `Cargo.toml` file, this lines(small performance boost, big decrease in binary size):\n```\n[profile.release]\nlto = \"thin\" # or \"fat\"\n```\n- **Speedup of CPU-bound tasks using native CPU optimizations**\n  When doing CPU bound tasks, compiling with native CPU optimizations can give a significant speedup(speedup on x86_64_v4, when hashing images is usually 10-20%):\n```\nRUSTFLAGS=\"-C target-cpu=native\" cargo build --release\n```\nor adding it globally to `~/.cargo/config.toml`\n```\n[target.x86_64-unknown-linux-gnu]\nlinker = \"clang\"\nrustflags = [\n       \"-C\", \"target-cpu=native\",\n]\n\n```\n- **Manually adding multiple directories**  \n  You can manually edit config file `czkawka_gui_config.txt` and add/remove/change directories as you want.  \n  After set required values, configuration must be loaded to Czkawka.\n- **Slow checking due long loading/saving to cache step**  \n  If you checked before a large number of files (several tens of thousands), then the required information about all of them are loaded and saved to the cache, even if you are working with only few files.  \n  You can rename one of cache file which starts from `cache_similar_image`(to be able to use it again) or delete it - cache will then regenerate but with smaller number of entries and this way it should load and save cache faster.\n- **Not all columns with data(modification date, file size) are always visible in gui**\n  For now it is possible that some columns will not be visible when some are too wide.  \n  There are 2 workarounds for now\n    - View can be scrolled via horizontal scroll bar (1 on image)\n    - Size of other columns can be slimmed (2)\n  This is checked if is possible to do in https://github.com/qarmin/czkawka/issues/169\n![AA](https://user-images.githubusercontent.com/41945903/125684641-728e264a-34ab-41b1-9853-ab45dc25551f.png)\n- **Opening parent folders**\n    - It is possible to open parent folder of selected items with double click with right mouse button(RMB) it is also possible to open such item with double click with left mouse button(LMB).\n- **Faster scanning for big number of duplicates**  \n  By default for all files grouped by same size are computed partial hash(hash from only of start 4KB each file). \n- Such hash is computed usually very fast, especially on SSD and fast multicore processors.  \n  But when scanning a hundred of thousands or millions of files with HDD or slow processor, typically this step can take much time.  \n  In settings exists option `Use prehash cache` which enables caching such things.  \n  It is disabled by default because can increase time of loading/saving cache, with big number of entries.\n- **Permanent store of cache entries**  \n  After each scan, entries in cache are validated and outdated ones(which points at non-existent files) are removed.  \n  This may be problematic when scanning external drivers(like pendrives, disks etc.) and later unplugging and plugging them again.  \n  In settings exists option `Delete outdated cache entries automatically` which automatically clear this, but this can be disabled.  \n  Disabling such option may create big cache files, so button `Remove outdated results` will do it manually.\n- **Partial scanning**\n  If you know that you can't scan all files at once, you can still try to scan all files and during scan just stop it, so already calculated hashes/data will be saved to cache and will speedup later scans.\n\n# Tools\n\n### Duplicate Finder\n\nDuplicate Finder allows you to search for files and group them according to a predefined criterion:\n\n- **By name** - Compares and groups files by name e.g. `/home/john/cats.txt` will be treated like a duplicate of a file named\n  `/home/lucy/cats.txt`. This is the fastest method, but it is very unreliable and should not be used unless you know\n  what you are doing.\n\n- **By size** - Compares and groups files by their size (in bytes and perfect matches only). It is as fast as the previous mode and\n  usually gives better results with duplicates, but I also do not recommend using it if you do not know what you are doing.\n\n- **By size and name** - A mode that first compares files by size and then by name. Just like checking by size and name, this mode is not reliable.\n\n- **By hash** - A mode containing a check of the hash (cryptographic hash) of a given file which determines with great\n  probability whether the files are identical.\n\n  This is the slowest, but almost 100% sure way to compare the files for being the same.\n\n  Because the hash is only checked inside groups of files of the same size, it is practically impossible for two different\n  files to be considered identical.\n\n  It consists of 3 steps:\n    - Grouping files of identical size - allows you to throw away files of unique size, which are already known to have no\n      duplicates at this stage.\n\n    - PreHash check - Each group of files of identical size is placed in a queue using all processor threads (each action in\n      the group is independent of the others). In each such group a small fragment of each file (2KB) is loaded in turn and\n      then hashed. All files whose partial hashes are unique within the group are removed from it. Using this step usually\n      allows me to reduce the time of searching for duplicates almost by half.\n\n    - Checking the hash - After leaving files that have the same beginning in groups, you should now check the whole contents\n      of the file to make sure they are identical.\n\n### Empty Files\nThis tool finds files with zero bytes size.\n\n**Process**\n- Scans all files in specified directories\n- Checks file metadata for size\n- Files with size of 0 bytes are marked as empty\n\nThis is a fast operation since it only requires reading file metadata without accessing file contents.\n\n### Empty Directories\nThis tool finds directories that contain no files or subdirectories.\n\n**Process**\n- Creates an entry for each directory with its parent path and empty status flag\n- Initially marks all directories as potentially empty\n- Examines each directory:\n  - If it contains files or subdirectories → marks it as not empty\n  - Marks all parent directories (direct and indirect) as not empty\n- After processing, directories still marked as potentially empty are confirmed as empty\n\n**Example**\n\nConsider four directories: `/cow/`, `/cow/ear/`, `/cow/ear/stack/`, `/cow/ear/flag/`\n\nIf `/cow/ear/flag/` contains a file:\n- `/cow/ear/flag/` is marked as not empty\n- Parent directories `/cow/ear/` and `/cow/` are marked as not empty\n- `/cow/ear/stack/` may still be empty\n\n### Big Files\nThis tool finds the largest or smallest files in the specified directories.\n\n**Process**\n- Scans all files and reads their sizes\n- Sorts files by size\n- Returns a user-specified number of largest or smallest files\n\nUseful for finding large files that take up disk space or identifying unusually small files that may be incomplete downloads.\n\n### Temporary Files\nThis tool finds temporary files based on a predefined list of common temporary file extensions and names.\n\n**Detected patterns**\nFiles with the following extensions or names are considered temporary:\n```\n[\"#\", \"thumbs.db\", \".bak\", \"~\", \".tmp\", \".temp\", \".ds_store\", \".crdownload\", \".part\", \".cache\", \".dmp\", \".download\", \".partial\"]\n```\n\nThis list covers the most common temporary files created by operating systems and applications. For more comprehensive system cleanup, consider using specialized tools like BleachBit.\n\n### Invalid Symlinks\nThis tool finds broken symbolic links.\n\n**Process**\n- Identifies all symlinks in the specified directories\n- For each symlink, checks if its target exists\n- Detects two types of errors:\n  - Non-existent target - symlink points to a file or directory that does not exist\n  - Infinite recursion - symlink chain exceeds maximum jump count (20), indicating a circular reference\n\nBoth error types are reported in the results.\n### Same Music\nThis tool finds duplicate or similar music files by comparing metadata tags or audio content.\n\n**Process**\n- Collects music files with extensions: `mp3`, `flac`, `m4a`\n- Reads metadata tags: `artist`, `title`, `year`, `bitrate`, `genre`, `length`\n\n**Duplicate Tags Mode**\n- User selects which tag groups to compare (e.g., artist + title)\n- Tags are normalized:\n  - Removes non-alphanumeric characters\n  - Converts to lowercase\n  - Optionally removes text in parentheses for approximate comparison (e.g., `bataty (feat. romba)` → `bataty`)\n- Only files with non-empty tags are compared\n\n**Similar Content Mode**\n- Optionally groups files by simplified title first to reduce hash calculations\n- Generates audio hash for each file\n- Compares hashes using user-defined similarity threshold\n- Requires minimum matching fragment length\n\nResults show groups of files with matching tags or similar audio content.\n\n### Similar Images\n\nA tool for detecting similar images that may differ in aspects such as watermarks, size, or compression artifacts.\n\nCurrently, it works well for images that have not been rotated.\n\n#### **Process Overview**\n\n1. **Collecting Images**\n  - The tool gathers images with specific extensions, including RAWs, JPEGs, and many others.\n\n2. **Loading Cached Data**\n  - Previously computed hashes are loaded from a cache file to avoid re-hashing the same files\n  - Cache entries pointing to non-existent files are automatically removed by default(this can be disabled in settings)\n\n3. **Generating Perceptual Hashes**\n  - Image is resized to 8x8, 16x16, 32x32, or 64x64 pixels (inside `image_hasher` crate) \n  - A perceptual hash is computed for each image that is not already present in the cache\n  - Unlike cryptographic hashes, which produce completely different outputs for slight variations:\n\n    ```\n    11110 ==>  AAAAAB  \n    11111 ==>  FWNTLW  \n    01110 ==>  TWMQLA  \n    ```  \n\n    Perceptual hashes generate similar outputs for similar images:\n\n    ```\n    11110 ==>  AAAAAB  \n    11111 ==>  AABABB  \n    01110 ==>  AAAACB  \n    ```  \n\n4. **Storing and Comparing Hashes**\n  - Computed hash data is stored in a specialized tree structure, allowing efficient comparison using [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance).\n  - The hashes are then saved to a file, ensuring images don’t need to be rehashed in future runs.\n  - Each hash is compared with others, and if the distance between them is below the user-defined threshold, the images are considered similar and removed from the pool of images to be checked.\n\n#### **Hashing and Resizing Options**\n\n- Users can select from **five different hash types**:\n  - `Gradient`\n  - `Mean`\n  - `VertGradient`\n  - `Blockhash`\n  - `DoubleGradient`\n\n- Before hashing, images are typically resized to simplify computations. Supported resizing algorithms:\n  - `Lanczos3`\n  - `Gaussian`\n  - `CatmullRom`\n  - `Triangle`\n  - `Nearest`\n\n- Supported hash sizes: `8x8`, `16x16`, `32x32`, `64x64`\n- Both the resizing method and hash size can be adjusted within the application.\n\nEach configuration generates separate cache files to prevent invalid results across different settings.\n\n#### **Additional Features and Considerations**\n\n- Some images may break hash functions, producing hashes filled entirely with `0` or `255`. These images are silently excluded from the final results but remain stored in the cache.\n- A **CLI testing tool** is available. To test an algorithm, place a `test.jpg` file in a folder and run `czkawka_cli tester -i`\n\n#### **Faster Comparison Mode**\nThe faster comparison option ensures that each pair of results is compared only once, significantly improving performance, especially when using a high similarity threshold.\n\n#### **Tidbits**\n- Smaller hash size does not always mean faster calculation.\n- `Blockhash` is the only algorithm that does not resize images before hashing.\n- The `Nearest` resizing algorithm can be up to **five times faster** than other methods but may produce worse results.\n\n### Similar Videos\n\nThis tool finds similar videos using perceptual hashing, similar to the Similar Images feature.\n\n**Requirements**\n- Requires **FFmpeg** installed on the system\n- Currently only compares videos with similar lengths\n\n**Process**\n- Collects video files based on their extensions (mp4, mkv, avi, mov, webm, etc.)\n- For each video:\n  - Extracts several frames\n  - Generates perceptual hashes for each frame\n- Stores hashes in cache file to avoid recalculating in future scans\n- Compares hashes using user-defined similarity tolerance\n- Groups similar videos in results\n\n### Broken Files\nThis tool detects corrupted or invalid files that cannot be properly opened.\n\n**Supported file types**\n- Images - jpg, jpeg, png, tiff, tif, tga, gif, bmp, ico, jfif, webp, exr, avif, and others\n- Audio - mp3, flac, wav, ogg, m4a, aac, and others\n- Video - mp4, mkv, avi, mov, webm, and others\n- Archives - zip, jar\n- Documents - pdf\n\n**Process**\n- Files are collected based on their extensions\n- Each file is validated by attempting to open it with appropriate libraries\n- If an error occurs during opening, the file is marked as corrupted (with some exceptions to avoid false positives)\n\n**Note**: Since this tool relies on external libraries, false positives may occur (e.g., [this issue](https://github.com/image-rs/jpeg-decoder/issues/130)). It is recommended to manually verify files before deletion.\n\n### Bad Extensions\nThis mode allows finding files whose content does not match their extension.\n\nIt works as follows:\n- Extracts the current file extension, e.g., `źrebię.zip` → `zip`\n- Reads a few bytes from the file\n- Matches these bytes with known signatures to determine the likely file type, e.g., `7z`\n- Retrieves the MIME type (which may return multiple values) based on the detected extension, e.g., `Mime::Archive`\n- Lists all file extensions associated with this MIME type, e.g., `rar, 7z, zip, p7`\n- Expands the list with additional extensions when needed (some files, like `exe` and `dll`, may have similar byte signatures)\n- If the file's current extension is in the list, it is likely correct; otherwise, it is flagged as having an invalid extension\n\nIn the **\"Proper Extension\"** column, the extension detected by the Infer library appears in parentheses, while extensions with the same MIME type are displayed outside.\n\n![ABC](https://user-images.githubusercontent.com/41945903/167214811-7d811829-6dba-4da0-9788-9e2f780e7279.png)\n\n### Bad Names\nThis tool finds files with problematic names that may cause issues on different operating systems or filesystems.\n\nIt can detect multiple naming problems:\n- Uppercase extensions - e.g., `file.JPG` instead of `file.jpg`\n- Emoji in filenames - e.g., `document😀.txt`\n- Spaces at the start or end of filename - e.g., ` file.txt` or `file.txt `\n- Non-ASCII characters - e.g., `файл.txt`, `文档.doc`\n- Characters outside restricted charset - only specific characters are allowed (e.g., only `_`, `-`, ` `, `.`)\n- Duplicated non-alphanumeric characters - e.g., `file___name.txt`, `doc---final.pdf`\n\nEach check can be enabled or disabled independently. The tool suggests corrected filenames for all problematic files found.\n\n### EXIF Remover\nThis tool finds image files containing EXIF metadata and allows selective removal of tags.\n\n**Process**\n- Scans image files with the following extensions: `jpg`, `jpeg`, `jfif`, `png`, `tiff`, `tif`, `avif`, `jxl`, `webp`, `heic`, `heif`\n- Reads EXIF metadata from each file\n- Lists all EXIF tags with their names, codes, and groups\n- User can specify tags to ignore (e.g., `Orientation`, `ColorSpace`)\n- Only files with non-ignored tags are shown in results\n\nThis is useful for finding images with privacy-sensitive metadata like GPS coordinates, camera serial numbers, or editing software information.\n\n### Video Optimizer\nThis tool helps optimize video files by detecting optimization opportunities. It operates in two modes:\n\n#### Transcode Mode\nIdentifies videos that could be re-encoded to a more efficient codec.\n\n- Scans video files (e.g., `.mp4`, `.avi`, `.mkv`)\n- Checks current video codec\n- Lists videos not using excluded codecs (user-specified)\n- Common use: find videos using older codecs (H264) that could be converted to newer ones (H265, AV1) for better compression\n\n#### Crop Mode\nDetects videos with black bars or static content that can be cropped.\n\n- Scans video files\n- Analyzes multiple frames to detect black bars or static content\n- Supports two detection mechanisms:\n  - Black bars detection - finds letterbox/pillarbox black borders\n  - Static content detection - finds unchanging areas at edges\n- Calculates optimal crop rectangle for each video\n- Shows crop dimensions of video that can be removed\n\n**Additional features**\n- Can generate thumbnails for preview (single frame or grid)\n- Thumbnail position configurable (percentage from video start)\n- Supports minimum crop size threshold to avoid cropping too small areas\n\n"
  },
  {
    "path": "instructions/Translations.md",
    "content": "# Translations\n\nCzkawka/Krokiet is available in around 20 languages. The main (default) language is English, and Polish is also officially supported.\n\nThe application uses simple Fluent localization system: https://projectfluent.org/\n\nMost translations are managed on Crowdin: https://crowdin.com/project/czkawka\n\nIf you want to translate Czkawka into your language, you can do so on that platform.\n\nWith each new release, many strings are added or modified. Because most of languages lack dedicated translators, parts of the translations are generated using Crowdin’s machine translation system or local LLM models. These translations may not be perfect and still require human review, but they significantly reduce the amount of work needed from human translators."
  },
  {
    "path": "justfile",
    "content": "set windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\nset export := true\n\n# Android related commands require these env variables to be set in your shell:\n# export ANDROID_HOME=~/android-sdk\n# export ANDROID_NDK_HOME=~/android-sdk/ndk/26.x.x\n\nadb := \"adb\"\napk_package := \"io.github.qarmin.cedinia\"\napk_activity := \"android.app.NativeActivity\"\n\nbuild_all: && fix\n    cargo build --release\n    cargo build\n    cargo clippy\n    cargo test\n\nitests:\n    [ ! -f TestFiles.zip ] && wget https://github.com/qarmin/czkawka/releases/download/6.0.0/TestFiles.zip || true\n    cd ci_tester && cargo build --release && cd ..\n    cargo build --release --bin czkawka_cli\n\n    RUST_BACKTRACE=1 ci_tester/target/release/ci_tester target/release/czkawka_cli\n\n## run\n\nrun +args:\n    cargo run --bin {{args}}\n\nrunr +args:\n    cargo run --profile fast_release --bin {{args}}\n\nrunc +args:\n    CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift cargo +nightly run -Zcodegen-backend --bin {{args}}\n\nruns +args:\n    export RUST_BACKTRACE=1 # or full depending on project\n    export ASAN_SYMBOLIZER_PATH=$(which llvm-symbolizer) # Version depends on your system\n    export ASAN_OPTIONS=\"symbolize=1:detect_leaks=0\" # Leak check is disabled, because is slow and ususally not needed\n    ASAN_OPTIONS=\"symbolize=1:detect_leaks=0\" RUSTFLAGS=\"-Zsanitizer=address\" cargo +nightly run --target x86_64-unknown-linux-gnu --bin {{args}}\n\nval bin:\n    valgrind --leak-check=full --show-leak-kinds=definite --track-origins=yes target/debug/{{bin}}\n\nvalr bin:\n    valgrind --leak-check=full --show-leak-kinds=definite --track-origins=yes target/release/{{bin}}\n\n\n# echo '-1' | sudo tee /proc/sys/kernel/perf_event_paranoid\n# // Or permanently\n# echo \"kernel.perf_event_paranoid = -1\" | sudo tee /etc/sysctl.d/99-perf.conf\n# sudo sysctl --system\n\nsamply bin *args:\n    cargo build --bin {{bin}}\n    samply record target/debug/{{bin}} {{args}}\n\nsamplyrd bin *args:\n    cargo build --bin {{bin}} --profile rdebug\n    samply record target/rdebug/{{bin}} {{args}}\n\n## Other\n\nsetup_sanitizer:\n    rustup install nightly\n    rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu\n    rustup component add llvm-tools-preview --toolchain nightly-x86_64-unknown-linux-gnu\n\n\nbench:\n    cd czkawka_core && cargo bench\n    xdg-open target/criterion/report/index.html\n\nbench_clean:\n    rm -rf target/criterion\n\nupgrade:\n    cargo +nightly -Z unstable-options update --breaking\n    cargo update\n\nclip:\n    cargo clippy --fix --allow-dirty --allow-staged --all-features --all-targets\n    cargo clippy --fix --allow-dirty --allow-staged --no-default-features --features winit_software --all-targets\n\nfix:\n    ruff format --line-length 120 --no-cache\n    mypy misc --strict\n\n    bash misc/run_checks.sh\n\n    cargo +nightly fmt\n    cargo clippy --fix --allow-dirty --allow-staged --all-features --all-targets\n    cargo +nightly fmt\n    cargo fmt\n\nfixn:\n    cargo +nightly fmt\n    cargo +nightly clippy --fix --allow-dirty --allow-staged --all-features --all-targets\n    cargo +nightly fmt\n\ntest_resize arg:\n    cd misc/test_image_perf; cargo build --release; sudo ./target/release/test_image_perf \"{{arg}}\"\n\n# Not works, due of edition 2024 and workspaces\nunused_features:\n    unused-features analyze\n    unused-features build-report --input krokiet/report.json\n    unused-features build-report --input czkawka_cli/report.json\n    unused-features build-report --input czkawka_core/report.json\n    unused-features build-report --input czkawka_gui/report.json\n    xdg-open krokiet/report.html\n    xdg-open czkawka_cli/report.html\n    xdg-open czkawka_core/report.html\n    xdg-open czkawka_gui/report.html\n\n##################### ANDROID #####################\n\nkeystore_dir := \"cedinia/android/keystore\"\n\ngen_keystores:\n    mkdir -p {{keystore_dir}}\n    [ -f {{keystore_dir}}/debug.keystore ] || keytool -genkey -v \\\n        -keystore {{keystore_dir}}/debug.keystore \\\n        -alias debug -keyalg RSA -keysize 2048 -validity 10000 \\\n        -storepass 123456 -keypass 123456 \\\n        -dname \"CN=Debug, OU=Debug, O=Debug, L=Debug, S=Debug, C=US\" \\\n        -noprompt\n    [ -f {{keystore_dir}}/release.keystore ] || keytool -genkey -v \\\n        -keystore {{keystore_dir}}/release.keystore \\\n        -alias release -keyalg RSA -keysize 2048 -validity 10000 \\\n        -storepass 123456 -keypass 123456 \\\n        -dname \"CN=Release, OU=Release, O=Release, L=Release, S=Release, C=US\" \\\n        -noprompt\n\nandroid_build: gen_keystores\n    cargo apk build -p cedinia --lib\n\nandroid_build_release: gen_keystores\n    cargo apk build -p cedinia --lib --release\n\nandroid_install:\n    {{adb}} install -r target/debug/apk/cedinia.apk\n\nandroid_install_release:\n    {{adb}} install -r target/release/apk/cedinia.apk\n\nandroid_run:\n    {{adb}} shell am start -n {{apk_package}}/{{apk_activity}}\n\nandroid_log:\n    {{adb}} logcat -s RustStdoutStderr:V *:S\n\nandroid_logc:\n    {{adb}} logcat | grep edinia\n\nandroid_devices:\n    {{adb}} devices -l\n\nandroid: android_build android_install android_run\n\nandroidr: android_build_release android_install_release android_run\n\n##################### BENCHMARKS #####################\n\nprepare_binaries:\n    mkdir -p benchmarks\n    wget https://github.com/qarmin/czkawka/releases/download/Nightly/linux_czkawka_cli -O benchmarks/czkawka_cli_normal\n    cd czkawka_cli; cargo build --release; cd ..; cp target/release/czkawka_cli benchmarks/czkawka_cli_v4\n    cd czkawka_cli; cargo build --profile fastest; cd ..; cp target/fastest/czkawka_cli benchmarks/czkawka_cli_fastest\n\nbenchmark media:\n    # benchmarks/czkawka_cli_old dup -d /media/rafal/Kotyk\n    # benchmarks/czkawka_cli_fastest dup -d /media/rafal/Kotyk -W -N -M -H\n    sudo echo \"AA\" # Ability to run as root is needed later by hyperfine\n    #hyperfine --prepare \"sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'; rm cache_duplicates_Blake3_70.bin || true\" 'benchmarks/czkawka_cli_fastest dup -d \"{{ media }}\" -W -N -M -H' 'benchmarks/czkawka_cli_v4 dup -d \"{{ media }}\" -W -N -M -H' 'benchmarks/czkawka_cli_normal dup -d \"{{ media }}\" -W -N -M -H' 'benchmarks/czkawka_cli_old image -d \"{{ media }}\" > /dev/null'\n    hyperfine --prepare \"sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'; rm /home/rafal/.cache/czkawka/cache_similar_images_16_Gradient_Nearest_80.bin || true\" 'benchmarks/czkawka_cli_fastest image -d \"{{ media }}\" -W -N -M -H' 'benchmarks/czkawka_cli_v4 image -d \"{{ media }}\" -W -N -M -H' 'benchmarks/czkawka_cli_normal image -d \"{{ media }}\" -W -N -M -H' 'benchmarks/czkawka_cli_old image -d \"{{ media }}\" > /dev/null'\n\n\ncheck_compilations:\n    git checkout Cargo.toml\n    #cargo install --path misc/test_compilation_speed_size\n    test_compilation_speed_size misc/test_compilation_speed_size/krokiet.json\n    python3 misc/test_compilation_speed_size/generate_md_and_plots.py\n\ntags:\n    tags=($(git tag --sort=version:refname | grep -v Nightly)); for ((i=0; i<${#tags[@]}-1; i++)); do from=${tags[$i]}; to=${tags[$i+1]}; echo \"$from -> $to : $(git diff --shortstat \"$from\" \"$to\")\"; done; last=${tags[-1]}; echo \"$last -> master : $(git diff --shortstat \"$last\" master)\"\n\ninstall:\n    cargo install --path czkawka_cli --locked\n    cargo install --path krokiet --locked\n    cargo install --path czkawka_gui --locked\n\n\nprepare_translations_deps:\n    @command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh\n    @command -v ollama >/dev/null 2>&1 || curl -fsSL https://ollama.com/install.sh | sh\n    cd misc/ai_translate; uv sync\n    # qwen2.5:7b - fast, but quite bad quality\n    # qwen2.5:32b - very slow, still not good quality\n    # zongwei/gemma3-translator:4b - not so fast, but looks quite good\n    # translategemma:12b - probably very slow - using only for small tasks\n    export OLLAMA_VULKAN=1; export HSA_OVERRIDE_GFX_VERSION=10.3.0; ollama pull translategemma:12b\n\ntranslate:\n    cd misc/ai_translate; uv run translate.py ../../czkawka_gui/i18n\n    cd misc/ai_translate; uv run translate.py ../../czkawka_core/i18n\n    cd misc/ai_translate; uv run translate.py ../../krokiet/i18n\n    just pack_translations\n\nvalidate_translations *args: # Available --fix argument, which removes invalid translations\n    cd misc/ai_translate; uv run validate_translations.py ../../czkawka_gui/i18n {{args}}\n    cd misc/ai_translate; uv run validate_translations.py ../../czkawka_core/i18n {{args}}\n    cd misc/ai_translate; uv run validate_translations.py ../../krokiet/i18n {{args}}\n\n# Crowdin allows to import zip file with structured translations\npack_translations:\n    rm -f i18n_translations.zip\n    mkdir -p /tmp/czkawka_i18n\n    for lang in czkawka_gui/i18n/*/; do \\\n        lang_code=$(basename \"$lang\"); \\\n        [ \"$lang_code\" = \"en\" ] && continue; \\\n        mkdir -p \"/tmp/czkawka_i18n/i18n/$lang_code\"; \\\n        [ -f \"czkawka_gui/i18n/$lang_code/czkawka_gui.ftl\" ] && cp \"czkawka_gui/i18n/$lang_code/czkawka_gui.ftl\" \"/tmp/czkawka_i18n/i18n/$lang_code/\" || true; \\\n        [ -f \"czkawka_core/i18n/$lang_code/czkawka_core.ftl\" ] && cp \"czkawka_core/i18n/$lang_code/czkawka_core.ftl\" \"/tmp/czkawka_i18n/i18n/$lang_code/\" || true; \\\n        [ -f \"krokiet/i18n/$lang_code/krokiet.ftl\" ] && cp \"krokiet/i18n/$lang_code/krokiet.ftl\" \"/tmp/czkawka_i18n/i18n/$lang_code/\" || true; \\\n    done\n    cd /tmp/czkawka_i18n && zip -r - i18n > \"{{justfile_directory()}}/i18n_translations.zip\"\n    rm -rf /tmp/czkawka_i18n\n    @echo \"Created i18n_translations.zip with all translations (excluding English)\"\n\nunpack_translations path_to_file:\n    @echo \"Unpacking translations from {{path_to_file}}...\"\n    mkdir -p /tmp/czkawka_unpack\n    unzip -q \"{{path_to_file}}\" -d /tmp/czkawka_unpack\n    for lang_dir in /tmp/czkawka_unpack/*/; do \\\n        lang_code=$(basename \"$lang_dir\"); \\\n        [ -f \"$lang_dir/czkawka_gui.ftl\" ] && mkdir -p \"czkawka_gui/i18n/$lang_code\" && cp \"$lang_dir/czkawka_gui.ftl\" \"czkawka_gui/i18n/$lang_code/\" && echo \"Copied czkawka_gui.ftl to czkawka_gui/i18n/$lang_code/\" || true; \\\n        [ -f \"$lang_dir/czkawka_core.ftl\" ] && mkdir -p \"czkawka_core/i18n/$lang_code\" && cp \"$lang_dir/czkawka_core.ftl\" \"czkawka_core/i18n/$lang_code/\" && echo \"Copied czkawka_core.ftl to czkawka_core/i18n/$lang_code/\" || true; \\\n        [ -f \"$lang_dir/krokiet.ftl\" ] && mkdir -p \"krokiet/i18n/$lang_code\" && cp \"$lang_dir/krokiet.ftl\" \"krokiet/i18n/$lang_code/\" && echo \"Copied krokiet.ftl to krokiet/i18n/$lang_code/\" || true; \\\n    done\n    rm -rf /tmp/czkawka_unpack\n    @echo \"Translations unpacked successfully\"\n\ncache:\n    xdg-open ~/.cache/czkawka\n\nconfigc:\n    xdg-open ~/.config/czkawka\n\nconfigk:\n    xdg-open ~/.config/krokiet\n\n\n##################### DEBUG SIZE, PERFORMANCE AND OTHERS #####################\nsetup_verify_tools:\n    rustup component add llvm-tools-preview\n    cargo install cargo-llvm-lines cargo-bloat cargo-deps flamegraph measureme\n    cargo install --git https://github.com/rust-lang/measureme crox flamegraph summarize\n\n# Prints lines of certain functions in binary\nllvm_lines:\n    cargo llvm-lines -p krokiet --bin krokiet | head -40\n    cargo llvm-lines -p czkawka_gui --bin czkawka_gui | head -40\n    cargo llvm-lines -p czkawka_cli --bin czkawka_cli | head -40\n\n# Prints size of functions in binary\nbloat_by_function:\n    cargo bloat --release --bin czkawka_cli -n 30\n    cargo bloat --release --bin czkawka_gui -n 30\n    cargo bloat --release --bin krokiet -n 30\n\n# Prints size of crates in binary\nbloat_by_crate:\n    cargo bloat --release --crates --bin czkawka_cli\n    cargo bloat --release --crates --bin czkawka_gui\n    cargo bloat --release --crates --bin krokiet\n\n# Draws dependency graphs of certain binaries(like regex, image, etc)\ndependencies_graph:\n    cd czkawka_core;cargo deps --all-deps | dot -Tpng > deps.png;cd ..\n    cd czkawka_cli;cargo deps --all-deps | dot -Tpng > deps.png;cd ..\n    cd czkawka_gui;cargo deps --all-deps | dot -Tpng > deps.png;cd ..\n    cd krokiet;cargo deps --all-deps | dot -Tpng > deps.png;cd ..\n\n# Shows llvm compilation data summary\nprofiling profile='debug' mode='build':\n    if [ \"{{profile}}\" = \"release\" ]; then release_flag=\"--release\"; else release_flag=\"\"; fi; \\\n    cargo clean; \\\n    for crate in czkawka_core czkawka_gui czkawka_cli krokiet; do \\\n        cd \"$crate\"; \\\n        rm ../*.mm_profdata || true; \\\n        rm *.mm_profdata || true; \\\n        RUSTFLAGS=\"-Zself-profile\" cargo +nightly rustc $release_flag; \\\n        summarize summarize ../*.mm_profdata || true; \\\n        cd ..; \\\n    done\n\n# Timings of crates compilation\ntimings profile='debug' mode='build':\n    if [ \"{{profile}}\" = \"release\" ]; then release_flag=\"--release\"; else release_flag=\"\"; fi; \\\n    cargo clean; \\\n    for crate in czkawka_core czkawka_gui czkawka_cli krokiet; do \\\n        cd \"$crate\"; \\\n        rm ../target/cargo-timings/*.html || true; \\\n        cargo \"{{mode}}\" $release_flag --timings; \\\n        cp \"$(find ../target/cargo-timings -maxdepth 1 -name '*.html' -print -quit)\" \"../$crate.html\" || true; \\\n        cd ..; \\\n    done\n    #cargo clean\n#    cd czkawka_core; RUSTFLAGS=\"-Ztime\" cargo +nightly rustc; cd ..;\n#    cargo clean\n#    cd czkawka_gui; RUSTFLAGS=\"-Ztime\" cargo +nightly rustc; cd ..;\n#    cargo clean\n#    cd czkawka_cli; RUSTFLAGS=\"-Ztime\" cargo +nightly rustc; cd ..;\n#    cargo clean\n#    cd krokiet; RUSTFLAGS=\"-Ztime\" cargo +nightly rustc; cd ..;\n\n# Per crate compilation times and ram usage\n# This is very verbose, so probably not really useful\ntime_passes:\n    cargo clean\n    cd czkawka_core; RUSTFLAGS=\"-Ztime-passes\" cargo +nightly rustc; cd ..;\n    cargo clean\n    cd czkawka_gui; RUSTFLAGS=\"-Ztime-passes\" cargo +nightly rustc; cd ..;\n    cargo clean\n    cd czkawka_cli; RUSTFLAGS=\"-Ztime-passes\" cargo +nightly rustc; cd ..;\n    cargo clean\n    cd krokiet; RUSTFLAGS=\"-Ztime-passes\" cargo +nightly rustc; cd ..;\n"
  },
  {
    "path": "krokiet/Cargo.toml",
    "content": "[package]\nname = \"krokiet\"\nversion = \"11.0.1\"\nauthors = [\"Rafał Mikrut <mikrutrafal@protonmail.com>\"]\nedition = \"2024\"\nrust-version = \"1.92.0\"\ndescription = \"Slint frontend of Czkawka Core\"\nlicense = \"GPL-3.0-only\"\nhomepage = \"https://github.com/qarmin/czkawka\"\nrepository = \"https://github.com/qarmin/czkawka\"\nbuild = \"build.rs\"\n\n[dependencies]\nczkawka_core = { version = \"11.0.1\", path = \"../czkawka_core\" }\nchrono = \"0.4.38\"\nopen = \"5.3\"\ncrossbeam-channel = \"0.5\"\nrfd = { version = \"0.17\", default-features = false, features = [\"xdg-portal\"] }\nhome = \"0.5\"\nlog = \"0.4.22\"\nserde = \"1.0\"\nserde_json = \"1.0\"\nhumansize = \"2.1\"\nimage = \"0.25\"\nrayon = \"1.10\"\nfs_extra = \"1.3\" # TODO replace with less buggy library\nfast_image_resize = { version = \"6.0.0\", features = [\"image\"] }\nnum_enum = \"0.7.5\"\nregex = \"1.11\"\n\n# Translations\ni18n-embed = { version = \"0.16\", features = [\"fluent-system\", \"desktop-requester\"] }\ni18n-embed-fl = \"0.10\"\nrust-embed = { version = \"8.5\", features = [\"debug-embed\"] }\n\n\n# Easier cross-compilation of app to e.g. 32 bit os, by using dlopen instead of linking libraries at compile time\nfontique = { version = \"0.7.0\", default-features = false, features = [\"fontconfig-dlopen\"] }\n\n# Try to use only needed features from https://github.com/slint-ui/slint/blob/master/api/rs/slint/Cargo.toml#L23-L31\n#slint = { path = \"/home/rafal/test/slint/api/rs/slint/\", default-features = false, features = [\"std\",\n#slint = { git = \"https://github.com/slint-ui/slint.git\", default-features = false, features = [\nslint = { version = \"1.15.0\", default-features = false, features = [\n    \"std\",\n    \"backend-winit\",\n    \"compat-1-2\"\n] }\n\nrodio = { version = \"0.22.0\", default-features = false, features = [\"playback\", \"symphonia-all\"], optional = true }\n\n[build-dependencies]\n#slint-build = { path = \"/home/rafal/test/slint/api/rs/build/\"}\n#slint-build = { git = \"https://github.com/slint-ui/slint.git\" }\nslint-build = \"1.15\"\n\n[features]\ndefault = [\"winit_femtovg\", \"winit_software\"]\n\n# Audio support\naudio = [\"rodio\"]\n\n# Renderers\nskia_opengl = [\"slint/renderer-skia-opengl\"]\nskia_vulkan = [\"slint/renderer-skia-vulkan\"]\nsoftware = [\"slint/renderer-software\"]\nfemtovg = [\"slint/renderer-femtovg\"]\nfemtovg_wgpu = [\"slint/renderer-femtovg-wgpu\"]\nwinit_femtovg = [\"slint/renderer-winit-femtovg\"]\nwinit_skia_opengl = [\"slint/renderer-winit-skia-opengl\"]\nwinit_skia_vulkan = [\"slint/renderer-winit-skia-vulkan\"]\nwinit_software = [\"slint/renderer-winit-software\"]\n\n# Additional features\nheif = [\"czkawka_core/heif\"]\nlibraw = [\"czkawka_core/libraw\"]\nlibavif = [\"czkawka_core/libavif\"]\n\n# Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails\n# No-op on other OSes, it is slower and provides less helpful error messages\nxdg_portal_trash = [\"czkawka_core/xdg_portal_trash\"]\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "krokiet/LICENSE_CC_BY_4_AUDIO_FILES",
    "content": "All audio files, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).\n\nCopyright (c) 2020-2026 Rafał Mikrut\n- audio/*\n\nLicense: CC-BY-4.0\n Creative Commons Attribution 4.0 International Public License\n .\n By exercising the Licensed Rights (defined below), You accept and agree\n to be bound by the terms and conditions of this Creative Commons\n Attribution 4.0 International Public License (\"Public\n License\"). To the extent this Public License may be interpreted as a\n contract, You are granted the Licensed Rights in consideration of Your\n acceptance of these terms and conditions, and the Licensor grants You\n such rights in consideration of benefits the Licensor receives from\n making the Licensed Material available under these terms and\n conditions.\n .\n Section 1 -- Definitions.\n .\n a. Adapted Material means material subject to Copyright and Similar\n Rights that is derived from or based upon the Licensed Material\n and in which the Licensed Material is translated, altered,\n arranged, transformed, or otherwise modified in a manner requiring\n permission under the Copyright and Similar Rights held by the\n Licensor. For purposes of this Public License, where the Licensed\n Material is a musical work, performance, or sound recording,\n Adapted Material is always produced where the Licensed Material is\n synched in timed relation with a moving image.\n .\n b. Adapter's License means the license You apply to Your Copyright\n and Similar Rights in Your contributions to Adapted Material in\n accordance with the terms and conditions of this Public License.\n .\n c. Copyright and Similar Rights means copyright and/or similar rights\n closely related to copyright including, without limitation,\n performance, broadcast, sound recording, and Sui Generis Database\n Rights, without regard to how the rights are labeled or\n categorized. For purposes of this Public License, the rights\n specified in Section 2(b)(1)-(2) are not Copyright and Similar\n Rights.\n .\n d. Effective Technological Measures means those measures that, in the\n absence of proper authority, may not be circumvented under laws\n fulfilling obligations under Article 11 of the WIPO Copyright\n Treaty adopted on December 20, 1996, and/or similar international\n agreements.\n .\n e. Exceptions and Limitations means fair use, fair dealing, and/or\n any other exception or limitation to Copyright and Similar Rights\n that applies to Your use of the Licensed Material.\n .\n f. Licensed Material means the artistic or literary work, database,\n or other material to which the Licensor applied this Public\n License.\n .\n g. Licensed Rights means the rights granted to You subject to the\n terms and conditions of this Public License, which are limited to\n all Copyright and Similar Rights that apply to Your use of the\n Licensed Material and that the Licensor has authority to license.\n .\n h. Licensor means the individual(s) or entity(ies) granting rights\n under this Public License.\n .\n i. Share means to provide material to the public by any means or\n process that requires permission under the Licensed Rights, such\n as reproduction, public display, public performance, distribution,\n dissemination, communication, or importation, and to make material\n available to the public including in ways that members of the\n public may access the material from a place and at a time\n individually chosen by them.\n .\n j. Sui Generis Database Rights means rights other than copyright\n resulting from Directive 96/9/EC of the European Parliament and of\n the Council of 11 March 1996 on the legal protection of databases,\n as amended and/or succeeded, as well as other essentially\n equivalent rights anywhere in the world.\n .\n k. You means the individual or entity exercising the Licensed Rights\n under this Public License. Your has a corresponding meaning.\n .\n Section 2 -- Scope.\n .\n a. License grant.\n .\n 1. Subject to the terms and conditions of this Public License,\n the Licensor hereby grants You a worldwide, royalty-free,\n non-sublicensable, non-exclusive, irrevocable license to\n exercise the Licensed Rights in the Licensed Material to:\n .\n a. reproduce and Share the Licensed Material, in whole or\n in part; and\n .\n b. produce, reproduce, and Share Adapted Material.\n .\n 2. Exceptions and Limitations. For the avoidance of doubt, where\n Exceptions and Limitations apply to Your use, this Public\n License does not apply, and You do not need to comply with\n its terms and conditions.\n .\n 3. Term. The term of this Public License is specified in Section\n 6(a).\n .\n 4. Media and formats; technical modifications allowed. The\n Licensor authorizes You to exercise the Licensed Rights in\n all media and formats whether now known or hereafter created,\n and to make technical modifications necessary to do so. The\n Licensor waives and/or agrees not to assert any right or\n authority to forbid You from making technical modifications\n necessary to exercise the Licensed Rights, including\n technical modifications necessary to circumvent Effective\n Technological Measures. For purposes of this Public License,\n simply making modifications authorized by this Section 2(a)\n (4) never produces Adapted Material.\n .\n 5. Downstream recipients.\n .\n a. Offer from the Licensor -- Licensed Material. Every\n recipient of the Licensed Material automatically\n receives an offer from the Licensor to exercise the\n Licensed Rights under the terms and conditions of this\n Public License.\n .\n b. No downstream restrictions. You may not offer or impose\n any additional or different terms or conditions on, or\n apply any Effective Technological Measures to, the\n Licensed Material if doing so restricts exercise of the\n Licensed Rights by any recipient of the Licensed\n Material.\n .\n 6. No endorsement. Nothing in this Public License constitutes or\n may be construed as permission to assert or imply that You\n are, or that Your use of the Licensed Material is, connected\n with, or sponsored, endorsed, or granted official status by,\n the Licensor or others designated to receive attribution as\n provided in Section 3(a)(1)(A)(i).\n .\n b. Other rights.\n .\n 1. Moral rights, such as the right of integrity, are not\n licensed under this Public License, nor are publicity,\n privacy, and/or other similar personality rights; however, to\n the extent possible, the Licensor waives and/or agrees not to\n assert any such rights held by the Licensor to the limited\n extent necessary to allow You to exercise the Licensed\n Rights, but not otherwise.\n .\n 2. Patent and trademark rights are not licensed under this\n Public License.\n .\n 3. To the extent possible, the Licensor waives any right to\n collect royalties from You for the exercise of the Licensed\n Rights, whether directly or through a collecting society\n under any voluntary or waivable statutory or compulsory\n licensing scheme. In all other cases the Licensor expressly\n reserves any right to collect such royalties.\n .\n Section 3 -- License Conditions.\n .\n Your exercise of the Licensed Rights is expressly made subject to the\n following conditions.\n .\n a. Attribution.\n .\n 1. If You Share the Licensed Material (including in modified\n form), You must:\n .\n a. retain the following if it is supplied by the Licensor\n with the Licensed Material:\n .\n i. identification of the creator(s) of the Licensed\n Material and any others designated to receive\n attribution, in any reasonable manner requested by\n the Licensor (including by pseudonym if\n designated);\n .\n ii. a copyright notice;\n .\n iii. a notice that refers to this Public License;\n .\n iv. a notice that refers to the disclaimer of\n warranties;\n .\n v. a URI or hyperlink to the Licensed Material to the\n extent reasonably practicable;\n .\n b. indicate if You modified the Licensed Material and\n retain an indication of any previous modifications; and\n .\n c. indicate the Licensed Material is licensed under this\n Public License, and include the text of, or the URI or\n hyperlink to, this Public License.\n .\n 2. You may satisfy the conditions in Section 3(a)(1) in any\n reasonable manner based on the medium, means, and context in\n which You Share the Licensed Material. For example, it may be\n reasonable to satisfy the conditions by providing a URI or\n hyperlink to a resource that includes the required\n information.\n .\n 3. If requested by the Licensor, You must remove any of the\n information required by Section 3(a)(1)(A) to the extent\n reasonably practicable.\n .\n 4. If You Share Adapted Material You produce, the Adapter's\n License You apply must not prevent recipients of the Adapted\n Material from complying with this Public License.\n .\n Section 4 -- Sui Generis Database Rights.\n .\n Where the Licensed Rights include Sui Generis Database Rights that\n apply to Your use of the Licensed Material:\n .\n a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n to extract, reuse, reproduce, and Share all or a substantial\n portion of the contents of the database;\n .\n b. if You include all or a substantial portion of the database\n contents in a database in which You have Sui Generis Database\n Rights, then the database in which You have Sui Generis Database\n Rights (but not its individual contents) is Adapted Material; and\n .\n c. You must comply with the conditions in Section 3(a) if You Share\n all or a substantial portion of the contents of the database.\n .\n For the avoidance of doubt, this Section 4 supplements and does not\n replace Your obligations under this Public License where the Licensed\n Rights include other Copyright and Similar Rights.\n .\n Section 5 -- Disclaimer of Warranties and Limitation of Liability.\n .\n a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n .\n b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n .\n c. The disclaimer of warranties and limitation of liability provided\n above shall be interpreted in a manner that, to the extent\n possible, most closely approximates an absolute disclaimer and\n waiver of all liability.\n .\n Section 6 -- Term and Termination.\n .\n a. This Public License applies for the term of the Copyright and\n Similar Rights licensed here. However, if You fail to comply with\n this Public License, then Your rights under this Public License\n terminate automatically.\n .\n b. Where Your right to use the Licensed Material has terminated under\n Section 6(a), it reinstates:\n .\n 1. automatically as of the date the violation is cured, provided\n it is cured within 30 days of Your discovery of the\n violation; or\n .\n 2. upon express reinstatement by the Licensor.\n .\n For the avoidance of doubt, this Section 6(b) does not affect any\n right the Licensor may have to seek remedies for Your violations\n of this Public License.\n .\n c. For the avoidance of doubt, the Licensor may also offer the\n Licensed Material under separate terms or conditions or stop\n distributing the Licensed Material at any time; however, doing so\n will not terminate this Public License.\n .\n d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n License.\n .\n Section 7 -- Other Terms and Conditions.\n .\n a. The Licensor shall not be bound by any additional or different\n terms or conditions communicated by You unless expressly agreed.\n .\n b. Any arrangements, understandings, or agreements regarding the\n Licensed Material not stated herein are separate from and\n independent of the terms and conditions of this Public License.\n .\n Section 8 -- Interpretation.\n .\n a. For the avoidance of doubt, this Public License does not, and\n shall not be interpreted to, reduce, limit, restrict, or impose\n conditions on any use of the Licensed Material that could lawfully\n be made without permission under this Public License.\n .\n b. To the extent possible, if any provision of this Public License is\n deemed unenforceable, it shall be automatically reformed to the\n minimum extent necessary to make it enforceable. If the provision\n cannot be reformed, it shall be severed from this Public License\n without affecting the enforceability of the remaining terms and\n conditions.\n .\n c. No term or condition of this Public License will be waived and no\n failure to comply consented to unless expressly agreed to by the\n Licensor.\n .\n d. Nothing in this Public License constitutes or may be interpreted\n as a limitation upon, or waiver of, any privileges and immunities\n that apply to the Licensor or You, including from the legal\n processes of any jurisdiction or authority.\n"
  },
  {
    "path": "krokiet/LICENSE_CC_BY_4_ICONS",
    "content": "Icons\nicons/*\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nLicense: CC-BY-4.0\n Creative Commons Attribution 4.0 International Public License\n .\n By exercising the Licensed Rights (defined below), You accept and agree\n to be bound by the terms and conditions of this Creative Commons\n Attribution 4.0 International Public License (\"Public\n License\"). To the extent this Public License may be interpreted as a\n contract, You are granted the Licensed Rights in consideration of Your\n acceptance of these terms and conditions, and the Licensor grants You\n such rights in consideration of benefits the Licensor receives from\n making the Licensed Material available under these terms and\n conditions.\n .\n Section 1 -- Definitions.\n .\n a. Adapted Material means material subject to Copyright and Similar\n Rights that is derived from or based upon the Licensed Material\n and in which the Licensed Material is translated, altered,\n arranged, transformed, or otherwise modified in a manner requiring\n permission under the Copyright and Similar Rights held by the\n Licensor. For purposes of this Public License, where the Licensed\n Material is a musical work, performance, or sound recording,\n Adapted Material is always produced where the Licensed Material is\n synched in timed relation with a moving image.\n .\n b. Adapter's License means the license You apply to Your Copyright\n and Similar Rights in Your contributions to Adapted Material in\n accordance with the terms and conditions of this Public License.\n .\n c. Copyright and Similar Rights means copyright and/or similar rights\n closely related to copyright including, without limitation,\n performance, broadcast, sound recording, and Sui Generis Database\n Rights, without regard to how the rights are labeled or\n categorized. For purposes of this Public License, the rights\n specified in Section 2(b)(1)-(2) are not Copyright and Similar\n Rights.\n .\n d. Effective Technological Measures means those measures that, in the\n absence of proper authority, may not be circumvented under laws\n fulfilling obligations under Article 11 of the WIPO Copyright\n Treaty adopted on December 20, 1996, and/or similar international\n agreements.\n .\n e. Exceptions and Limitations means fair use, fair dealing, and/or\n any other exception or limitation to Copyright and Similar Rights\n that applies to Your use of the Licensed Material.\n .\n f. Licensed Material means the artistic or literary work, database,\n or other material to which the Licensor applied this Public\n License.\n .\n g. Licensed Rights means the rights granted to You subject to the\n terms and conditions of this Public License, which are limited to\n all Copyright and Similar Rights that apply to Your use of the\n Licensed Material and that the Licensor has authority to license.\n .\n h. Licensor means the individual(s) or entity(ies) granting rights\n under this Public License.\n .\n i. Share means to provide material to the public by any means or\n process that requires permission under the Licensed Rights, such\n as reproduction, public display, public performance, distribution,\n dissemination, communication, or importation, and to make material\n available to the public including in ways that members of the\n public may access the material from a place and at a time\n individually chosen by them.\n .\n j. Sui Generis Database Rights means rights other than copyright\n resulting from Directive 96/9/EC of the European Parliament and of\n the Council of 11 March 1996 on the legal protection of databases,\n as amended and/or succeeded, as well as other essentially\n equivalent rights anywhere in the world.\n .\n k. You means the individual or entity exercising the Licensed Rights\n under this Public License. Your has a corresponding meaning.\n .\n Section 2 -- Scope.\n .\n a. License grant.\n .\n 1. Subject to the terms and conditions of this Public License,\n the Licensor hereby grants You a worldwide, royalty-free,\n non-sublicensable, non-exclusive, irrevocable license to\n exercise the Licensed Rights in the Licensed Material to:\n .\n a. reproduce and Share the Licensed Material, in whole or\n in part; and\n .\n b. produce, reproduce, and Share Adapted Material.\n .\n 2. Exceptions and Limitations. For the avoidance of doubt, where\n Exceptions and Limitations apply to Your use, this Public\n License does not apply, and You do not need to comply with\n its terms and conditions.\n .\n 3. Term. The term of this Public License is specified in Section\n 6(a).\n .\n 4. Media and formats; technical modifications allowed. The\n Licensor authorizes You to exercise the Licensed Rights in\n all media and formats whether now known or hereafter created,\n and to make technical modifications necessary to do so. The\n Licensor waives and/or agrees not to assert any right or\n authority to forbid You from making technical modifications\n necessary to exercise the Licensed Rights, including\n technical modifications necessary to circumvent Effective\n Technological Measures. For purposes of this Public License,\n simply making modifications authorized by this Section 2(a)\n (4) never produces Adapted Material.\n .\n 5. Downstream recipients.\n .\n a. Offer from the Licensor -- Licensed Material. Every\n recipient of the Licensed Material automatically\n receives an offer from the Licensor to exercise the\n Licensed Rights under the terms and conditions of this\n Public License.\n .\n b. No downstream restrictions. You may not offer or impose\n any additional or different terms or conditions on, or\n apply any Effective Technological Measures to, the\n Licensed Material if doing so restricts exercise of the\n Licensed Rights by any recipient of the Licensed\n Material.\n .\n 6. No endorsement. Nothing in this Public License constitutes or\n may be construed as permission to assert or imply that You\n are, or that Your use of the Licensed Material is, connected\n with, or sponsored, endorsed, or granted official status by,\n the Licensor or others designated to receive attribution as\n provided in Section 3(a)(1)(A)(i).\n .\n b. Other rights.\n .\n 1. Moral rights, such as the right of integrity, are not\n licensed under this Public License, nor are publicity,\n privacy, and/or other similar personality rights; however, to\n the extent possible, the Licensor waives and/or agrees not to\n assert any such rights held by the Licensor to the limited\n extent necessary to allow You to exercise the Licensed\n Rights, but not otherwise.\n .\n 2. Patent and trademark rights are not licensed under this\n Public License.\n .\n 3. To the extent possible, the Licensor waives any right to\n collect royalties from You for the exercise of the Licensed\n Rights, whether directly or through a collecting society\n under any voluntary or waivable statutory or compulsory\n licensing scheme. In all other cases the Licensor expressly\n reserves any right to collect such royalties.\n .\n Section 3 -- License Conditions.\n .\n Your exercise of the Licensed Rights is expressly made subject to the\n following conditions.\n .\n a. Attribution.\n .\n 1. If You Share the Licensed Material (including in modified\n form), You must:\n .\n a. retain the following if it is supplied by the Licensor\n with the Licensed Material:\n .\n i. identification of the creator(s) of the Licensed\n Material and any others designated to receive\n attribution, in any reasonable manner requested by\n the Licensor (including by pseudonym if\n designated);\n .\n ii. a copyright notice;\n .\n iii. a notice that refers to this Public License;\n .\n iv. a notice that refers to the disclaimer of\n warranties;\n .\n v. a URI or hyperlink to the Licensed Material to the\n extent reasonably practicable;\n .\n b. indicate if You modified the Licensed Material and\n retain an indication of any previous modifications; and\n .\n c. indicate the Licensed Material is licensed under this\n Public License, and include the text of, or the URI or\n hyperlink to, this Public License.\n .\n 2. You may satisfy the conditions in Section 3(a)(1) in any\n reasonable manner based on the medium, means, and context in\n which You Share the Licensed Material. For example, it may be\n reasonable to satisfy the conditions by providing a URI or\n hyperlink to a resource that includes the required\n information.\n .\n 3. If requested by the Licensor, You must remove any of the\n information required by Section 3(a)(1)(A) to the extent\n reasonably practicable.\n .\n 4. If You Share Adapted Material You produce, the Adapter's\n License You apply must not prevent recipients of the Adapted\n Material from complying with this Public License.\n .\n Section 4 -- Sui Generis Database Rights.\n .\n Where the Licensed Rights include Sui Generis Database Rights that\n apply to Your use of the Licensed Material:\n .\n a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n to extract, reuse, reproduce, and Share all or a substantial\n portion of the contents of the database;\n .\n b. if You include all or a substantial portion of the database\n contents in a database in which You have Sui Generis Database\n Rights, then the database in which You have Sui Generis Database\n Rights (but not its individual contents) is Adapted Material; and\n .\n c. You must comply with the conditions in Section 3(a) if You Share\n all or a substantial portion of the contents of the database.\n .\n For the avoidance of doubt, this Section 4 supplements and does not\n replace Your obligations under this Public License where the Licensed\n Rights include other Copyright and Similar Rights.\n .\n Section 5 -- Disclaimer of Warranties and Limitation of Liability.\n .\n a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n .\n b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n .\n c. The disclaimer of warranties and limitation of liability provided\n above shall be interpreted in a manner that, to the extent\n possible, most closely approximates an absolute disclaimer and\n waiver of all liability.\n .\n Section 6 -- Term and Termination.\n .\n a. This Public License applies for the term of the Copyright and\n Similar Rights licensed here. However, if You fail to comply with\n this Public License, then Your rights under this Public License\n terminate automatically.\n .\n b. Where Your right to use the Licensed Material has terminated under\n Section 6(a), it reinstates:\n .\n 1. automatically as of the date the violation is cured, provided\n it is cured within 30 days of Your discovery of the\n violation; or\n .\n 2. upon express reinstatement by the Licensor.\n .\n For the avoidance of doubt, this Section 6(b) does not affect any\n right the Licensor may have to seek remedies for Your violations\n of this Public License.\n .\n c. For the avoidance of doubt, the Licensor may also offer the\n Licensed Material under separate terms or conditions or stop\n distributing the Licensed Material at any time; however, doing so\n will not terminate this Public License.\n .\n d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n License.\n .\n Section 7 -- Other Terms and Conditions.\n .\n a. The Licensor shall not be bound by any additional or different\n terms or conditions communicated by You unless expressly agreed.\n .\n b. Any arrangements, understandings, or agreements regarding the\n Licensed Material not stated herein are separate from and\n independent of the terms and conditions of this Public License.\n .\n Section 8 -- Interpretation.\n .\n a. For the avoidance of doubt, this Public License does not, and\n shall not be interpreted to, reduce, limit, restrict, or impose\n conditions on any use of the Licensed Material that could lawfully\n be made without permission under this Public License.\n .\n b. To the extent possible, if any provision of this Public License is\n deemed unenforceable, it shall be automatically reformed to the\n minimum extent necessary to make it enforceable. If the provision\n cannot be reformed, it shall be severed from this Public License\n without affecting the enforceability of the remaining terms and\n conditions.\n .\n c. No term or condition of this Public License will be waived and no\n failure to comply consented to unless expressly agreed to by the\n Licensor.\n .\n d. Nothing in this Public License constitutes or may be interpreted\n as a limitation upon, or waiver of, any privileges and immunities\n that apply to the Licensor or You, including from the legal\n processes of any jurisdiction or authority."
  },
  {
    "path": "krokiet/LICENSE_GPL_APP",
    "content": "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n\nCopyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble\n\nThe GNU General Public License is a free, copyleft license for software and other kinds of works.\n\nThe licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.\n\nSome devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\nTERMS AND CONDITIONS\n\n0. Definitions.\n\n“This License” refers to version 3 of the GNU General Public License.\n\n“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.\n\n“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.\n\nTo “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.\n\nA “covered work” means either the unmodified Program or a work based on the Program.\n\nTo “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.\n\nTo “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.\n\n1. Source Code.\nThe “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.\n\nA “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.\n\nThe “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.\n\nThe “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.\n\nThe Corresponding Source for a work in source code form is that same work.\n\n2. Basic Permissions.\nAll rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\nNo covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.\n\nWhen you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.\n\n4. Conveying Verbatim Copies.\nYou may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\nYou may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:\n\n     a) The work must carry prominent notices stating that you modified it, and giving a relevant date.\n\n     b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.\n\n     c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.\n\n     d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.\n\nA compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.\n\n6. Conveying Non-Source Forms.\nYou may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:\n\n     a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.\n\n     b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.\n\n     c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.\n\n     d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.\n\n     e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.\n\nA “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.\n\n“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.\n\nIf you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).\n\nThe requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.\n\n7. Additional Terms.\n“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:\n\n     a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or\n\n     b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or\n\n     c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or\n\n     d) Limiting the use for publicity purposes of names of licensors or authors of the material; or\n\n     e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or\n\n     f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.\n\nAll other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.\n\n8. Termination.\nYou may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).\n\nHowever, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.\n\nTermination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.\n\n9. Acceptance Not Required for Having Copies.\nYou are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\nEach time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.\n\nAn “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.\n\n11. Patents.\nA “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.\n\nA contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.\n\nIn the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.\n\nA patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\nIf conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License.\nNotwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.\n\n14. Revised Versions of this License.\nThe Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.\n\nLater license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.\n\n15. Disclaimer of Warranty.\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\nIf the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.\n\n     <one line to give the program's name and a brief idea of what it does.>\n     Copyright (C) <year>  <name of author>\n\n     This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\n     This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.\n\n     You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:\n\n     Krokiet Copyright (C) 2024  Rafał Mikrut\n     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n     This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.\n\nYou should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.\n\nThe GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "krokiet/LICENSE_MIT_CODE",
    "content": "MIT License\n\nCopyright (c) 2020-2026 Rafał Mikrut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "krokiet/README.md",
    "content": "\n![krokiet_logo](https://github.com/user-attachments/assets/f5e4b290-d001-4cf4-9f52-dab65a30e441)\n\nKrokiet is a new Czkawka frontend written in Slint.\n\n![Krokiet](https://github.com/user-attachments/assets/720e98c3-598a-41aa-a04b-0c0c1d8a28e6)\n\nIt aims to provide a more consistent experience across all platforms (Linux, Windows, macOS) compared to the previous GTK 4 frontend.\n\n## How to install?\nPrebuilt binaries are available for Windows 10/11, Mac and Ubuntu 22.04(base)/24.04(with additional libraries) - or distros with same/newer glibc/libraries versions.\n\nYou can download them from https://github.com/qarmin/czkawka/releases/, which contains recommendations, which variant to use depending on your needs.\n\n## Compilation\n\nAnother option is to compile it yourself.\n\nThe easiest way is to install newest run and run\n```\ncargo install krokiet\n```\n\nwhich will install, the latest and optimized version of Krokiet.\n\nCompilation with `cargo build --release` should produce a working binary, that without any additional dependencies should run on user os.\n\nIf you have installed new `cargo`, you can easily compile and install it via `cargo install krokiet`\n\n## Additional Renderers\n\nBy default, only femtovg (OpenGL) and the software renderer are enabled, but you can enable more renderers by compiling the app\nwith additional features.\n\nMost users will want to use the app with a windowing system/compositor, so features starting with `winit` in the name are\nrecommended.\n\nFor example:\n\n```\ncargo build --release --features \"winit_skia_opengl\"\ncargo build --release --features \"winit_software\"\n```\n\nTo run the app with a different renderer, set the `SLINT_BACKEND` environment variable(but app must be compiled with the appropriate feature):\n\n```\nSLINT_BACKEND=winit-femtovg ./target/release/krokiet\nSLINT_BACKEND=software ./target/release/krokiet\nSLINT_BACKEND=skia ./target/release/krokiet\n```\n\nIf you use an invalid or non-existing backend, the app will show a warning:\n\n```\nslint winit: unrecognized renderer skia, falling back to FemtoVG\n```\n\nTo check which backend is actually used, add the `SLINT_DEBUG_PERFORMANCE=refresh_lazy,console,overlay` environment variable:\n\n```\nSLINT_DEBUG_PERFORMANCE=refresh_lazy,console,overlay cargo run\n```\n\nYou should see output like:\n\n```\nSlint: Build config: debug; Backend: software\n```\n\n## Scaling the Application\n\nBy default, the Slint application will automatically scale to match your system settings, but you can also manually set the scaling factor with the `SLINT_SCALE_FACTOR` environment variable:\n\n```\nSLINT_SCALE_FACTOR=2 cargo run \n```\n\n## Different Theme\n\nBy default, Czkawka was created with the `fluent` theme in mind, but Slint also supports other themes, which are not officially supported by this app and may look broken.\n\n```\nSLINT_STYLE=cupertino-light cargo run -- --path .\nSLINT_STYLE=cupertino-dark cargo run -- --path .\nSLINT_STYLE=material-light cargo run -- --path .\nSLINT_STYLE=material-dark cargo run -- --path .\n```\n\n## Why create a new frontend instead of improving the existing Czkawka GTK 4 app?\n\nFor many, it might seem surprising to abandon the existing GTK 4 frontend of Czkawka especially considering that GTK is one of the most popular GUI frameworks and replace it with a new one based on Slint, which is still relatively unknown.\n\nThis decision was driven by several key factors:\n- **GTK on Windows and macOS performs poorly** – There are random bugs that don’t appear on Linux or on other systems with similar environments. Slint, on the other hand, behaves consistently and reliably across all platforms.\n- **Complicated compilation and cross-compilation** – Due to GTK’s complexity on Windows, the easiest way to compile the application is by using a Docker image with Linux. This makes testing and debugging on Windows much more difficult.\n- **External dependencies** – I’m a fan of applications that work right after downloading, without requiring installation. With GTK, this is rarely the case. On Linux and macOS, several dynamically linked libraries must be installed first, and they may exist in different versions across systems. On Windows, you often have to manually include DLLs. This wouldn't be such an issue if the GTK team officially distributed these libraries and maintained a list of required files, but they don’t, so you’re left compiling everything yourself or, like in my case, relying on external Docker images. With Slint, all I need is a single binary file that runs out of the box on almost any system.\n- **GTK version fragmentation across platforms** – On Linux, GTK is dynamically linked, and different versions may introduce unique bugs or inconsistencies. On Windows, the libraries are bundled, but outdated in my app, since newer ones aren’t available in the Docker image I use, and some versions crash on some OSes. macOS (with Homebrew) is in the best position here, as it usually keeps GTK up to date. With Slint, each Krokiet release is bundled with the latest Slint version, ensuring consistency across all systems and reducing platform-specific issues.\n- **Cambalache is the only no-code GUI tool** – While Cambalache itself works reasonably well, it isn’t officially supported or maintained by the GTK team, but by an independent developer. In contrast, while the Slint GUI is mostly created via code, it offers live previews in VS Code/VSCodium, which is extremely convenient.\n- **Difficult to modify built-in widgets** – GTK enforces a specific visual style, which can be very restrictive. In my case, I had to tweak internal widget parameters just to achieve the desired look, something that might cause a lot of issues in the future. Slint takes the opposite approach: its built-in widgets are quite limited, which often makes it easier to build fully custom components from scratch.\n- **GTK is still C code** – Even though the library is wrapped and provides a relatively safe Rust interface, you still occasionally have to work with low-level structures, which have caused issues and crashes for me in the past. Another downside is the large number of warnings printed to the console, even with correct code, due to internal GTK issues. These warnings are often unhelpful and rarely assist in identifying actual bugs.\n\n## License\n\nThe code is licensed under the MIT license, but the entire project is licensed under GPL-3.0 due to Slint license restrictions.\n\nAll icons and images are licensed under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license.\n\n## Name\n\nWhy Krokiet (eng. Croquette)?  \nBecause I like croquettes (the Polish version), the ones with meat and mushrooms wrapped in breadcrumbs... it makes my mouth water.  \nI also considered other dishes I like, such as pierogi, żurek, pączek, schabowy, or zapiekanka.  \nThis name should be much easier to remember than czkawka or szyszka.\n"
  },
  {
    "path": "krokiet/build.rs",
    "content": "use std::env;\n\nfn main() {\n    if env::var(\"SLINT_STYLE\").is_err() || env::var(\"SLINT_STYLE\") == Ok(String::new()) {\n        slint_build::compile_with_config(\"ui/main_window.slint\", slint_build::CompilerConfiguration::new().with_style(\"fluent-dark\".into())).expect(\"Unable to compile slint file\");\n    } else {\n        slint_build::compile(\"ui/main_window.slint\").expect(\"Unable to compile slint file\");\n    }\n}\n"
  },
  {
    "path": "krokiet/i18n/ar/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = خطأ حرج أثناء بدء التطبيق\nrust_init_error_message = \n        حدث خطأ حرج أثناء بدء التطبيق:\n\n        { $error_message }\n\n        قد يكون ذلك بسبب نقص أو تلف تعريفات OpenGL/Vulkan، أو تشغيل التطبيق في جهاز افتراضي أو خلل في Krokiet أو أحد مكتباته.\n\n        يمكنك محاولة تشغيل إصدارات مختلفة (skia_opengl، skia_vulkan، femtovg_opengl - الافتراضي) أو مع مُسرِّع رسومي للبرامج لتحديد ما إذا كان ذلك يحل المشكلة.\nrust_loaded_preset = تم تحميل الإعداد المسبق { $preset_idx }\nrust_file_already_exists = الملف \"{ $file }\" موجود بالفعل، ولن يتم الكتابة فوقه\nrust_error_removing_file_after_copy = خطأ أثناء حذف الملف \"{ $file }\" (بعد نسخه إلى قسم مختلف)، السبب: { $reason }\nrust_error_copying_file = خطأ أثناء نسخ \"{ $input }\" إلى \"{ $output }\"، السبب: { $reason }\nrust_loading_tags_cache = تحميل ذاكرة التخزين المؤقت للعلامات\nrust_loading_fingerprints_cache = تحميل ذاكرة التخزين المؤقت لبصمات الأصابع\nrust_saving_tags_cache = حفظ ذاكرة التخزين المؤقت للعلامات\nrust_saving_fingerprints_cache = حفظ ذاكرة التخزين المؤقت لبصمات الأصابع\nrust_loading_prehash_cache = تحميل ذاكرة التخزين المؤقت\nrust_saving_prehash_cache = حفظ ذاكرة التخزين المؤقت\nrust_loading_hash_cache = تحميل ذاكرة التخزين المؤقت للتجزئة\nrust_saving_hash_cache = حفظ ذاكرة التخزين المؤقت\nrust_loading_exif_cache = تحميل ذاكرة التخزين المؤقت EXIF\nrust_saving_exif_cache = حفظ ذاكرة التخزين المؤقت EXIF\nrust_scanning_name = فحص اسم الملف { $entries_checked }\nrust_scanning_size_name = حجم واسم ملف { $entries_checked }\nrust_scanning_size = حجم مسح الملف { $entries_checked }\nrust_scanning_file = فحص الملف { $entries_checked }\nrust_scanning_folder = فحص { $entries_checked } مجلد\nrust_checked_tags = تم التحقق من العلامات { $items_stats }\nrust_checked_content = المحتوى المختار من { $items_stats } ({ $size_stats })\nrust_compared_tags = مقارنة العلامات { $items_stats }\nrust_compared_content = مقارنة محتوى { $items_stats }\nrust_hashed_images = تجزئة { $items_stats } صور ({ $size_stats })\nrust_compared_image_hashes = مقارنة تجزئة الصور من { $items_stats }\nrust_hashed_videos = مجزأة { $items_stats } مقاطع فيديو\nrust_created_thumbnails = أنشئ مصغرات لـ { $items_stats } مقاطع فيديو\nrust_checked_files = تم تحديد الملف { $items_stats } ({ $size_stats })\nrust_checked_files_bad_extensions = تم التحقق من الملف { $items_stats }\nrust_checked_files_bad_names = تم التحقق من الملف { $items_stats }\nrust_checked_videos = تم التحقق من { $items_stats } مقاطع فيديو ({ $size_stats })\nrust_analyzed_partial_hash = تم تحليل التجزئة الجزئية للملفات { $items_stats } ({ $size_stats })\nrust_analyzed_full_hash = تم تحليل التجزئة الكاملة من ملفات { $items_stats } ({ $size_stats })\nrust_failed_to_rename_file = فشل في إعادة تسمية الملف { $old_path } إلى { $new_path }، الخطأ: { $error }\nrust_no_included_paths = لا يمكن بدء المسح عند عدم تحديد المسارات المضمنة.\nrust_all_paths_referenced = لا يمكن بدء المسح عندما تكون جميع المسارات المضمنة مضبوطة كمسارات مرجعية، تحتاج إلى تعطيل مربع الاختيار بجوار المسار المدخل.\nrust_found_empty_folders = تم العثور على { $items_found } مجلدات فارغة في { $time }\nrust_found_empty_files = تم العثور على { $items_found } ملفات فارغة في { $time }\nrust_found_similar_images = تم العثور على { $items_found } ملفات صور مماثلة في { $groups } مجموعات في { $time }\nrust_found_similar_videos = تم العثور على { $items_found } ملفات فيديو مماثلة في { $groups } مجموعات في { $time }\nrust_found_similar_music_files = تم العثور على { $items_found } ملفات موسيقية مماثلة في { $groups } مجموعات في { $time }\nrust_found_invalid_symlinks = تم العثور على { $items_found } روابط رموز غير صالحة في { $time }\nrust_found_temporary_files = تم العثور على { $items_found } ملفات مؤقتة في { $time }\nrust_no_file_type_selected = لا يمكن العثور على الملفات المكسورة بدون أي نوع من الملفات المحددة.\nrust_found_broken_files = تم العثور على { $items_found } ملفات مكسورة أخذت { $size } في { $time }\nrust_found_bad_extensions = تم العثور على { $items_found } ملفات ذات ملحقات سيئة في { $time }\nrust_found_bad_names = تم العثور على { $items_found } ملفات بأسماء سيئة في { $time }\nrust_found_video_optimizer = تم العثور على { $items_found } ملفات لتحسينها في { $time }\nrust_found_duplicate_files = تم العثور على { $items_found } ملفات مكررة في { $groups } مجموعات أخذت { $size } في { $time }\nrust_found_duplicate_files_no_lost_space = تم العثور على { $items_found } ملفات مكررة في { $groups } مجموعات في { $time }\nrust_found_big_files = تم العثور على { $items_found } ملفات كبيرة بحجم { $size } في { $time }\nrust_found_exif_files = تم العثور على { $items_found } ملفات مع بيانات EXIF في { $time }\nrust_cannot_load_preset = لا يمكن تغيير وتحميل الإعداد المسبق { $preset_idx } - السبب { $reason }، باستخدام الإعدادات الافتراضية بدلاً من ذلك\nrust_saved_preset = تم الحفظ مسبقا { $preset_idx }\nrust_cannot_save_preset = لا يمكن حفظ الإعداد المسبق { $preset_idx } - السبب { $reason }\nrust_reset_preset = استرجع التعيين المسبق { $preset_idx }\nrust_cannot_create_output_folder = لا يمكن إنشاء مجلد الإخراج { $output_folder }، السبب: { $error }\nrust_delete_summary = حذف { $deleted } عناصر ، فشل في إزالة { $failed } عناصر ، من أصل { $total } عناصر\nrust_rename_summary = إعادة تسمية العناصر { $renamed } ، فشل في إعادة تسمية العناصر { $failed } ، من أصل { $total } عناصر\nrust_move_summary = نقل { $moved } عناصر, فشل في نقل { $failed } عناصر, من { $total } عناصر\nrust_hardlink_summary = مرتبط بالرابط { $hardlinked } عناصر، فشل ربط الرابط { $failed } عناصر، من أصل { $total } عناصر\nrust_symlink_summary = ربط رمزي { $symlinked } عناصر، فشل ربط رمزي { $failed } عناصر، من أصل { $total } عناصر\nrust_optimize_video_summary = مقاطع فيديو مُحسّنة { $optimized }، وفشلت في تحسين { $failed }، وخرجت من { $total } مقاطع فيديو\nrust_clean_exif_summary = تمت إزالة EXIF المُنظَّفة من { $cleaned } ملفات، وفشلت في تنظيف { $failed } ملفات، من أصل { $total } ملفات\nrust_deleting_files = حذف ملف { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = حذف ملف { $items_stats }\nrust_renaming_files = إعادة تسمية الملف { $items_stats }\nrust_moving_files = نقل الملف { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = نقل ملف { $items_stats }\nrust_hardlinking_files = الرابط الصلب { $items_stats } الملف ({ $size_stats })\nrust_hardlinking_no_size_files = الرابط الصلب { $items_stats } ملف\nrust_symlinking_files = الرابط الرمزية { $items_stats } الملف ({ $size_stats })\nrust_symlinking_no_size_files = الرابط الرمزية { $items_stats } ملف\nrust_optimizing_videos = مُحسَّن { $items_stats } فيديو ({ $size_stats })\nrust_optimizing_no_size_videos = مُحسَّن { $items_stats } فيديو\nrust_cleaning_exif = تنظيف EXIF من ملف { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = تنظيف EXIF من ملف { $items_stats }\nrust_no_files_deleted = لا توجد ملفات أو مجلدات محددة للحذف\nrust_no_files_renamed = لا توجد ملفات أو مجلدات محددة لإعادة التسمية\nrust_no_files_moved = لا توجد ملفات أو مجلدات محددة للانتقال\nrust_no_files_hardlinked = لا توجد ملفات أو مجلدات محددة لإنشاء الروابط الصلبة\nrust_no_files_symlinked = لا توجد ملفات أو مجلدات محددة لإنشاء الروابط الرمزية\nrust_no_videos_optimized = لا توجد فيديوهات مُحدَّدة للتحسين\nrust_no_exif_cleaned = لا توجد ملفات مُحدَّدة لتنظيف EXIF\nrust_extracted_exif_tags = تم استخراج علامات EXIF من ملفات { $items_stats } ({ $size_stats })\nrust_delete_confirmation = هل أنت متأكد من أنك تريد حذف العناصر المحددة؟\nrust_delete_confirmation_number_simple = { $items } العناصر المحددة.\nrust_delete_confirmation_number_groups = { $items } العناصر المحددة في { $groups } مجموعات.\nrust_delete_confirmation_selected_all_in_group = جميع العناصر المحددة في مجموعات { $groups }.\nrust_move_confirmation = هل أنت متأكد من أنك تريد نقل العناصر المحددة؟\nrust_move_confirmation_number_simple = { $items } عناصر محددة.\nrust_clean_exif_confirmation = هل أنت متأكد من أنك تريد إزالة بيانات EXIF من العناصر المحددة؟\nrust_clean_exif_confirmation_number_simple = { $items } عناصر محددة.\nclean_exif_overwrite_files_text = استبدل الملفات\nrust_optimize_video_confirmation = هل أنت متأكد من أنك تريد تحسين مقاطع الفيديو المحددة؟\nrust_optimize_video_confirmation_number_simple = { $items } عناصر محددة.\nrust_hardlink_confirmation = هل أنت متأكد من أنك تريد إنشاء روابط صلبة للعناصر المحددة؟\nrust_hardlink_confirmation_number_simple = { $items } عناصر محددة.\nrust_symlink_confirmation = هل أنت متأكد من أنك تريد إنشاء روابط رمزية للعناصر المحددة؟\nrust_symlink_confirmation_number_simple = { $items } عناصر محددة.\nrust_rename_confirmation = هل أنت متأكد من أنك تريد إعادة تسمية العناصر المحددة؟\nrust_rename_confirmation_number_simple = { $items } عناصر محددة.\nrust_cache_processed_files = تمت معالجة ملفات التخزين المؤقت { $files }\nrust_cache_entries_stats = تمت إزالة { $removed } من جميع { $all }، { $left } متبقية\nrust_cache_size_reduced = تم تقليل حجم ملفات التخزين المؤقت بنسبة { $size }\nrust_cache_time_elapsed = الوقت المنقضي: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = فشل ربط الروابط الصلبة { $name } بـ { $target }، والسبب { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = التحديد\ncolumn_size = الحجم\ncolumn_file_name = اسم الملف\ncolumn_path = المسار\ncolumn_modification_date = تاريخ التعديل\ncolumn_similarity = تماثل\ncolumn_dimensions = الأبعاد\ncolumn_new_dimensions = أبعاد جديدة\ncolumn_title = العنوان\ncolumn_artist = الفنان\ncolumn_year = السنة\ncolumn_bitrate = معدل\ncolumn_length = طول\ncolumn_genre = النوع\ncolumn_type_of_error = نوع الخطأ\ncolumn_symlink_name = اسم الرابط الرمزي\ncolumn_symlink_folder = مجلد الرابط الرمزي\ncolumn_destination_path = مسار الوجهة\ncolumn_current_extension = التمديد الحالي\ncolumn_proper_extension = التمديد الصحيح\ncolumn_fps = fps\ncolumn_codec = ترميز\ncolumn_duration = المدة\ncolumn_exif_tags = وسوم EXIF\ncolumn_new_name = اسم جديد\n# Slint translations\nok_button = حسناً\ncancel_button = إلغاء\ndo_you_want_to_continue = هل تريد المتابعة؟\nmain_window_title = كروكييت - منظف البيانات\nscan_button = فحص\nstop_button = توقف\nstop_text = توقف\nselect_button = حدد\nmove_button = نقل\ndelete_button = حذف\nsave_button = حفظ\nsort_button = فرز\nrename_button = إعادة تسمية\nmotto = هذا البرنامج حر في الاستخدام وسوف يكون دائما.\\nراجع رخصة MIT/GPL للحصول على التفاصيل.\nunicorn = قد لا تنظر إلى وحيد القرن، ولكن وحيد القرن ينظر إليك دائما.\nrepository = المستودع\ninstruction = تعليمات\ndonation = تبرع\ntranslation = الترجمة\nincluded_paths = المسارات المضمنة\nexcluded_paths = المسارات المستبعدة\nref = مرجع\npath = المسار\ntool_duplicate_files = تكرار الملفات\ntool_empty_folders = مجلدات فارغة\ntool_big_files = ملفات كبيرة\ntool_empty_files = ملفات فارغة\ntool_temporary_files = الملفات المؤقتة\ntool_similar_images = صور مشابهة\ntool_similar_videos = مقاطع فيديو مماثلة\ntool_music_duplicates = مكرر الموسيقى\ntool_invalid_symlinks = الروابط الرمزية غير صالحة\ntool_broken_files = الملفات المكسورة\ntool_bad_extensions = ملحقات سيئة\ntool_bad_names = أسماء سيئة\ntool_video_optimizer = مُحسِّن الفيديو\ntool_exif_remover = مزيل إكسيف\nsort_by_full_name = الترتيب حسب الاسم الكامل\nsort_by_selection = الترتيب حسب التحديد\nsort_reverse = عكس الترتيب\nselection_all = حدد الكل\nselection_deselect_all = إلغاء تحديد الكل\nselection_invert_selection = عكس التحديد\nselection_the_biggest_size = حدد أكبر حجم\nselection_the_biggest_resolution = حدد أكبر دقة\nselection_the_smallest_size = حدد أصغر حجم\nselection_the_smallest_resolution = حدد أصغر دقة\nselection_newest = حدد الأحدث\nselection_oldest = حدد الأقدم\nselection_shortest_path = اختر أقصر مسار\nselection_longest_path = اختر أطول مسار\nstage_current = المرحلة الحالية:\nstage_all = جميع المراحل:\nsubsettings = الإعدادات الفرعية\nsubsettings_images_hash_size = حجم التجزئة\nsubsettings_images_resize_algorithm = تغيير حجم الخوارزمية\nsubsettings_images_ignore_same_size = تجاهل الصور بنفس الحجم\nsubsettings_images_max_difference = الفرق الأقصى\nsubsettings_images_duplicates_hash_type = نوع التجزئة\nsubsettings_duplicates_check_method = طريقة التحقق\nsubsettings_duplicates_name_case_sensitive = حالة حساسة (طرق الاسم فقط)\nsubsettings_biggest_files_sub_method = الطريقة\nsubsettings_biggest_files_sub_number_of_files = عدد الملفات\nsubsettings_videos_max_difference = الفرق الأقصى\nsubsettings_videos_ignore_same_size = تجاهل مقاطع الفيديو بنفس الحجم\nsubsettings_music_audio_check_type = نوع التحقق من الصوت\nsubsettings_music_approximate_comparison = مقارنة العلامات التقريبية\nsubsettings_music_compared_tags = مقارنة العلامات\nsubsettings_music_title = العنوان\nsubsettings_music_artist = الفنان\nsubsettings_music_bitrate = معدل\nsubsettings_music_genre = النوع\nsubsettings_music_year = السنة\nsubsettings_music_length = طول\nsubsettings_music_max_difference = الفرق الأقصى\nsubsettings_music_minimal_fragment_duration = الحد الأدنى من مدة الشظايا\nsubsettings_music_compare_fingerprints_only_with_similar_titles = مقارنة داخل مجموعات من العناوين المتشابهة\nsubsettings_broken_files_type = نوع الملفات المراد التحقق منها\nsubsettings_broken_files_audio = الصوت\nsubsettings_broken_files_pdf = بي دي إف\nsubsettings_broken_files_archive = أرشيف\nsubsettings_broken_files_image = صورة\nsubsettings_broken_files_video = فيديو\nsubsettings_broken_files_video_info = يستخدم ffmpeg/ffprobe. بطيء جداً وقد يكتشف أخطاءً تافهة حتى لو كان الملف يعمل بشكل جيد.\nsubsettings_bad_names_issues = فحص أسماء الملفات\nsubsettings_bad_names_uppercase_extension = توسيع علوي\nsubsettings_bad_names_uppercase_extension_hint = يجد الملفات التي تحتوي على حروف كبيرة في الامتداد (مثل .JPG، .Mp3) ويقترح النسخة الصغيرة\nsubsettings_bad_names_emoji_used = إيموجي في الاسم\nsubsettings_bad_names_emoji_used_hint = يجد الملفات التي تحتوي على أحرف تعبيرية (😀، 🎉، إلخ) في الاسم ويقترح حذفها\nsubsettings_bad_names_space_at_start_end = مسافات بادئة / مسافات لاحقة\nsubsettings_bad_names_space_at_start_end_hint = يجد الملفات التي تحتوي على مسافات في بداية أو نهاية الاسم ويقترح قصها\nsubsettings_bad_names_non_ascii = أحرف غير ASCII\nsubsettings_bad_names_non_ascii_hint = يجد أحرفًا غير ASCII (ą، ć، ñ، إلخ) ويقترح استبدالها بمرادفاتها ASCII (أ، ج، ن) أو إزالتها إذا لم يكن هناك تعيين\nsubsettings_bad_names_restricted_charset = مجموعة أحرف محدودة\nsubsettings_bad_names_restricted_charset_hint = يحول إلى ASCII الأحرف غير ASCII غير القابلة للطباعة، ثم يجد الملفات التي تحتوي على أحرف خارج 0-9أ-ي-ز و أحرف مسموح بها محددة من قبل المستخدم\nsubsettings_bad_names_allowed_chars = السماح بحروف\nsubsettings_bad_names_remove_duplicated = أحرف مكررة\nsubsettings_bad_names_remove_duplicated_hint = يجد الأحرف غير الحرفية المتكررة المتجاورة (مثل \"ملف---اسم..txt\") ويقترح إزالة التكرارات\nsettings_global_settings = الإعدادات العامة\nsettings_dark_theme = السمة المظلمة\nsettings_show_only_icons = إظهار الأيقونات فقط\nsettings_excluded_items = البند المستبعد:\nsettings_allowed_extensions = الإضافات المسموح بها:\nsettings_excluded_extensions = الإضافات المستبعدة:\nsettings_file_size = حجم الملف (كيلوبايتات)\nsettings_minimum_file_size = دقيقة:\nsettings_maximum_file_size = الحد الأقصى:\nsettings_recursive_search = البحث المتكرر\nsettings_use_cache = استخدام ذاكرة التخزين المؤقت\nsettings_save_as_json = حفظ ذاكرة التخزين المؤقت أيضا كملف JSON\nsettings_move_to_trash = نقل الملفات المحذوفة إلى سلة المهملات\nsettings_ignore_other_filesystems = تجاهل نظم الملفات الأخرى (Linux)\nsettings_delete_outdated_cache_entries = حذف إدخالات ذاكرة التخزين المؤقت القديمة تلقائيًا\nsettings_delete_outdated_cache_entries_hint = عند التفعيل، ستقوم التطبيق بالتحقق أثناء تحميل ذاكرة التخزين المؤقت (بحد أقصى مرة واحدة في الأسبوع) لمعرفة ما إذا كانت السجلات المخزنة لا تزال تشير إلى ملفات/بيانات موجودة وغير معدلة\nsettings_hide_hard_links = إخفاء الروابط الصلبة\nsettings_hide_hard_links_hint = إخفاء الروابط الصلبة للملفات نفسها في النتائج\nsettings_thread_number = رقم الموضوع\nsettings_restart_required = ---أنت بحاجة إلى إعادة تشغيل التطبيق لتطبيق التغييرات في رقم الموضوع --\nsettings_duplicate_image_preview = معاينة الصورة\nsettings_duplicate_minimal_hash_cache_size = الحجم الأدنى للملفات المخزنة مؤقتاً - هاش (KB)\nsettings_duplicate_use_prehash = استخدام ما قبل التجزئة\nsettings_duplicate_minimal_prehash_cache_size = الحجم الأدنى للملفات المخزنة مؤقتاً - بريهاش (KB)\nsettings_similar_images_show_image_preview = معاينة الصورة\nsettings_application_scale_text = تطبيق النطاق\nsettings_application_scale_hint_text = عند تفعيل المقياس اليدوي، يتيح لك ذلك اختيار عامل مقياس مخصص، ولكنه يعطل تمامًا التوسيع التلقائي بناءً على دقة الشاشة (DPI).\nsettings_restart_required_scale_text = ---يجب إعادة تشغيل التطبيق لتطبيق التغييرات في المقياس---\nsettings_use_manual_application_scale_text = استخدم مقياس تطبيق يدوي\nsettings_video_thumbnails_preview = معاينة الصورة\nsettings_open_config_folder = فتح مجلد التكوين\nsettings_open_cache_folder = فتح مجلد ذاكرة التخزين المؤقت\nsettings_language = اللغة\nsettings_current_preset = المسبق الحالي:\nsettings_edit_name = تحرير الاسم\nsettings_choose_name_for_prefix = اختر اسم البادئة\nsettings_save = حفظ\nsettings_load = تحميل\nsettings_reset = إعادة تعيين\nsettings_similar_videos_tool = أداة فيديو مشابهة\nsettings_video_thumbnails_clear_unused_thumbnails = حذف صورthumbnails للفيديو غير المستخدمة التي يزيد عمرها عن 7 أيام عند بدء تشغيل التطبيق\nsettings_video_thumbnails_header = صورة مصغرة للفيديو\nsettings_video_thumbnails_generate = إنشاء صور مصغرة\nsettings_video_thumbnails_position = موضع الصورة المصغرة في الفيديو (%)\nsettings_video_thumbnails_generate_grid = إنشاء شبكة صور مصغرة بدلاً من صورة واحدة\nsettings_video_thumbnails_generate_grid_hint = إن إنشاء صور متعددة في شبكة أبطأ بكثير من إنشاء صورة مصغرة واحدة\nsettings_video_thumbnails_grid_tiles_per_side = عدد البلاطات في كل جانب في شبكة الصورة المصغرة\nsettings_video_thumbnails_grid_tiles_per_side_hint = عدد مربعات الصور المصغرة في كل جانب من الشبكة. على سبيل المثال، تحديد 2 ينشئ شبكة 2 × 2، مما ينتج عنه صورة مصغرة واحدة تتكون من 4 صور.\nsettings_similar_images_tool = أداة مشابهة للصور\nsettings_general_settings = الإعدادات العامة\nsettings_cache_header_text = إعدادات التخزين المؤقت\nsettings_clean_cache_button_text = امسح ذاكرة التخزين المؤقت القديمة\nsettings_settings = الإعدادات\nsettings_load_tabs_sizes_at_startup = تحميل أحجام علامات التبويب عند بدء التشغيل\nsettings_load_windows_size_at_startup = تحميل حجم النوافذ عند بدء التشغيل\nsettings_limit_lines_of_messages = قصر الرسائل على 500 سطر (العمل على أداة تحرير نص بطيئ)\nsettings_play_audio_on_scan_completion_text = تشغيل الصوت عند اكتمال المسح بنجاح\nsettings_audio_feature_hint_text = متاح فقط عند التجميع مع الميزة الصوتية\nsettings_audio_env_variable_hint_text = يمكن تغيير الصوت عن طريق تعيين متغير البيئة KROKIET_AUDIO_STOP_FILE إلى مسار ملف صوتي صالح\npopup_save_title = حفظ النتائج\npopup_save_message = سيؤدي هذا إلى حفظ النتائج إلى 3 ملفات مختلفة\npopup_rename_title = إعادة تسمية الملفات\npopup_new_paths_title = أضف مسارات سطرًا واحدًا لكل سطر\npopup_move_title = نقل الملفات\npopup_move_copy_checkbox = نسخ الملفات بدلاً من النقل\npopup_move_preserve_folder_checkbox = الحفاظ على هيكل المجلد\nmove_confirmation_text = هل أنت متأكد من أنك تريد نقل العناصر المحددة؟\nrename_confirmation_text = هل أنت متأكد من أنك تريد إعادة تسمية العناصر المحددة؟\ndelete = حذف العناصر\nstopping_scan = إيقاف المسح، الرجاء الانتظار...\nsearching = يبحث...\nsubsettings_videos_crop_detect = طريقة الكشف عن المحاصيل\nsubsettings_videos_skip_forward_amount = تخطي المدة [s]\nsubsettings_videos_vid_hash_duration = مدة تجزئة الفيديو\nsettings_cache_number_size_text = حجم ملفات التخزين المؤقت: { $size }، عدد الملفات: { $number }\nsettings_video_thumbnails_number_size_text = حجم الصور المصغرة للفيديو: { $size }، عدد الملفات: { $number }\nsettings_log_number_size_text = حجم ملفات السجل: { $size }، عدد الملفات: { $number }\npopup_clean_cache_title_text = مسح ذاكرة التخزين المؤقت القديمة\npopup_clean_cache_confirmation_text = هل أنت متأكد من أنك تريد مسح إدخالات ذاكرة التخزين المؤقت القديمة؟ سيؤدي ذلك إلى إزالة إدخالات ذاكرة التخزين المؤقت للملفات التي لم تعد موجودة أو تم تعديلها.\npopup_clean_cache_progress_text = جاري معالجة ملف ذاكرة التخزين المؤقت:\npopup_clean_cache_current_file_text = الملف الحالي:\npopup_clean_cache_file_progress_text = التقدم الحالي للملف:\npopup_clean_cache_overall_progress_text = التقدم العام:\npopup_clean_cache_stopped_by_user_text = تم إيقاف تنظيف ذاكرة التخزين المؤقت بواسطة المستخدم\npopup_clean_cache_finished_text = تم تنظيف ذاكرة التخزين المؤقت بنجاح!\npopup_clean_cache_error_details_text = تفاصيل الخطأ:\npopup_clean_cache_files_with_errors = ملفات بها أخطاء:\nsubsettings_video_optimizer_mode = وضع\nsubsettings_video_optimizer_crop_type = نوع المحصول\nsubsettings_video_optimizer_black_pixel_threshold = حد\\_السطوع\\_الأسود\nsubsettings_video_optimizer_black_pixel_threshold_hint = القيمة القصوى لـ RGB لكل قناة بكسل لاعتبارها سوداء (0-128). القيمة الافتراضية: 20\nsubsettings_video_optimizer_black_bar_min_percentage = شريط أسود الحد الأدنى للنسبة المئوية\nsubsettings_video_optimizer_black_bar_min_percentage_hint = الحد الأدنى لنسبة بكسلات سوداء في صف/عمود لاعتبارها شريطًا أسود (50-100). القيمة الافتراضية: 90\nsubsettings_video_optimizer_max_samples = أقصى عينات\nsubsettings_video_optimizer_max_samples_hint = الحد الأقصى لعدد الإطارات لتحليلها لكل فيديو (5-1000). القيمة الافتراضية: 60\nsubsettings_video_optimizer_min_crop_size = من Crop Size\nsubsettings_video_optimizer_min_crop_size_hint = الحد الأدنى لعدد وحدات البكسل التي يتم القص فيها على أي جانب (1-1000). يتم تجاهل القصص الأصغر. القيمة الافتراضية: 5\nsubsettings_video_optimizer_video_codec = فيديو كودك\nsubsettings_video_optimizer_excluded_codecs = محذوفات الترميز\nsubsettings_video_optimizer_video_quality = جودة الفيديو (CRF)\nsubsettings_reset = إعادة تعيين\nsubsettings_exif_ignored_tags_text = تجاهل العلامات:\nsubsettings_exif_ignored_tags_hint_text = قائمة مفرغة بفواصل من العلامات المستبعدة من الفحص (مثل GPS، Thumbnail). بعض العلامات، مثل ImageWidth في ملفات TIFF، مخفية لمنع كسر الصورة.\nclean_button_text = نظيف\nclean_text = بيانات EXIF ​​النظيفة\nclean_confirmation_text = هل أنت متأكد من أنك تريد إزالة بيانات EXIF من العناصر المحددة؟\ncrop_videos_text = قص الفيديو\ncrop_video_confirmation_text = هل أنت متأكد من أنك تريد اقتطاف الفيديوهات المحددة؟\ncrop_reencode_video_text = إعادة ترميز الفيديو\nreencode_videos_text = إعادة ترميز الفيديوهات\noptimize_button_text = التحسين\noptimize_confirmation_text = هل أنت متأكد من أنك تريد إعادة ترميز الفيديوهات المحددة؟\noptimize_fail_if_bigger_text = فشل إذا كان الملف المحسن أكبر\noptimize_overwrite_files_text = استبدل الملفات\noptimize_limit_video_size_text = حدّ حجم الفيديو\noptimize_max_width_text = الحد الأقصى للعرض:\noptimize_max_height_text = الحد الأقصى للارتفاع:\nhardlink_button_text = رابط صلب\nhardlink_text = إنشاء روابط صلبة\nhardlink_confirmation_text = هل أنت متأكد من أنك تريد إنشاء روابط صلبة للعناصر المحددة؟\nsoftlink_button_text = سولت لينك\nsoftlink_text = إنشاء روابط رمزية\nsoftlink_confirmation_text = هل أنت متأكد من أنك تريد إنشاء روابط رمزية (symlinks) للعناصر المحددة؟\n"
  },
  {
    "path": "krokiet/i18n/bg/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Критична грешка по време на стартиране на приложението\nrust_init_error_message = \n        Възникна критична грешка при стартиране на приложението:\n\n        { $error_message }\n\n        Това може да се дължи на липсващи или дефектни драйвери OpenGL/Vulkan, стартиране на приложението във виртуална машина или бъг в Krokiet или една от библиотеките му.\n\n        Можете да опитате да стартирате различни версии (skia_opengl, skia_vulkan, femtovg_opengl - стандартната) или с софтуерен ренделер, за да видите дали това разрешава проблема.\nrust_loaded_preset = Заредена запазена настройка { $preset_idx }\nrust_file_already_exists = Файл \"{ $file }\" вече съществува и няма да бъде презаписан\nrust_error_removing_file_after_copy = Грешка при премахване на файла \"{ $file }\" (след копиране в различен дял), причина: { $reason }\nrust_error_copying_file = Грешка при копирането на \"{ $input }\" към \"{ $output }\", причина: { $reason }\nrust_loading_tags_cache = Зареждане на кеш със тагове\nrust_loading_fingerprints_cache = Зареждане на кеш с отпечатъци\nrust_saving_tags_cache = Запис на кеш с тагове\nrust_saving_fingerprints_cache = Запис на кеш с отпечатъци\nrust_loading_prehash_cache = Зареждане на prehash кеш\nrust_saving_prehash_cache = Запис на prehash кеш\nrust_loading_hash_cache = Зареждане на hash кеш\nrust_saving_hash_cache = Запис на hash кеш\nrust_loading_exif_cache = Зарежда EXIF кеш\nrust_saving_exif_cache = Запазване на EXIF кеш\nrust_scanning_name = Сканирано име на { $entries_checked } файл\nrust_scanning_size_name = Сканиране на размер и име на { $entries_checked } файл\nrust_scanning_size = Сканиране на размера на { $entries_checked } файл\nrust_scanning_file = Сканиране на { $entries_checked } файл\nrust_scanning_folder = Сканиране на { $entries_checked } папка\nrust_checked_tags = Проверени тагове за { $items_stats }\nrust_checked_content = Проверено съдържание за { $items_stats } ({ $size_stats })\nrust_compared_tags = Сравнени тагове за { $items_stats }\nrust_compared_content = Сравнено съдържание за { $items_stats }\nrust_hashed_images = Хеширани { $items_stats } изображения ({ $size_stats })\nrust_compared_image_hashes = Сравни хашовете на снимките от { $items_stats }\nrust_hashed_videos = Хеширани { $items_stats } видеа\nrust_created_thumbnails = Създадох миниатюри за { $items_stats } видеофайла\"default\"\nrust_checked_files = Проверени { $items_stats } файла ({ $size_stats })\nrust_checked_files_bad_extensions = Проверени { $items_stats } файла (с неподходящо разширение)\nrust_checked_files_bad_names = Проверени { $items_stats } файла (с неподходящо разширение)\nrust_checked_videos = Проверени { $items_stats } видеа ({ $size_stats })\nrust_analyzed_partial_hash = Анализиран частичен hash за { $items_stats } файлове ({ $size_stats })\nrust_analyzed_full_hash = Анализиран пълен hash за { $items_stats } файлове ({ $size_stats })\nrust_failed_to_rename_file = Не се мени файл { $old_path } в { $new_path }, грешка: { $error }\nrust_no_included_paths = Не може да се стартира сканиране, когато не са зададени включени пътища.\nrust_all_paths_referenced = Не може да се стартира сканиране, когато всички включени пътища са зададени като пътища с препратки, трябва да деактивирате отметката до входния път.\nrust_found_empty_folders = Намираме { $items_found } празни папки в { $time }\nrust_found_empty_files = Найдени са { $items_found } празни файла в { $time }\nrust_found_similar_images = Намерени са { $items_found } подобни изображения в { $groups } групи за { $time }\nrust_found_similar_videos = Намерени са { $items_found } подобри видеофайла в { $groups } групи за { $time }\nrust_found_similar_music_files = Найдени са { $items_found } подобни музикални файлове в { $groups } групи за { $time }\nrust_found_invalid_symlinks = Намерени { $items_found } невалидни символни свръзки в { $time }\nrust_found_temporary_files = Найдено { $items_found } променливи файлове в течeníе на { $time }\nrust_no_file_type_selected = Не можете да намерите разбити файлове без избор на вид файл.\nrust_found_broken_files = Намерил { $items_found } повреждени файлове,occupying { $size } в { $time }\nrust_found_bad_extensions = Найдени са { $items_found } файлове с грешни расширения в { $time }\nrust_found_bad_names = Намерени { $items_found } файла с лоши имена в { $time }\nrust_found_video_optimizer = Намерени { $items_found } файла за оптимизиране в { $time }\nrust_found_duplicate_files = Намерени са { $items_found } дублирани файла в { $groups } групи,occupying { $size } за { $time }\nrust_found_duplicate_files_no_lost_space = Найдени са { $items_found } дублиращи файлове в групи { $groups } във времето { $time }\nrust_found_big_files = Намерени { $items_found } големи файлове с размер { $size } в течност на { $time }\nrust_found_exif_files = Намерени { $items_found } файла(а) с EXIF данни в { $time }\nrust_cannot_load_preset = Не може да се зареди или смени preset { $preset_idx } – причина: { $reason }. Ще се използват настройки по подразбиране\nrust_saved_preset = Preset { $preset_idx } е запазен\nrust_cannot_save_preset = Не може да се запази preset { $preset_idx } – причина: { $reason }\nrust_reset_preset = Preset { $preset_idx } е нулиран\nrust_cannot_create_output_folder = Не може да се създаде изходна папка { $output_folder } – причина: { $error }\nrust_delete_summary = Изтрих { $deleted } стойност(и), не успях да убягна { $failed } стойности, от общия брой { $total } стойности\nrust_rename_summary = Променени { $renamed } стойности, неуспешно променени { $failed } стойности, от общия брой { $total } стойности\nrust_move_summary = Пренесли са { $moved } стойност(и), не успяхме да пренесем { $failed } стойност(и) от общия брой на { $total } стойност(и)\nrust_hardlink_summary = Хардлинковани { $hardlinked } елементи, не успяха да харлдлинкат { $failed } елементи, от общо { $total } елементи\nrust_symlink_summary = Свързани са { $symlinked } елемента, не са успели да се свържат { $failed } елемента, от общо { $total } елемента\nrust_optimize_video_summary = Оптимизирани { $optimized } видеа, неуспешни за оптимизация { $failed } видеа, от общо { $total } видеа\nrust_clean_exif_summary = Почистени EXIF от { $cleaned } файлове, не успя да почисти { $failed } файлове, от { $total } файлове\nrust_deleting_files = Изтриване на { $items_stats } файл(ове) ({ $size_stats })\nrust_deleting_no_size_files = Изтриване на { $items_stats } файл(ове)\nrust_renaming_files = Преименуване на { $items_stats } файл(ове)\nrust_moving_files = Преместване на { $items_stats } файл(ове) ({ $size_stats })\nrust_moving_no_size_files = Преместване на { $items_stats } файл(ове)\nrust_hardlinking_files = Хардлинкинг { $items_stats } файл ({ $size_stats })\nrust_hardlinking_no_size_files = Хардлинкиране { $items_stats } файл\nrust_symlinking_files = Свързване по символи на файла { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Свързване по символи на файла { $items_stats }\nrust_optimizing_videos = Оптимизирано { $items_stats } видео ({ $size_stats })\nrust_optimizing_no_size_videos = Оптимизирано { $items_stats } видео\nrust_cleaning_exif = Почистване на EXIF от { $items_stats } файл ({ $size_stats })\nrust_cleaning_no_size_exif = Почистване на EXIF от { $items_stats } файл\nrust_no_files_deleted = Няма избрана файлова или папочна структура за изтриване\nrust_no_files_renamed = Ни файлове, ни папки са избрана за пременяване на имейла\nrust_no_files_moved = Няма файлове или папки selectени за преместване\nrust_no_files_hardlinked = Няма избрани файлове или папки за хардурен линк\nrust_no_files_symlinked = Няма избрани файлове или папки за символично линкване\nrust_no_videos_optimized = Няма избрани видеа за оптимизация\nrust_no_exif_cleaned = Няма избрани файлове за почистване на EXIF\nrust_extracted_exif_tags = Извлечени EXIF тагове от { $items_stats } файлове ({ $size_stats })\nrust_delete_confirmation = Наистина ли искате да изтриете избраните елементи?\nrust_delete_confirmation_number_simple = { $items } артикула избрано.\nrust_delete_confirmation_number_groups = { $items } елемента се избрани в { $groups } групи.\nrust_delete_confirmation_selected_all_in_group = Всички избрани елементи във { $groups } групи.\nrust_move_confirmation = Наистина ли искате да преместите избраните елементи?\nrust_move_confirmation_number_simple = { $items } предмети избрани.\nrust_clean_exif_confirmation = Наистина ли искате да премахнете данните EXIF от избраните елементи?\nrust_clean_exif_confirmation_number_simple = { $items } предмети избрани.\nclean_exif_overwrite_files_text = Презапиши файлове\nrust_optimize_video_confirmation = Наистина ли искате да оптимизирате избраните видеоклипове?\nrust_optimize_video_confirmation_number_simple = { $items } предмети избрани.\nrust_hardlink_confirmation = Наистина ли искате да създадете твърди връзки за избраните елементи?\nrust_hardlink_confirmation_number_simple = { $items } предмети избрани.\nrust_symlink_confirmation = Наистина ли искате да създадете символични връзки за избраните елементи?\nrust_symlink_confirmation_number_simple = { $items } предмети избрани.\nrust_rename_confirmation = Наистина ли искате да преименувате избраните елементи?\nrust_rename_confirmation_number_simple = { $items } предмети избрани.\nrust_cache_processed_files = Обработени { $files } кеш файлове\nrust_cache_entries_stats = Премахнати са { $removed } записи от всички { $all }, { $left } останаха\nrust_cache_size_reduced = Намален размер на файловете в кеша с { $size }\nrust_cache_time_elapsed = Преминало време: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Неуспех при създаване на твърд линк за { $name } в { $target }, причина { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Избор\ncolumn_size = Размер\ncolumn_file_name = Име на файл\ncolumn_path = Път\ncolumn_modification_date = Дата на промяна\ncolumn_similarity = Приличие\ncolumn_dimensions = Размери\ncolumn_new_dimensions = Нови измерения\ncolumn_title = Заглавие\ncolumn_artist = Изпълнител\ncolumn_year = Година\ncolumn_bitrate = Битрейт\ncolumn_length = Продължителност\ncolumn_genre = Жанр\ncolumn_type_of_error = Тип грешка\ncolumn_symlink_name = Име на символна връзка\ncolumn_symlink_folder = Папка на символна връзка\ncolumn_destination_path = Целева директория\ncolumn_current_extension = Настоящо разширение\ncolumn_proper_extension = Правилно разширение\ncolumn_fps = БПС\ncolumn_codec = Кодек\ncolumn_duration = Дължина\ncolumn_exif_tags = EXIF Тагове\ncolumn_new_name = Ново име\n# Slint translations\nok_button = Ок\ncancel_button = Отказ\ndo_you_want_to_continue =\n    Бележки: Текстът е прекосен без промени в тон и стил. Разполагайте за специализирана форматиране или заместители, ако са нужни.\n    Превод:\n    Желирате да продължите?\nmain_window_title = Krokiet (Крокет) - Чистител на данни\nscan_button = Сканирай\nstop_button = Спри\nstop_text = Спри\nselect_button = Избери\nmove_button = Премести\ndelete_button = Изтрий\nsave_button = Запази\nsort_button = Сортирай\nrename_button = Преименувай\nmotto = Този програмен код е свободен за използване и завинаги ще бъде.\\nПроверете Лицензията MIT/GPL за детайли.\nunicorn = М May не gледате на сърче, но сърчето винаги ви гледа.\nrepository = Хранилище\ninstruction = Инструкции\ndonation = Дарение\ntranslation = Преводи\nincluded_paths = Включени пътища\nexcluded_paths = Изключени пътища\nref = Референция\npath = Път\ntool_duplicate_files = Повтарящи се файлове\ntool_empty_folders = Празни папки\ntool_big_files = Големи файлове\ntool_empty_files = Празни файлове\ntool_temporary_files = Временни файлове\ntool_similar_images = Сходни изображения\ntool_similar_videos = Подобни видеа\ntool_music_duplicates = Музикални дубликати\ntool_invalid_symlinks = Невалидни симлинкове\ntool_broken_files = Повредени файлове\ntool_bad_extensions = Повредени разширения\ntool_bad_names = Лоши имена\ntool_video_optimizer = Оптимизатор на видеоклипове\ntool_exif_remover = Премахващ EXIF\nsort_by_full_name = Сортиране по пълно име\nsort_by_selection = Сортиране по избор\nsort_reverse = Обратен ред\nselection_all = Избери всички\nselection_deselect_all = Отключи всичко\nselection_invert_selection = Инверсия на избора\nselection_the_biggest_size = Избери най-голям файл\nselection_the_biggest_resolution = Избери най-голямо резолюция\nselection_the_smallest_size = Избери най-малък файл\nselection_the_smallest_resolution = Избери най-малка резолюция\nselection_newest = Избери най-новото\nselection_oldest = Избери най-старото\nselection_shortest_path = Изберете най-краткия път\nselection_longest_path = Изберете най-дългата пътека\nstage_current = Текуща стъпка:\nstage_all = Всички стъпки:\nsubsettings = Поднастройка\nsubsettings_images_hash_size = Размер хеш\nsubsettings_images_resize_algorithm = Алгоритъм за оразмеряване\nsubsettings_images_ignore_same_size = Игнорирай изображения с еднакъв размер\nsubsettings_images_max_difference = Максимална разлика\nsubsettings_images_duplicates_hash_type = Тип хеш\nsubsettings_duplicates_check_method = Провери метод\nsubsettings_duplicates_name_case_sensitive = Големи/малки букви (само режим на име)\nsubsettings_biggest_files_sub_method = Метод\nsubsettings_biggest_files_sub_number_of_files = Брой файлове\nsubsettings_videos_max_difference = Максимална разлика\nsubsettings_videos_ignore_same_size = Игнорирай видеа с еднакъв размер\nsubsettings_music_audio_check_type = Тип аудио проверка\nsubsettings_music_approximate_comparison = Приблизително сравнение на тагове\nsubsettings_music_compared_tags = Сравнени тагове\nsubsettings_music_title = Заглавие\nsubsettings_music_artist = Изпълнител\nsubsettings_music_bitrate = Битрейт\nsubsettings_music_genre = Жанр\nsubsettings_music_year = Година\nsubsettings_music_length = Продължителност\nsubsettings_music_max_difference = Максимална разлика\nsubsettings_music_minimal_fragment_duration = Минимална продължителност на фрагмент\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Сравни в групи от подобни заглавия\nsubsettings_broken_files_type = Тип файлове за проверка\nsubsettings_broken_files_audio = Аудио\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Архив\nsubsettings_broken_files_image = Изображение\nsubsettings_broken_files_video = Видео\nsubsettings_broken_files_video_info = Използва ffmpeg/ffprobe. Доста бавно и може да открие педантични грешки дори ако файлът се възпроизвежда добре.\nsubsettings_bad_names_issues = Проверки на файлове\nsubsettings_bad_names_uppercase_extension = Голяма буква\nsubsettings_bad_names_uppercase_extension_hint = Намира файлове с главни букви в разширение (напр. .JPG, .Mp3) и предлага малки букви\nsubsettings_bad_names_emoji_used = Емоджи в име\nsubsettings_bad_names_emoji_used_hint = Намира файлове с емотикони (😀, 🎉, и др.) в името и предлага тяхното премахване\nsubsettings_bad_names_space_at_start_end = Водещи/затъркани интервали\nsubsettings_bad_names_space_at_start_end_hint = Намира файлове с интервали в началото или края на името и ги предлага да бъдат подрязани\nsubsettings_bad_names_non_ascii = Не-ASCII знаци\nsubsettings_bad_names_non_ascii_hint = Намира не-ASCII знаци (ą, ć, ñ, и др.) и предлага замяна с ASCII еквиваленти (a, c, n) или премахване, ако няма съответствие\nsubsettings_bad_names_restricted_charset = Ограничен набор знаци\nsubsettings_bad_names_restricted_charset_hint = Транслитерира не-ASCII знаци към ASCII, след което намира файлове, съдържащи знаци извън 0-9a-zA-Z и потребителски дефинирани разрешени знаци\nsubsettings_bad_names_allowed_chars = Разрешени знаци\nsubsettings_bad_names_remove_duplicated = Дуплицирани знаци\nsubsettings_bad_names_remove_duplicated_hint = Намира последователно дублирани не-алфанумерови знаци (напр. \"file---name..txt\") и предлага премахване на дублиранията\nsettings_global_settings = Глобални настройки\nsettings_dark_theme = Тъмна тема\nsettings_show_only_icons = Покажи само икони\nsettings_excluded_items = Изключени елементи:\nsettings_allowed_extensions = Позволени разширения:\nsettings_excluded_extensions = Изключени разширения:\nsettings_file_size = Размер на файл (килобайта)\nsettings_minimum_file_size = Мин:\nsettings_maximum_file_size = Макс:\nsettings_recursive_search = Рекурсивно търсене\nsettings_use_cache = Използвай кеш\nsettings_save_as_json = Запиши кеш също и като JSON\nsettings_move_to_trash = Премести изтритите файлове в кошчето\nsettings_ignore_other_filesystems = Игнориране на други файлови системи (само за Linux)\nsettings_delete_outdated_cache_entries = Изтрийте автоматично остарели записи в кеша\nsettings_delete_outdated_cache_entries_hint = Когато е активиран, приложението ще проверява по време на зареждане на кеша (максимум веднъж седмично), дали записите в кеша все още сочат към съществуващи и непокътнати файлове/данни\nsettings_hide_hard_links = Скрий твърди връзки\nsettings_hide_hard_links_hint = Скрий трудни връзки към еднакви файлове в резултатите\nsettings_thread_number = Брой на нишки\nsettings_restart_required = ---Трябва да рестартирате приложението за да се приложат настройките за брой нишки---\nsettings_duplicate_image_preview = Визуализация на изображение\nsettings_duplicate_minimal_hash_cache_size = Минимален размер на кеширани файлове - Хеш (КБ)\nsettings_duplicate_use_prehash = Използвай предхеширане\nsettings_duplicate_minimal_prehash_cache_size = Минимален размер на кеширани файлове - Предхеш (КБ)\nsettings_similar_images_show_image_preview = Визуализация на изображения\nsettings_application_scale_text = Мащаб на приложението\nsettings_application_scale_hint_text = Когато е активиран ръчният мащаб, това ви позволява да изберете персонализиран мащабен фактор, но напълно деактивира автоматичното мащабиране, базирано на DPI на монитора.\nsettings_restart_required_scale_text = ---Трябва да рестартирате приложението, за да приложите промените в мащаба---\nsettings_use_manual_application_scale_text = Използвайте ръчен мащаб за приложение\nsettings_video_thumbnails_preview = Визуализация на изображения\nsettings_open_config_folder = Отвори конфигурационна папка\nsettings_open_cache_folder = Отвори кеш папка\nsettings_language = Език\nsettings_current_preset = Настоящ preset:\nsettings_edit_name = Редактирай име\nsettings_choose_name_for_prefix = Избери име за префикс\nsettings_save = Запази\nsettings_load = Зареди\nsettings_reset = Нулирай\nsettings_similar_videos_tool = Инструмент за сходни видеа\nsettings_video_thumbnails_clear_unused_thumbnails = Изтрийте неизползвани видео миниатюри, по-стари от 7 дни при стартиране на приложението\nsettings_video_thumbnails_header = Видео миниатюри\nsettings_video_thumbnails_generate = Генерирай миниатюри\nsettings_video_thumbnails_position = Позиция на миникарта в видео (%)\nsettings_video_thumbnails_generate_grid = Генериране на мрежа от миниатюри вместо едно изображение\nsettings_video_thumbnails_generate_grid_hint = Генерирането на множество изображения в мрежа е много по-бавно от генерирането на един миниатюрен образ\nsettings_video_thumbnails_grid_tiles_per_side = Брой плочки на страна в миниатюрната мрежа\nsettings_video_thumbnails_grid_tiles_per_side_hint = Брой на миниатюрни плочки на страна в мрежата. Например, избиране на 2 създава 2 x 2 мрежа, което води до една миниатюра, състояща се от 4 изображения.\nsettings_similar_images_tool = Инструмент за сходни изображения\nsettings_general_settings = Общи настройки\nsettings_cache_header_text = Настройки на кеша\nsettings_clean_cache_button_text = Почисти остарялата кеш\nsettings_settings = Настройки\nsettings_load_tabs_sizes_at_startup = Зареди размера на табове при стартиране\nsettings_load_windows_size_at_startup = Зареди размера на прозореца при стартиране\nsettings_limit_lines_of_messages = Ограничете съобщенията до 500 реда (работно решение за медленния TextEdit вidget)\nsettings_play_audio_on_scan_completion_text = Пусни звук, когато сканирането завърши успешно\nsettings_audio_feature_hint_text = Наличен само при компилиране с аудио функция\nsettings_audio_env_variable_hint_text = Могат да се променят звуците, като се зададе променливата на околната среда KROKIET_AUDIO_STOP_FILE към валичен път към аудио файл\npopup_save_title = Запазване на резултати\npopup_save_message = Това ще запази резултатите в 3 различни файла\npopup_rename_title = Преименуване на файлове\npopup_new_paths_title = Моля, добавете пътища по един на ред\npopup_move_title = Преместване на елементи\npopup_move_copy_checkbox = Копирай вместо преместване\npopup_move_preserve_folder_checkbox = Запази структурата на папките\nmove_confirmation_text = Наистина ли искате да преместите избраните елементи?\nrename_confirmation_text = Наистина ли искате да преименувате избраните елементи?\ndelete = Изтрий елементи\nstopping_scan = Спиране на сканиране, моля изчакайте….\nsearching = Търсене….\nsubsettings_videos_crop_detect = Метод за детекция на кроп\nsubsettings_videos_skip_forward_amount = Прескочи интервал [с]\nsubsettings_videos_vid_hash_duration = Интервал на видео хеш\nsettings_cache_number_size_text = Качеството на файловете от кеш: { $size }, брой файла: { $number }\nsettings_video_thumbnails_number_size_text = Видео тумблнатъл размер: { $size }, брой файлове: { $number }\nsettings_log_number_size_text = Размер лог файлов: { $size }, брой файлове: { $number }\npopup_clean_cache_title_text = Изчисти остарялата кеш\npopup_clean_cache_confirmation_text = Наистина ли искате да изтриете остарели записи в кеша? Това ще премахне записи в кеша за файлове, които вече не съществуват или са били променени.\npopup_clean_cache_progress_text = Обработка на кеширан файл:\npopup_clean_cache_current_file_text = Текущ файл:\npopup_clean_cache_file_progress_text = Текущ напредък на файла:\npopup_clean_cache_overall_progress_text = Общ напредък:\npopup_clean_cache_stopped_by_user_text = Почистването на кеша беше спряно от потребителя\npopup_clean_cache_finished_text = Почистването на кеша приключи успешно!\npopup_clean_cache_error_details_text = Детайли за грешка:\npopup_clean_cache_files_with_errors = Файлове с грешки:\nsubsettings_video_optimizer_mode = Режим\nsubsettings_video_optimizer_crop_type = Тип култура\nsubsettings_video_optimizer_black_pixel_threshold = Черен Пикселен Праг\nsubsettings_video_optimizer_black_pixel_threshold_hint = Максимална RGB стойност за всеки пикселен канал да се счита за черен (0-128). По подразбиране: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Черен Бар Минимален Процент\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Минимален процент на черни пиксели в ред/колона, за да се счита за черна лента (50-100). По подразбиране: 90\nsubsettings_video_optimizer_max_samples = Макс Примери\nsubsettings_video_optimizer_max_samples_hint = Максимален брой кадри за анализ на видео (5-1000). По подразбиране: 60\nsubsettings_video_optimizer_min_crop_size = Минимум Размер на Отрязъка\nsubsettings_video_optimizer_min_crop_size_hint = Минимален брой пиксели за изрязване от всяка страна (1-1000). По-малките изрязвания се игнорират. По подразбиране: 5\nsubsettings_video_optimizer_video_codec = Видео кодек\nsubsettings_video_optimizer_excluded_codecs = Изключени кодеци\nsubsettings_video_optimizer_video_quality = Видео качество (CRF)\nsubsettings_reset = Ресет\nsubsettings_exif_ignored_tags_text = Игнорирани тагове:\nsubsettings_exif_ignored_tags_hint_text = Списък с разделителни знаци, разделени с кама, на етикети, които да бъдат изключени от сканирането (напр. GPS, Thumbnail). Някои етикети, като ImageWidth в TIFF файлове, са скрити, за да се предотврати повреждането на изображението.\nclean_button_text = Чист\nclean_text = Чисти EXIF данни\nclean_confirmation_text = Наистина ли искате да премахнете данните EXIF от избраните елементи?\ncrop_videos_text = Кроснати видеа\ncrop_video_confirmation_text = Наистина ли искате да изрежете избраните видеоклипове?\ncrop_reencode_video_text = Прекодиране на видео\nreencode_videos_text = Прекодиране на видеоклипове\noptimize_button_text = Оптимизирай\noptimize_confirmation_text = Наистина ли искате да прекодирате избраните видеоклипове?\noptimize_fail_if_bigger_text = Неуспех, ако оптимизираният файл е по-голям\noptimize_overwrite_files_text = Презапиши файлове\noptimize_limit_video_size_text = Ограничи размер на видеото\noptimize_max_width_text = Максимална ширина:\noptimize_max_height_text = Максимална височина:\nhardlink_button_text = Хардлинк\nhardlink_text = Създай твърди връзки\nhardlink_confirmation_text = Наистина ли искате да създадете твърди връзки за избраните елементи?\nsoftlink_button_text = Softlink\nsoftlink_text = Създай символични връзки\nsoftlink_confirmation_text = Наистина ли искате да създадете символики (symlinks) за избраните елементи?\n"
  },
  {
    "path": "krokiet/i18n/cs/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Kritická chyba při spuštění aplikace\nrust_init_error_message = \n        Kritická chyba se vyskytla při spuštění aplikace:\n\n        { $error_message }\n\n        Může to být způsobeno chybějícími nebo nefunkčními ovladači OpenGL/Vulkan, spuštěním aplikace ve virtuálním stroji nebo chybou v Krokiet nebo jedné z jeho knihoven.\n\n        Můžete zkusit spustit různé buildy (skia_opengl, skia_vulkan, femtovg_opengl - výchozí) nebo s grafickým renderem, abyste zjistili, zda to vyřeší problém.\nrust_loaded_preset = Načtená předvolba { $preset_idx }\nrust_file_already_exists = Soubor \"{ $file }\" již existuje a nebude přepsán\nrust_error_removing_file_after_copy = Chyba při odstraňování souboru \"{ $file }\" (po kopírování do jiného oddílu), důvod: { $reason }\nrust_error_copying_file = Chyba při kopírování \"{ $input }\" do \"{ $output }\", důvod: { $reason }\nrust_loading_tags_cache = Načítání mezipaměti značek\nrust_loading_fingerprints_cache = Načítání mezipaměti otisků prstů\nrust_saving_tags_cache = Ukládání mezipaměti štítků\nrust_saving_fingerprints_cache = Ukládání mezipaměti otisků prstů\nrust_loading_prehash_cache = Načítání mezipaměti rozpoznání\nrust_saving_prehash_cache = Ukládání mezipaměti rozpoznání\nrust_loading_hash_cache = Načítání hash keše\nrust_saving_hash_cache = Ukládání keše hash\nrust_loading_exif_cache = Načítá EXIF mezipaměť\nrust_saving_exif_cache = Ukládání EXIF mezipaměti\nrust_scanning_name = Skenování názvu { $entries_checked } souboru\nrust_scanning_size_name = Velikost prohledávání a název souboru { $entries_checked }\nrust_scanning_size = Skenování velikosti { $entries_checked } souboru\nrust_scanning_file = Skenování { $entries_checked } souboru\nrust_scanning_folder = Skenování složky { $entries_checked }\nrust_checked_tags = Kontrolované štítky { $items_stats }\nrust_checked_content = Kontrolovaný obsah { $items_stats } ({ $size_stats })\nrust_compared_tags = Porovnávané štítky { $items_stats }\nrust_compared_content = Porovnávaný obsah { $items_stats }\nrust_hashed_images = Hashed { $items_stats } obrázky ({ $size_stats })\nrust_compared_image_hashes = Porovnávané hashy obrázků { $items_stats }\nrust_hashed_videos = Hashované { $items_stats } videa\nrust_created_thumbnails = Vytvořené náhledy pro videa { $items_stats }\nrust_checked_files = Kontrola { $items_stats } souboru ({ $size_stats })\nrust_checked_files_bad_extensions = Kontrolovaný soubor { $items_stats }\nrust_checked_files_bad_names = Zkontrolován soubor { $items_stats }\nrust_checked_videos = Zkontrolováno { $items_stats } videí ({ $size_stats })\nrust_analyzed_partial_hash = Analyzováno částečné hash { $items_stats } souborů ({ $size_stats })\nrust_analyzed_full_hash = Analyzováno plné hash { $items_stats } souborů ({ $size_stats })\nrust_failed_to_rename_file = Nepodařilo se přejmenovat soubor { $old_path } na { $new_path }, chyba: { $error }\nrust_no_included_paths = Nemožno spustit skenovanie, ak nie sú nastavené zahrnuté cesty.\nrust_all_paths_referenced = Nemožno spustit skenovanie, keď sú všetky zahrnuté cesty nastavené ako referenčné cesty, potrebujete vypnúť políčko pre odkaz pre vstupnú cestu.\nrust_found_empty_folders = Nalezeno { $items_found } prázdné složky v { $time }\nrust_found_empty_files = Nalezeno { $items_found } prázdných souborů v { $time }\nrust_found_similar_images = Nalezeno { $items_found } podobných obrázků v { $groups } skupinách v { $time }\nrust_found_similar_videos = Nalezeno { $items_found } podobných video souborů v { $groups } skupinách v { $time }\nrust_found_similar_music_files = Nalezeno { $items_found } podobných hudebních souborů v { $groups } skupinách v { $time }\nrust_found_invalid_symlinks = Nalezeno { $items_found } neplatných symbolických odkazů v { $time }\nrust_found_temporary_files = Nalezeno { $items_found } dočasných souborů v { $time }\nrust_no_file_type_selected = Nelze najít poškozené soubory bez vybraného typu souboru.\nrust_found_broken_files = Nalezeno { $items_found } rozbitých souborů s { $size } v { $time }\nrust_found_bad_extensions = Nalezeno { $items_found } souborů s chybnými příponami v { $time }\nrust_found_bad_names = Nalezlo se { $items_found } souborů s chybnými názvy v { $time }\nrust_found_video_optimizer = Nalezlo { $items_found } souborů pro optimalizaci v { $time }\nrust_found_duplicate_files = Nalezeno { $items_found } duplicitních souborů v { $groups } skupinách při převzetí { $size } v { $time }\nrust_found_duplicate_files_no_lost_space = Nalezeno { $items_found } duplicitních souborů v { $groups } skupinách v { $time }\nrust_found_big_files = Nalezeno { $items_found } velkých souborů o velikosti { $size } v { $time }\nrust_found_exif_files = Nalezlo { $items_found } souborů s EXIF daty v { $time }\nrust_cannot_load_preset = Nelze změnit a načíst předvolbu { $preset_idx } - důvod { $reason }, použijte místo toho výchozí nastavení\nrust_saved_preset = Uložená předvolba { $preset_idx }\nrust_cannot_save_preset = Nelze uložit předvolbu { $preset_idx } - důvod { $reason }\nrust_reset_preset = Resetovat výchozí nastavení { $preset_idx }\nrust_cannot_create_output_folder = Nelze vytvořit výstupní složku { $output_folder }, důvod: { $error }\nrust_delete_summary = Smazáno { $deleted } položek, nepodařilo se odstranit { $failed } položky, z { $total } položek\nrust_rename_summary = Přejmenováno { $renamed } položek, nepodařilo se přejmenovat { $failed } položky, z { $total } položek\nrust_move_summary = Přesunuto { $moved } položek, nelze přesunout { $failed } položky, z { $total } položek\nrust_hardlink_summary = Tvrdě odkazované { $hardlinked } položky, selhalo tvrdé odkazování { $failed } položek, z { $total } položek\nrust_symlink_summary = Symbolicky odkazované { $symlinked } položky, selhalo symbolické odkazování { $failed } položek, z celkem { $total } položek\nrust_optimize_video_summary = Optimalizované { $optimized } videa, neoptimalizované { $failed } videa, z celkem { $total } videí\nrust_clean_exif_summary = Vyčištěné EXIF z { $cleaned } souborů, nebylo možné vyčistit { $failed } souborů, z celkem { $total } souborů\nrust_deleting_files = Mazání { $items_stats } souboru ({ $size_stats })\nrust_deleting_no_size_files = Mazání { $items_stats } souboru\nrust_renaming_files = Přejmenování { $items_stats } souboru\nrust_moving_files = Přesouvání { $items_stats } souboru ({ $size_stats })\nrust_moving_no_size_files = Přesouvání { $items_stats } souboru\nrust_hardlinking_files = Tvrdé odkazování { $items_stats } soubor ({ $size_stats })\nrust_hardlinking_no_size_files = Tvrdé odkazování { $items_stats } soubor\nrust_symlinking_files = Symlinkování souboru { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Symlink { $items_stats } soubor\nrust_optimizing_videos = Optimalizovaný { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimalizovaný { $items_stats } video\nrust_cleaning_exif = Odstraňování EXIF z souboru { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = Odstraňování EXIF z souboru { $items_stats }\nrust_no_files_deleted = Pro smazání nejsou vybrány žádné soubory ani složky\nrust_no_files_renamed = Pro přejmenování nejsou vybrány žádné soubory ani složky\nrust_no_files_moved = Pro přesunutí nejsou vybrány žádné soubory ani složky\nrust_no_files_hardlinked = Žádné soubory ani složky vybrány pro tvrdé odkazování\nrust_no_files_symlinked = Žádné soubory ani složky vybrány pro symlinkování\nrust_no_videos_optimized = Žádné videa vybrány pro optimalizaci\nrust_no_exif_cleaned = Žádné soubory vybrány pro čištění EXIF\nrust_extracted_exif_tags = Extrahované EXIF tagy z { $items_stats } souborů ({ $size_stats })\nrust_delete_confirmation = Jste si jisti, že chcete odstranit vybrané položky?\nrust_delete_confirmation_number_simple = { $items } vybraných položek.\nrust_delete_confirmation_number_groups = { $items } položek vybráno ve skupinách { $groups }.\nrust_delete_confirmation_selected_all_in_group = Všechny položky vybrané ve skupinách { $groups }.\nrust_move_confirmation = Jste si jistí, že chcete přesunout vybrané položky?\nrust_move_confirmation_number_simple = { $items } položek vybráno.\nrust_clean_exif_confirmation = Jste si jisti, že chcete odstranit EXIF data z vybraných položek?\nrust_clean_exif_confirmation_number_simple = { $items } položek vybráno.\nclean_exif_overwrite_files_text = Přepsat soubory\nrust_optimize_video_confirmation = Jste si jistí, že chcete optimalizovat vybrané videa?\nrust_optimize_video_confirmation_number_simple = { $items } položek vybráno.\nrust_hardlink_confirmation = Jste si jisti, že chcete vytvořit tvrdé odkazy pro vybrané položky?\nrust_hardlink_confirmation_number_simple = { $items } položek vybráno.\nrust_symlink_confirmation = Jste si jisti, že chcete vytvořit symlinky pro vybrané položky?\nrust_symlink_confirmation_number_simple = { $items } položek vybráno.\nrust_rename_confirmation = Jste si jisti, že chcete přejmenovat vybrané položky?\nrust_rename_confirmation_number_simple = { $items } položek vybráno.\nrust_cache_entries_stats = Odstraněny { $removed } záznamy z { $all }, { $left } zbývá\nrust_cache_size_reduced = Snížená velikost souborů mezipaměti o { $size }\nrust_cache_time_elapsed = Čas uplynul: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Selhalo vytvoření pevného odkazu { $name } na { $target }, důvod { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Výběr\ncolumn_size = Velikost\ncolumn_file_name = Název souboru\ncolumn_path = Cesta\ncolumn_modification_date = Datum změny\ncolumn_similarity = Podobnost\ncolumn_dimensions = Rozměry\ncolumn_new_dimensions = Nové dimenze\ncolumn_title = Hlava 1 – Celkem\ncolumn_artist = Umělec\ncolumn_year = Rok\ncolumn_bitrate = Bitová sazba\ncolumn_length = Délka\ncolumn_genre = Žánr\ncolumn_type_of_error = Typ chyby\ncolumn_symlink_name = Název symbolického odkazu\ncolumn_symlink_folder = Složka symbolického odkazu\ncolumn_destination_path = Cílová cesta\ncolumn_current_extension = Aktuální rozšíření\ncolumn_proper_extension = Řádné rozšíření\ncolumn_fps = FPS\ncolumn_codec = Kodek\ncolumn_duration = Doba trvání\ncolumn_exif_tags = EXIF tagy\ncolumn_new_name = Nový název\n# Slint translations\nok_button = Dobře\ncancel_button = Zrušit\ndo_you_want_to_continue = Chcete pokračovat?\nmain_window_title = Krokiet - Čistič dat\nscan_button = Skenovat\nstop_button = Zastavit\nstop_text = Zastavit\nselect_button = Vybrat\nmove_button = Přesunout\ndelete_button = Vymazat\nsave_button = Uložit\nsort_button = Seřadit\nrename_button = Přejmenovat\nmotto = Tento program je zdarma a bude vždy používán.\\nPodrobnosti naleznete v licenci MIT/GPL.\nunicorn = Nemusíte se podívat na jednorožce, ale jednorožec se na vás vždy dívá.\nrepository = Repozitář\ninstruction = Instrukce\ndonation = Darovat\ntranslation = Překlad\nincluded_paths = Zahrnuté cesty\nexcluded_paths = Vyloučené cesty\nref = Pozn\npath = Cesta\ntool_duplicate_files = Duplikovat soubory\ntool_empty_folders = Prázdné složky\ntool_big_files = Velké soubory\ntool_empty_files = Prázdné soubory\ntool_temporary_files = Dočasné soubory\ntool_similar_images = Podobné obrázky\ntool_similar_videos = Podobná videa\ntool_music_duplicates = Hudební duplikáty\ntool_invalid_symlinks = Neplatné symbolické odkazy\ntool_broken_files = Rozbité soubory\ntool_bad_extensions = Špatná rozšíření\ntool_bad_names = Špatná jména\ntool_video_optimizer = Video Optimalizátor\ntool_exif_remover = Odstraňovač EXIF\nsort_by_full_name = Seřadit podle celého jména\nsort_by_selection = Seřadit podle výběru\nsort_reverse = Obrácené pořadí\nselection_all = Vybrat vše\nselection_deselect_all = Zrušit výběr všech\nselection_invert_selection = Invertovat výběr\nselection_the_biggest_size = Vyberte největší velikost\nselection_the_biggest_resolution = Vyberte největší rozlišení\nselection_the_smallest_size = Vyberte nejmenší velikost\nselection_the_smallest_resolution = Vyberte nejmenší rozlišení\nselection_newest = Vybrat nejnovější\nselection_oldest = Vyberte nejstarší\nselection_shortest_path = Vyberte nejkratší cestu\nselection_longest_path = Vyberte nejdelší cestu\nstage_current = Aktuální fáze:\nstage_all = Všechny fáze:\nsubsettings = Podnastavení\nsubsettings_images_hash_size = Velikost hash\nsubsettings_images_resize_algorithm = Změnit velikost algoritmu\nsubsettings_images_ignore_same_size = Ignorovat obrázky se stejnou velikostí\nsubsettings_images_max_difference = Maximální rozdíl\nsubsettings_images_duplicates_hash_type = Typ Hash\nsubsettings_duplicates_check_method = Metoda kontroly\nsubsettings_duplicates_name_case_sensitive = Citlivost písmen (pouze režimy jména)\nsubsettings_biggest_files_sub_method = Metoda\nsubsettings_biggest_files_sub_number_of_files = Počet souborů\nsubsettings_videos_max_difference = Maximální rozdíl\nsubsettings_videos_ignore_same_size = Ignorovat videa se stejnou velikostí\nsubsettings_music_audio_check_type = Typ kontroly zvuku\nsubsettings_music_approximate_comparison = Přibližné porovnání štítků\nsubsettings_music_compared_tags = Porovnávané štítky\nsubsettings_music_title = Hlava 1 – Celkem\nsubsettings_music_artist = Umělec\nsubsettings_music_bitrate = Bitová供不应求率\nsubsettings_music_genre = Žánr\nsubsettings_music_year = Rok\nsubsettings_music_length = Délka\nsubsettings_music_max_difference = Maximální rozdíl\nsubsettings_music_minimal_fragment_duration = Minimální doba trvání fragmentu\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Porovnat v rámci skupin podobných názvů\nsubsettings_broken_files_type = Typ souborů ke kontrole\nsubsettings_broken_files_audio = Zvuk\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archiv\nsubsettings_broken_files_image = Obrázek\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Používá ffmpeg/ffprobe. Poměrně pomalé a může detekovat pedantické chyby i když se soubor hraje v pořádku.\nsubsettings_bad_names_issues = Kontrola jmen souborů\nsubsettings_bad_names_uppercase_extension = Velké rozšíření\nsubsettings_bad_names_uppercase_extension_hint = Najde soubory s velkými písmeny v příkladu názvu souboru (např. .JPG, .Mp3) a navrhne verzi s malými písmeny\nsubsettings_bad_names_emoji_used = Emoji v jméně\nsubsettings_bad_names_emoji_used_hint = Najde soubory s emoji znaky (😀, 🎉, atd.) v názvu a navrhuje je odstranit\nsubsettings_bad_names_space_at_start_end = Vedení/závěrečné mezery\nsubsettings_bad_names_space_at_start_end_hint = Najde soubory s mezerami na začátku nebo na konci názvu a navrhuje je oříznout\nsubsettings_bad_names_non_ascii = Nekódované znaky\nsubsettings_bad_names_non_ascii_hint = Najde ne-ASCII znaky (ą, ć, ñ, atd.) a navrhuje je nahradit ASCII ekvivalenty (a, c, n) nebo je odstranit, pokud neexistuje mapování\nsubsettings_bad_names_restricted_charset = Omezená sada znaků\nsubsettings_bad_names_restricted_charset_hint = Transliteruje ne-ASCII znaky do ASCII, pak hledá soubory obsahující znaky mimo 0-9a-zA-Z a povolené znaky definované uživatelem\nsubsettings_bad_names_allowed_chars = Povolena znaků\nsubsettings_bad_names_remove_duplicated = Duplikované znaky\nsubsettings_bad_names_remove_duplicated_hint = Najde se opakující se duplicitní znaky, které nejsou alfanumerické (např. \"file---name..txt\") a navrhuje odstranění duplikátů\nsettings_global_settings = Globální nastavení\nsettings_dark_theme = Tmavý motiv\nsettings_show_only_icons = Zobrazit pouze ikony\nsettings_excluded_items = Vyloučené položky:\nsettings_allowed_extensions = Povolené rozšíření:\nsettings_excluded_extensions = Vyloučená rozšíření:\nsettings_file_size = Velikost souboru (kilobytů)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Maximálně:\nsettings_recursive_search = Rekurentní vyhledávání\nsettings_use_cache = Použít keš\nsettings_save_as_json = Ukládat mezipaměť také jako soubor JSON\nsettings_move_to_trash = Přesunout smazané soubory do koše\nsettings_ignore_other_filesystems = Ignorovat ostatní souborové systémy (pouze Linux)\nsettings_delete_outdated_cache_entries = Smažte automaticky zastaralé položky v mezipaměti\nsettings_delete_outdated_cache_entries_hint = Když je povoleno, aplikace prověří během načítání z mezipaměti (maximálně jednou za týden), zda stále ukazují uložené záznamy na existující a nezměněné soubory/data\nsettings_hide_hard_links = Skrýt pevné odkazy\nsettings_hide_hard_links_hint = Skrýt tvrdé odkazy na stejné soubory v výsledcích\nsettings_thread_number = Číslo vlákna\nsettings_restart_required = ---Pro použití změn ve vlákně musíte restartovat aplikaci ---\nsettings_duplicate_image_preview = Náhled obrázku\nsettings_duplicate_minimal_hash_cache_size = Minimální velikost uložených souborů v mezipaměti - Hash (KB)\nsettings_duplicate_use_prehash = Použít rozpoznání\nsettings_duplicate_minimal_prehash_cache_size = Minimální velikost uložených souborů v mezipaměti - Prehash (KB)\nsettings_similar_images_show_image_preview = Náhled obrázku\nsettings_application_scale_text = Škálování aplikace\nsettings_application_scale_hint_text = Když je povoleno ruční měřítko, umožňuje vám to vybrat vlastní škálovací faktor, ale zcela vypne automatické škálování na základě DPI monitoru.\nsettings_restart_required_scale_text = ---Potřebujete znovu spustit aplikaci, aby se aplikovaly změny ve měřítku---\nsettings_use_manual_application_scale_text = Použijte ruční měřicí stupnici\nsettings_video_thumbnails_preview = Předběžný náhled obrázku\nsettings_open_config_folder = Otevřít konfigurační složku\nsettings_open_cache_folder = Otevřít složku mezipaměti\nsettings_language = Jazyk\nsettings_current_preset = Aktuální přednastavení:\nsettings_edit_name = Upravit název\nsettings_choose_name_for_prefix = Zvolte název prefixu\nsettings_save = Uložit\nsettings_load = Načíst\nsettings_reset = Obnovit\nsettings_similar_videos_tool = Podobný nástroj pro videa\nsettings_video_thumbnails_clear_unused_thumbnails = Smažte nepoužívané videové náhledy starší než 7 dní při spuštění aplikace\nsettings_video_thumbnails_header = Video Miniatury\nsettings_video_thumbnails_generate = Generuj náhledy\nsettings_video_thumbnails_position = Miniatura pozice ve videu (%)\nsettings_video_thumbnails_generate_grid = Generovat dlaždicovou galerii namísto jedné obrázku\nsettings_video_thumbnails_generate_grid_hint = Generování více obrázků v mřížce je mnohem pomalejší než generování jednoho náhledu\nsettings_video_thumbnails_grid_tiles_per_side = Počet dlaždic na straně v miniaturovém gride\nsettings_video_thumbnails_grid_tiles_per_side_hint = Počet miniaturových dlaždic na straně v mřížce. Například výběr 2 vytvoří mřížku 2 x 2, což vede k jedné miniaturě složené z 4 obrázků.\nsettings_similar_images_tool = Podobný nástroj pro obrázky\nsettings_general_settings = Obecná nastavení\nsettings_cache_header_text = Nastavení mezipaměti\nsettings_clean_cache_button_text = Vyčisti zastaralý mezipaměť\nsettings_settings = Nastavení\nsettings_load_tabs_sizes_at_startup = Načíst velikost záložek při startu\nsettings_load_windows_size_at_startup = Načíst velikost oken při spuštění\nsettings_limit_lines_of_messages = Omezit zprávy na 500 řádků (fungují pro pomalý TextEdit widget)\nsettings_play_audio_on_scan_completion_text = Přehrát zvuk při úspěšném dokončení skenování\nsettings_audio_feature_hint_text = Dostupné pouze při kompilaci s audio funkcí\nsettings_audio_env_variable_hint_text = Zvuk může být změněn, nastavením proměnné prostředí KROKIET_AUDIO_STOP_FILE na platný zvukový souborový cestu\npopup_save_title = Ukládání výsledků\npopup_save_message = Tímto uložíte výsledky do 3 různých souborů\npopup_rename_title = Přejmenovávání souborů\npopup_new_paths_title = Prosím přidejte cesty jednu za každou řádek\npopup_move_title = Přesouvání souborů\npopup_move_copy_checkbox = Kopírovat soubory místo přesunutí\npopup_move_preserve_folder_checkbox = Zachovat strukturu složek\nmove_confirmation_text = Jste si jistí, že chcete přesunout vybrané položky?\nrename_confirmation_text = Jste si jisti, že chcete přejmenovat vybrané položky?\ndelete = Odstranit položky\nstopping_scan = Zastavuji skenování, čekejte prosím...\nsearching = Vyhledávání...\nsubsettings_videos_crop_detect = Oříznout metodu detekce\nsubsettings_videos_skip_forward_amount = Přeskočit trvání [s]\nsubsettings_videos_vid_hash_duration = Doba trvání hash videa\nsettings_cache_number_size_text = Velikost souborů mezipaměti: { $size }, počet souborů: { $number }\nsettings_video_thumbnails_number_size_text = Velikost náhledů videa: { $size }, počet souborů: { $number }\nsettings_log_number_size_text = Velikost souborů protokolu: { $size }, počet souborů: { $number }\npopup_clean_cache_title_text = Vyčisti zastaralý mezipaměť\npopup_clean_cache_confirmation_text = Jste si jistí, že chcete vymazat zastaralé položky v mezipaměti? To odstraní položky v mezipaměti pro soubory, které již neexistují nebo byly změněny.\npopup_clean_cache_progress_text = Zpracovává se soubor mezipaměti:\npopup_clean_cache_current_file_text = Soubor:\npopup_clean_cache_file_progress_text = Souborový průběh:\npopup_clean_cache_overall_progress_text = Celkový pokrok:\npopup_clean_cache_stopped_by_user_text = Úklid mezipaměti byl uživatelem zastaven\npopup_clean_cache_finished_text = Úklid mezipaměti dokončen úspěšně!\npopup_clean_cache_error_details_text = Chyby:\npopup_clean_cache_files_with_errors = Soubory s chybami:\nsubsettings_video_optimizer_mode = Režim\nsubsettings_video_optimizer_crop_type = Typ kategorie\nsubsettings_video_optimizer_black_pixel_threshold = Černá pixelová prahová hodnota\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maximální RGB hodnota pro každý barevný kanál, která má být považována za černou (0-128). Výchozí: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Černá lišta Minimální procento\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimální procento černých pixelů v řadě/sloupcové, aby se považovala za černou lištu (50-100). Výchozí: 90\nsubsettings_video_optimizer_max_samples = Maximální počet vzorků\nsubsettings_video_optimizer_max_samples_hint = Maximální počet snímků k analýze na video (5-1000). Výchozí: 60\nsubsettings_video_optimizer_min_crop_size = Minimální velikost ořezu\nsubsettings_video_optimizer_min_crop_size_hint = Minimální počet pixelů pro prořezání na kterékoli straně (1-1000). Menší prořezávky se ignorují. Výchozí: 5\nsubsettings_video_optimizer_video_codec = Kodek videa\nsubsettings_video_optimizer_excluded_codecs = Vyloučené kodeky\nsubsettings_video_optimizer_video_quality = Video kvalita (CRF)\nsubsettings_reset = Obnovit\nsubsettings_exif_ignored_tags_text = Ignorované tagy:\nsubsettings_exif_ignored_tags_hint_text = Červy oddělené čárkami seznam štítků, které se mají vyloučit z skenování (např. GPS, Miniatura). Některé štítky, jako například ImageWidth ve formátech TIFF, jsou skryté, aby se zabránilo poškození obrázku.\nclean_button_text = Čistý\nclean_text = Čistá EXIF data\nclean_confirmation_text = Jste si jisti, že chcete odstranit EXIF data z vybraných položek?\ncrop_videos_text = Střihejte videa\ncrop_video_confirmation_text = Jste si jistí, že chcete oříznout vybrané videa?\ncrop_reencode_video_text = Rekóduj video\nreencode_videos_text = Znovu zakódujte videa\noptimize_button_text = Optimalizovat\noptimize_confirmation_text = Jste si jistí, že chcete znovu zakódovat vybrané videa?\noptimize_fail_if_bigger_text = Selhání, pokud je optimalizovaný soubor větší\noptimize_overwrite_files_text = Přepsat soubory\noptimize_limit_video_size_text = Omezte velikost videa\noptimize_max_width_text = Max šířka:\noptimize_max_height_text = Max výška:\nhardlink_button_text = Tvrdý odkaz\nhardlink_text = Vytvoř tvrdé odkazy\nhardlink_confirmation_text = Jste si jisti, že chcete vytvořit tvrdé odkazy pro vybrané položky?\nsoftlink_button_text = Měkký odkaz\nsoftlink_text = Vytvořit softlinky\nsoftlink_confirmation_text = Jste si jisti, že chcete vytvořit softlinky (symlinky) pro vybrané položky?\n\nrust_cache_processed_files = Zpracováno { $files } souborů mezipaměti"
  },
  {
    "path": "krokiet/i18n/de/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Kritischer Fehler während des App-Startlaufs\nrust_init_error_message = \n        Beim Starten der Anwendung ist ein kritischer Fehler aufgetreten:\n\n        { $error_message }\n\n        Dies kann durch fehlende oder fehlerhafte OpenGL/Vulkan-Treiber, das Ausführen der Anwendung in einer virtuellen Maschine oder einem Fehler in Krokiet oder einer seiner Bibliotheken verursacht werden.\n\n        Sie können versuchen, verschiedene Builds (skia_opengl, skia_vulkan, femtovg_opengl - Standard) oder mit dem Software-Renderer auszuführen, um zu sehen, ob das Problem dadurch behoben wird.\nrust_loaded_preset = Geladene Voreinstellung { $preset_idx }\nrust_file_already_exists = Datei \"{ $file }\" existiert bereits und wird nicht überschrieben\nrust_error_removing_file_after_copy = Fehler beim Entfernen der Datei \"{ $file }\" (nach Kopieren auf eine andere Partition), Grund: { $reason }\nrust_error_copying_file = Fehler beim Kopieren \"{ $input }\" zu \"{ $output }\", Grund: { $reason }\nrust_loading_tags_cache = Tag-Cache wird geladen\nrust_loading_fingerprints_cache = Cache für Fingerabdrücke wird geladen\nrust_saving_tags_cache = Speichere Tag-Cache\nrust_saving_fingerprints_cache = Speichere Fingerabdruck-Cache\nrust_loading_prehash_cache = Lade Vorhash-Cache\nrust_saving_prehash_cache = Speichere Vorhash-Cache\nrust_loading_hash_cache = Hash-Cache wird geladen\nrust_saving_hash_cache = Speichere Hash-Cache\nrust_loading_exif_cache = Lade EXIF-Cache\nrust_saving_exif_cache = Speichern EXIF-Cache\nrust_scanning_name = Scanne Name der { $entries_checked } Datei\nrust_scanning_size_name = Scanne Größe und Name der { $entries_checked } Datei\nrust_scanning_size = Scanne Größe der { $entries_checked } Datei\nrust_scanning_file = Scanne { $entries_checked } Datei\nrust_scanning_folder = Scanne { $entries_checked } Ordner\nrust_checked_tags = Überprüfte Tags von { $items_stats }\nrust_checked_content = Geprüfter Inhalt von { $items_stats } ({ $size_stats })\nrust_compared_tags = Verglichen mit Tags von { $items_stats }\nrust_compared_content = Verglichener Inhalt von { $items_stats }\nrust_hashed_images = Hashed { $items_stats } Bilder ({ $size_stats })\nrust_compared_image_hashes = Vergleiche Bildhashes von { $items_stats }\nrust_hashed_videos = Hashed { $items_stats } Videos\nrust_created_thumbnails = Thumbnails für { $items_stats } Videos erstellt\nrust_checked_files = Überprüft { $items_stats } Datei ({ $size_stats })\nrust_checked_files_bad_extensions = Überprüfte { $items_stats } Datei\nrust_checked_files_bad_names = Überprüft { $items_stats } Datei\nrust_checked_videos = Überprüft { $items_stats } Videos ({ $size_stats })\nrust_analyzed_partial_hash = Analysierter Teilhash der { $items_stats } Dateien ({ $size_stats })\nrust_analyzed_full_hash = Analysiert voller Hash der { $items_stats } Dateien ({ $size_stats })\nrust_failed_to_rename_file = Fehler beim Umbenennen der Datei { $old_path } in { $new_path }, Fehler: { $error }\nrust_no_included_paths = Kann den Scan nicht starten, wenn keine enthaltenen Pfade gesetzt sind.\nrust_all_paths_referenced = Kann den Scan nicht starten, wenn alle angegebenen Pfade als referenzierte Pfade gesetzt sind, Sie müssen das Kontrollkästchen neben dem Eingabepfad deaktivieren.\nrust_found_empty_folders = { $items_found } leere Ordner in { $time } gefunden\nrust_found_empty_files = { $items_found } leere Dateien in { $time } gefunden\nrust_found_similar_images = { $items_found } ähnliche Bilddateien in { $groups } Gruppen in { $time } gefunden\nrust_found_similar_videos = { $items_found } ähnliche Videodateien in { $groups } Gruppen in { $time } gefunden\nrust_found_similar_music_files = { $items_found } ähnliche Musikdateien in { $groups } Gruppen in { $time } gefunden\nrust_found_invalid_symlinks = { $items_found } ungültige Symlinks in { $time } gefunden\nrust_found_temporary_files = { $items_found } temporäre Dateien in { $time } gefunden\nrust_no_file_type_selected = defekte Dateien ohne ausgewählten Dateityp nicht gefunden.\nrust_found_broken_files = { $items_found } fehlerhafte Dateien gefunden, die { $size } in { $time } einnehmen\nrust_found_bad_extensions = { $items_found } Dateien mit schlechten Erweiterungen in { $time } gefunden\nrust_found_bad_names = Gefunden { $items_found } Dateien mit schlechten Namen in { $time}\nrust_found_video_optimizer = Gefunden { $items_found } Dateien zur Optimierung in { $time }\nrust_found_duplicate_files = { $items_found } Duplikate in { $groups } Gruppen gefunden, die { $size } in { $time } einnehmen\nrust_found_duplicate_files_no_lost_space = { $items_found } Duplikate in { $groups } Gruppen in { $time } gefunden\nrust_found_big_files = { $items_found } große Dateien mit der Größe { $size } in { $time } gefunden\nrust_found_exif_files = Gefunden { $items_found } Dateien mit EXIF-Daten in { $time }\nrust_cannot_load_preset = Kann die Voreinstellung { $preset_idx } nicht laden - Grund { $reason }, verwenden Sie stattdessen die Standardeinstellungen\nrust_saved_preset = Voreinstellung { $preset_idx } gespeichert\nrust_cannot_save_preset = Kann Voreinstellung { $preset_idx } nicht speichern - Grund { $reason }\nrust_reset_preset = Resetvorlage { $preset_idx }\nrust_cannot_create_output_folder = Ausgabeordner { $output_folder }kann nicht erstellt werden, Grund: { $error }\nrust_delete_summary = Gelöschte { $deleted } Elemente, Fehler beim Entfernen von { $failed } Objekten, von { $total } Elementen\nrust_rename_summary = Umbenennen von { $renamed } Objekten, fehlgeschlagen { $failed } Objekten, von { $total } Elementen\nrust_move_summary = Bewegte { $moved } Elemente, konnte { $failed } Elemente nicht verschieben, aus { $total } Elementen\nrust_hardlink_summary = Hardgelinkte { $hardlinked } Elemente, fehlgeschlagener Hardlink von { $failed } Elementen, von insgesamt { $total } Elementen\nrust_symlink_summary = Symlink{ $symlinked } Elemente, fehlgeschlagener Symlink{ $failed } Elemente, von { $total } Elementen\nrust_optimize_video_summary = Optimierte { $optimized } Videos, fehlgeschlagene Optimierungen für { $failed } Videos, von insgesamt { $total } Videos\nrust_clean_exif_summary = Bereinigte EXIF aus { $cleaned } Dateien, konnte { $failed } Dateien nicht bereinigen, von { $total } Dateien\nrust_deleting_files = { $items_stats } Datei löschen ({ $size_stats })\nrust_deleting_no_size_files = Lösche { $items_stats } Datei\nrust_renaming_files = { $items_stats } Datei umbenennen\nrust_moving_files = Verschiebe { $items_stats } Datei ({ $size_stats })\nrust_moving_no_size_files = Verschiebe { $items_stats } Datei\nrust_hardlinking_files = Hardlinking { $items_stats } Datei ({ $size_stats })\nrust_hardlinking_no_size_files = Hardlinking { $items_stats } Datei\nrust_symlinking_files = Symlinking { $items_stats } Datei ({ $size_stats })\nrust_symlinking_no_size_files = Symlinking { $items_stats } Datei\nrust_optimizing_videos = Optimiert { $items_stats } Video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimiertes { $items_stats } Video\nrust_cleaning_exif = Bereinigen EXIF aus { $items_stats } Datei ({ $size_stats })\nrust_cleaning_no_size_exif = Bereinigen EXIF aus { $items_stats } Datei\nrust_no_files_deleted = Keine Dateien oder Ordner zum Löschen ausgewählt\nrust_no_files_renamed = Keine Dateien oder Ordner zum Umbenennen ausgewählt\nrust_no_files_moved = Keine Dateien oder Ordner zum Verschieben ausgewählt\nrust_no_files_hardlinked = Keine Dateien oder Ordner für Hardlinks ausgewählt\nrust_no_files_symlinked = Keine Dateien oder Ordner für symbolische Verknüpfung ausgewählt\nrust_no_videos_optimized = Keine Videos für die Optimierung ausgewählt\nrust_no_exif_cleaned = Keine Dateien für die EXIF-Bereinigung ausgewählt\nrust_extracted_exif_tags = Extrahierte EXIF-Tags aus { $items_stats } Dateien ({ $size_stats })\nrust_delete_confirmation = Sind Sie sicher, dass Sie die ausgewählten Elemente löschen möchten?\nrust_delete_confirmation_number_simple = { $items } Elemente ausgewählt.\nrust_delete_confirmation_number_groups = { $items } Elemente ausgewählt in { $groups } Gruppen.\nrust_delete_confirmation_selected_all_in_group = Alle Elemente in { $groups } Gruppen ausgewählt.\nrust_move_confirmation = Sind Sie sicher, dass Sie die ausgewählten Elemente verschieben möchten?\nrust_move_confirmation_number_simple = { $items } Elemente ausgewählt.\nrust_clean_exif_confirmation = Sind Sie sicher, dass Sie die EXIF-Daten aus den ausgewählten Elementen entfernen möchten?\nrust_clean_exif_confirmation_number_simple = { $items } Elemente ausgewählt.\nclean_exif_overwrite_files_text = Überschreiben von Dateien\nrust_optimize_video_confirmation = Sind Sie sicher, dass Sie die ausgewählten Videos optimieren möchten?\nrust_optimize_video_confirmation_number_simple = { $items } Elemente ausgewählt.\nrust_hardlink_confirmation = Sind Sie sicher, dass Sie harte Links für die ausgewählten Elemente erstellen möchten?\nrust_hardlink_confirmation_number_simple = { $items } Elemente ausgewählt.\nrust_symlink_confirmation = Sind Sie sicher, dass Sie Symlinks für die ausgewählten Elemente erstellen möchten?\nrust_symlink_confirmation_number_simple = { $items } Elemente ausgewählt.\nrust_rename_confirmation = Sind Sie sicher, dass Sie die ausgewählten Elemente umbenennen möchten?\nrust_rename_confirmation_number_simple = { $items } Elemente ausgewählt.\nrust_cache_processed_files = Verarbeitete { $files } Cache-Dateien\nrust_cache_entries_stats = Entfernt { $removed } Einträge aus { $all }, { $left } Einträge übrig\nrust_cache_size_reduced = Reduzierte Cache-Dateigröße um { $size }\nrust_cache_time_elapsed = Zeit vergangen: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Fehlgeschlagenes Hardlink von { $name } zu { $target }, Grund { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Auswahl\ncolumn_size = Größe\ncolumn_file_name = Dateiname\ncolumn_path = Pfad\ncolumn_modification_date = Änderungsdatum\ncolumn_similarity = Ähnlichkeit\ncolumn_dimensions = Abmessungen\ncolumn_new_dimensions = Neue Dimensionen\ncolumn_title = Titel\ncolumn_artist = Künstler\ncolumn_year = Jahr\ncolumn_bitrate = Bitteschwindigkeit\ncolumn_length = Länge\ncolumn_genre = Genretyp\ncolumn_type_of_error = Typ des Fehlers\ncolumn_symlink_name = Symlink-Name\ncolumn_symlink_folder = Symlink-Ordner\ncolumn_destination_path = Zielpfad\ncolumn_current_extension = Aktuelle Erweiterung\ncolumn_proper_extension = Richtige Erweiterung\ncolumn_fps = FPS\ncolumn_codec = Codec\ncolumn_duration = Dauer\ncolumn_exif_tags = EXIF-Tags\ncolumn_new_name = Neuer Name\n# Slint translations\nok_button = Okay\ncancel_button = Abbrechen\ndo_you_want_to_continue = Möchten Sie fortfahren?\nmain_window_title = Krokiet - Datenreiniger\nscan_button = Scannen\nstop_button = Stoppen\nstop_text = Stoppen\nselect_button = Auswählen\nmove_button = Bewegen\ndelete_button = Löschen\nsave_button = Speichern\nsort_button = Sortieren\nrename_button = Umbenennen\nmotto = Dieses Programm ist frei zu benutzen und wird es immer sein.\\nSiehe die MIT/GPL Lizenz für Details.\nunicorn = Man kann nicht auf einen Einhorn schauen, aber das Einhorn schaut immer auf Sie.\nrepository = Speicherort\ninstruction = Anleitung\ndonation = Spende\ntranslation = Übersetzung\nincluded_paths = Einbezogene Pfade\nexcluded_paths = Ausgeschlossene Pfade\nref = Referenz\npath = Pfad\ntool_duplicate_files = Dateien duplizieren\ntool_empty_folders = Leere Ordner\ntool_big_files = Große Dateien\ntool_empty_files = Leere Dateien\ntool_temporary_files = Temporäre Dateien\ntool_similar_images = Ähnliche Bilder\ntool_similar_videos = Ähnliche Videos\ntool_music_duplicates = Musik-Duplikate\ntool_invalid_symlinks = Ungültige Symlinks\ntool_broken_files = Defekte Dateien\ntool_bad_extensions = Falsche Erweiterungen\ntool_bad_names = Schlechte Namen\ntool_video_optimizer = Video-Optimierer\ntool_exif_remover = Exif Entferner\nsort_by_full_name = Nach Vollnamen sortieren\nsort_by_selection = Nach Auswahl sortieren\nsort_reverse = Reihenfolge umkehren\nselection_all = Alles auswählen\nselection_deselect_all = Alle abwählen\nselection_invert_selection = Auswahl umkehren\nselection_the_biggest_size = Wählen Sie die größte Größe\nselection_the_biggest_resolution = Wählen Sie die größte Auflösung\nselection_the_smallest_size = Wählen Sie die kleinste Größe\nselection_the_smallest_resolution = Wählen Sie die kleinste Auflösung\nselection_newest = Neueste auswählen\nselection_oldest = Älteste auswählen\nselection_shortest_path = Wähle den kürzesten Pfad\nselection_longest_path = Wähle den längsten Pfad\nstage_current = Aktuelle Phase:\nstage_all = Alle Stufen:\nsubsettings = Untereinstellungen\nsubsettings_images_hash_size = Hash-Größe\nsubsettings_images_resize_algorithm = Algorithmus skalieren\nsubsettings_images_ignore_same_size = Bilder mit gleicher Größe ignorieren\nsubsettings_images_max_difference = Maximaler Unterschied\nsubsettings_images_duplicates_hash_type = Hash Typ\nsubsettings_duplicates_check_method = Prüfmethode\nsubsettings_duplicates_name_case_sensitive = Groß-/Kleinschreibung (nur Namensmodi)\nsubsettings_biggest_files_sub_method = Methode\nsubsettings_biggest_files_sub_number_of_files = Anzahl der Dateien\nsubsettings_videos_max_difference = Maximaler Unterschied\nsubsettings_videos_ignore_same_size = Ignoriere Videos mit gleicher Größe\nsubsettings_music_audio_check_type = Audio-Prüfungstyp\nsubsettings_music_approximate_comparison = Ungefährer Tag-Vergleich\nsubsettings_music_compared_tags = Verglichene Tags\nsubsettings_music_title = Titel\nsubsettings_music_artist = Künstler\nsubsettings_music_bitrate = Bitrate\nsubsettings_music_genre = Genretyp\nsubsettings_music_year = Jahr\nsubsettings_music_length = Länge\nsubsettings_music_max_difference = Maximaler Unterschied\nsubsettings_music_minimal_fragment_duration = Minimale Fragmentdauer\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Vergleiche innerhalb von Gruppen ähnlicher Titel\nsubsettings_broken_files_type = Art der zu prüfenden Dateien\nsubsettings_broken_files_audio = Audio\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archivieren\nsubsettings_broken_files_image = Bild\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Verwendet ffmpeg/ffprobe. Sehr langsam und kann pedantische Fehler erkennen, auch wenn die Datei fehlerfrei abgespielt wird.\nsubsettings_bad_names_issues = Dateibaumnachprüfungen\nsubsettings_bad_names_uppercase_extension = Großbuchstaben-Erweiterung\nsubsettings_bad_names_uppercase_extension_hint = Findet Dateien mit Großbuchstaben im Dateinamen (z. B. .JPG, .Mp3) und schlägt die Kleinbuchstaben-Version vor\nsubsettings_bad_names_emoji_used = Emoji in Name\nsubsettings_bad_names_emoji_used_hint = Findet Dateien mit Emojifizigen (😀, 🎉, etc.) im Namen und schlägt deren Löschung vor\nsubsettings_bad_names_space_at_start_end = Führende/nachgestellte Leerzeichen\nsubsettings_bad_names_space_at_start_end_hint = Findet Dateien mit Leerzeichen am Anfang oder Ende des Namens und schlägt das Abschneiden vor\nsubsettings_bad_names_non_ascii = Nicht-ASCII Zeichen\nsubsettings_bad_names_non_ascii_hint = Findet nicht-ASCII-Zeichen (ą, ć, ñ, usw.) und schlägt deren Ersetzung durch ASCII-Äquivalente (a, c, n) oder Löschung vor, wenn keine Zuordnung existiert\nsubsettings_bad_names_restricted_charset = Begrenzte Zeichenkodierung\nsubsettings_bad_names_restricted_charset_hint = Transliteriert nicht-ASCII Zeichen in ASCII, dann findet Dateien, die Zeichen außerhalb von 0-9a-zA-Z und von Benutzer definierten erlaubten Zeichen enthalten\nsubsettings_bad_names_allowed_chars = Erlaubte Zeichen\nsubsettings_bad_names_remove_duplicated = Duplizierte Zeichen\nsubsettings_bad_names_remove_duplicated_hint = Findet aufeinanderfolgende duplizierte nicht-alphanumerische Zeichen (z. B. \"file---name..txt\") und schlägt das Entfernen von Duplikaten vor\nsettings_global_settings = Globale Einstellungen\nsettings_dark_theme = Dunkles Design\nsettings_show_only_icons = Nur Symbole anzeigen\nsettings_excluded_items = Ausgeschlossenes Item:\nsettings_allowed_extensions = Erlaubte Erweiterungen:\nsettings_excluded_extensions = Ausgeschlossene Erweiterungen:\nsettings_file_size = Dateigröße (Kilobyte)\nsettings_minimum_file_size = Min. :\nsettings_maximum_file_size = Max:\nsettings_recursive_search = Rekursive Suche\nsettings_use_cache = Cache verwenden\nsettings_save_as_json = Cache auch als JSON-Datei speichern\nsettings_move_to_trash = Gelöschte Dateien in den Papierkorb verschieben\nsettings_ignore_other_filesystems = Andere Dateisysteme ignorieren (nur Linux)\nsettings_delete_outdated_cache_entries = Lösche automatisch veraltete Cache-Einträge\nsettings_delete_outdated_cache_entries_hint = Wenn aktiviert, wird die App während des Cache-Ladevorgangs (höchstens einmal pro Woche) überprüfen, ob die zwischengespeicherten Aufzeichnungen noch auf bestehende und unveränderte Dateien/Daten verweisen\nsettings_hide_hard_links = Verstecke harte Links\nsettings_hide_hard_links_hint = Verstecke harte Links zu gleichen Dateien in den Ergebnissen\nsettings_thread_number = Thread-Nummer\nsettings_restart_required = ---Sie müssen die App neu starten, um Änderungen in der Thread-Nummer anzuwenden---\nsettings_duplicate_image_preview = Bildvorschau\nsettings_duplicate_minimal_hash_cache_size = Minimale Größe der zwischengespeicherten Dateien - Hash (KB)\nsettings_duplicate_use_prehash = Benutze Vorhash\nsettings_duplicate_minimal_prehash_cache_size = Minimale Größe der zwischengespeicherten Dateien - Prehash (KB)\nsettings_similar_images_show_image_preview = Bildvorschau\nsettings_application_scale_text = Anwendungsmaßstab\nsettings_application_scale_hint_text = Wenn die manuelle Skalierung aktiviert ist, ermöglicht dies Ihnen, einen benutzerdefinierten Skalierungsfaktor auszuwählen, aber die automatische Skalierung basierend auf dem DPI des Monitors vollständig deaktiviert.\nsettings_restart_required_scale_text = ---Sie müssen die App neu starten, um Änderungen an der Skalierung anzuwenden---\nsettings_use_manual_application_scale_text = Verwenden Sie manuelle Anwendungsmaß\nsettings_video_thumbnails_preview = Bildvorschau\nsettings_open_config_folder = Konfigurationsordner öffnen\nsettings_open_cache_folder = Cache-Ordner öffnen\nsettings_language = Sprache\nsettings_current_preset = Aktuelle Voreinstellung:\nsettings_edit_name = Name bearbeiten\nsettings_choose_name_for_prefix = Name für Präfix auswählen\nsettings_save = Speichern\nsettings_load = Laden\nsettings_reset = Zurücksetzen\nsettings_similar_videos_tool = Ähnliches Videowerkzeug\nsettings_video_thumbnails_clear_unused_thumbnails = Lösche nicht verwendete Videobilder älter als 7 Tage beim App-Start\nsettings_video_thumbnails_header = Video-Thumbnails\nsettings_video_thumbnails_generate = Erstelle Vorschaubilder\nsettings_video_thumbnails_position = Miniaturbildposition in Video (%)\nsettings_video_thumbnails_generate_grid = Erstelle Raster-Miniaturansichten anstelle von Einzelbildern\nsettings_video_thumbnails_generate_grid_hint = Mehrere Bilder im Raster generieren ist viel langsamer als das Generieren eines einzelnen Miniaturbilds\nsettings_video_thumbnails_grid_tiles_per_side = Anzahl der Kacheln pro Seite im Miniaturgitter\nsettings_video_thumbnails_grid_tiles_per_side_hint = Anzahl der Miniaturtafeln pro Seite im Raster. Zum Beispiel erzeugt die Auswahl von 2 ein 2 x 2 Raster, was eine einzelne Miniaturtafel mit 4 Bildern ergibt.\nsettings_similar_images_tool = Ähnliches Bildwerkzeug\nsettings_general_settings = Allgemeine Einstellungen\nsettings_cache_header_text = Cache Einstellungen\nsettings_clean_cache_button_text = Leeren Sie den alten Cache\nsettings_settings = Einstellungen\nsettings_load_tabs_sizes_at_startup = Tabs beim Start laden\nsettings_load_windows_size_at_startup = Fenstergröße beim Start laden\nsettings_limit_lines_of_messages = Begrenze Nachrichten auf 500 Zeilen (Workaround für langsames TextEdit Widget)\nsettings_play_audio_on_scan_completion_text = Spiele Ton, wenn Scan erfolgreich abgeschlossen wurde\nsettings_audio_feature_hint_text = Nur verfügbar, wenn mit der Audio-Funktion kompiliert wird\nsettings_audio_env_variable_hint_text = Das Geräusch kann geändert werden, indem die Umgebungsvariable KROKIET_AUDIO_STOP_FILE auf einen gültigen Pfad zu einer Audiodatei gesetzt wird\npopup_save_title = Ergebnisse speichern\npopup_save_message = Dies wird Ergebnisse in 3 verschiedenen Dateien speichern\npopup_rename_title = Dateien umbenennen\npopup_new_paths_title = Bitte fügen Sie Pfade ein, jeweils in einer Zeile\npopup_move_title = Dateien verschieben\npopup_move_copy_checkbox = Dateien kopieren statt verschieben\npopup_move_preserve_folder_checkbox = Ordnerstruktur beibehalten\nmove_confirmation_text = Sind Sie sicher, dass Sie die ausgewählten Elemente verschieben möchten?\nrename_confirmation_text = Sind Sie sicher, dass Sie die ausgewählten Elemente umbenennen möchten?\ndelete = Elemente löschen\nstopping_scan = Scan wird gestoppt, bitte warten...\nsearching = Suche...\nsubsettings_videos_crop_detect = Methode zur Zuschneiden\nsubsettings_videos_skip_forward_amount = Überspringe Dauer [s]\nsubsettings_videos_vid_hash_duration = Hashdauer\nsettings_cache_number_size_text = Cache-Dateigröße: { $size }, Anzahl der Dateien: { $number }\nsettings_video_thumbnails_number_size_text = Größe der Video-Vorschaubilder: { $size }, Anzahl der Dateien: { $number }\nsettings_log_number_size_text = Log-Dateigröße: { $size }, Anzahl der Dateien: { $number }\npopup_clean_cache_title_text = Leeren Sie den Cache für veraltete Daten aus\npopup_clean_cache_confirmation_text = Sind Sie sicher, dass Sie alte Cache-Einträge bereinigen möchten? Dadurch werden Cache-Einträge für Dateien entfernt, die nicht mehr existieren oder geändert wurden.\npopup_clean_cache_progress_text = Verarbeitung von Cache-Datei:\npopup_clean_cache_current_file_text = Aktuelle Datei:\npopup_clean_cache_file_progress_text = Aktueller Dateivorgang:\npopup_clean_cache_overall_progress_text = Gesamtfortschritt:\npopup_clean_cache_stopped_by_user_text = Der Cache-Bereinigung wurde durch den Benutzer gestoppt\npopup_clean_cache_finished_text = Cachebereinigung erfolgreich abgeschlossen!\npopup_clean_cache_error_details_text = Fehlerdetails:\npopup_clean_cache_files_with_errors = Fehlerhafte Dateien:\nsubsettings_video_optimizer_mode = Modus\nsubsettings_video_optimizer_crop_type = Anbauart\nsubsettings_video_optimizer_black_pixel_threshold = Schwarzer Pixel-Schwellenwert\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maximaler RGB-Wert für jeden Farbkanal, der als Schwarz (0-128) betrachtet wird. Standard: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Schwarze Balken Mindestprozentsatz\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Mindestprozentsatz der schwarzen Pixel in einer Zeile/Spalte, um als schwarze Linie zu gelten (50-100). Standard: 90\nsubsettings_video_optimizer_max_samples = Max Samples\nsubsettings_video_optimizer_max_samples_hint = Maximal Anzahl an Frames zur Analyse pro Video (5-1000). Standard: 60\nsubsettings_video_optimizer_min_crop_size = Minimale Bildgröße\nsubsettings_video_optimizer_min_crop_size_hint = Mindestanzahl der zu abschneidenden Pixel auf jeder Seite (1-1000). Kleinere Abschneidungen werden ignoriert. Standard: 5\nsubsettings_video_optimizer_video_codec = Video Codec\nsubsettings_video_optimizer_excluded_codecs = Ausgeschlossene Codecs\nsubsettings_video_optimizer_video_quality = Videoqualität (CRF)\nsubsettings_reset = Zurücksetzen\nsubsettings_exif_ignored_tags_text = Ignoriert Tags:\nsubsettings_exif_ignored_tags_hint_text = Komma-getrennte Liste von Tags, die von der Suche ausgeschlossen werden sollen (z. B. GPS, Vorschaubild). Einige Tags, wie z. B. ImageWidth in TIFF-Dateien, werden ausgeblendet, um zu verhindern, dass das Bild unterbrochen wird.\nclean_button_text = Rein\nclean_text = Saubere EXIF-Daten\nclean_confirmation_text = Sind Sie sicher, dass Sie die EXIF-Daten aus den ausgewählten Elementen entfernen möchten?\ncrop_videos_text = Schneiden Videos\ncrop_video_confirmation_text = Bist du sicher, dass du die ausgewählten Videos zuschneiden möchtest?\ncrop_reencode_video_text = Re-encode Video\nreencode_videos_text = Re-codieren Videos\noptimize_button_text = Optimieren\noptimize_confirmation_text = Sind Sie sicher, dass Sie die ausgewählten Videos erneut codieren möchten?\noptimize_fail_if_bigger_text = Fehler, wenn optimiertes File größer ist\noptimize_overwrite_files_text = Überschreiben von Dateien\noptimize_limit_video_size_text = Begrenze die Video-Größe\noptimize_max_width_text = Max Breite:\noptimize_max_height_text = Max Höhe:\nhardlink_button_text = Harmlink\nhardlink_text = Erstelle harte Links\nhardlink_confirmation_text = Sind Sie sicher, dass Sie harte Links für die ausgewählten Elemente erstellen möchten?\nsoftlink_button_text = Softlink\nsoftlink_text = Erstelle Softlinks\nsoftlink_confirmation_text = Sind Sie sicher, dass Sie Softlinks (Symlinks) für die ausgewählten Elemente erstellen möchten?\n"
  },
  {
    "path": "krokiet/i18n/el/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Κρίσιμο Σφάλμα Κατά την Εκκίνηση της Εφαρμογής\nrust_init_error_message = \n        Σημειώθηκε ένα κρίσιμο σφάλμα κατά την εκκίνηση της εφαρμογής:\n\n        { $error_message }\n\n        Αυτό μπορεί να οφείλεται σε ελλείψεις ή ελαττωματικούς οδηγούς OpenGL/Vulkan, στην εκτέλεση της εφαρμογής σε εικονικό μηχάνημα ή σε σφάλμα στο Krokiet ή σε μία από τις βιβλιοθήκες του.\n\n        Μπορείτε να δοκιμάσετε να εκτελέσετε διαφορετικές εκδόσεις (skia_opengl, skia_vulkan, femtovg_opengl - η προεπιλεγμένη) ή με renderer υλικού για να δείτε αν αυτό επιλύσει το πρόβλημα.\nrust_loaded_preset = Φορτωμένη προκαθορισμένη τιμή { $preset_idx }\nrust_file_already_exists = Το αρχείο \"{ $file }\" υπάρχει ήδη και δεν θα αντικατασταθεί\nrust_error_removing_file_after_copy = Σφάλμα κατά την αφαίρεση του αρχείου \"{ $file }\" (μετά αντιγραφή σε διαφορετικό διαμέριση), λόγος: { $reason }\nrust_error_copying_file = Σφάλμα κατά την αντιγραφή του \"{ $input }\" στο \"{ $output }\", λόγος: { $reason }\nrust_loading_tags_cache = Φόρτωση cache ετικετών\nrust_loading_fingerprints_cache = Φόρτωση προσωρινής μνήμης αποτυπωμάτων\nrust_saving_tags_cache = Αποθήκευση λανθάνουσας μνήμης ετικετών\nrust_saving_fingerprints_cache = Αποθήκευση προσωρινής μνήμης δακτυλικών αποτυπωμάτων\nrust_loading_prehash_cache = Φόρτωση προσωρινής μνήμης\nrust_saving_prehash_cache = Αποθήκευση προσωρινής μνήμης prehash\nrust_loading_hash_cache = Φόρτωση προσωρινής μνήμης hash\nrust_saving_hash_cache = Αποθήκευση λανθάνουσας μνήμης\nrust_loading_exif_cache = Φόρτωση cache EXIF\nrust_saving_exif_cache = Αποθήκευση EXIF cache\nrust_scanning_name = Σάρωση ονόματος αρχείου { $entries_checked }\nrust_scanning_size_name = Σάρωση μεγέθους και ονόματος αρχείου { $entries_checked }\nrust_scanning_size = Μέγεθος σάρωσης του αρχείου { $entries_checked }\nrust_scanning_file = Σάρωση { $entries_checked } αρχείου\nrust_scanning_folder = Σάρωση φακέλου { $entries_checked }\nrust_checked_tags = Επιλεγμένες ετικέτες του { $items_stats }\nrust_checked_content = Ελέγξτε το περιεχόμενο των { $items_stats } (των { $size_stats })\nrust_compared_tags = Συγκρίθηκαν ετικέτες του { $items_stats }\nrust_compared_content = Συγκριτικό περιεχόμενο του { $items_stats }\nrust_hashed_images = Χασημένες { $items_stats } εικόνες ({ $size_stats })\nrust_compared_image_hashes = Σύγκριση κατακερμάτων εικόνων του { $items_stats }\nrust_hashed_videos = Διακεκομμένα βίντεο { $items_stats }\nrust_created_thumbnails = Δημιουργήθηκε μικρογραφίες για { $items_stats } βίντεο\nrust_checked_files = Επιλεγμένο { $items_stats } αρχείο ({ $size_stats })\nrust_checked_files_bad_extensions = Επιλεγμένο αρχείο { $items_stats }\nrust_checked_files_bad_names = Έλεγχος { $items_stats } αρχείου\nrust_checked_videos = Έλεγξα { $items_stats } βίντεο ({ $size_stats })\nrust_analyzed_partial_hash = Αναλυμένο μερικό hash των { $items_stats } αρχείων ({ $size_stats })\nrust_analyzed_full_hash = Ανάλυση πλήρους hash των { $items_stats } αρχείων ({ $size_stats })\nrust_failed_to_rename_file = Αποτυχία μετονομασίας αρχείου { $old_path } σε { $new_path }, σφάλμα: { $error }\nrust_no_included_paths = Δεν μπορεί να ξεκινήσει η σάρωση όταν δεν έχουν οριστεί καμία διαδρομή που περιλαμβάνεται.\nrust_all_paths_referenced = Δεν μπορεί να ξεκινήσει η σάρωση όταν έχουν ρυθμιστεί όλα τα συμπεριλημμένα μονοπάτια ως αναφερόμενα μονοπάτια, πρέπει να απενεργοποιήσετε το πλαίσιο ελέγχου δίπλα στο εισαγόμενο μονοπάτι.\nrust_found_empty_folders = Βρέθηκαν { $items_found } άδειοι φάκελοι στο { $time }\nrust_found_empty_files = Βρέθηκαν { $items_found } κενά αρχεία στο { $time }\nrust_found_similar_images = Βρέθηκαν { $items_found } παρόμοια αρχεία εικόνας σε { $groups } ομάδες σε { $time }\nrust_found_similar_videos = Βρέθηκαν { $items_found } παρόμοια αρχεία βίντεο σε { $groups } ομάδες σε { $time }\nrust_found_similar_music_files = Βρέθηκαν { $items_found } παρόμοια αρχεία μουσικής σε { $groups } ομάδες σε { $time }\nrust_found_invalid_symlinks = Βρέθηκαν { $items_found } μη έγκυρα symlinks στο { $time }\nrust_found_temporary_files = Βρέθηκαν { $items_found } προσωρινά αρχεία στο { $time }\nrust_no_file_type_selected = Αδυναμία εύρεσης κατεστραμμένων αρχείων χωρίς κανέναν επιλεγμένο τύπο αρχείου.\nrust_found_broken_files = Βρέθηκαν { $items_found } σπασμένα αρχεία λαμβάνοντας { $size } σε { $time }\nrust_found_bad_extensions = Βρέθηκαν { $items_found } αρχεία με κακές επεκτάσεις στο { $time }\nrust_found_bad_names = Βρέθηκαν { $items_found } αρχεία με κακές ονομασίες σε { $time}\nrust_found_video_optimizer = Βρέθηκαν { $items_found } αρχεία για βελτιστοποίηση σε { $time }\nrust_found_duplicate_files = Βρέθηκαν { $items_found } διπλότυπα αρχεία σε { $groups } ομάδες που λαμβάνουν { $size } σε { $time }\nrust_found_duplicate_files_no_lost_space = Βρέθηκαν { $items_found } διπλότυπα αρχεία σε { $groups } ομάδες σε { $time }\nrust_found_big_files = Βρέθηκαν { $items_found } μεγάλα αρχεία με μέγεθος { $size } στο { $time }\nrust_found_exif_files = Βρέθηκαν { $items_found } αρχεία με exif δεδομένα στις { $time }\nrust_cannot_load_preset = Αδυναμία αλλαγής και φόρτωσης προκαθορισμένου { $preset_idx } - λόγος { $reason }, αντί αυτού χρησιμοποιώντας προεπιλεγμένες ρυθμίσεις\nrust_saved_preset = Αποθηκευμένη προκαθορισμένη τιμή { $preset_idx }\nrust_cannot_save_preset = Αδυναμία αποθήκευσης προκαθορισμένου { $preset_idx } - λόγος { $reason }\nrust_reset_preset = Είσοδος προετοιμασίας { $preset_idx }\nrust_cannot_create_output_folder = Αδυναμία δημιουργίας φακέλου εξόδου { $output_folder }, λόγος: { $error }\nrust_delete_summary = Διαγράφηκε { $deleted } αντικείμενα, απέτυχε να αφαιρέσει { $failed } αντικείμενα, από { $total } αντικείμενα\nrust_rename_summary = Μετονομασία { $renamed } αντικειμένων, απέτυχε να μετονομάσει { $failed } αντικείμενα, από { $total } αντικείμενα\nrust_move_summary = Μετακινήθηκε { $moved } αντικείμενα, απέτυχε να μετακινήσει { $failed } αντικείμενα, έξω από { $total } αντικείμενα\nrust_hardlink_summary = Συνδεδεμένα { $hardlinked } αντικείμενα, απέτυχαν να συνδέσουν { $failed } αντικείμενα, από { $total } αντικείμενα\nrust_symlink_summary = Συνδέθηκαν { $symlinked } αντικείμενα, αποτυχημένη σύνδεση { $failed } αντικειμένων, από { $total } αντικείμενα\nrust_optimize_video_summary = Βελτιστοποιημένα { $optimized } βίντεο, αποτυχημένα βίντεο βελτιστοποίησης { $failed }, εκ των { $total } βίντεο\nrust_clean_exif_summary = Καθαρισμένα EXIF από τα { $cleaned } αρχεία, αποτυχημένο καθάρισμα των { $failed } αρχείων, συνολικά { $total } αρχεία\nrust_deleting_files = Διαγραφή αρχείου { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Διαγραφή αρχείου { $items_stats }\nrust_renaming_files = Μετονομασία αρχείου { $items_stats }\nrust_moving_files = Μετακίνηση { $items_stats } αρχείου ({ $size_stats })\nrust_moving_no_size_files = Μετακίνηση { $items_stats } αρχείου\nrust_hardlinking_files = Δημιουργία συντόμου συνδέσμου { $items_stats } αρχείου ({ $size_stats })\nrust_hardlinking_no_size_files = Δημιουργία σκληρού συνδέσμου { $items_stats } αρχείου\nrust_symlinking_files = Σύνδεση συμβολική { $items_stats } αρχείου ({ $size_stats })\nrust_symlinking_no_size_files = Σύνδεση symbolic { $items_stats } αρχείου\nrust_optimizing_videos = Βελτιστοποιημένο { $items_stats } βίντεο ({ $size_stats })\nrust_optimizing_no_size_videos = Βελτιστοποιημένο { $items_stats } βίντεο\nrust_cleaning_exif = Καθαρισμός EXIF από το { $items_stats } αρχείο ({ $size_stats })\nrust_cleaning_no_size_exif = Καθαρισμός EXIF από το { $items_stats } αρχείο\nrust_no_files_deleted = Δεν επιλέχθηκαν αρχεία ή φάκελοι για διαγραφή\nrust_no_files_renamed = Δεν επιλέχθηκαν αρχεία ή φάκελοι για μετονομασία\nrust_no_files_moved = Δεν επιλέχθηκαν αρχεία ή φάκελοι για μετακίνηση\nrust_no_files_hardlinked = Δεν έχουν επιλεχθεί αρχεία ή φακέλους για σκληρό σύνδεσμο\nrust_no_files_symlinked = Δεν έχουν επιλεγεί αρχεία ή φακέλους για συμβολικό σύνδεσμο\nrust_no_videos_optimized = Δεν έχουν επιλεγεί βίντεο για βελτιστοποίηση\nrust_no_exif_cleaned = Δεν έχουν επιλεχθεί αρχεία για καθαρισμό EXIF\nrust_extracted_exif_tags = Εξαχρέστησαν τα EXIF tags από τα { $items_stats } αρχεία ({ $size_stats })\nrust_delete_confirmation = Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα αντικείμενα?\nrust_delete_confirmation_number_simple = Επιλέχθηκαν { $items } αντικείμενα.\nrust_delete_confirmation_number_groups = { $items } επιλεγμένα αντικείμενα σε ομάδες { $groups }.\nrust_delete_confirmation_selected_all_in_group = Όλα τα επιλεγμένα αντικείμενα σε ομάδες { $groups }.\nrust_move_confirmation = Είστε βέβαια ότι θέλετε να μετακινήσετε τα επιλεγμένα αντικείμενα;\nrust_move_confirmation_number_simple = { $items } αντικείμενα επιλεγμένα.\nrust_clean_exif_confirmation = Είστε βέβαια ότι θέλετε να διαγράψετε τα δεδομένα EXIF από τα επιλεγμένα αντικείμενα;\nrust_clean_exif_confirmation_number_simple = { $items } αντικείμενα επιλεγμένα.\nclean_exif_overwrite_files_text = αντικαταστήστε αρχεία\nrust_optimize_video_confirmation = Είστε βέβαια ότι θέλετε να βελτιστοποιήσετε τα επιλεγμένα βίντεο;\nrust_optimize_video_confirmation_number_simple = { $items } αντικείμενα επιλεγμένα.\nrust_hardlink_confirmation = Είστε βέβαια ότι θέλετε να δημιουργήσετε σκληρούς συνδέσμους για τα επιλεγμένα αντικείμενα;\nrust_hardlink_confirmation_number_simple = { $items } αντικείμενα επιλεγμένα.\nrust_symlink_confirmation = Είστε βέβαια ότι θέλετε να δημιουργήσετε συμβολικούς συνδέσμους για τα επιλεγμένα αντικείμενα;\nrust_symlink_confirmation_number_simple = { $items } αντικείμενα επιλεγμένα.\nrust_rename_confirmation = Είστε βέβαια ότι θέλετε να μετονομάσετε τα επιλεγμένα αντικείμενα;\nrust_rename_confirmation_number_simple = { $items } αντικείμενα επιλεγμένα.\nrust_cache_processed_files = Επεξεργάστηκαν { $files } αρχεία cache\nrust_cache_entries_stats = Αφαίρεσε { $removed } εγγραφές από τις { $all }, { $left } απομένουν\nrust_cache_size_reduced = Μειώθηκε το μέγεθος των αρχείων cache κατά { $size }\nrust_cache_time_elapsed = Χρόνος που πέρασε: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Αποτυχία δημιουργίας σκληρού συνδέσμου { $name } προς { $target }, λόγος { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Επιλογή\ncolumn_size = Μέγεθος\ncolumn_file_name = Όνομα Αρχείου\ncolumn_path = Διαδρομή\ncolumn_modification_date = Ημερομηνία Τροποποίησης\ncolumn_similarity = Ομοιότητα\ncolumn_dimensions = Διαστάσεις\ncolumn_new_dimensions = Νέες Διαστάσεις\ncolumn_title = Τίτλος\ncolumn_artist = Καλλιτέχνης\ncolumn_year = Έτος\ncolumn_bitrate = Ρυθμός Bit\ncolumn_length = Μήκος\ncolumn_genre = Είδος\ncolumn_type_of_error = Τύπος σφάλματος\ncolumn_symlink_name = Όνομα Συντόμευσης\ncolumn_symlink_folder = Φάκελος Συντόμευσης\ncolumn_destination_path = Διαδρομή Προορισμού\ncolumn_current_extension = Τρέχουσα Επέκταση\ncolumn_proper_extension = Κατάλληλη Επέκταση\ncolumn_fps = FPS\ncolumn_codec = Κωδικοποιητής\ncolumn_duration = Διάρκεια\ncolumn_exif_tags = Ετικέτες EXIF\ncolumn_new_name = Νέα Επωνυμία\n# Slint translations\nok_button = Εντάξει\ncancel_button = Ακύρωση\ndo_you_want_to_continue = Θέλετε να συνεχίσετε?\nmain_window_title = Krokiet - Καθαρισμός Δεδομένων\nscan_button = Σάρωση\nstop_button = Διακοπή\nstop_text = Στάμα\nselect_button = Επιλογή\nmove_button = Μετακίνηση\ndelete_button = Διαγραφή\nsave_button = Αποθήκευση\nsort_button = Ταξινόμηση\nrename_button = Μετονομασία\nmotto = Αυτό το πρόγραμμα είναι δωρεάν και πάντα θα είναι.\\nΔείτε την άδεια MIT/GPL για λεπτομέρειες.\nunicorn = Μπορεί να μην κοιτάξετε έναν μονόκερο, αλλά ο μονόκερος σας κοιτάζει πάντα.\nrepository = Αποθετήριο\ninstruction = Οδηγίες\ndonation = Δωρεά\ntranslation = Μετάφραση\nincluded_paths = Συμπεριλημμένες Διαδρομές\nexcluded_paths = Αποκλεισμένοι Δρόμοι\nref = Αναφ\npath = Διαδρομή\ntool_duplicate_files = Αντίγραφο Αρχείων\ntool_empty_folders = Άδειασμα Φακέλων\ntool_big_files = Μεγάλα Αρχεία\ntool_empty_files = Κενά Αρχεία\ntool_temporary_files = Προσωρινά Αρχεία\ntool_similar_images = Παρόμοιες Εικόνες\ntool_similar_videos = Παρόμοια Βίντεο\ntool_music_duplicates = Αντίγραφο Μουσικής\ntool_invalid_symlinks = Μη Έγκυρα Symlinks\ntool_broken_files = Κατεστραμμένα Αρχεία\ntool_bad_extensions = Εσφαλμένες Επεκτάσεις\ntool_bad_names = Κακές Ονομασίες\ntool_video_optimizer = Βελτιστοποιητής Βίντεο\ntool_exif_remover = Αφαίρεση Exif\nsort_by_full_name = Ταξινόμηση κατά πλήρες όνομα\nsort_by_selection = Ταξινόμηση κατά επιλογή\nsort_reverse = Αντίστροφη σειρά\nselection_all = Επιλογή όλων\nselection_deselect_all = Αποεπιλογή όλων\nselection_invert_selection = Αντιστροφή επιλογής\nselection_the_biggest_size = Επιλέξτε το μεγαλύτερο μέγεθος\nselection_the_biggest_resolution = Επιλέξτε τη μεγαλύτερη ανάλυση\nselection_the_smallest_size = Επιλέξτε το μικρότερο μέγεθος\nselection_the_smallest_resolution = Επιλέξτε τη μικρότερη ανάλυση\nselection_newest = Επιλογή νεότερου\nselection_oldest = Επιλογή παλαιότερου\nselection_shortest_path = Επιλέξτε τη συντομότερη διαδρομή\nselection_longest_path = Επιλέξτε τη μεγαλύτερη διαδρομή\nstage_current = Τρέχον Στάδιο:\nstage_all = Όλα Τα Στάδια:\nsubsettings = Υπορυθμίσεις\nsubsettings_images_hash_size = Μέγεθος Κατακερματισμού\nsubsettings_images_resize_algorithm = Αλλαγή Μεγέθους Αλγορίθμου\nsubsettings_images_ignore_same_size = Παράβλεψη εικόνων με ίδιο μέγεθος\nsubsettings_images_max_difference = Μέγιστη διαφορά\nsubsettings_images_duplicates_hash_type = Τύπος Κατακερματισμού\nsubsettings_duplicates_check_method = Έλεγχος μεθόδου\nsubsettings_duplicates_name_case_sensitive = Διάκριση πεζών(μόνο λειτουργίες ονόματος)\nsubsettings_biggest_files_sub_method = Μέθοδος\nsubsettings_biggest_files_sub_number_of_files = Αριθμός αρχείων\nsubsettings_videos_max_difference = Μέγιστη διαφορά\nsubsettings_videos_ignore_same_size = Παράβλεψη βίντεο με ίδιο μέγεθος\nsubsettings_music_audio_check_type = Τύπος ελέγχου ήχου\nsubsettings_music_approximate_comparison = Κατά Προσέγγιση Σύγκριση Ετικετών\nsubsettings_music_compared_tags = Συγκρίθηκαν ετικέτες\nsubsettings_music_title = Τίτλος\nsubsettings_music_artist = Καλλιτέχνης\nsubsettings_music_bitrate = Ρυθμός Bit\nsubsettings_music_genre = Είδος\nsubsettings_music_year = Έτος\nsubsettings_music_length = Μήκος\nsubsettings_music_max_difference = Μέγιστη διαφορά\nsubsettings_music_minimal_fragment_duration = Ελάχιστη διάρκεια θραύσματος\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Σύγκριση μεταξύ ομάδων παρόμοιων τίτλων\nsubsettings_broken_files_type = Τύπος αρχείων για έλεγχο\nsubsettings_broken_files_audio = Ήχος\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Αρχειοθέτηση\nsubsettings_broken_files_image = Εικόνα\nsubsettings_broken_files_video = Βίντεο\nsubsettings_broken_files_video_info = Χρησιμοποιεί ffmpeg/ffprobe. Πολύ αργό και μπορεί να ανιχνεύσει αυστηρές ατέλειες ακόμη και αν το αρχείο παίζει κανονικά.\nsubsettings_bad_names_issues = Έλεγχος ονομάτων αρχείων\nsubsettings_bad_names_uppercase_extension = Μεγάλα Γράμματα επέκταση\nsubsettings_bad_names_uppercase_extension_hint = Βρίσκει αρχεία με κεφαλαία γράμματα στην επέκταση (π.χ., .JPG, .Mp3) και προτείνει τη μικρά γραμμάτωση\nsubsettings_bad_names_emoji_used = Emoji στην ονομασία\nsubsettings_bad_names_emoji_used_hint = Βρίσκει αρχεία με χαρακτήρες emoji (😀, 🎉, κ.λπ.) στο όνομα και προτείνει να τα διαγράψει\nsubsettings_bad_names_space_at_start_end = Ειδικοί/Επικεφαλής κενά\nsubsettings_bad_names_space_at_start_end_hint = Βρίσκει αρχεία με κενά στο ξεκίνημα ή στο τέλος του ονόματος και προτείνει να τα κόψει\nsubsettings_bad_names_non_ascii = Χαρακτήρες μη-ASCII\nsubsettings_bad_names_non_ascii_hint = Βρίσκει μη-ASCII χαρακτήρες (ą, ć, ñ, κ.λπ.) και προτείνει να τους αντικαταστήσει με ASCII αντίστοιχους (a, c, n) ή να τους διαγράψει εάν δεν υπάρχει αντιστοιχία\nsubsettings_bad_names_restricted_charset = Περιορισμένος χαρακτήρας\nsubsettings_bad_names_restricted_charset_hint = Μετατροπή μη-ASCII χαρακτήρων σε ASCII, στη συνέχεια αναζήτηση αρχείων που περιέχουν χαρακτήρες εκτός 0-9a-zA-Z και επιτρεπόμενων χαρακτήρων που ορίζονται από τον χρήστη\nsubsettings_bad_names_allowed_chars = Επιτρεπόμενες χαρακτήρες\nsubsettings_bad_names_remove_duplicated = Διπλάσια γράμματα\nsubsettings_bad_names_remove_duplicated_hint = Βρίσκει διαδοχικά διπλώματα μη-αλφαριθμητικών χαρακτήρων (π.χ., \"file---name..txt\") και προτείνει την αφαίρεση των διπλοτύπων\nsettings_global_settings = Καθολικές Ρυθμίσεις\nsettings_dark_theme = Σκοτεινό θέμα\nsettings_show_only_icons = Εμφάνιση μόνο εικονιδίων\nsettings_excluded_items = Εξαιρούμενο στοιχείο:\nsettings_allowed_extensions = Επιτρεπόμενες επεκτάσεις:\nsettings_excluded_extensions = Εξαιρούμενες επεκτάσεις:\nsettings_file_size = Размер αρχείου (Κιλοβайτά)\nsettings_minimum_file_size = Ελάχιστο:\nsettings_maximum_file_size = Μέγιστο:\nsettings_recursive_search = Αναδρομική αναζήτηση\nsettings_use_cache = Χρήση προσωρινής μνήμης\nsettings_save_as_json = Επίσης αποθήκευση προσωρινής μνήμης ως αρχείο JSON\nsettings_move_to_trash = Μετακίνηση διαγραμμένων αρχείων στον κάδο απορριμμάτων\nsettings_ignore_other_filesystems = Αγνόηση άλλων συστημάτων αρχείων (μόνο Linux)\nsettings_delete_outdated_cache_entries = Διαγραφή αυτόματα εγγραφών προσωρινής αποθήκευσης που έχουν απομειωθεί\nsettings_delete_outdated_cache_entries_hint = Όταν είναι ενεργοποιημένο, η εφαρμογή θα επαληθεύει κατά τη φόρτωση της μνήμης προσωρινής αποθήκευσης (τουλάχιστον μία φορά την εβδομάδα) εάν οι εγγραφές της μνήμης προσωρινής αποθήκευσης εξακολουθούν να δείχνουν σε υπάρχοντα και μη τροποποιημένα αρχεία/δεδομένα\nsettings_hide_hard_links = Κρύψε σκληρούς συνδέσμους\nsettings_hide_hard_links_hint = Κρύψε σκληρούς συνδέσμους σε ίδιους αρχεία στα αποτελέσματα\nsettings_thread_number = Αριθμός νήματος\nsettings_restart_required = ---Πρέπει να επανεκκινήσετε την εφαρμογή για να εφαρμόσετε αλλαγές στον αριθμό νήματος---\nsettings_duplicate_image_preview = Προεπισκόπηση εικόνας\nsettings_duplicate_minimal_hash_cache_size = Ελάχιστο μέγεθος των προσωρινά αποθηκευμένων αρχείων - Hash (KB)\nsettings_duplicate_use_prehash = Χρήση prehash\nsettings_duplicate_minimal_prehash_cache_size = Ελάχιστο μέγεθος των προσωρινά αποθηκευμένων αρχείων - Prehash (KB)\nsettings_similar_images_show_image_preview = Προεπισκόπηση εικόνας\nsettings_application_scale_text = Επίπεδο εφαρμογής\nsettings_application_scale_hint_text = Όταν είναι ενεργοποιημένη η χειροκίνητη κλίμακα, αυτό σας επιτρέπει να επιλέξετε έναν προσαρμοσμένο συντελεστή κλίμακας, αλλά απενεργοποιεί πλήρως την αυτόματη κλίμακα με βάση την ανάλυση της οθόνης (DPI).\nsettings_restart_required_scale_text = —Πρέπει να επανεκκινήσετε την εφαρμογή για να εφαρμοστούν οι αλλαγές κλίμακας—\nsettings_use_manual_application_scale_text = Χρησιμοποιήστε χειροκίνητη κλίμακα εφαρμογής\nsettings_video_thumbnails_preview = Προεπισκόπηση εικόνας\nsettings_open_config_folder = Άνοιγμα φακέλου ρυθμίσεων\nsettings_open_cache_folder = Άνοιγμα φακέλου cache\nsettings_language = Γλώσσα\nsettings_current_preset = Τρέχουσα Προεπιλογή:\nsettings_edit_name = Επεξεργασία ονόματος\nsettings_choose_name_for_prefix = Επιλέξτε όνομα για πρόθεμα\nsettings_save = Αποθήκευση\nsettings_load = Φόρτωση\nsettings_reset = Επαναφορά\nsettings_similar_videos_tool = Παρόμοιο εργαλείο βίντεο\nsettings_video_thumbnails_clear_unused_thumbnails = Διαγραφή άχρηστων μικρογραφιών βίντεο παλαιότερες των 7 ημερών κατά την εκκίνηση της εφαρμογής\nsettings_video_thumbnails_header = Εικόνες Ενότητων Βίντεο\nsettings_video_thumbnails_generate = Δημιουργία μικρογραφιών\nsettings_video_thumbnails_position = Θέση μικρογραφικού σε βίντεο (%)\nsettings_video_thumbnails_generate_grid = Δημιουργία πλέγματος μικρογραφιών αντί για μία εικόνα\nsettings_video_thumbnails_generate_grid_hint = Δημιουργία πολλαπλών εικόνων σε πλέγμα είναι πολύ πιο αργή από τη δημιουργία μιας μοναδικής μικρογραφίας\nsettings_video_thumbnails_grid_tiles_per_side = Αριθμός πλακίδων ανά πλευρά στην έξυπνη στήλη\nsettings_video_thumbnails_grid_tiles_per_side_hint = Ο αριθμός των μικρογραφικών πλακιδίων ανά πλευρά του πλέγματος. Για παράδειγμα, η επιλογή 2 δημιουργεί ένα πλέγμα 2 x 2, με αποτέλεσμα μια μεμονωμένη μικρογραφία που αποτελείται από 4 εικόνες.\nsettings_similar_images_tool = Παρόμοιες εικόνες εργαλείο\nsettings_general_settings = Γενικές Ρυθμίσεις\nsettings_cache_header_text = Ρυθμίσεις Αποθήκευσης\nsettings_clean_cache_button_text = Καθαρισμός παλαιού προσωρινού αποθήκευσης\nsettings_settings = Ρυθμίσεις\nsettings_load_tabs_sizes_at_startup = Φόρτωση μεγεθών καρτελών κατά την εκκίνηση\nsettings_load_windows_size_at_startup = Φόρτωση μεγέθους παραθύρων κατά την εκκίνηση\nsettings_limit_lines_of_messages = Όριο μηνυμάτων σε 500 γραμμές (workaround για αργό TextEdit πλήκτρο εισαγωγής)\nsettings_play_audio_on_scan_completion_text = Παίξε ήχο όταν ολοκληρωθεί επιτυχώς η σάρωση\nsettings_audio_feature_hint_text = Διαθέσιμο μόνο κατά την μεταγλώττιση με την λειτουργία ήχου\nsettings_audio_env_variable_hint_text = Μπορεί να τροποποιηθεί ο ήχος, ορίζοντας τη μεταβλητή περιβάλλοντος KROKIET_AUDIO_STOP_FILE σε έγκυρο μονοπάτι αρχείου ήχου\npopup_save_title = Εξοικονόμηση αποτελεσμάτων\npopup_save_message = Αυτό θα αποθηκεύσει αποτελέσματα σε 3 διαφορετικά αρχεία\npopup_rename_title = Μετονομασία αρχείων\npopup_new_paths_title = Παρακαλώ προσθέστε τις διαδρομές μία ανά γραμμή\npopup_move_title = Μετακίνηση αρχείων\npopup_move_copy_checkbox = Αντιγραφή αρχείων αντί για μετακίνηση\npopup_move_preserve_folder_checkbox = Διατήρηση δομής φακέλου\nmove_confirmation_text = Είστε βέβαια ότι θέλετε να μετακινήσετε τα επιλεγμένα αντικείμενα;\nrename_confirmation_text = Είστε βέβαια ότι θέλετε να μετονομάσετε τα επιλεγμένα αντικείμενα;\ndelete = Διαγραφή στοιχείων\nstopping_scan = Διακοπή σάρωσης, παρακαλώ περιμένετε...\nsearching = Αναζήτηση...\nsubsettings_videos_crop_detect = Μέθοδος ανίχνευσης καλλιεργειών\nsubsettings_videos_skip_forward_amount = Διάρκεια παράλειψης [s]\nsubsettings_videos_vid_hash_duration = Διάρκεια κατακερματισμού βίντεο\nsettings_cache_number_size_text = Μέγεθος αρχείων λανθάνουσας μνήμης: { $size }, αριθμός αρχείων: { $number }\nsettings_video_thumbnails_number_size_text = Μέγεθος μικρογραφιών βίντεο: { $size }, αριθμός αρχείων: { $number }\nsettings_log_number_size_text = Μέγεθος αρχείων: { $size }, αριθμός αρχείων: { $number }\npopup_clean_cache_title_text = Καθαρισμός Εκτεθειμένου Κενού\npopup_clean_cache_confirmation_text = Είστε βέβαια ότι θέλετε να καθαρίσετε παλαιά αρχεία προσωρινής μνήμης; Αυτό θα διαγράψει αρχεία προσωρινής μνήμης για αρχεία που δεν υπάρχουν πλέον ή έχουν τροποποιηθεί.\npopup_clean_cache_progress_text = Επεξεργασία αρχείου cache:\npopup_clean_cache_current_file_text = Τρέχουσα αρχείο:\npopup_clean_cache_file_progress_text = Τρέχουσα προέκυψη αρχείου:\npopup_clean_cache_overall_progress_text = Συνολική πρόοδος:\npopup_clean_cache_stopped_by_user_text = Η καθαρισμός της προσωρινής μνήμης σταματήθηκε από τον χρήστη\npopup_clean_cache_finished_text = Η διαγραφή της προσωρινής μνήμης ολοκληρώθηκε επιτυχώς!\npopup_clean_cache_error_details_text = Λεπτομέρειες σφάλματος:\npopup_clean_cache_files_with_errors = Αρχεία με σφάλματα:\nsubsettings_video_optimizer_mode = Λειτουργία\nsubsettings_video_optimizer_crop_type = Τύπος Καλλιέργειας\nsubsettings_video_optimizer_black_pixel_threshold = Μαύρο Πίξελ Κατώφλι\nsubsettings_video_optimizer_black_pixel_threshold_hint = Τιμή RGB μέγιστης τιμής για κάθε κανάλι χρώματος να θεωρείται μαύρη (0-128). Προεπιλογή: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Μαύρο Μέγιστο Ποσοστό Μετώπου\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Ελάχιστο ποσοστό μαύρων pixel σε σειρά/στήλη για να θεωρείται μαύρη γραμμή (50-100). Προεπιλογή: 90\nsubsettings_video_optimizer_max_samples = Μέγιστος Αριθμός Δειγμάτων\nsubsettings_video_optimizer_max_samples_hint = Μέγιστος αριθμός καρέ που θα αναλυθούν ανά βίντεο (5-1000). Προεπιλογή: 60\nsubsettings_video_optimizer_min_crop_size = Ελάχιστο Μέγεθος Φωτογραφίας\nsubsettings_video_optimizer_min_crop_size_hint = Ελάχιστο μέγεθος pixel που θα κόψετε σε οποιοδήποτε πλευρό (1-1000). Αγνοούνται τα μικρότερα κοψίματα. Προεπιλογή: 5\nsubsettings_video_optimizer_video_codec = βιογραφικός κωδικοποιητής\nsubsettings_video_optimizer_excluded_codecs = Εξαιρέθειες κωδικοποιητές\nsubsettings_video_optimizer_video_quality = Ποιότητα βίντεο (CRF)\nsubsettings_reset = Επαναφορά\nsubsettings_exif_ignored_tags_text = Εγκαταλειμμένα tags:\nsubsettings_exif_ignored_tags_hint_text = Λίστα με διαχωρισμό κόμματων ετικετών που θα εξαιρεθούν από την σάρωση (π.χ. GPS, Εικόνα μικρογραφίας). Ορισμένες ετικέτες, όπως η ImageWidth σε αρχεία TIFF, είναι κρυμμένες για να αποτραπεί η διακοπή της εικόνας.\nclean_button_text = Καθαρό\nclean_text = Καθαρά δεδομένα EXIF\nclean_confirmation_text = Είστε βέβαια ότι θέλετε να διαγράψετε τα δεδομένα EXIF από τα επιλεγμένα αντικείμενα;\ncrop_videos_text = Κόψε βίντεο\ncrop_video_confirmation_text = Είστε βέβαια ότι θέλετε να κοπούν τα επιλεγμένα βίντεο;\ncrop_reencode_video_text = Επανκωδικοποίηση βίντεο\nreencode_videos_text = Επανκωδικοποίηση βίντεο\noptimize_button_text = Βελτιστοποίηση\noptimize_confirmation_text = Είστε βέβαια ότι θέλετε να επανακωδικοποιήσετε τα επιλεγμένα βίντεο;\noptimize_fail_if_bigger_text = Αποτύχημα αν το βελτιστοποιημένο αρχείο είναι μεγαλύτερο\noptimize_overwrite_files_text = αντικαταστήστε αρχεία\noptimize_limit_video_size_text = Όριο μεγέθους βίντεο\noptimize_max_width_text = Μέγιστο πλάτος:\noptimize_max_height_text = Μέγιστο ύψος:\nhardlink_button_text = Απόσπασμα σύνδεσης\nhardlink_text = Δημιουργία σκληρών συνδέσμων\nhardlink_confirmation_text = Είστε βέβαια ότι θέλετε να δημιουργήσετε σκληρούς συνδέσμους για τα επιλεγμένα αντικείμενα;\nsoftlink_button_text = Softlink\nsoftlink_text = Δημιουργία συμβολικών συνδέσμων\nsoftlink_confirmation_text = Είστε βέβαια ότι θέλετε να δημιουργήσετε συντόμευτες συνδέσμους (symlinks) για τα επιλεγμένα αντικείμενα;\n"
  },
  {
    "path": "krokiet/i18n/en/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Critical Error During App Startup\nrust_init_error_message =\n    A critical error occurred while starting the application:\n\n    { $error_message }\n\n    This may be caused by missing or malfunctioning OpenGL/Vulkan drivers, running the application in a virtual machine or a bug in Krokiet or one of its libraries.\n\n    You can try running different builds (skia_opengl, skia_vulkan, femtovg_opengl - the default) or with software renderer to see if that resolves the issue.\n\nrust_loaded_preset = Loaded preset { $preset_idx }\nrust_file_already_exists = File \"{ $file }\" already exists, and will not be overridden\nrust_error_removing_file_after_copy = Error while removing file \"{ $file }\" (after copying into different partition), reason: { $reason }\nrust_error_copying_file = Error while copying \"{ $input }\" to \"{ $output }\", reason: { $reason }\nrust_loading_tags_cache = Loading tags cache\nrust_loading_fingerprints_cache = Loading fingerprints cache\nrust_saving_tags_cache = Saving tags cache\nrust_saving_fingerprints_cache = Saving fingerprints cache\nrust_loading_prehash_cache = Loading prehash cache\nrust_saving_prehash_cache = Saving prehash cache\nrust_loading_hash_cache = Loading hash cache\nrust_saving_hash_cache = Saving hash cache\nrust_loading_exif_cache = Loading EXIF cache\nrust_saving_exif_cache = Saving EXIF cache\nrust_scanning_name = Scanning name of { $entries_checked } file\nrust_scanning_size_name = Scanning size and name of { $entries_checked } file\nrust_scanning_size = Scanning size of { $entries_checked } file\nrust_scanning_file = Scanning { $entries_checked } file\nrust_scanning_folder = Scanning { $entries_checked } folder\nrust_checked_tags = Checked tags of { $items_stats }\nrust_checked_content = Checked content of { $items_stats } ({ $size_stats })\nrust_compared_tags = Compared tags of { $items_stats }\nrust_compared_content = Compared content of { $items_stats }\nrust_hashed_images = Hashed { $items_stats } images ({ $size_stats })\nrust_compared_image_hashes = Compared image hashes of { $items_stats }\nrust_hashed_videos = Hashed { $items_stats } videos\nrust_created_thumbnails = Created thumbnails for { $items_stats } videos\nrust_checked_files = Checked { $items_stats } file ({ $size_stats })\nrust_checked_files_bad_extensions = Checked { $items_stats } file\nrust_checked_files_bad_names = Checked { $items_stats } file\nrust_checked_videos = Checked { $items_stats } videos ({ $size_stats })\nrust_analyzed_partial_hash = Analyzed partial hash of { $items_stats } files ({ $size_stats })\nrust_analyzed_full_hash = Analyzed full hash of { $items_stats } files ({ $size_stats })\nrust_failed_to_rename_file = Failed to rename file { $old_path } to { $new_path }, error: { $error }\nrust_no_included_paths = Cannot start scan when no included paths are set.\nrust_all_paths_referenced = Cannot start scan when all included paths are set as referenced paths, you need to disable reference checkbox next to input path.\nrust_found_empty_folders = Found { $items_found } empty folders in { $time }\nrust_found_empty_files = Found { $items_found } empty files in { $time }\nrust_found_similar_images = Found { $items_found } similar image files in { $groups } groups in { $time }\nrust_found_similar_videos = Found { $items_found } similar video files in { $groups } groups in { $time }\nrust_found_similar_music_files = Found { $items_found } similar music files in { $groups } groups in { $time }\nrust_found_invalid_symlinks = Found { $items_found } invalid symlinks in { $time }\nrust_found_temporary_files = Found { $items_found } temporary files in { $time }\nrust_no_file_type_selected = Cannot find broken files without any selected file type.\nrust_found_broken_files = Found { $items_found } broken files taking { $size } in { $time }\nrust_found_bad_extensions = Found { $items_found } files with bad extensions in { $time }\nrust_found_bad_names = Found { $items_found } files with bad names in { $time }\nrust_found_video_optimizer = Found { $items_found } files to optimize in { $time }\nrust_found_duplicate_files = Found { $items_found } duplicate files in { $groups } groups taking { $size } in { $time }\nrust_found_duplicate_files_no_lost_space = Found { $items_found } duplicate files in { $groups } groups in { $time }\nrust_found_big_files = Found { $items_found } big files with size { $size } in { $time }\nrust_found_exif_files = Found { $items_found } files with exif data in { $time }\nrust_cannot_load_preset = Cannot change and load preset { $preset_idx } - reason { $reason }, using default settings instead\nrust_saved_preset = Saved preset { $preset_idx }\nrust_cannot_save_preset = Cannot save preset { $preset_idx } - reason { $reason }\nrust_reset_preset = Reset preset { $preset_idx }\nrust_cannot_create_output_folder = Cannot create output folder { $output_folder }, reason: { $error }\n\nrust_delete_summary = Deleted { $deleted } items, failed to remove { $failed } items, out of { $total } items\nrust_rename_summary = Renamed { $renamed } items, failed to rename { $failed } items, out of { $total } items\nrust_move_summary = Moved { $moved } items, failed to move { $failed } items, out of { $total } items\nrust_hardlink_summary = Hardlinked { $hardlinked } items, failed to hardlink { $failed } items, out of { $total } items\nrust_symlink_summary = Symlinked { $symlinked } items, failed to symlink { $failed } items, out of { $total } items\nrust_optimize_video_summary = Optimized { $optimized } videos, failed to optimize { $failed } videos, out of { $total } videos\nrust_clean_exif_summary = Cleaned EXIF from { $cleaned } files, failed to clean { $failed } files, out of { $total } files\nrust_deleting_files = Deleting { $items_stats } file ({ $size_stats })\nrust_deleting_no_size_files = Deleting { $items_stats } file\nrust_renaming_files = Renaming { $items_stats } file\nrust_moving_files = Moving { $items_stats } file ({ $size_stats })\nrust_moving_no_size_files = Moving { $items_stats } file\nrust_hardlinking_files = Hardlinking { $items_stats } file ({ $size_stats })\nrust_hardlinking_no_size_files = Hardlinking { $items_stats } file\nrust_symlinking_files = Symlinking { $items_stats } file ({ $size_stats })\nrust_symlinking_no_size_files = Symlinking { $items_stats } file\nrust_optimizing_videos = Optimized { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimized { $items_stats } video\nrust_cleaning_exif = Cleaning EXIF from { $items_stats } file ({ $size_stats })\nrust_cleaning_no_size_exif = Cleaning EXIF from { $items_stats } file\nrust_no_files_deleted = No files or folders selected for deletion\nrust_no_files_renamed = No files or folders selected for renaming\nrust_no_files_moved = No files or folders selected for moving\nrust_no_files_hardlinked = No files or folders selected for hardlinking\nrust_no_files_symlinked = No files or folders selected for symlinking\nrust_no_videos_optimized = No videos selected for optimization\nrust_no_exif_cleaned = No files selected for EXIF cleaning\nrust_extracted_exif_tags = Extracted EXIF tags from { $items_stats } files ({ $size_stats })\n\nrust_delete_confirmation = Are you sure you want to delete the selected items?\nrust_delete_confirmation_number_simple = { $items } items selected.\nrust_delete_confirmation_number_groups = { $items } items selected in { $groups } groups.\nrust_delete_confirmation_selected_all_in_group = All items selected in { $groups } groups.\n\nrust_move_confirmation = Are you sure you want to move the selected items?\nrust_move_confirmation_number_simple = { $items } items selected.\n\nrust_clean_exif_confirmation = Are you sure you want to remove EXIF data from the selected items?\nrust_clean_exif_confirmation_number_simple = { $items } items selected.\n\nclean_exif_overwrite_files_text = Overwrite files\n\nrust_optimize_video_confirmation = Are you sure you want to optimize the selected videos?\nrust_optimize_video_confirmation_number_simple = { $items } items selected.\n\nrust_hardlink_confirmation = Are you sure you want to create hardlinks for the selected items?\nrust_hardlink_confirmation_number_simple = { $items } items selected.\n\nrust_symlink_confirmation = Are you sure you want to create symlinks for the selected items?\nrust_symlink_confirmation_number_simple = { $items } items selected.\n\nrust_rename_confirmation = Are you sure you want to rename the selected items?\nrust_rename_confirmation_number_simple = { $items } items selected.\n\nrust_cache_processed_files = Processed { $files } cache files\nrust_cache_entries_stats = Removed { $removed } entries out of all { $all }, { $left } left\nrust_cache_size_reduced = Reduced cache files size by { $size }\nrust_cache_time_elapsed = Time elapsed: { $time }\n\nrust_symlink_failed = Failed to symlink {$name} to {$target}, reason {$reason}\nrust_hardlink_failed = Failed to hardlink { $name } to { $target }, reason { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Selection\ncolumn_size = Size\ncolumn_file_name = File Name\ncolumn_path = Path\ncolumn_modification_date = Modification Date\ncolumn_similarity = Similarity\ncolumn_dimensions = Dimensions\ncolumn_new_dimensions = New Dimensions\ncolumn_title = Title\ncolumn_artist = Artist\ncolumn_year = Year\ncolumn_bitrate = Bitrate\ncolumn_length = Length\ncolumn_genre = Genre\ncolumn_type_of_error = Type of Error\ncolumn_symlink_name = Symlink Name\ncolumn_symlink_folder = Symlink Folder\ncolumn_destination_path = Destination Path\ncolumn_current_extension = Current Extension\ncolumn_proper_extension = Proper Extension\ncolumn_fps = FPS\ncolumn_codec = Codec\ncolumn_duration = Duration\ncolumn_exif_tags = EXIF Tags\ncolumn_new_name = New Name\ncolumn_full_path = Full Path\n\n# Slint translations\nok_button = Ok\ncancel_button = Cancel\ndo_you_want_to_continue = Do you want to continue?\nmain_window_title = Krokiet - Data Cleaner\nfile_dialog_open = Close the dialog window to continue\nscan_button = Scan\nstop_button = Stop\nstop_text = Stop\nselect_button = Select\nmove_button = Move\ndelete_button = Delete\nsave_button = Save\nsort_button = Sort\nrename_button = Rename\nmotto = This program is free to use and will always be.\\nSee the MIT/GPL License for details.\nunicorn = You may not look at a unicorn, but the unicorn always looks at you.\nrepository = Repository\ninstruction = Instruction\ndonation = Donation\ntranslation = Translation\nincluded_paths = Included Paths\nexcluded_paths = Excluded Paths\nref = Ref\npath = Path\ntool_duplicate_files = Duplicate Files\ntool_empty_folders = Empty Folders\ntool_big_files = Big Files\ntool_empty_files = Empty Files\ntool_temporary_files = Temporary Files\ntool_similar_images = Similar Images\ntool_similar_videos = Similar Videos\ntool_music_duplicates = Music Duplicates\ntool_invalid_symlinks = Invalid Symlinks\ntool_broken_files = Broken Files\ntool_bad_extensions = Bad Extensions\ntool_bad_names = Bad Names\ntool_video_optimizer = Video Optimizer\ntool_exif_remover = Exif Remover\nsort_by_full_name = Sort by full name\nsort_by_selection = Sort by selection\nsort_reverse = Reverse order\nselection_all = Select all\nselection_deselect_all = Deselect all\nselection_invert_selection = Invert selection\nselection_the_biggest_size = Select the biggest size\nselection_the_biggest_resolution = Select the biggest resolution\nselection_the_smallest_size = Select the smallest size\nselection_the_smallest_resolution = Select the smallest resolution\nselection_newest = Select newest\nselection_oldest = Select oldest\nselection_shortest_path = Select the shortest path\nselection_longest_path = Select the longest path\nselection_custom_select_unselect = Custom Select/Unselect\nstage_current = Current Stage:\nstage_all = All Stages:\nsubsettings = Subsettings\nsubsettings_images_hash_size = Hash Size\nsubsettings_images_resize_algorithm = Resize Algorithm\nsubsettings_images_ignore_same_size = Ignore images with same size\nsubsettings_images_max_difference = Max difference\nsubsettings_images_duplicates_hash_type = Hash Type\nsubsettings_duplicates_check_method = Check method\nsubsettings_duplicates_name_case_sensitive = Case Sensitive(only name modes)\nsubsettings_biggest_files_sub_method = Method\nsubsettings_biggest_files_sub_number_of_files = Number of files\nsubsettings_videos_max_difference = Max difference\nsubsettings_videos_ignore_same_size = Ignore videos with same size\nsubsettings_music_audio_check_type = Audio check type\nsubsettings_music_approximate_comparison = Approximate Tag Comparison\nsubsettings_music_compared_tags = Compared tags\nsubsettings_music_title = Title\nsubsettings_music_artist = Artist\nsubsettings_music_bitrate = Bitrate\nsubsettings_music_genre = Genre\nsubsettings_music_year = Year\nsubsettings_music_length = Length\nsubsettings_music_max_difference = Max difference\nsubsettings_music_minimal_fragment_duration = Minimal fragment duration\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Compare within groups of similar titles\nsubsettings_broken_files_type = Type of files to check\nsubsettings_broken_files_audio = Audio\nsubsettings_broken_files_pdf = Pdf\nsubsettings_broken_files_archive = Archive\nsubsettings_broken_files_image = Image\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Uses ffmpeg/ffprobe. Quite slow and may detect pedantic errors even if file plays fine.\nsubsettings_bad_names_issues = Filename checks\nsubsettings_bad_names_uppercase_extension = Uppercase extension\nsubsettings_bad_names_uppercase_extension_hint = Finds files with uppercase letters in extension (e.g., .JPG, .Mp3) and suggests lowercase version\nsubsettings_bad_names_emoji_used = Emoji in name\nsubsettings_bad_names_emoji_used_hint = Finds files with emoji characters (😀, 🎉, etc.) in name and suggests removing them\nsubsettings_bad_names_space_at_start_end = Leading/trailing spaces\nsubsettings_bad_names_space_at_start_end_hint = Finds files with spaces at the start or end of the name and suggests trimming them\nsubsettings_bad_names_non_ascii = Non-ASCII chars\nsubsettings_bad_names_non_ascii_hint = Finds non-ASCII characters (ą, ć, ñ, etc.) and suggests replacing them with ASCII equivalents (a, c, n) or removing if no mapping exists\nsubsettings_bad_names_restricted_charset = Limited charset\nsubsettings_bad_names_restricted_charset_hint = Transliterates non-ASCII chars to ASCII, then finds files containing characters outside 0-9a-zA-Z and user-defined allowed chars\nsubsettings_bad_names_allowed_chars = Allowed chars\nsubsettings_bad_names_remove_duplicated = Duplicated chars\nsubsettings_bad_names_remove_duplicated_hint = Finds consecutive duplicated non-alphanumeric characters (e.g., \"file---name..txt\") and suggests removing duplicates\nsettings_global_settings = Global Settings\nsettings_dark_theme = Dark theme\nsettings_show_only_icons = Show only icons\nsettings_excluded_items = Excluded item:\nsettings_allowed_extensions = Allowed extensions:\nsettings_excluded_extensions = Excluded extensions:\nsettings_file_size = File Size(Kilobytes)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Max:\nsettings_recursive_search = Recursive search\nsettings_use_cache = Use cache\nsettings_save_as_json = Also save cache as JSON file\nsettings_move_to_trash = Move deleted files to trash\nsettings_ignore_other_filesystems = Ignore other filesystems (only Linux)\nsettings_delete_outdated_cache_entries = Delete automatically outdated cache entries\nsettings_delete_outdated_cache_entries_hint = When enabled, the app will verify during cache loading (at most once per week) whether the cached records still point to existing and unmodified files/data\nsettings_hide_hard_links = Hide hard links\nsettings_hide_hard_links_hint = Hide hard links to same files in results\nsettings_thread_number = Thread number\nsettings_restart_required = ---You need to restart app to apply changes in thread number---\nsettings_duplicate_image_preview = Image preview\nsettings_duplicate_minimal_hash_cache_size = Minimal size of cached files - Hash (KB)\nsettings_duplicate_use_prehash = Use prehash\nsettings_duplicate_minimal_prehash_cache_size = Minimal size of cached files - Prehash (KB)\nsettings_similar_images_show_image_preview = Image preview\nsettings_similar_videos_preview_hint = Preview is visible only when \"Generate thumbnails\" is enabled, or when a thumbnail has already been generated.\nsettings_application_scale_text = Application scale\nsettings_application_scale_hint_text = When manual scale is enabled, this allows you to choose a custom scale factor, but completely disables automatic scaling based on the monitor’s DPI.\nsettings_restart_required_scale_text = ---You need to restart app to apply changes in scale---\nsettings_use_manual_application_scale_text = Use manual application scale\n\nsettings_video_thumbnails_preview = Image preview\nsettings_open_config_folder = Open config folder\nsettings_open_cache_folder = Open cache folder\nsettings_language = Language\nsettings_current_preset = Current Preset:\nsettings_edit_name = Edit name\nsettings_choose_name_for_prefix = Choose name for prefix\nsettings_save = Save\nsettings_load = Load\nsettings_reset = Reset\nsettings_similar_videos_tool = Similar Videos tool\nsettings_video_thumbnails_clear_unused_thumbnails = Delete unused video thumbnails older than 7 days at app startup\nsettings_video_thumbnails_header = Video Thumbnails\nsettings_video_thumbnails_generate = Generate thumbnails\nsettings_video_thumbnails_position = Thumbnail position in video (%)\nsettings_video_thumbnails_generate_grid = Generate thumbnail grid instead of single image\nsettings_video_thumbnails_generate_grid_hint = Generating multiple images in grid is a lot slower than generating single thumbnail\nsettings_video_thumbnails_grid_tiles_per_side = Number of tiles per side in thumbnail grid\nsettings_video_thumbnails_grid_tiles_per_side_hint = Number of thumbnail tiles per side in the grid. For example, selecting 2 creates a 2 x 2 grid, resulting in a single thumbnail composed of 4 images.\nsettings_similar_images_tool = Similar Images tool\nsettings_general_settings = General Settings\nsettings_cache_header_text = Cache Settings\nsettings_clean_cache_button_text = Clean outdated cache\nsettings_settings = Settings\nsettings_load_tabs_sizes_at_startup = Load tabs sizes at startup\nsettings_load_windows_size_at_startup = Load windows size at startup\nsettings_limit_lines_of_messages = Limit messages to 500 lines(workaround for slow TextEdit widget)\nsettings_play_audio_on_scan_completion_text = Play sound when scan completes successfully\nsettings_audio_feature_hint_text = Available only when compiling with audio feature\nsettings_audio_env_variable_hint_text = Sound can be changed, by setting KROKIET_AUDIO_STOP_FILE environment variable to a valid audio file path\npopup_save_title = Saving results\npopup_save_message = This will save results to 3 different files\npopup_rename_title = Renaming files\npopup_new_paths_title = Please add paths one per line\npopup_move_title = Moving files\npopup_move_copy_checkbox = Copy files instead of moving\npopup_move_preserve_folder_checkbox = Preserve folder structure\nmove_confirmation_text = Are you sure you want to move the selected items?\nrename_confirmation_text = Are you sure you want to rename the selected items?\ndelete = Delete items\nstopping_scan = Stopping scan, please wait...\nsearching = Searching...\nsubsettings_videos_crop_detect = Crop detect method\nsubsettings_videos_skip_forward_amount = Skip duration [s]\nsubsettings_videos_vid_hash_duration = Video hash duration\nsettings_cache_number_size_text = Cache files size: { $size }, number of files: { $number }\nsettings_video_thumbnails_number_size_text = Video thumbnails size: { $size }, number of files: { $number }\nsettings_log_number_size_text = Log files size: { $size }, number of files: { $number }\npopup_clean_cache_title_text = Clean Outdated Cache\npopup_clean_cache_confirmation_text = Are you sure you want to clean outdated cache entries? This will remove cache entries for files that no longer exist or have been modified.\npopup_clean_cache_progress_text = Processing cache file:\npopup_clean_cache_current_file_text = Current file:\npopup_clean_cache_file_progress_text = Current file progress:\npopup_clean_cache_overall_progress_text = Overall progress:\npopup_clean_cache_stopped_by_user_text = Cache cleaning was stopped by user\npopup_clean_cache_finished_text = Cache cleaning completed successfully!\npopup_clean_cache_error_details_text = Error details:\npopup_clean_cache_files_with_errors = Files with errors:\nsubsettings_video_optimizer_mode = Mode\nsubsettings_video_optimizer_crop_type = Crop Type\nsubsettings_video_optimizer_black_pixel_threshold = Black Pixel Threshold\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maximum RGB value for each pixel channel to be considered black (0-128). Default: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Black Bar Min Percentage\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimum percentage of black pixels in a row/column to be considered a black bar (50-100). Default: 90\nsubsettings_video_optimizer_max_samples = Max Samples\nsubsettings_video_optimizer_max_samples_hint = Maximum number of frames to analyze per video (5-1000). Default: 60\nsubsettings_video_optimizer_min_crop_size = Min Crop Size\nsubsettings_video_optimizer_min_crop_size_hint = Minimum pixels to crop on any side (1-1000). Smaller crops are ignored. Default: 5\nsubsettings_video_optimizer_video_codec = Video codec\nsubsettings_video_optimizer_excluded_codecs = Excluded codecs\nsubsettings_video_optimizer_video_quality = Video quality (CRF)\nsubsettings_reset = Reset\nsubsettings_exif_ignored_tags_text = Ignored tags:\nsubsettings_exif_ignored_tags_hint_text = Comma-separated list of tags to exclude from scanning (e.g. GPS, Thumbnail). Some tags, such as ImageWidth in TIFF files, are hidden to prevent breaking the image.\nclean_button_text = Clean\nclean_text = Clean EXIF data\nclean_confirmation_text = Are you sure you want to remove EXIF data from the selected items?\ncrop_videos_text = Crop videos\ncrop_video_confirmation_text = Are you sure you want to crop the selected videos?\ncrop_reencode_video_text = Re-encode video\nreencode_videos_text = Re-encode videos\noptimize_button_text = Optimize\noptimize_confirmation_text = Are you sure you want to re-encode the selected videos?\noptimize_fail_if_bigger_text = Fail if optimized file is bigger\noptimize_overwrite_files_text = Overwrite files\noptimize_limit_video_size_text = Limit video size\noptimize_max_width_text = Max width:\noptimize_max_height_text = Max height:\nhardlink_button_text = Hardlink\nhardlink_text = Create hardlinks\nhardlink_confirmation_text = Are you sure you want to create hardlinks for the selected items?\nsoftlink_button_text = Softlink\nsoftlink_text = Create softlinks\nsoftlink_confirmation_text = Are you sure you want to create softlinks (symlinks) for the selected items?\npopup_custom_select_title_text = Custom Select / Unselect\npopup_custom_select_button_text = Select\npopup_custom_unselect_button_text = Unselect\npopup_custom_column_name_header_text = Column\npopup_custom_filter_value_header_text = Filter value (wildcard / regex)\npopup_custom_case_sensitive_text = Case sensitive\npopup_custom_leave_one_in_group_text = Select all items except one, in each group\npopup_custom_hint_str_text = Text columns: wildcards  *name*  /home/*  *.rs\npopup_custom_hint_int_text = Size [KB] / numeric columns: >= 2048  < 512  = 0  (operators: >=  <=  >  <  =)\npopup_custom_hint_date_text = Date columns: DD-MM-YYYY or YYYY-MM-DD, optional time HH:MM:SS  e.g.  >= 2020-01-01  or  < 31-12-2022 23:59:59\n\n"
  },
  {
    "path": "krokiet/i18n/es-ES/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Error Crítico Durante el Inicio de la Aplicación\nrust_init_error_message = \n        Ocurrió un error crítico al iniciar la aplicación:\n\n        { $error_message }\n\n        Esto puede ser causado por controladores OpenGL/Vulkan faltantes o defectuosos, ejecutando la aplicación en una máquina virtual o un error en Krokiet o en una de sus bibliotecas.\n\n        Puede intentar ejecutar diferentes versiones (skia_opengl, skia_vulkan, femtovg_opengl - la predeterminada) o con renderizador de software para ver si eso resuelve el problema.\nrust_loaded_preset = Preset cargado { $preset_idx }\nrust_file_already_exists = Archivo \"{ $file }\" ya existe, y no será sobreescrito\nrust_error_removing_file_after_copy = Error al eliminar el archivo \"{ $file }\" (después de copiar en una partición diferente), razón: { $reason }\nrust_error_copying_file = Error al copiar \"{ $input }\" a \"{ $output }\", razón: { $reason }\nrust_loading_tags_cache = Cargando caché de etiquetas\nrust_loading_fingerprints_cache = Cargando caché de huellas dactilares\nrust_saving_tags_cache = Guardando caché de etiquetas\nrust_saving_fingerprints_cache = Guardando caché de huellas dactilares\nrust_loading_prehash_cache = Cargando caché prehash\nrust_saving_prehash_cache = Guardando caché prehash\nrust_loading_hash_cache = Cargando caché hash\nrust_saving_hash_cache = Guardando caché hash\nrust_loading_exif_cache = Cargando caché EXIF\nrust_saving_exif_cache = Guardar caché EXIF\nrust_scanning_name = Escaneando el nombre del archivo { $entries_checked }\nrust_scanning_size_name = Escaneando tamaño y nombre del archivo { $entries_checked }\nrust_scanning_size = Escaneando el tamaño del archivo { $entries_checked }\nrust_scanning_file = Escaneando archivo { $entries_checked }\nrust_scanning_folder = Escaneando carpeta { $entries_checked }\nrust_checked_tags = Etiquetas comprobadas de { $items_stats }\nrust_checked_content = Contenido comprobado de { $items_stats } ({ $size_stats })\nrust_compared_tags = Etiquetas comparadas de { $items_stats }\nrust_compared_content = Contenido comparado de { $items_stats }\nrust_hashed_images = Imágenes Hash { $items_stats } ({ $size_stats })\nrust_compared_image_hashes = Valores de imagen comparados de { $items_stats }\nrust_hashed_videos = Vídeos { $items_stats } en Hash\nrust_created_thumbnails = Miniaturas creadas para vídeos { $items_stats }\nrust_checked_files = Archivo { $items_stats } marcado ({ $size_stats })\nrust_checked_files_bad_extensions = Archivo { $items_stats } marcado\nrust_checked_files_bad_names = Verificado { $items_stats } archivo\nrust_checked_videos = Revisados { $items_stats } videos ({ $size_stats })\nrust_analyzed_partial_hash = Se ha analizado el hash parcial de archivos { $items_stats } ({ $size_stats })\nrust_analyzed_full_hash = Se ha analizado el hash completo de archivos { $items_stats } ({ $size_stats })\nrust_failed_to_rename_file = Error al renombrar el archivo { $old_path } a { $new_path }, error: { $error }\nrust_no_included_paths = No se puede iniciar la escaneo cuando no se han establecido las rutas incluidas.\nrust_all_paths_referenced = No se puede iniciar el escaneo cuando todas las rutas incluidas están configuradas como rutas referenciadas, necesita desmarcar la casilla de verificación junto a la ruta de entrada.\nrust_found_empty_folders = Se encontraron carpetas vacías { $items_found } en { $time }\nrust_found_empty_files = Se encontraron { $items_found } archivos vacíos en { $time }\nrust_found_similar_images = Se encontraron { $items_found } archivos de imágenes similares en grupos { $groups } en { $time }\nrust_found_similar_videos = Se encontraron { $items_found } archivos de vídeo similares en grupos { $groups } en { $time }\nrust_found_similar_music_files = Se encontraron archivos de música similares { $items_found } en grupos { $groups } en { $time }\nrust_found_invalid_symlinks = Encontrados { $items_found } enlaces simbólicos no válidos en { $time }\nrust_found_temporary_files = Encontrados archivos temporales { $items_found } en { $time }\nrust_no_file_type_selected = No se pueden encontrar archivos rotos sin ningún tipo de archivo seleccionado.\nrust_found_broken_files = Se encontraron { $items_found } archivos dañados ocupando { $size } en { $time }\nrust_found_bad_extensions = Se encontraron archivos { $items_found } con extensiones incorrectas en { $time }\nrust_found_bad_names = Encontrados { $items_found } archivos con nombres incorrectos en { $time}\nrust_found_video_optimizer = Encontrados { $items_found } archivos para optimizar en { $time }\nrust_found_duplicate_files = Se encontraron { $items_found } archivos duplicados en { $groups } grupos ocupando { $size } en { $time }\nrust_found_duplicate_files_no_lost_space = Se encontraron { $items_found } archivos duplicados en { $groups } grupos en { $time }\nrust_found_big_files = Encontrados { $items_found } archivos grandes con tamaño { $size } en { $time }\nrust_found_exif_files = Encontrados { $items_found } archivos con exif data en { $time }\nrust_cannot_load_preset = No se puede cambiar y cargar el preset { $preset_idx } - razón { $reason }, usando la configuración predeterminada en su lugar\nrust_saved_preset = Preajuste guardado { $preset_idx }\nrust_cannot_save_preset = No se puede guardar el preset { $preset_idx } - razón { $reason }\nrust_reset_preset = Reiniciar preestablecimiento { $preset_idx }\nrust_cannot_create_output_folder = No se puede crear la carpeta de salida { $output_folder }, razón: { $error }\nrust_delete_summary = Eliminado { $deleted } elementos, falló al eliminar elementos { $failed } , de { $total } elementos\nrust_rename_summary = Renombrado { $renamed } elementos, falló al renombrar elementos { $failed } , de { $total } elementos\nrust_move_summary = No se han podido mover elementos { $moved } y no se han podido mover elementos { $failed } , de { $total }\nrust_hardlink_summary = Enlazados de forma rígida { $hardlinked } elementos, no pudieron enlazar de forma rígida { $failed } elementos, de un total de { $total } elementos\nrust_symlink_summary = Enlaceté { $symlinked } elementos, no pude enlacetar { $failed } elementos, de un total de { $total } elementos\nrust_optimize_video_summary = Vídeos optimizados { $optimized }, vídeos que no optimizaron { $failed }, fuera de { $total } vídeos\nrust_clean_exif_summary = Limpiados EXIF de { $cleaned } archivos, fallo al limpiar { $failed } archivos, de { $total } archivos\nrust_deleting_files = Eliminando archivo { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Eliminando archivo { $items_stats }\nrust_renaming_files = Renombrando archivo { $items_stats }\nrust_moving_files = Moviendo archivo { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = Moviendo archivo { $items_stats }\nrust_hardlinking_files = Enlace duro { $items_stats } archivo ({ $size_stats })\nrust_hardlinking_no_size_files = Enlace duro { $items_stats } archivo\nrust_symlinking_files = Enlazar simbólico al archivo { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Enlazar simbólico al archivo { $items_stats }\nrust_optimizing_videos = Optimizado { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimizado { $items_stats } video\nrust_cleaning_exif = Limpiar EXIF de { $items_stats } archivo ({ $size_stats })\nrust_cleaning_no_size_exif = Limpiar EXIF de { $items_stats } archivo\nrust_no_files_deleted = No hay archivos o carpetas seleccionados para su eliminación\nrust_no_files_renamed = No hay archivos o carpetas seleccionados para renombrar\nrust_no_files_moved = No hay archivos o carpetas seleccionados para mover\nrust_no_files_hardlinked = No archivos ni carpetas seleccionados para el enlace duro\nrust_no_files_symlinked = No archivos ni carpetas seleccionados para el enlace simbólico\nrust_no_videos_optimized = No videos seleccionados para optimización\nrust_no_exif_cleaned = No archivos seleccionados para limpieza de EXIF\nrust_extracted_exif_tags = Extraídos las etiquetas EXIF de { $items_stats } archivos ({ $size_stats })\nrust_delete_confirmation = ¿Está seguro que desea eliminar los elementos seleccionados?\nrust_delete_confirmation_number_simple = { $items } elementos seleccionados.\nrust_delete_confirmation_number_groups = { $items } elementos seleccionados en grupos { $groups }.\nrust_delete_confirmation_selected_all_in_group = Todos los elementos seleccionados en grupos { $groups }.\nrust_move_confirmation = ¿Está usted seguro de que desea mover los elementos seleccionados?\nrust_move_confirmation_number_simple = { $items } elementos seleccionados.\nrust_clean_exif_confirmation = ¿Está usted seguro de que desea eliminar los datos EXIF de los elementos seleccionados?\nrust_clean_exif_confirmation_number_simple = { $items } elementos seleccionados.\nclean_exif_overwrite_files_text = Sobrescribir archivos\nrust_optimize_video_confirmation = ¿Está usted seguro de que desea optimizar los videos seleccionados?\nrust_optimize_video_confirmation_number_simple = { $items } elementos seleccionados.\nrust_hardlink_confirmation = ¿Está usted seguro de que desea crear enlaces duros para los elementos seleccionados?\nrust_hardlink_confirmation_number_simple = { $items } elementos seleccionados.\nrust_symlink_confirmation = ¿Está usted seguro de que desea crear enlaces simbólicos para los elementos seleccionados?\nrust_symlink_confirmation_number_simple = { $items } elementos seleccionados.\nrust_rename_confirmation = ¿Está usted seguro de que desea renombrar los elementos seleccionados?\nrust_rename_confirmation_number_simple = { $items } elementos seleccionados.\nrust_cache_processed_files = Procesados { $files } archivos en caché\nrust_cache_entries_stats = Eliminados { $removed } entradas de todas { $all }, { $left } restantes\nrust_cache_size_reduced = Reducido tamaño de archivos en caché por { $size }\nrust_cache_time_elapsed = Tiempo transcurrido: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Falló crear el enlace duro { $name } a { $target }, motivo { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Selección\ncolumn_size = Tamaño\ncolumn_file_name = Nombre del archivo\ncolumn_path = Ruta\ncolumn_modification_date = Fecha de modificación\ncolumn_similarity = similaridad\ncolumn_dimensions = Dimensiones\ncolumn_new_dimensions = Nuevas Dimensiones\ncolumn_title = Título\ncolumn_artist = Artista\ncolumn_year = Año\ncolumn_bitrate = Tasa de bits\ncolumn_length = Longitud\ncolumn_genre = Género\ncolumn_type_of_error = Tipo de error\ncolumn_symlink_name = Nombre Symlink\ncolumn_symlink_folder = Carpeta Symlink\ncolumn_destination_path = Ruta de destino\ncolumn_current_extension = Extensión actual\ncolumn_proper_extension = Extensión adecuada\ncolumn_fps = FPS\ncolumn_codec = Codificador\ncolumn_duration = Duración\ncolumn_exif_tags = Etiquetas EXIF\ncolumn_new_name = Nuevo Nombre\n# Slint translations\nok_button = De acuerdo\ncancel_button = Cancelar\ndo_you_want_to_continue = ¿Quieres continuar?\nmain_window_title = Krokiet - Limpiador de datos\nscan_button = Escanear\nstop_button = Parar\nstop_text = Parar\nselect_button = Seleccionar\nmove_button = Mover\ndelete_button = Eliminar\nsave_button = Guardar\nsort_button = Ordenar\nrename_button = Renombrar\nmotto = Este programa es gratuito para usar y siempre lo será.\\nConsulte la Licencia MIT/GPL para detalles.\nunicorn = Puede que no mires a un unicornio, pero el unicornio siempre te mira.\nrepository = Repositorio\ninstruction = Instrucción\ndonation = Donación\ntranslation = Traducción\nincluded_paths = Rutas incluidas\nexcluded_paths = Rutas excluidas\nref = Ref\npath = Ruta\ntool_duplicate_files = Archivos duplicados\ntool_empty_folders = Carpetas vacías\ntool_big_files = Archivos grandes\ntool_empty_files = Archivos vacíos\ntool_temporary_files = Archivos temporales\ntool_similar_images = Imágenes similares\ntool_similar_videos = Videos similares\ntool_music_duplicates = Duplicados de música\ntool_invalid_symlinks = Enlaces simbólicos inválidos\ntool_broken_files = Archivos rotos\ntool_bad_extensions = Extensiones incorrectas\ntool_bad_names = Malas Nombres\ntool_video_optimizer = Optimizador de Video\ntool_exif_remover = Eliminador de Exif\nsort_by_full_name = Ordenar por nombre completo\nsort_by_selection = Ordenar por selección\nsort_reverse = Orden inversa\nselection_all = Seleccionar todo\nselection_deselect_all = Deseleccionar todo\nselection_invert_selection = Invertir selección\nselection_the_biggest_size = Seleccione el tamaño más grande\nselection_the_biggest_resolution = Seleccione la resolución más grande\nselection_the_smallest_size = Selecciona el tamaño más pequeño\nselection_the_smallest_resolution = Selecciona la resolución más pequeña\nselection_newest = Seleccionar más reciente\nselection_oldest = Seleccionar más antiguo\nselection_shortest_path = Seleccione la ruta más corta\nselection_longest_path = Seleccione la ruta más larga\nstage_current = Etapa actual:\nstage_all = Todas las etapas:\nsubsettings = Subajustes\nsubsettings_images_hash_size = Tamaño Hash\nsubsettings_images_resize_algorithm = Redimensionar algoritmo\nsubsettings_images_ignore_same_size = Ignorar imágenes del mismo tamaño\nsubsettings_images_max_difference = Diferencia máxima\nsubsettings_images_duplicates_hash_type = Tipo de Hash\nsubsettings_duplicates_check_method = Comprobar método\nsubsettings_duplicates_name_case_sensitive = Sensitivo de mayúsculas (sólo modos de nombres)\nsubsettings_biggest_files_sub_method = Método\nsubsettings_biggest_files_sub_number_of_files = Número de archivos\nsubsettings_videos_max_difference = Diferencia máxima\nsubsettings_videos_ignore_same_size = Ignorar vídeos con el mismo tamaño\nsubsettings_music_audio_check_type = Tipo de verificación de audio\nsubsettings_music_approximate_comparison = Comparación de etiquetas aproximada\nsubsettings_music_compared_tags = Etiquetas comparadas\nsubsettings_music_title = Título\nsubsettings_music_artist = Artista\nsubsettings_music_bitrate = Tasa de bits\nsubsettings_music_genre = Género\nsubsettings_music_year = Año\nsubsettings_music_length = Longitud\nsubsettings_music_max_difference = Diferencia máxima\nsubsettings_music_minimal_fragment_duration = Duración mínima del fragmento\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Comparar dentro de grupos de títulos similares\nsubsettings_broken_files_type = Tipo de archivos a comprobar\nsubsettings_broken_files_audio = Audio\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archivar\nsubsettings_broken_files_image = Imagen\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Utiliza ffmpeg/ffprobe. Bastante lento y puede detectar errores pedantes incluso si el archivo reproduce bien.\nsubsettings_bad_names_issues = Verificaciones de nombre de archivo\nsubsettings_bad_names_uppercase_extension = Mayúscula extensión\nsubsettings_bad_names_uppercase_extension_hint = Encuentra archivos con letras mayúsculas en la extensión (ej., .JPG, .Mp3) y sugiere la versión en minúsculas\nsubsettings_bad_names_emoji_used = Emoji en nombre\nsubsettings_bad_names_emoji_used_hint = Encuentra archivos con caracteres emoji (😀, 🎉, etc.) en el nombre y sugiere eliminarlos\nsubsettings_bad_names_space_at_start_end = espacios iniciales/espacios finales\nsubsettings_bad_names_space_at_start_end_hint = Encuentra archivos con espacios al principio o al final del nombre y sugiere recortarlos\nsubsettings_bad_names_non_ascii = Caracteres no ASCII\nsubsettings_bad_names_non_ascii_hint = Encuentra caracteres no-ASCII (ą, ć, ñ, etc.) y sugiere reemplazarlos con equivalentes ASCII (a, c, n) o eliminarlos si no existe un mapeo\nsubsettings_bad_names_restricted_charset = Conjunto de caracteres limitado\nsubsettings_bad_names_restricted_charset_hint = Transliterates caracteres no-ASCII a ASCII, luego encuentra archivos que contienen caracteres fuera de 0-9a-zA-Z y caracteres permitidos definidos por el usuario\nsubsettings_bad_names_allowed_chars = Caracteres permitidos\nsubsettings_bad_names_remove_duplicated = Caracteres duplicados\nsubsettings_bad_names_remove_duplicated_hint = Encuentra caracteres no alfanuméricos duplicados consecutivos (por ejemplo, \"file---name..txt\") y sugiere la eliminación de duplicados\nsettings_global_settings = Configuración global\nsettings_dark_theme = Tema oscuro\nsettings_show_only_icons = Mostrar sólo iconos\nsettings_excluded_items = Artículo excluido:\nsettings_allowed_extensions = Extensiones permitidas:\nsettings_excluded_extensions = Excluir extensiones:\nsettings_file_size = Tamaño de archivo (kilobytes)\nsettings_minimum_file_size = Mín:\nsettings_maximum_file_size = Máx:\nsettings_recursive_search = Búsqueda recursiva\nsettings_use_cache = Usar caché\nsettings_save_as_json = Guarda también la caché como archivo JSON\nsettings_move_to_trash = Mover archivos borrados a la papelera\nsettings_ignore_other_filesystems = Ignorar otros sistemas de ficheros (sólo Linux)\nsettings_delete_outdated_cache_entries = Borrar automáticamente entradas de caché obsoletas\nsettings_delete_outdated_cache_entries_hint = Cuando esté habilitado, la aplicación verificará durante la carga en caché (una vez como máximo por semana) si los registros en caché aún apuntan a archivos/datos existentes y sin modificar\nsettings_hide_hard_links = Ocultar enlaces duros\nsettings_hide_hard_links_hint = Ocultar enlaces duros a mismos archivos en resultados\nsettings_thread_number = Número del hilo\nsettings_restart_required = ---Necesitas reiniciar la aplicación para aplicar cambios en el número de hilos---\nsettings_duplicate_image_preview = Vista previa de imagen\nsettings_duplicate_minimal_hash_cache_size = Tamaño mínimo de los archivos caché - Hash (KB)\nsettings_duplicate_use_prehash = Usar prehash\nsettings_duplicate_minimal_prehash_cache_size = Tamaño mínimo de los archivos caché - Prehash (KB)\nsettings_similar_images_show_image_preview = Vista previa de imagen\nsettings_application_scale_text = Escala de aplicación\nsettings_application_scale_hint_text = Cuando está habilitada la escala manual, esto permite que elijas un factor de escala personalizado, pero desactiva completamente la escala automática basada en el DPI del monitor.\nsettings_restart_required_scale_text = ---Necesitas reiniciar la aplicación para aplicar los cambios de escala---\nsettings_use_manual_application_scale_text = Usar escala de aplicación manual\nsettings_video_thumbnails_preview = Vista previa de imagen\nsettings_open_config_folder = Abrir carpeta de configuración\nsettings_open_cache_folder = Abrir carpeta de caché\nsettings_language = Idioma\nsettings_current_preset = Preset Actual:\nsettings_edit_name = Editar nombre\nsettings_choose_name_for_prefix = Elegir nombre para el prefijo\nsettings_save = Guardar\nsettings_load = Cargar\nsettings_reset = Reiniciar\nsettings_similar_videos_tool = Herramienta similar de Videos\nsettings_video_thumbnails_clear_unused_thumbnails = Eliminar miniaturas de video no utilizadas que tengan más de 7 días al inicio de la aplicación\nsettings_video_thumbnails_header = Miniaturas de video\nsettings_video_thumbnails_generate = Generar miniaturas\nsettings_video_thumbnails_position = Posición de miniatura en video (%)\nsettings_video_thumbnails_generate_grid = Generar cuadrícula de miniaturas en lugar de una sola imagen\nsettings_video_thumbnails_generate_grid_hint = Generar múltiples imágenes en cuadrícula es mucho más lento que generar una sola miniatura\nsettings_video_thumbnails_grid_tiles_per_side = Número de baldosas por lado en la cuadrícula de miniatura\nsettings_video_thumbnails_grid_tiles_per_side_hint = Número de baldosas de miniatura por lado en la cuadrícula. Por ejemplo, seleccionar 2 crea una cuadrícula de 2 x 2, lo que resulta en una sola miniatura compuesta de 4 imágenes.\nsettings_similar_images_tool = Herramienta de imágenes similares\nsettings_general_settings = Configuración General\nsettings_cache_header_text = Configuración de Caché\nsettings_clean_cache_button_text = Limpiar caché obsoleta\nsettings_settings = Ajustes\nsettings_load_tabs_sizes_at_startup = Cargar tamaños de pestañas al inicio\nsettings_load_windows_size_at_startup = Cargar tamaño de ventanas al iniciar\nsettings_limit_lines_of_messages = Limitar mensajes a 500 líneas (solución temporal para widget de TextEdit lento)\nsettings_play_audio_on_scan_completion_text = Reproducir sonido cuando la exploración se completa con éxito\nsettings_audio_feature_hint_text = Disponible solo al compilar con la función de audio\nsettings_audio_env_variable_hint_text = Se puede cambiar el sonido, estableciendo la variable de entorno KROKIET_AUDIO_STOP_FILE a una ruta de archivo de audio válida\npopup_save_title = Guardando resultados\npopup_save_message = Esto guardará los resultados en 3 archivos diferentes\npopup_rename_title = Renombrar archivos\npopup_new_paths_title = Por favor, añadir rutas una por línea\npopup_move_title = Moviendo archivos\npopup_move_copy_checkbox = Copiar archivos en lugar de moverse\npopup_move_preserve_folder_checkbox = Conservar estructura de carpetas\nmove_confirmation_text = ¿Está usted seguro de que desea mover los elementos seleccionados?\nrename_confirmation_text = ¿Está usted seguro de que desea renombrar los elementos seleccionados?\ndelete = Eliminar elementos\nstopping_scan = Deteniendo el escaneo, por favor espere...\nsearching = Buscando...\nsubsettings_videos_crop_detect = Método de detección de recorte\nsubsettings_videos_skip_forward_amount = Omitir duración [s]\nsubsettings_videos_vid_hash_duration = Duración del hash de vídeo\nsettings_cache_number_size_text = Tamaño de los archivos de caché: { $size }, número de archivos: { $number }\nsettings_video_thumbnails_number_size_text = Tamaño de las miniaturas de vídeo: { $size }, número de archivos: { $number }\nsettings_log_number_size_text = Tamaño de los archivos de registro: { $size }, número de archivos: { $number }\npopup_clean_cache_title_text = Limpiar Caché Obsoleta\npopup_clean_cache_confirmation_text = ¿Está usted seguro de que desea eliminar las entradas de caché obsoletas? Esto eliminará las entradas de caché para archivos que ya no existen o han sido modificados.\npopup_clean_cache_progress_text = Procesando archivo de caché:\npopup_clean_cache_current_file_text = Archivo actual:\npopup_clean_cache_file_progress_text = Progreso actual del archivo:\npopup_clean_cache_overall_progress_text = Progreso general:\npopup_clean_cache_stopped_by_user_text = La limpieza de caché fue detenida por el usuario\npopup_clean_cache_finished_text = Limpieza de caché completada con éxito\npopup_clean_cache_error_details_text = Detalles de error:\npopup_clean_cache_files_with_errors = Archivos con errores:\nsubsettings_video_optimizer_mode = Modo\nsubsettings_video_optimizer_crop_type = Tipo de cultivo\nsubsettings_video_optimizer_black_pixel_threshold = Umbral de píxel negro\nsubsettings_video_optimizer_black_pixel_threshold_hint = El valor RGB máximo para cada canal de píxel a ser considerado negro (0-128). Predeterminado: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Barra Negra Porcentaje Mínimo\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Mínimo porcentaje de píxeles negros en una fila/columna para ser considerado una barra negra (50-100). Predeterminado: 90\nsubsettings_video_optimizer_max_samples = Máximo de muestras\nsubsettings_video_optimizer_max_samples_hint = Número máximo de fotogramas a analizar por video (5-1000). Predeterminado: 60\nsubsettings_video_optimizer_min_crop_size = Tamaño Mínimo de Cosecha\nsubsettings_video_optimizer_min_crop_size_hint = Mínimo píxeles para recortar en cualquier lado (1-1000). Los recortes más pequeños son ignorados. Predeterminado: 5\nsubsettings_video_optimizer_video_codec = Video códec\nsubsettings_video_optimizer_excluded_codecs = Códecs excluidos\nsubsettings_video_optimizer_video_quality = Calidad de video (CRF)\nsubsettings_reset = Restablecer\nsubsettings_exif_ignored_tags_text = Ignorados etiquetas:\nsubsettings_exif_ignored_tags_hint_text = Lista separada por comas de etiquetas a excluir del escaneo (por ejemplo, GPS, Miniatura). Algunas etiquetas, como ImageWidth en archivos TIFF, están ocultas para evitar que se rompa la imagen.\nclean_button_text = Limpio\nclean_text = Datos EXIF limpios\nclean_confirmation_text = ¿Está usted seguro de que desea eliminar los datos EXIF de los elementos seleccionados?\ncrop_videos_text = Cortar videos\ncrop_video_confirmation_text = ¿Está usted seguro de que desea recortar los videos seleccionados?\ncrop_reencode_video_text = Re-codificar video\nreencode_videos_text = Re-codificar videos\noptimize_button_text = Optimizar\noptimize_confirmation_text = ¿Está usted seguro de que desea volver a codificar los videos seleccionados?\noptimize_fail_if_bigger_text = Fallar si el archivo optimizado es más grande\noptimize_overwrite_files_text = Sobrescribir archivos\noptimize_limit_video_size_text = Limitar tamaño de video\noptimize_max_width_text = Máximo ancho:\noptimize_max_height_text = Máximo alto:\nhardlink_button_text = Enlace duro\nhardlink_text = Crear enlaces duros\nhardlink_confirmation_text = ¿Está usted seguro de que desea crear enlaces duros para los elementos seleccionados?\nsoftlink_button_text = Softlink\nsoftlink_text = Crear enlaces simbólicos\nsoftlink_confirmation_text = ¿Está usted seguro de que desea crear enlaces simbólicos (softlinks) para los elementos seleccionados?\n"
  },
  {
    "path": "krokiet/i18n/fa/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = خطای حیاتی در هنگام راه‌اندازی برنامه\nrust_init_error_message = \n        خطای حیاتی در هنگام راه‌اندازی برنامه رخ داد:\n\n        { $error_message }\n\n        این ممکن است به دلیل نبودن یا عملکرد نادرست درایورهای OpenGL/Vulkan، اجرای برنامه در یک ماشین مجازی یا یک باگ در Krokiet یا یکی از کتابخانه‌های آن باشد.\nrust_loaded_preset = پیش‌-installed سفارش { $preset_idx }\nrust_file_already_exists = فایل \"{ $file }\" قبلاً وجود دارد و بر روی آن پوشیده نمی‌شود\nrust_error_removing_file_after_copy = خطای هنگام حذف فایل \"{ $file }\" (پس از کپی شدن به پارتیشن متفاوت)، دلیل: { $reason }\nrust_error_copying_file = خطای کپی \"{ $input }\" به \"{ $output }\"، دلیل: { $reason }\nrust_loading_tags_cache = چャک سیم کانه را بارگذاری کنید\nrust_loading_fingerprints_cache = ارتباطات از طوابق отاگرده را بارگذاری کنید\nrust_saving_tags_cache = موجودیت پنجره‌های کشی\nrust_saving_fingerprints_cache = انبار دruk‌های شاخص را ذخیره کنید\nrust_loading_prehash_cache = _cache پیش هاش آماده_ Dezeer\nrust_saving_prehash_cache = ذخیره کش存在的错误导致无法直接翻译，请确认“prehash cache”是否为专业术语，并提供其准确含义或直接保留英文。若需直接翻译成波斯语的近义词，可告知以便进行翻译。 meantime，保持原始英文：Saving prehash cache\nrust_loading_hash_cache = بارگذاری کاش هشینگ\nrust_saving_hash_cache = گذรده کاش هش را ذخیره کنید\nrust_loading_exif_cache = بارگذاری کش EXIF\nrust_saving_exif_cache = ذخیره کش EXIF\nrust_scanning_name = پیمایش نام { $entries_checked } فایل\nrust_scanning_size_name = اندازه‌ی و نام فایل‌های { $entries_checked } را پردازش کنید\nrust_scanning_size = اندازه‌ی بررسی شده { $entries_checked } فایل\nrust_scanning_file = سkenری‌شدن { $entries_checked } فایل\nrust_scanning_folder = پیمایش { $entries_checked } مappe\nrust_checked_tags = برچسب‌های معتبر شده { $items_stats }\nrust_checked_content = محتوای { $items_stats } ({ $size_stats }) بررسی کردید\nrust_compared_tags = برای شناسایی تگ‌های { $items_stats }\nrust_compared_content = محتوای مقایسه‌ای از { $items_stats }\nrust_hashed_images = پوشیده شده { $items_stats } تصویر ({ $size_stats })\nrust_compared_image_hashes = اشباع هش‌های تصاویر مقایسه شده از { $items_stats }\nrust_hashed_videos = پس انداز شده { $items_stats } ویدئو\nrust_created_thumbnails = تصاویر کمکی برای { $items_stats } ویدیو ساختم\nrust_checked_files = چک شده فایل { $items_stats } (اندازه { $size_stats })\nrust_checked_files_bad_extensions = چک شده فایل { $items_stats }\nrust_checked_files_bad_names = چک شده فایل { $items_stats }\nrust_checked_videos = بررسی شد { $items_stats } ویدیو ({ $size_stats })\nrust_analyzed_partial_hash = جزییات هاش تحلیل شده از { $items_stats } فایل ({ $size_stats })\nrust_analyzed_full_hash = انالیز هش کامل { $items_stats } فایل ({ $size_stats })\nrust_failed_to_rename_file = خطا در تغییر نام فایل { $old_path } به { $new_path }, помک: { $error }\nrust_no_included_paths = امکان شروع اسکن وجود ندارد، زمانی که مسیرهای گنجانده شده تعیین نشده باشند.\nrust_all_paths_referenced = امکان شروع اسکن وجود ندارد، زمانی که تمام مسیرهای گنجانده شده به عنوان مسیرهای ارجاعی تنظیم شده باشند، باید ابتدا تیک گزینه‌ی ارجاع کنار مسیر ورودی را غیرفعال کنید.\nrust_found_empty_folders = دایره‌های خالی { $items_found } را در { $time } پیدا کردم\nrust_found_empty_files = { $items_found } فایل خالی در { $time } پیدا شد\nrust_found_similar_images = { $items_found } تصویر مشابه در { $groups } گروه در { $time } پیدا شده است\nrust_found_similar_videos = مثیه‌{ $items_found } فایل ویدئو مشابه در { $groups } گروه در { $time } پیدا کردіم\nrust_found_similar_music_files = یکشمار { $items_found } فایل موسیقی مشابه در { $groups } گروه در { $time } پیدا کرده‌ام\nrust_found_invalid_symlinks = موجودی { $items_found } لینک منحرف نامعتبر در { $time }\nrust_found_temporary_files = { $items_found } فایل موقت دست به دست کرد در { $time }\nrust_no_file_type_selected = بدون انتخاب فرمت فایل، سیستم نمی‌تواند فایل‌های شکسته را پیدا کند.\nrust_found_broken_files = مورد پیدا کردن { $items_found } فایل شکسته با حجم { $size } در { $time }\nrust_found_bad_extensions = موجودی { $items_found } فایل با پسوند نادرست در { $time }\nrust_found_bad_names = یافته { $items_found } فایل با نام‌های نامناسب در { $time }\nrust_found_video_optimizer = یافته { $items_found } فایل برای بهینه‌سازی در { $time }\nrust_found_duplicate_files = موجودی { $items_found } فایل تکراری در { $groups } گروه با حجم کل { $size } در { $time } پیدا شده است\nrust_found_duplicate_files_no_lost_space = در { $items_found } فایل تکراری در { $groups } گروه در { $time } پیدا شده است\nrust_found_big_files = یافته شد { $items_found } فایل بزرگ با حجم { $size } در { $time }\nrust_found_exif_files = یافته { $items_found } فایل با داده‌های exif در { $time }\nrust_cannot_load_preset = نمی‌توان به روزرسانی و تحمیل تنظیمات پیشفرض { $preset_idx } - دلیل { $reason }، به جای آن از تنظیمات پیش فرض استفاده می‌شود\nrust_saved_preset = پیش‌فرض ذخیره شده { $preset_idx }\nrust_cannot_save_preset = نمی‌توان پیش فرض { $preset_idx } را ذخیره - دلیل { $reason }\nrust_reset_preset = تنظیم پیش‌فرض { $preset_idx } را بازنشانی کنید\nrust_cannot_create_output_folder = نمی‌توانم مسیر خروجی { $output_folder } را ایجاد کنم، دلیل: { $error }\nrust_delete_summary = حذف شده‌ی { $deleted } مورد، نجات نشده‌ی { $failed } مورد، از کل { $total } مورد\nrust_rename_summary = نام { $renamed } آیتم تغییر کرد، نام { $failed } آیتم موفق به تغییر نشد، از مجموع { $total } آیتم\nrust_move_summary = انتقال { $moved } قطعه، موفقیت آمیز نبود { $failed } قطعه، از کل { $total } قطعه\nrust_hardlink_summary = پیوند داده شده { $hardlinked } آیتم‌ها، نتوانستند { $failed } آیتم را پیوند دهند، از { $total } آیتم\nrust_symlink_summary = پیوند داده شد { $symlinked } آیتم، ناموفق بود در پیوند دادن { $failed } آیتم، از { $total } آیتم\nrust_optimize_video_summary = ویدیوهای بهینه‌شده { $optimized }، ویدیوهای ناموفق در بهینه‌سازی { $failed }، از مجموع { $total } ویدیو\nrust_clean_exif_summary = تمیز کردن EXIF از فایل‌های { $cleaned }، ناموفق در پاکسازی فایل‌های { $failed }، از مجموع { $total } فایل\nrust_deleting_files = حذف فایل { $items_stats } (سایز { $size_stats })\nrust_deleting_no_size_files = حذف فایل { $items_stats }\nrust_renaming_files = تغییر نام فایل { $items_stats }\nrust_moving_files = منتقل کردن فایل { $items_stats }(سیز { $size_stats })\nrust_moving_no_size_files = منتقل کردن فایل { $items_stats }\nrust_hardlinking_files = پیوند سخت { $items_stats } فایل ({ $size_stats })\nrust_hardlinking_no_size_files = پیوند سخت { $items_stats } فایل\nrust_symlinking_files = ارتباط پیوند نمادین { $items_stats } فایل ({ $size_stats })\nrust_symlinking_no_size_files = ارتباط پیوند نمادین { $items_stats } فایل\nrust_optimizing_videos = بهینه‌سازی شده { $items_stats } ویدیوی ({ $size_stats })\nrust_optimizing_no_size_videos = ویدیو بهینه‌سازی شده { $items_stats }\nrust_cleaning_exif = پاک‌سازی EXIF از فایل { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = پاک کردن EXIF از فایل { $items_stats }\nrust_no_files_deleted = لیست نرم‌افزار یا پوشه مورد حذف انتخاب نشده است\nrust_no_files_renamed = هیچ فایل یا پوشه‌ی انتخابی برای نامبردهش وجود ندارد\nrust_no_files_moved = هیچ فایل یا mapه نم chosen برای منتقل کردن است\nrust_no_files_hardlinked = فایل یا پوشه‌ای برای پیوند سخت انتخاب نشده است\nrust_no_files_symlinked = هیچ فایل یا پوشه‌ای برای پیوند نمایی انتخاب نشده است\nrust_no_videos_optimized = بهینه‌سازی نشده / ویدیو انتخاب نشده برای بهینه‌سازی\nrust_no_exif_cleaned = فایل‌های انتخابی برای پاکسازی EXIF وجود ندارد\nrust_extracted_exif_tags = استخراج برچسب‌های EXIF از فایل‌های { $items_stats } ({ $size_stats })\nrust_delete_confirmation = هستید وظیفه‌ای را برای حذف قطعات انتخاب شده را اطمینان دارید؟\nrust_delete_confirmation_number_simple = { $items } آیتم انتخاب شد.\nrust_delete_confirmation_number_groups = { $items } مورد در { $groups } گروه انتخاب شده است.\nrust_delete_confirmation_selected_all_in_group = همه آیتم‌ها در { $groups } گروه انتخاب شده است.\nrust_move_confirmation = آیا مطمئن هستید که می‌خواهید آیتم‌های انتخاب شده را جابجا کنید؟\nrust_move_confirmation_number_simple = { $items } آیتم‌های انتخاب شده.\nrust_clean_exif_confirmation = آیا مطمئن هستید که می‌خواهید داده‌های EXIF را از آیتم‌های انتخاب شده حذف کنید؟\nrust_clean_exif_confirmation_number_simple = { $items } آیتم‌های انتخاب شده.\nclean_exif_overwrite_files_text = بازنویسی فایل‌ها\nrust_optimize_video_confirmation = آیا مطمئن هستید که می‌خواهید ویدیوهای انتخاب شده را بهینه‌سازی کنید؟\nrust_optimize_video_confirmation_number_simple = { $items } آیتم‌های انتخاب شده.\nrust_hardlink_confirmation = آیا مطمئن هستید که می‌خواهید لینک‌های سخت برای آیتم‌های انتخاب شده ایجاد کنید؟\nrust_hardlink_confirmation_number_simple = { $items } آیتم‌های انتخاب شده.\nrust_symlink_confirmation = آیا مطمئن هستید که می‌خواهید برای آیتم‌های انتخاب شده پیوند نمایی ایجاد کنید؟\nrust_symlink_confirmation_number_simple = { $items } آیتم‌های انتخاب شده.\nrust_rename_confirmation = آیا مطمئن هستید که می‌خواهید آیتم‌های انتخاب شده را تغییر نام دهید؟\nrust_rename_confirmation_number_simple = { $items } آیتم‌های انتخاب شده.\nrust_cache_processed_files = فایل‌های کش { $files } پردازش شدند\nrust_cache_entries_stats = حذف { $removed } از { $all }، { $left } باقی‌مانده\nrust_cache_size_reduced = کاهش اندازه فایل‌های کش شده توسط { $size }\nrust_cache_time_elapsed = زمان سپری شده: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = به ترکیب مجدد { $name } به { $target } امکان پذیر نشد، دلیل { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = انتخاب\ncolumn_size = سایز\ncolumn_file_name = نام فایل\ncolumn_path = مسیر\ncolumn_modification_date = تاریخ تغییر\"http://www.example.com/\"\ncolumn_similarity = شباهت\ncolumn_dimensions = بعد از اندازه‌گیریاته کنید\ncolumn_new_dimensions = новых измерения\ncolumn_title = عنوان\ncolumn_artist = خاطره\ncolumn_year = سال\ncolumn_bitrate = bitrate\ncolumn_length = طول\ncolumn_genre = ژانر\ncolumn_type_of_error = نوع خطا\ncolumn_symlink_name = نام سایمنلکت\ncolumn_symlink_folder = لینک جایگزین پوشه\ncolumn_destination_path = مسیر مقصد\ncolumn_current_extension = توسیع حاضر\ncolumn_proper_extension = توسعه مناسب\ncolumn_fps = فپس\ncolumn_codec = کدکوډ ( Codec )\ncolumn_duration = مدت زمان\ncolumn_exif_tags = تگ‌های EXIF\ncolumn_new_name = نام جدید\n# Slint translations\nok_button = اوکی\ncancel_button = لغو\ndo_you_want_to_continue = شما عازم به دنباله هستید؟\nmain_window_title = کروکیت - پاکنامه داده‌ها\nscan_button = سکن\nstop_button = ストップ\nstop_text = ストップ\nselect_button = انتخاب کنید\nmove_button = برح<label class=\"move\">الی</label>mite\ndelete_button = حذف\nsave_button = پای 若要继续，请输入完整的问题或命令。保存已被直接翻译为“پای”，但在你的要求中似乎有误，正确的应是“ذخیره”来对应“Save”。请确认并提供具体指令以获得准确翻译。\nsort_button = مرتب کردن\nrename_button = تغییر نام\nmotto = این برنامه را می‌توانید به طور آزاد استفاده کنید و همیشه اینطور خواهد بود.\\nبرای جزئیات، به پروتکلライセンス MIT/GPL نگاه کنید.\nunicorn = \n    شما ممکن است یک گوسفند سرخ نگاه نکنید، اما گوسفند سرخ همいつante始发时间总是过去时，你总是在它之后。永远不要认为你了解了什么，因为每一次的遇见都是一次新的开始。\n    你可能不会去看一只独角兽，但独角兽总是看着你。.\nrepository = گارایو\ninstruction = دکمه دستورالعمل\ndonation = پیمانکار\ntranslation = ترجمه:\nincluded_paths = مسارات شامل‌شده\nexcluded_paths = مسیرهای حذف‌شده\nref = .\"',()'\npath = مسیر\ntool_duplicate_files = ملف‌های تکراری\ntool_empty_folders = _folders تهی завدهندی\ntool_big_files = فایل‌های بزرگ\ntool_empty_files = فایل‌های خالی\ntool_temporary_files = فایل‌های موقت\ntool_similar_images = تصویرهای مشابه\ntool_similar_videos = вیدیوهای مشابه\ntool_music_duplicates = 拷贝의 음악\ntool_invalid_symlinks = تعدادی از لینک‌های نامعتبر\ntool_broken_files = فایل‌های شکسته\ntool_bad_extensions = مدیریت‌های بد\ntool_bad_names = نام‌های بد\ntool_video_optimizer = بهینه‌ساز ویدیو\ntool_exif_remover = حذف EXIF\nsort_by_full_name = sort با نام کامل\nsort_by_selection = مرتب سازی بر اساس انتخاب\nsort_reverse = سیر opposition\nselection_all = انتخاب همه\nselection_deselect_all = تمامی را حذف کنید\nselection_invert_selection = انتخاب را دور زنید\nselection_the_biggest_size = حجم بزرگترین را انتخاب کنید\nselection_the_biggest_resolution = گزینه‌ی بزرگترین حلوله را انتخاب کنید\nselection_the_smallest_size = چویید حجم کوچکترین را\nselection_the_smallest_resolution = انتخاب توزیع کوچکتر را انجام دهید\nselection_newest = انتخاب جدید‌ترین\nselection_oldest = انتخاب جوان‌تر\nselection_shortest_path = انتخاب کوتاه‌ترین مسیر را\nselection_longest_path = انتخاب مسیر طولانی‌ترین را\nstage_current = حالي‌گراي شايد:\nstage_all = همه مرحله‌ها:\nsubsettings = تنظیمات زیر Menú\nsubsettings_images_hash_size = سایز هاش\nsubsettings_images_resize_algorithm = الگوریتم سایز دهی\nsubsettings_images_ignore_same_size = تصویرهای با سازه‌ی شابло را تجربه نکنید\nsubsettings_images_max_difference = مaks تفاوت\nsubsettings_images_duplicates_hash_type = نوع هاش\nsubsettings_duplicates_check_method = Rihanna روش بررسی کنید\nsubsettings_duplicates_name_case_sensitive = حساس به حروف بزرگ و کوچک (فقط موارد نام)\nsubsettings_biggest_files_sub_method = روش\nsubsettings_biggest_files_sub_number_of_files = تعداد فایل‌ها\nsubsettings_videos_max_difference = مaks تفاوت\nsubsettings_videos_ignore_same_size = ایمیل‌های با ابعاد تکراری را忽略相同尺寸的视频\nsubsettings_music_audio_check_type = چک آمادگی صوتی\nsubsettings_music_approximate_comparison = _COMPاراسیم برچسب‌های تقریبی_\nsubsettings_music_compared_tags = مقایسه پیوند‌ها\nsubsettings_music_title = عنوان\nsubsettings_music_artist = موسیقیدان\nsubsettings_music_bitrate = بیتریت Axe (This appears to be a placeholder or typo in the original text)\nsubsettings_music_genre = جنسیت\nsubsettings_music_year = سال\nsubsettings_music_length = طول\nsubsettings_music_max_difference = م Axeس دیفرانس\nsubsettings_music_minimal_fragment_duration = مدت زمان کوتاهترین بخش\nsubsettings_music_compare_fingerprints_only_with_similar_titles = در مقایسه گروه‌های با موضوعات مشابه\nsubsettings_broken_files_type = نوع فایل‌هایی که بررسی کنید\nsubsettings_broken_files_audio = آهنگ\nsubsettings_broken_files_pdf = پdf\nsubsettings_broken_files_archive = ارشیو\nsubsettings_broken_files_image = عکس\nsubsettings_broken_files_video = ویدئو\nsubsettings_broken_files_video_info = از ffmpeg/ffprobe استفاده می‌کند. نسبتاً کند است و ممکن است حتی اگر فایل به درستی پخش شود، خطاهای دقیق را تشخیص دهد.\nsubsettings_bad_names_issues = بررسی نام فایل\nsubsettings_bad_names_uppercase_extension = افزایش حروف بزرگ\nsubsettings_bad_names_uppercase_extension_hint = پیدا کردن فایل‌هایی با حروف بزرگ در پسوند (مانند .JPG، .Mp3) و پیشنهاد نسخه کوچک‌نویسی‌شده\nsubsettings_bad_names_emoji_used = ایموجی در نام\nsubsettings_bad_names_emoji_used_hint = پیدا کردن فایل‌هایی با کاراکترهای ایموجی (😀، 🎉، و غیره) در نام و پیشنهاد حذف آن‌ها\nsubsettings_bad_names_space_at_start_end = فضاهای پیشانی/پایانی\nsubsettings_bad_names_space_at_start_end_hint = پیدا کردن فایل‌هایی با فضاهای خالی در ابتدا یا انتهای نام و پیشنهاد حذف آن‌ها\nsubsettings_bad_names_non_ascii = کاراکترهای غیر ASCII\nsubsettings_bad_names_non_ascii_hint = پیدا می‌کند کاراکترهای غیر ASCII (مانند ą، ć، ñ و غیره) و پیشنهاد می‌دهد آن‌ها را با معادل‌های ASCII (مانند a، c، n) جایگزین کند یا اگر هیچ نگاشت وجود نداشته باشد، حذف کند\nsubsettings_bad_names_restricted_charset = charset محدود\nsubsettings_bad_names_restricted_charset_hint = تبدیل می‌کند کاراکترهای غیر-ASCII را به ASCII، سپس فایل‌هایی را که شامل کاراکترهایی خارج از 0-9a-zA-Z و کاراکترهای مجاز تعریف‌شده توسط کاربر هستند، پیدا می‌کند\nsubsettings_bad_names_allowed_chars = مجاز است\nsubsettings_bad_names_remove_duplicated = حروف تکراری\nsubsettings_bad_names_remove_duplicated_hint = پیدا کردن کاراکترهای تکراری پشت سر هم غیر الفبایی (مانند \"file---name..txt\") و پیشنهاد حذف تکرارها\nsettings_global_settings = تنظیمات جهانی\nsettings_dark_theme = نیکت سیاه\nsettings_show_only_icons = نمایش فقط علامت‌گذاری\nsettings_excluded_items = مورد محرمانه:\nsettings_allowed_extensions = مدت زمان پذیرش:\nsettings_excluded_extensions = امتدادات استثنا:\\\nsettings_file_size = حجمDos (کیLOBایت)\nsettings_minimum_file_size = من:\nsettings_maximum_file_size = مکس:\nsettings_recursive_search = جستجو خودکار\nsettings_use_cache = استفاده از کشی\nsettings_save_as_json = همچنین кеш را به فایل JSON ذخیره کنید\nsettings_move_to_trash = فایل‌های حذف شده را به سبد اسکیز منتقل کنید\nsettings_ignore_other_filesystems = سایر سیستم‌هايisis FileSystem را ددغناييد (فقط لينукس)\nsettings_delete_outdated_cache_entries = حذف خودکار ورودی‌های کش منسوخ‌شده به‌طور خودکار\nsettings_delete_outdated_cache_entries_hint = هنگام فعال‌سازی، برنامه در هنگام بارگذاری کش (حداکثر یک بار در هفته) بررسی می‌کند که آیا رکودهای کش‌شده هنوز به فایل‌ها/داده‌های موجود و بدون تغییر اشاره می‌کنند یا خیر\nsettings_hide_hard_links = پناه از پیوندهای سخت را بپناهید\nsettings_hide_hard_links_hint = پنهان کردن پیوند‌های سخت به یکسان فایل‌ها در نتایج\nsettings_thread_number = شماره‌ی tread\nsettings_restart_required = شما باید برنامه را دوباره اجرا کنید تا تغییرات در شماره نادل را اعمال کنید---\nsettings_duplicate_image_preview = نمایش نیمه تصویر\nsettings_duplicate_minimal_hash_cache_size = حجم مinیمum پرونده‌های کش شده - چکیده (کیلوبات)\nsettings_duplicate_use_prehash = استفاده از prehash\nsettings_duplicate_minimal_prehash_cache_size = حجم مینیمم فایل‌های کاش شده - پرسیکس (کیلو بایت)\nsettings_similar_images_show_image_preview = نمای prevی شاخص 이미ジプレビュー\nsettings_application_scale_text = مقیاس درخواست\nsettings_application_scale_hint_text = هنگامی که مقیاس دستی فعال است، این به شما این امکان را می‌دهد که یک فاکتور مقیاس سفارشی انتخاب کنید، اما به طور کامل مقیاس‌بندی خودکار را بر اساس DPI مانیتور غیرفعال می‌کند.\nsettings_restart_required_scale_text = ---شما باید برنامه را برای اعمال تغییرات مقیاس راه‌اندازی مجدد کنید---\nsettings_use_manual_application_scale_text = استفاده از مقیاس دستی\nsettings_video_thumbnails_preview = نمایش پیش‌نمایشbeeld\nsettings_open_config_folder = دایرکتوری تنظیمات را باز کنید\nsettings_open_cache_folder = پوشه کشیش را باز کنید\nsettings_language = زبان\nsettings_current_preset = پیش‌فرض فعلی:\nsettings_edit_name = ویرایش نام\nsettings_choose_name_for_prefix = نام مختصری برای پروزش انتخاب کنید\nsettings_save = ذخیره\nsettings_load = caricaturه\nsettings_reset = بازگشت\nsettings_similar_videos_tool = gereh ویدئوهای مشابه\nsettings_video_thumbnails_clear_unused_thumbnails = حذف تصاویر پس‌زمینه ویدیویی غیرفعال قدیمی‌تر از 7 روز در هنگام راه‌اندازی برنامه\nsettings_video_thumbnails_header = تصاویر کوچک ویدیو\nsettings_video_thumbnails_generate = تولید پیش‌نمایش‌ها\nsettings_video_thumbnails_position = pozیション تصویر کمپین در ویدئو (%)\nsettings_video_thumbnails_generate_grid = ایجاد شبکه پیش‌نمایه‌ها به جای یک تصویر واحد\nsettings_video_thumbnails_generate_grid_hint = ایجاد چندین تصویر در یک شبکه، کندتر از تولید یک پیش‌نمایش تک است\nsettings_video_thumbnails_grid_tiles_per_side = تعداد تراشه ها در هر طرف در شبکه کوچک تصویر\nsettings_video_thumbnails_grid_tiles_per_side_hint = تعداد بلاک‌های تصویر کوچک در هر طرف در شبکه. به عنوان مثال، انتخاب 2 یک شبکه 2x2 ایجاد می‌کند که منجر به یک تصویر کوچک واحدی می‌شود که از 4 تصویر تشکیل شده است.\nsettings_similar_images_tool = آبکاری شبیه آنگانه‌ها کارساز\nsettings_general_settings = تنظیمات کلی\nsettings_cache_header_text = تنظیمات کش\nsettings_clean_cache_button_text = پاک کردن کش قدیمی\nsettings_settings = تنظیمات\nsettings_load_tabs_sizes_at_startup = سایز های قوائم را در آغاز بارگذاری بارگذاری کنید\nsettings_load_windows_size_at_startup = اکتشاف سایز پنجره در زمان شروع\nsettings_limit_lines_of_messages = متن پیام‌ها را به سرعت ۵۰۰ خط حفظ کنید(حلی برای ویجت sluggish TextEdit)\nsettings_play_audio_on_scan_completion_text = صدای پخش هنگام اتمام موفقیت‌آمیز اسکن\nsettings_audio_feature_hint_text = فقط در صورت کامپایل با ویژگی صوتی در دسترس است\nsettings_audio_env_variable_hint_text = می‌تواند صدا را تغییر داد، با تنظیم متغیر محیطی KROKIET_AUDIO_STOP_FILE به یک مسیر فایل صوتی معتبر\npopup_save_title = ذخیره نتایج\npopup_save_message = این متن نتیجه را به سه فایل متفاوت ذخیره خواهد کرد\npopup_rename_title = بازنامه فایل‌ها\npopup_new_paths_title = لطفا مسیرها را یکی در هر سطر اضافه کنید\npopup_move_title = انتقال فایل‌ها\npopup_move_copy_checkbox = کپی فایل‌ها به جای منتقل کردن\npopup_move_preserve_folder_checkbox = ساختار پوشه را حفظ کنید\nmove_confirmation_text = آیا مطمئن هستید که می‌خواهید آیتم‌های انتخاب شده را جابجا کنید؟\nrename_confirmation_text = آیا مطمئن هستید که می‌خواهید آیتم‌های انتخاب شده را تغییر نام دهید؟\ndelete = حذف منازعات\nstopping_scan = بررسی متوقف شد، لطفاً صبور باشید...\nsearching = جستجو کردن...\nsubsettings_videos_crop_detect = روش شناساندن کره\nsubsettings_videos_skip_forward_amount = مدت زمان hop [س]\nsubsettings_videos_vid_hash_duration = مدت زمان ویدیو هاش\nsettings_cache_number_size_text = حجم فایل‌های پوше: { $size }, تعداد فایل‌ها: { $number }\nsettings_video_thumbnails_number_size_text = rozمغزهای ویدیو: { $size }، تعداد فایل‌ها: { $number }\nsettings_log_number_size_text = اندازه فایل‌های لگ: { $size }, تعداد فایل‌ها: { $number }\npopup_clean_cache_title_text = پاک کردن کش قدیمی\npopup_clean_cache_confirmation_text = آیا مطمئن هستید که می‌خواهید آیتم‌های کش قدیمی را پاک کنید؟ این کار آیتم‌های کش را برای فایل‌هایی که دیگر وجود ندارند یا تغییر کرده‌اند حذف می‌کند.\npopup_clean_cache_progress_text = در حال پردازش فایل کش:\npopup_clean_cache_current_file_text = فایل فعلی:\npopup_clean_cache_file_progress_text = پیشرفت فایل فعلی:\npopup_clean_cache_overall_progress_text = پیشرفت کلی:\npopup_clean_cache_stopped_by_user_text = پاکسازی کش توسط کاربر متوقف شد\npopup_clean_cache_finished_text = پاکسازی کش با موفقیت انجام شد!\npopup_clean_cache_error_details_text = جزئیات خطا:\npopup_clean_cache_files_with_errors = فایل‌های دارای خطا:\nsubsettings_video_optimizer_mode = حالت\nsubsettings_video_optimizer_crop_type = نوع محصول\nsubsettings_video_optimizer_black_pixel_threshold = آستانه پیکسلی سیاه\nsubsettings_video_optimizer_black_pixel_threshold_hint = حداکثر مقدار RGB برای هر کانال پیکسل در نظر گرفته شود سیاه (0-128). پیش‌فرض: 20\nsubsettings_video_optimizer_black_bar_min_percentage = ٪ حداقل نوار سیاه\nsubsettings_video_optimizer_black_bar_min_percentage_hint = حداقل درصد پیکسل‌های سیاه در یک ردیف/ستون برای در نظر گرفته شدن به عنوان یک نوار سیاه (50-100). پیش‌فرض: 90\nsubsettings_video_optimizer_max_samples = حداکثر نمونه‌ها\nsubsettings_video_optimizer_max_samples_hint = حداکثر تعداد فریم‌های قابل تجزیه و تحلیل در هر ویدیو (5-1000). پیش‌فرض: 60\nsubsettings_video_optimizer_min_crop_size = حداقل اندازه محصول\nsubsettings_video_optimizer_min_crop_size_hint = حداقل پیکسل‌های قابل برش در هر طرف (1-1000). برش‌های کوچک نادیده گرفته می‌شوند. پیش‌فرض: 5\nsubsettings_video_optimizer_video_codec = ویدئو کُدک\nsubsettings_video_optimizer_excluded_codecs = استثنی شده کدک‌ها\nsubsettings_video_optimizer_video_quality = کیفیت ویدیو (CRF)\nsubsettings_reset = بازنشانی\nsubsettings_exif_ignored_tags_text = تگ‌های نادیده گرفته شده:\nsubsettings_exif_ignored_tags_hint_text = لیست جدا شده با کاما از برچسب‌هایی که باید از اسکن حذف شوند (به عنوان مثال، GPS، Thumbnail). برخی از برچسب‌ها، مانند ImageWidth در فایل‌های TIFF، پنهان شده‌اند تا از شکستن تصویر جلوگیری شود.\nclean_button_text = تمیز\nclean_text = داده‌های EXIF تمیز\nclean_confirmation_text = آیا مطمئن هستید که می‌خواهید داده‌های EXIF را از آیتم‌های انتخاب شده حذف کنید؟\ncrop_videos_text = برش ویدیوها\ncrop_video_confirmation_text = آیا مطمئن هستید که می‌خواهید ویدیوهای انتخاب شده را برش دهید؟\ncrop_reencode_video_text = باز‌رمزنگاری ویدیو\nreencode_videos_text = بازکدگذاری ویدیوها\noptimize_button_text = بهینه‌سازی\noptimize_confirmation_text = آیا مطمئن هستید که می‌خواهید ویدیوهای انتخاب شده را دوباره رمزگذاری کنید؟\noptimize_fail_if_bigger_text = اگر فایل بهینه شده بزرگتر شد، شکست بخشد\noptimize_overwrite_files_text = بازنویسی فایل‌ها\noptimize_limit_video_size_text = حداکثر اندازه ویدیو\noptimize_max_width_text = حداکثر عرض:\noptimize_max_height_text = حداکثر ارتفاع:\nhardlink_button_text = هاردلینک\nhardlink_text = ایجاد پیوند سخت\nhardlink_confirmation_text = آیا مطمئن هستید که می‌خواهید لینک‌های سخت برای آیتم‌های انتخاب شده ایجاد کنید؟\nsoftlink_button_text = سافت‌لینک\nsoftlink_text = ایجاد پیوند نرم\nsoftlink_confirmation_text = آیا مطمئن هستید که می‌خواهید پیوند نرم‌افزاری (symlinks) را برای آیتم‌های انتخاب شده ایجاد کنید؟\n"
  },
  {
    "path": "krokiet/i18n/fr/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Erreur Critique Pendant Le Démarrage De L'Application\nrust_init_error_message = \n        Une erreur critique s'est produite lors du démarrage de l'application :\n\n        { $error_message }\n\n        Cela peut être dû à des pilotes OpenGL/Vulkan manquants ou défectueux, à l'exécution de l'application dans une machine virtuelle ou à un bug dans Krokiet ou l'une de ses bibliothèques.\n\n        Vous pouvez essayer d'exécuter différentes versions (skia_opengl, skia_vulkan, femtovg_opengl - par défaut) ou avec un rendu logiciel pour voir si cela résout le problème.\nrust_loaded_preset = Préréglage { $preset_idx } chargé\nrust_file_already_exists = Fichier \"{ $file }\" existe déjà et ne sera pas écrasé\nrust_error_removing_file_after_copy = Erreur lors de la suppression du fichier \"{ $file }\" (après copie sur une autre partition), la raison : { $reason }\nrust_error_copying_file = Erreur lors de la copie de \"{ $input }\" vers \"{ $output }\", la raison : { $reason }\nrust_loading_tags_cache = Chargement du cache des étiquettes\nrust_loading_fingerprints_cache = Chargement du cache des empreintes digitales\nrust_saving_tags_cache = Sauvegarde du cache des étiquettes\nrust_saving_fingerprints_cache = Enregistrement du cache des empreintes digitales\nrust_loading_prehash_cache = Chargement du cache du prehash\nrust_saving_prehash_cache = Sauvegarde du cache du prehash\nrust_loading_hash_cache = Chargement du cache de hachage\nrust_saving_hash_cache = Sauvegarde du cache de hachage\nrust_loading_exif_cache = Chargement de cache EXIF\nrust_saving_exif_cache = Enregistrement du cache EXIF\nrust_scanning_name = Analyse du nom du fichier { $entries_checked }\nrust_scanning_size_name = Analyse de la taille et du nom du fichier { $entries_checked }\nrust_scanning_size = Analyse de la taille du fichier { $entries_checked }\nrust_scanning_file = Analyse du fichier { $entries_checked }\nrust_scanning_folder = Analyse du dossier { $entries_checked }\nrust_checked_tags = Étiquettes vérifiées pour { $items_stats }\nrust_checked_content = Contenu vérifié pour { $items_stats } ({ $size_stats })\nrust_compared_tags = Étiquettes comparées pour { $items_stats }\nrust_compared_content = Contenu comparé pour { $items_stats }\nrust_hashed_images = { $items_stats } images hachées ({ $size_stats })\nrust_compared_image_hashes = Hachages d'images comparés pour { $items_stats }\nrust_hashed_videos = { $items_stats } vidéos hachées\nrust_created_thumbnails = Miniatures créées pour { $items_stats } vidéos\nrust_checked_files = { $items_stats } fichiers vérifiés ({ $size_stats })\nrust_checked_files_bad_extensions = { $items_stats } fichiers vérifiés\nrust_checked_files_bad_names = { $items_stats } fichiers vérifiés\nrust_checked_videos = { $items_stats } vidéos vérifiées ({ $size_stats })\nrust_analyzed_partial_hash = Hash partiel analysé pour { $items_stats } fichiers ({ $size_stats })\nrust_analyzed_full_hash = Hash complet analysé des { $items_stats } fichiers ({ $size_stats })\nrust_failed_to_rename_file = Impossible de renommer le fichier { $old_path } en { $new_path }, erreur: { $error }\nrust_no_included_paths = Impossible de démarrer l'analyse lorsque aucun chemin inclus n'est défini.\nrust_all_paths_referenced = Impossible de démarrer l'analyse lorsque tous les chemins inclus sont définis comme des chemins référencés, vous devez désactiver la case à cocher à côté du chemin d'entrée.\nrust_found_empty_folders = Trouvé les dossiers { $items_found } vides dans { $time }\nrust_found_empty_files = Fichier { $items_found } vide trouvé dans { $time }\nrust_found_similar_images = Trouvé { $items_found } fichiers images similaires dans les groupes { $groups } dans { $time }\nrust_found_similar_videos = Trouvé { $items_found } fichiers vidéo similaires dans les groupes { $groups } dans { $time }\nrust_found_similar_music_files = Trouvé { $items_found } fichiers de musique similaires dans les groupes { $groups } dans { $time }\nrust_found_invalid_symlinks = Liens symboliques { $items_found } non valides dans { $time }\nrust_found_temporary_files = Fichiers temporaires { $items_found } trouvés dans { $time }\nrust_no_file_type_selected = Impossible de trouver des fichiers cassés sans aucun type de fichier sélectionné.\nrust_found_broken_files = Trouvé { $items_found } fichiers cassés prenant { $size } en { $time }\nrust_found_bad_extensions = Fichiers { $items_found } trouvés avec des extensions incorrectes dans { $time }\nrust_found_bad_names = Trouvés { $items_found } fichiers avec des noms incorrects dans { $time }\nrust_found_video_optimizer = { $items_found } fichiers à optimiser trouvés en { $time }\nrust_found_duplicate_files = { $items_found } fichiers dupliqués trouvés dans { $groups } groupes prenant { $size } en { $time }\nrust_found_duplicate_files_no_lost_space = { $items_found } fichiers dupliqués trouvés dans { $groups } groupes en { $time }\nrust_found_big_files = { $items_found } gros fichiers trouvés de taille { $size } en { $time }\nrust_found_exif_files = { $items_found } fichiers à optimiser trouvés en { $time }\nrust_cannot_load_preset = Impossible de modifier et de charger le préréglage { $preset_idx } - raison { $reason }. Utilisation des paramètres par défaut à la place\nrust_saved_preset = Préréglage { $preset_idx } enregistré\nrust_cannot_save_preset = Impossible d'enregistrer le pré-réglage { $preset_idx } - raison { $reason }\nrust_reset_preset = Réinitialiser le prédéfini { $preset_idx }\nrust_cannot_create_output_folder = Impossible de créer le dossier de sortie { $output_folder }, raison : { $error }\nrust_delete_summary = Éléments { $deleted } supprimés, impossible de supprimer les éléments { $failed } , sur { $total } éléments\nrust_rename_summary = Éléments { $renamed } renommés, impossible de renommer les éléments { $failed } , sur { $total } éléments\nrust_move_summary = Les éléments { $moved } déplacés, impossible de déplacer les éléments { $failed } , sur { $total } éléments\nrust_hardlink_summary = { $hardlinked } éléments convertis en liens durs, { $failed } éléments impossibles à convertir, pour { $total } éléments\nrust_symlink_summary = { $symlinked } éléments convertis en liens symboliques, { $failed } éléments impossibles à convertir, sur { $total } éléments\nrust_optimize_video_summary = { $optimized } vidéos optimisées, { $failed } échecs d'optimisation, pour { $total } vidéos\nrust_clean_exif_summary = { $cleaned } fichiers nettoyés des EXIF, { $failed } fichiers non nettoyés, pour { $total } fichiers\nrust_deleting_files = Suppression du fichier { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Suppression du fichier { $items_stats }\nrust_renaming_files = Renommage du fichier { $items_stats }\nrust_moving_files = Déplacement du fichier { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = Déplacement du fichier { $items_stats }\nrust_hardlinking_files = Conversion en liens durs de { $items_stats } fichiers ({ $size_stats })\nrust_hardlinking_no_size_files = Conversion en lien dur du fichier { $items_stats }\nrust_symlinking_files = Conversion en lien symbolique du fichier { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Conversion en lien symbolique du fichier { $items_stats }\nrust_optimizing_videos = Optimisation de la vidéo { $items_stats } ({ $size_stats })\nrust_optimizing_no_size_videos = Vidéo { $items_stats } optimisée\nrust_cleaning_exif = Nettoyage EXIF du fichier { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = Nettoyage EXIF du fichier { $items_stats }\nrust_no_files_deleted = Aucun fichier ou dossier sélectionné pour la suppression\nrust_no_files_renamed = Aucun fichier ou dossier sélectionné pour renommer\nrust_no_files_moved = Aucun fichier ou dossier sélectionné pour le déplacement\nrust_no_files_hardlinked = Aucun fichier ou dossier sélectionné pour la conversion en lien dur\nrust_no_files_symlinked = Aucun fichier ou dossier sélectionné pour la conversion en lien symbolique\nrust_no_videos_optimized = Aucune vidéo sélectionnée pour l'optimisation\nrust_no_exif_cleaned = Aucun fichier sélectionné pour le nettoyage EXIF\nrust_extracted_exif_tags = Étiquettes EXIF extraites de { $items_stats } fichiers ({ $size_stats })\nrust_delete_confirmation = Êtes-vous sûr de vouloir supprimer les éléments sélectionnés ?\nrust_delete_confirmation_number_simple = { $items } éléments sélectionnés.\nrust_delete_confirmation_number_groups = { $items } éléments sélectionnés dans les groupes { $groups }.\nrust_delete_confirmation_selected_all_in_group = Tous les éléments sélectionnés dans les groupes { $groups }.\nrust_move_confirmation = Êtes-vous sûr de vouloir déplacer les éléments sélectionnés ?\nrust_move_confirmation_number_simple = { $items } éléments sélectionnés.\nrust_clean_exif_confirmation = Êtes-vous sûr de vouloir supprimer les données EXIF des éléments sélectionnés ?\nrust_clean_exif_confirmation_number_simple = { $items } éléments sélectionnés.\nclean_exif_overwrite_files_text = Écraser les fichiers\nrust_optimize_video_confirmation = Êtes-vous sûr de vouloir optimiser les vidéos sélectionnées ?\nrust_optimize_video_confirmation_number_simple = { $items } éléments sélectionnés.\nrust_hardlink_confirmation = Êtes-vous sûr de vouloir créer des liens matériels pour les éléments sélectionnés ?\nrust_hardlink_confirmation_number_simple = { $items } éléments sélectionnés.\nrust_symlink_confirmation = Êtes-vous sûr de vouloir créer des liens symboliques pour les éléments sélectionnés ?\nrust_symlink_confirmation_number_simple = { $items } éléments sélectionnés.\nrust_rename_confirmation = Êtes-vous sûr de vouloir renommer les éléments sélectionnés ?\nrust_rename_confirmation_number_simple = { $items } éléments sélectionnés.\nrust_cache_processed_files = Les fichiers de cache { $files } ont été traités\nrust_cache_entries_stats = Supprimés { $removed } entrées sur { $all }, { $left } restantes\nrust_cache_size_reduced = Réduit la taille des fichiers de cache de { $size }\nrust_cache_time_elapsed = Temps écoulé : { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Échec du lien dur { $name } vers { $target }, la raison { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Sélection\ncolumn_size = Taille\ncolumn_file_name = Nom du fichier\ncolumn_path = Chemin d'accès\ncolumn_modification_date = Date de modification\ncolumn_similarity = Similitude\ncolumn_dimensions = Dimensions\ncolumn_new_dimensions = Nouvelles Dimensions\ncolumn_title = Titre de la page\ncolumn_artist = Artiste\ncolumn_year = Année\ncolumn_bitrate = Débit binaire\ncolumn_length = Longueur\ncolumn_genre = Genre\ncolumn_type_of_error = Type d'erreur\ncolumn_symlink_name = Nom du lien symbolique\ncolumn_symlink_folder = Dossier Symlink\ncolumn_destination_path = Chemin de destination\ncolumn_current_extension = Extension actuelle\ncolumn_proper_extension = Propre extension\ncolumn_fps = FPS\ncolumn_codec = Codec\ncolumn_duration = Durée\ncolumn_exif_tags = Étiquettes EXIF\ncolumn_new_name = Nouveau Nom\n# Slint translations\nok_button = D'accord\ncancel_button = Abandonner\ndo_you_want_to_continue = Voulez-vous continuer ?\nmain_window_title = Krokiet - Nettoyeur de données\nscan_button = Analyser\nstop_button = Arrêter\nstop_text = Arrêter\nselect_button = Sélectionner\nmove_button = Déplacer\ndelete_button = Supprimez\nsave_button = Enregistrer\nsort_button = Trier\nrename_button = Renommer\nmotto = Ce programme est gratuit et il le sera toujours.\\nVoir la licence MIT/GPL pour plus de détails.\nunicorn = Vous ne regardez peut-être pas une licorne, mais la licorne vous regarde toujours.\nrepository = Dépôt\ninstruction = Instructions\ndonation = Faire un don\ntranslation = Traduction\nincluded_paths = Chemins inclus\nexcluded_paths = Chemins exclus\nref = Réf\npath = Chemin d'accès\ntool_duplicate_files = Fichiers dupliqués\ntool_empty_folders = Dossiers vides\ntool_big_files = Grands fichiers\ntool_empty_files = Fichiers vides\ntool_temporary_files = Fichiers temporaires\ntool_similar_images = Images similaires\ntool_similar_videos = Vidéos similaires\ntool_music_duplicates = Doublons de musique\ntool_invalid_symlinks = Liens symboliques non valides\ntool_broken_files = Fichiers cassés\ntool_bad_extensions = Mauvaises extensions\ntool_bad_names = Mauvais Noms\ntool_video_optimizer = Optimiseur de vidéo\ntool_exif_remover = Suppression d'EXIF\nsort_by_full_name = Trier par nom complet\nsort_by_selection = Trier par sélection\nsort_reverse = Inverser l'ordre\nselection_all = Tout sélectionner\nselection_deselect_all = Désélectionner tout\nselection_invert_selection = Inverser la sélection\nselection_the_biggest_size = Sélectionnez la taille la plus grande\nselection_the_biggest_resolution = Sélectionnez la plus grande résolution\nselection_the_smallest_size = Sélectionnez la taille la plus petite\nselection_the_smallest_resolution = Sélectionnez la résolution la plus petite\nselection_newest = Sélectionner le plus récent\nselection_oldest = Sélectionner le plus ancien\nselection_shortest_path = Sélectionner le chemin le plus court\nselection_longest_path = Sélectionner le chemin le plus long\nstage_current = Étape actuelle :\nstage_all = Toutes les étapes :\nsubsettings = Sous-paramètres\nsubsettings_images_hash_size = Taille du hachage\nsubsettings_images_resize_algorithm = Algorithme de redimensionnage\nsubsettings_images_ignore_same_size = Ignorer les images avec la même taille\nsubsettings_images_max_difference = Différence max\nsubsettings_images_duplicates_hash_type = Type de hachage\nsubsettings_duplicates_check_method = Méthode de vérification\nsubsettings_duplicates_name_case_sensitive = Sensible à la casse (uniquement les modes de nom)\nsubsettings_biggest_files_sub_method = Méthode\nsubsettings_biggest_files_sub_number_of_files = Nombre de fichiers\nsubsettings_videos_max_difference = Différence max\nsubsettings_videos_ignore_same_size = Ignorer les vidéos avec la même taille\nsubsettings_music_audio_check_type = Type de vérification audio\nsubsettings_music_approximate_comparison = Comparaison approximative des étiquettes\nsubsettings_music_compared_tags = Étiquettes comparées\nsubsettings_music_title = Titre de la page\nsubsettings_music_artist = Artiste\nsubsettings_music_bitrate = Débit binaire\nsubsettings_music_genre = Genre\nsubsettings_music_year = Année\nsubsettings_music_length = Longueur\nsubsettings_music_max_difference = Différence max\nsubsettings_music_minimal_fragment_duration = Durée minimale du fragment\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Comparer au sein des groupes de titres similaires\nsubsettings_broken_files_type = Type de fichiers à vérifier\nsubsettings_broken_files_audio = Audio\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archivage\nsubsettings_broken_files_image = Image\nsubsettings_broken_files_video = Vidéo\nsubsettings_broken_files_video_info = Utilise ffmpeg/ffprobe. Lent et peut détecter des erreurs pédantes même si le fichier joue correctement.\nsubsettings_bad_names_issues = Vérifications de nom de fichier\nsubsettings_bad_names_uppercase_extension = Extension en majuscules\nsubsettings_bad_names_uppercase_extension_hint = Trouve des fichiers avec des lettres majuscules dans l'extension (par exemple, .JPG, .Mp3) et suggère la version en minuscules\nsubsettings_bad_names_emoji_used = Emoji dans le nom\nsubsettings_bad_names_emoji_used_hint = Trouve des fichiers avec des caractères emoji (😀, 🎉, etc.) dans le nom et suggère de les supprimer\nsubsettings_bad_names_space_at_start_end = Espaces débutants/finissants\nsubsettings_bad_names_space_at_start_end_hint = Trouve des fichiers avec des espaces au début ou à la fin du nom et suggère de les supprimer\nsubsettings_bad_names_non_ascii = Caractères non-ASCII\nsubsettings_bad_names_non_ascii_hint = Trouve les caractères non-ASCII (ą, ć, ñ, etc.) et suggère de les remplacer par des équivalents ASCII (a, c, n) ou de les supprimer si aucun mappage n'existe\nsubsettings_bad_names_restricted_charset = Caractère limité\nsubsettings_bad_names_restricted_charset_hint = Translitère les caractères non-ASCII vers ASCII, puis trouve les fichiers contenant des caractères en dehors de 0-9a-zA-Z et des caractères autorisés par l'utilisateur\nsubsettings_bad_names_allowed_chars = Caractères autorisés\nsubsettings_bad_names_remove_duplicated = Caractères dupliqués\nsubsettings_bad_names_remove_duplicated_hint = Trouve des caractères non alphanumériques consécutifs dupliqués (par exemple, \"file---name..txt\") et suggère de supprimer les doublons\nsettings_global_settings = Paramètres globaux\nsettings_dark_theme = Thème sombre\nsettings_show_only_icons = Afficher uniquement les icônes\nsettings_excluded_items = Élément exclu :\nsettings_allowed_extensions = Extensions autorisées :\nsettings_excluded_extensions = Extensions exclues:\nsettings_file_size = Taille du fichier (kilooctets)\nsettings_minimum_file_size = Min :\nsettings_maximum_file_size = Max :\nsettings_recursive_search = Recherche récursive\nsettings_use_cache = Utiliser le cache\nsettings_save_as_json = Enregistrer également le cache en tant que fichier JSON\nsettings_move_to_trash = Déplacer les fichiers supprimés vers la corbeille\nsettings_ignore_other_filesystems = Ignorer les autres systèmes de fichiers (uniquement Linux)\nsettings_delete_outdated_cache_entries = Supprimer automatiquement les entrées de cache obsolètes\nsettings_delete_outdated_cache_entries_hint = Lorsque cela est activé, l'application vérifiera pendant le chargement du cache (au maximum une fois par semaine) si les enregistrements en cache pointent toujours vers des fichiers/données existants et non modifiés\nsettings_hide_hard_links = Masquer les liens durs\nsettings_hide_hard_links_hint = Masquer les liens durs vers les mêmes fichiers dans les résultats\nsettings_thread_number = Nombre de sujets\nsettings_restart_required = ---Vous devez redémarrer l'application pour appliquer les changements dans le numéro---\nsettings_duplicate_image_preview = Aperçu de l'image\nsettings_duplicate_minimal_hash_cache_size = Taille minimale des fichiers mis en cache - Hachage (KB)\nsettings_duplicate_use_prehash = Utiliser le prehash\nsettings_duplicate_minimal_prehash_cache_size = Taille minimale des fichiers mis en cache - Prehash (KB)\nsettings_similar_images_show_image_preview = Aperçu de l'image\nsettings_application_scale_text = Échelle de l'application\nsettings_application_scale_hint_text = Lorsque l'échelle manuelle est activée, cela vous permet de choisir un facteur d'échelle personnalisé, mais désactive complètement le redimensionnement automatique en fonction du DPI de l'écran.\nsettings_restart_required_scale_text = --- Vous devez redémarrer l'application pour appliquer les changements d'échelle ---\nsettings_use_manual_application_scale_text = Utiliser l'échelle manuelle de l'application\nsettings_video_thumbnails_preview = Aperçu de l'image\nsettings_open_config_folder = Ouvrir le dossier de configuration\nsettings_open_cache_folder = Ouvrir le dossier de cache\nsettings_language = Langue\nsettings_current_preset = Préréglage actuel :\nsettings_edit_name = Modifier le nom\nsettings_choose_name_for_prefix = Choisir le nom pour le préfixe\nsettings_save = Enregistrer\nsettings_load = Charger\nsettings_reset = Réinitialiser\nsettings_similar_videos_tool = Outil Vidéos similaires\nsettings_video_thumbnails_clear_unused_thumbnails = Supprimer les vignettes vidéo inutilisées ayant plus de 7 jours au démarrage de l'application\nsettings_video_thumbnails_header = Visuels miniature de vidéos\nsettings_video_thumbnails_generate = Générer des miniatures\nsettings_video_thumbnails_position = Position miniature dans la vidéo (%)\nsettings_video_thumbnails_generate_grid = Générer une grille miniature au lieu d'une seule image\nsettings_video_thumbnails_generate_grid_hint = Générer plusieurs images en grille est beaucoup plus lent que de générer une miniature unique\nsettings_video_thumbnails_grid_tiles_per_side = Nombre de tuiles par côté dans le grille miniature\nsettings_video_thumbnails_grid_tiles_per_side_hint = Nombre de tuiles miniatures par côté dans le réseau. Par exemple, sélectionner 2 crée un réseau 2 x 2, ce qui donne une seule miniature composée de 4 images.\nsettings_similar_images_tool = Outil d'images similaires\nsettings_general_settings = Paramètres généraux\nsettings_cache_header_text = Paramètres du cache\nsettings_clean_cache_button_text = Vider le cache obsolète\nsettings_settings = Réglages\nsettings_load_tabs_sizes_at_startup = Charger la taille des onglets au démarrage\nsettings_load_windows_size_at_startup = Charger la taille des fenêtres au démarrage\nsettings_limit_lines_of_messages = Limiter les messages à 500 lignes (contournement pour le widget TextEdit lent)\nsettings_play_audio_on_scan_completion_text = Lire le son lors du scan réussi\nsettings_audio_feature_hint_text = Disponible uniquement lors de la compilation avec la fonctionnalité audio\nsettings_audio_env_variable_hint_text = Le son peut être modifié en définissant la variable d'environnement KROKIET_AUDIO_STOP_FILE à un chemin de fichier audio valide\npopup_save_title = Sauvegarde des résultats\npopup_save_message = Cela va enregistrer les résultats dans 3 fichiers différents\npopup_rename_title = Renommer les fichiers\npopup_new_paths_title = Veuillez ajouter les chemins un par ligne\npopup_move_title = Déplacement des fichiers\npopup_move_copy_checkbox = Copier les fichiers au lieu de les déplacer\npopup_move_preserve_folder_checkbox = Conserver la structure du dossier\nmove_confirmation_text = Êtes-vous sûr de vouloir déplacer les éléments sélectionnés ?\nrename_confirmation_text = Êtes-vous sûr de vouloir renommer les éléments sélectionnés ?\ndelete = Supprimer les éléments\nstopping_scan = Arrêt de l'analyse, veuillez patienter...\nsearching = Recherche en cours...\nsubsettings_videos_crop_detect = Méthode de détection de rognage\nsubsettings_videos_skip_forward_amount = Passer la durée [s]\nsubsettings_videos_vid_hash_duration = Durée du hachage vidéo\nsettings_cache_number_size_text = Taille des fichiers de cache : { $size }, nombre de fichiers : { $number }\nsettings_video_thumbnails_number_size_text = Taille des vignettes de la vidéo : { $size }, nombre de fichiers : { $number }\nsettings_log_number_size_text = Taille des fichiers journaux : { $size }, nombre de fichiers : { $number }\npopup_clean_cache_title_text = Vider le cache obsolète\npopup_clean_cache_confirmation_text = Êtes-vous sûr de vouloir supprimer les entrées de cache obsolètes ? Cela supprimera les entrées de cache pour les fichiers qui n'existent plus ou qui ont été modifiés.\npopup_clean_cache_progress_text = Traitement du fichier de cache :\npopup_clean_cache_current_file_text = Fichier actuel:\npopup_clean_cache_file_progress_text = Progression fichier actuelle :\npopup_clean_cache_overall_progress_text = Avancement global :\npopup_clean_cache_stopped_by_user_text = Le nettoyage du cache a été arrêté par l'utilisateur\npopup_clean_cache_finished_text = Nettoyage du cache terminé avec succès !\npopup_clean_cache_error_details_text = Détails de l'erreur:\npopup_clean_cache_files_with_errors = Fichiers avec des erreurs:\nsubsettings_video_optimizer_mode = Mode\nsubsettings_video_optimizer_crop_type = Type de culture\nsubsettings_video_optimizer_black_pixel_threshold = Seuil Pixel Noir\nsubsettings_video_optimizer_black_pixel_threshold_hint = Valeur RGB maximale pour chaque canal de pixel à être considérée comme noire (0-128). Valeur par défaut : 20\nsubsettings_video_optimizer_black_bar_min_percentage = Barre noire pourcentage minimum\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimum pourcentage de pixels noirs dans une ligne/colonne à être considéré comme une barre noire (50-100). Valeur par défaut : 90\nsubsettings_video_optimizer_max_samples = Max Samples\nsubsettings_video_optimizer_max_samples_hint = Nombre maximal de séquences à analyser par vidéo (5-1000). Valeur par défaut : 60\nsubsettings_video_optimizer_min_crop_size = Taille minimale de l'image\nsubsettings_video_optimizer_min_crop_size_hint = Minimum pixels à rogner sur n'importe quel côté (1-1000). Les rognages plus petits sont ignorés. Valeur par défaut : 5\nsubsettings_video_optimizer_video_codec = Codec vidéo\nsubsettings_video_optimizer_excluded_codecs = Codecs exclus\nsubsettings_video_optimizer_video_quality = Qualité vidéo (CRF)\nsubsettings_reset = Réinitialiser\nsubsettings_exif_ignored_tags_text = Étiquettes ignorées :\nsubsettings_exif_ignored_tags_hint_text = Liste des étiquettes séparées par des virgules à exclure de la vérification (par exemple : GPS, vignette). Certaines étiquettes, comme ImageWidth dans les fichiers TIFF, sont cachées pour éviter de corrompre l'image.\nclean_button_text = Nettoyer\nclean_text = Nettoyer les données EXIF\nclean_confirmation_text = Êtes-vous sûr de vouloir supprimer les données EXIF des éléments sélectionnés ?\ncrop_videos_text = Couper des vidéos\ncrop_video_confirmation_text = Êtes-vous sûr de vouloir recroiser les vidéos sélectionnées ?\ncrop_reencode_video_text = Re-encoder vidéo\nreencode_videos_text = Re-encoder des vidéos\noptimize_button_text = Optimiser\noptimize_confirmation_text = Êtes-vous sûr de vouloir réencodage les vidéos sélectionnées ?\noptimize_fail_if_bigger_text = Échec si le fichier optimisé est plus grand\noptimize_overwrite_files_text = Écraser les fichiers\noptimize_limit_video_size_text = Limite la taille de la vidéo\noptimize_max_width_text = Largeur max:\noptimize_max_height_text = Hauteur max :\nhardlink_button_text = Lien dur\nhardlink_text = Créer des liens matériels\nhardlink_confirmation_text = Êtes-vous sûr de vouloir créer des liens matériels pour les éléments sélectionnés ?\nsoftlink_button_text = Lien Softlink\nsoftlink_text = Créer des liens logiciels\nsoftlink_confirmation_text = Êtes-vous sûr de vouloir créer des liens logiciels (liens symboliques) pour les éléments sélectionnés ?\n"
  },
  {
    "path": "krokiet/i18n/it/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Errore Critico Durante l'Avvio dell'App\nrust_init_error_message = \n        Si è verificato un errore critico durante l'avvio dell'applicazione:\n\n        { $error_message }\n\n        Questo può essere causato da driver OpenGL/Vulkan mancanti o malfunzionanti, dall'esecuzione dell'applicazione in una macchina virtuale o da un bug in Krokiet o in uno dei suoi librerie.\n\n        È possibile provare ad eseguire diverse build (skia_opengl, skia_vulkan, femtovg_opengl - predefinito) o con il renderer software per vedere se ciò risolve il problema.\nrust_loaded_preset = Predefinito caricato { $preset_idx }\nrust_file_already_exists = File \"{ $file }\" già esistente, e non verrà sovrascritto\nrust_error_removing_file_after_copy = Errore durante l'eliminazione del file \"{ $file }\" (dopo averlo copiato in una partizione diversa), motivo: { $reason }\nrust_error_copying_file = Errore durante la copia di \"{ $input }\" in \"{ $output }\", motivo: { $reason }\nrust_loading_tags_cache = Caricamento tag cache\nrust_loading_fingerprints_cache = Caricamento cache impronte digitali\nrust_saving_tags_cache = Salvataggio cache tag\nrust_saving_fingerprints_cache = Salvataggio delle impronte digitali cache\nrust_loading_prehash_cache = Caricamento della cache prehash\nrust_saving_prehash_cache = Salvataggio della cache prehash\nrust_loading_hash_cache = Caricamento della cache hash\nrust_saving_hash_cache = Salvataggio della cache hash\nrust_loading_exif_cache = Caricamento cache EXIF\nrust_saving_exif_cache = Salva la cache EXIF\nrust_scanning_name = Scansione del nome del file { $entries_checked }\nrust_scanning_size_name = Dimensione e nome della scansione del file { $entries_checked }\nrust_scanning_size = Dimensione della scansione del file { $entries_checked }\nrust_scanning_file = Scansione del file { $entries_checked }\nrust_scanning_folder = Scansione cartella { $entries_checked }\nrust_checked_tags = Etichette controllate di { $items_stats }\nrust_checked_content = Contenuto controllato di { $items_stats } ({ $size_stats })\nrust_compared_tags = Etichette confrontate di { $items_stats }\nrust_compared_content = Confrontato contenuto di { $items_stats }\nrust_hashed_images = Hashed { $items_stats } immagini ({ $size_stats })\nrust_compared_image_hashes = Hash di immagine raffrontati di { $items_stats }\nrust_hashed_videos = Hashed { $items_stats } video\nrust_created_thumbnails = Miniature create per video { $items_stats }\nrust_checked_files = File { $items_stats } verificato ({ $size_stats })\nrust_checked_files_bad_extensions = File { $items_stats } verificato\nrust_checked_files_bad_names = File { $items_stats } verificato\nrust_checked_videos = Controllati { $items_stats } video ({ $size_stats })\nrust_analyzed_partial_hash = Hash parziale analizzato di file { $items_stats } ({ $size_stats })\nrust_analyzed_full_hash = Hash completo analizzato di file { $items_stats } ({ $size_stats })\nrust_failed_to_rename_file = Impossibile rinominare il file { $old_path } in { $new_path }, errore: { $error }\nrust_no_included_paths = Impossibile avviare la scansione quando non sono impostati percorsi inclusi.\nrust_all_paths_referenced = Impossibile avviare la scansione quando tutti i percorsi inclusi sono impostati come percorsi di riferimento, è necessario disabilitare la casella di controllo accanto al percorso di input.\nrust_found_empty_folders = Trovate { $items_found } cartelle vuote in { $time }\nrust_found_empty_files = Sono stati trovati { $items_found } file vuoti in { $time }\nrust_found_similar_images = Ho trovato { $items_found } file di immagini simili in { $groups } gruppi in { $time }\nrust_found_similar_videos = Trovi { $items_found } file video simili in { $groups } gruppi in { $time }\nrust_found_similar_music_files = Trovati { $items_found } file musicali simili in { $groups } gruppi nel tempo di { $time }\nrust_found_invalid_symlinks = Trovati { $items_found } simboliche inválide in { $time }\nrust_found_temporary_files = Trovati { $items_found } file temporanei in { $time }\nrust_no_file_type_selected = Impossibile trovare file corrotti senza alcun tipo di file selezionato.\nrust_found_broken_files = Trovato { $items_found } file interrotti che prendono { $size } in { $time }\nrust_found_bad_extensions = Trovati { $items_found } file con estensioni errate in { $time }\nrust_found_bad_names = Trovati { $items_found } file(s) con nomi scadenti in { $time}\nrust_found_video_optimizer = Trovati { $items_found } file da ottimizzare in { $time }\nrust_found_duplicate_files = Trova { $items_found } file duplicati in { $groups } gruppi occupando { $size } nel tempo di { $time }\nrust_found_duplicate_files_no_lost_space = Trovate { $items_found } file duplicati in { $groups } gruppi in { $time }\nrust_found_big_files = Trovati { $items_found } file grandi con dimensione { $size } in { $time }\nrust_found_exif_files = Trovati { $items_found } file con dati exif in { $time }\nrust_cannot_load_preset = Impossibile cambiare e caricare le preimpostazioni { $preset_idx } - motivo { $reason }, usando invece le impostazioni predefinite\nrust_saved_preset = Preimpostazione salvata { $preset_idx }\nrust_cannot_save_preset = Impossibile salvare la preimpostazione { $preset_idx } - motivo { $reason }\nrust_reset_preset = Ripristina impostazione predefinita { $preset_idx }\nrust_cannot_create_output_folder = Impossibile creare la cartella di output { $output_folder }, motivo: { $error }\nrust_delete_summary = Eliminato elementi { $deleted } , non è stato possibile rimuovere elementi { $failed } , su { $total }\nrust_rename_summary = Rinominato elementi { $renamed } , non è stato possibile rinominare elementi { $failed } , su { $total }\nrust_move_summary = Spostati elementi { $moved } , non è stato possibile spostare elementi { $failed } , fuori da { $total }\nrust_hardlink_summary = Elementi collegati in modo persistente { $hardlinked }, non sono riusciti a collegare in modo persistente { $failed } elementi, su un totale di { $total } elementi\nrust_symlink_summary = Collegato simbolicamente { $symlinked } elementi, non è stato possibile collegare simbolicamente { $failed } elementi, su un totale di { $total } elementi\nrust_optimize_video_summary = Video ottimizzati { $optimized }, video non ottimizzati { $failed }, fuori da { $total } video\nrust_clean_exif_summary = Pulite EXIF da { $cleaned } file, fallito pulire { $failed } file, di { $total } file\nrust_deleting_files = Eliminazione del file { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Eliminazione del file { $items_stats }\nrust_renaming_files = Rinomina il file { $items_stats }\nrust_moving_files = Spostamento del file { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = Spostamento del file { $items_stats }\nrust_hardlinking_files = Hardlinking file { $items_stats } ({ $size_stats })\nrust_hardlinking_no_size_files = Collegamento stretto { $items_stats } file\nrust_symlinking_files = Collegamento simbolico { $items_stats } file ({ $size_stats })\nrust_symlinking_no_size_files = Simvolico { $items_stats } file\nrust_optimizing_videos = Video ottimizzato { $items_stats } ({ $size_stats })\nrust_optimizing_no_size_videos = Video ottimizzato { $items_stats }\nrust_cleaning_exif = Pulire EXIF da { $items_stats } file ({ $size_stats })\nrust_cleaning_no_size_exif = Pulire EXIF da { $items_stats } file\nrust_no_files_deleted = Nessun file o cartelle selezionati per l'eliminazione\nrust_no_files_renamed = Nessun file o cartelle selezionati per la rinominazione\nrust_no_files_moved = Nessun file o cartelle selezionati per lo spostamento\nrust_no_files_hardlinked = Nessun file o cartella selezionato per l'hardlinking\nrust_no_files_symlinked = Nessun file o cartella selezionato per il symlinking\nrust_no_videos_optimized = Nessun video selezionato per l'ottimizzazione\nrust_no_exif_cleaned = Nessun file selezionato per la pulizia EXIF\nrust_extracted_exif_tags = Estratti i tag EXIF da { $items_stats } file ({ $size_stats })\nrust_delete_confirmation = Sei sicuro di voler eliminare gli elementi selezionati?\nrust_delete_confirmation_number_simple = { $items } elementi selezionati.\nrust_delete_confirmation_number_groups = { $items } elementi selezionati in gruppi { $groups }.\nrust_delete_confirmation_selected_all_in_group = Tutti gli elementi selezionati in gruppi { $groups }.\nrust_move_confirmation = Sei sicuro di voler spostare gli elementi selezionati?\nrust_move_confirmation_number_simple = { $items } elementi selezionati.\nrust_clean_exif_confirmation = Sei sicuro di voler rimuovere i dati EXIF dagli elementi selezionati?\nrust_clean_exif_confirmation_number_simple = { $items } elementi selezionati.\nclean_exif_overwrite_files_text = Sovrascrivi file\nrust_optimize_video_confirmation = Sei sicuro di voler ottimizzare i video selezionati?\nrust_optimize_video_confirmation_number_simple = { $items } elementi selezionati.\nrust_hardlink_confirmation = Sei sicuro di voler creare collegamenti duri per gli elementi selezionati?\nrust_hardlink_confirmation_number_simple = { $items } elementi selezionati.\nrust_symlink_confirmation = Sei sicuro di voler creare i collegamenti simbolici per gli elementi selezionati?\nrust_symlink_confirmation_number_simple = { $items } elementi selezionati.\nrust_rename_confirmation = Sei sicuro di voler rinominare gli elementi selezionati?\nrust_rename_confirmation_number_simple = { $items } elementi selezionati.\nrust_cache_processed_files = File { $files } cache elaborati\nrust_cache_entries_stats = Rimosse { $removed } voci da tutte le { $all }, { $left } voci rimanenti\nrust_cache_size_reduced = Diminuiti le dimensioni dei file della cache di { $size }\nrust_cache_time_elapsed = Tempo trascorso: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Impossibile creare il collegamento diretto a { $name } in { $target }, motivo { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Selezione\ncolumn_size = Dimensione\ncolumn_file_name = Nome File\ncolumn_path = Percorso\ncolumn_modification_date = Data Di Modifica\ncolumn_similarity = Somiglianza\ncolumn_dimensions = Dimensioni\ncolumn_new_dimensions = Nuove Dimensioni\ncolumn_title = Titolo\ncolumn_artist = Artista\ncolumn_year = Anno\ncolumn_bitrate = Tasso di bit\ncolumn_length = Lunghezza\ncolumn_genre = Genere\ncolumn_type_of_error = Tipo di errore\ncolumn_symlink_name = Nome Link Simbolico\ncolumn_symlink_folder = Cartella Collegamenti Simbolici\ncolumn_destination_path = Percorso Di Destinazione\ncolumn_current_extension = Estensione Corrente\ncolumn_proper_extension = Estensione Corretta\ncolumn_fps = FPS\ncolumn_codec = Codicechnittelausta\ncolumn_duration = Durata\ncolumn_exif_tags = Tag EXIF\ncolumn_new_name = Nuovo Nome\n# Slint translations\nok_button = D'accordo\ncancel_button = Annulla\ndo_you_want_to_continue = Vuoi continuare?\nmain_window_title = Krokiet - Pulitore Dei Dati\nscan_button = Scansione\nstop_button = Ferma\nstop_text = Interrompi\nselect_button = Seleziona\nmove_button = Sposta\ndelete_button = Elimina\nsave_button = Salva\nsort_button = Ordina\nrename_button = Rinomina\nmotto = Questo programma è gratuito e sarà sempre disponibile.\\nVedi la licenza MIT/GPL per i dettagli.\nunicorn = Non puoi guardare un unicorno, ma l’unicorno ti guarda sempre.\nrepository = Repo\ninstruction = Istruzioni\ndonation = Donazione\ntranslation = Traduzione\nincluded_paths = Percorsi Inclusi\nexcluded_paths = Percorsi Esclusi\nref = Rif\npath = Percorso\ntool_duplicate_files = Duplica File\ntool_empty_folders = Cartelle Vuote\ntool_big_files = File Grandi\ntool_empty_files = File Vuoti\ntool_temporary_files = File Temporanei\ntool_similar_images = Immagini Simili\ntool_similar_videos = Video Simili\ntool_music_duplicates = Duplicati Musicali\ntool_invalid_symlinks = Collegamenti invalidi\ntool_broken_files = File Interrotti\ntool_bad_extensions = Estensioni Errate\ntool_bad_names = Nomi Cattivi\ntool_video_optimizer = Ottimizzatore Video\ntool_exif_remover = Rimuovi Exif\nsort_by_full_name = Ordina per nome completo\nsort_by_selection = Ordina per selezione\nsort_reverse = Ordine inverso\nselection_all = Seleziona tutto\nselection_deselect_all = Deseleziona tutto\nselection_invert_selection = Inverti selezione\nselection_the_biggest_size = Seleziona la dimensione più grande\nselection_the_biggest_resolution = Seleziona la risoluzione più grande\nselection_the_smallest_size = Seleziona la dimensione più piccola\nselection_the_smallest_resolution = Seleziona la risoluzione più piccola\nselection_newest = Seleziona nuovi\nselection_oldest = Seleziona il più vecchio\nselection_shortest_path = Seleziona il percorso più breve\nselection_longest_path = Seleziona il percorso più lungo\nstage_current = Fase Attuale:\nstage_all = Tutte Le Stagioni:\nsubsettings = Sotto-impostazioni\nsubsettings_images_hash_size = Dimensione Hash\nsubsettings_images_resize_algorithm = Ridimensiona Algoritmo\nsubsettings_images_ignore_same_size = Ignora immagini con la stessa dimensione\nsubsettings_images_max_difference = Differenza massima\nsubsettings_images_duplicates_hash_type = Tipo Di Hash\nsubsettings_duplicates_check_method = Metodo di controllo\nsubsettings_duplicates_name_case_sensitive = Case Sensitive(solo modalità nome)\nsubsettings_biggest_files_sub_method = Metodo\nsubsettings_biggest_files_sub_number_of_files = Numero di file\nsubsettings_videos_max_difference = Differenza massima\nsubsettings_videos_ignore_same_size = Ignora video con la stessa dimensione\nsubsettings_music_audio_check_type = Tipo di controllo audio\nsubsettings_music_approximate_comparison = Confronto Approssimativo Tag\nsubsettings_music_compared_tags = Etichette confrontate\nsubsettings_music_title = Titolo\nsubsettings_music_artist = Artista\nsubsettings_music_bitrate = Tasso di bit\nsubsettings_music_genre = Genere\nsubsettings_music_year = Anno\nsubsettings_music_length = Lunghezza\nsubsettings_music_max_difference = Differenza massima\nsubsettings_music_minimal_fragment_duration = Durata minima del frammento\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Confronta all'interno di gruppi di titoli simili\nsubsettings_broken_files_type = Tipo di file da controllare\nsubsettings_broken_files_audio = Audio\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archivio\nsubsettings_broken_files_image = Immagine\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Utilizza ffmpeg/ffprobe. Molto lento e potrebbe rilevare errori pedanti anche se il file suona bene.\nsubsettings_bad_names_issues = Verifica dei nomi file\nsubsettings_bad_names_uppercase_extension = Estensione maiuscola\nsubsettings_bad_names_uppercase_extension_hint = Trova file con lettere maiuscole nell'estensione (ad esempio, .JPG, .Mp3) e suggerisce la versione minuscola\nsubsettings_bad_names_emoji_used = Emoji in nome\nsubsettings_bad_names_emoji_used_hint = Trova file con caratteri emoji (😀, 🎉, ecc.) nel nome e suggerisce di rimuoverli\nsubsettings_bad_names_space_at_start_end = Spazi iniziali/finali\nsubsettings_bad_names_space_at_start_end_hint = Trova file con spazi all'inizio o alla fine del nome e suggerisce di troncare questi\nsubsettings_bad_names_non_ascii = Caratteri non-ASCII\nsubsettings_bad_names_non_ascii_hint = Trova caratteri non-ASCII (ą, ć, ñ, ecc.) e suggerisce di sostituirli con equivalenti ASCII (a, c, n) o rimuoverli se non esiste una mappatura\nsubsettings_bad_names_restricted_charset = Carattere limitato\nsubsettings_bad_names_restricted_charset_hint = Traduce caratteri non-ASCII in ASCII, quindi trova file contenenti caratteri al di fuori di 0-9a-zA-Z e caratteri consentiti dall'utente\nsubsettings_bad_names_allowed_chars = Caratteri consentiti\nsubsettings_bad_names_remove_duplicated = Caratteri duplicati\nsubsettings_bad_names_remove_duplicated_hint = Trova caratteri non alfanumerici duplicati consecutivi (ad esempio, \"file---name..txt\") e suggerisce la rimozione dei duplicati\nsettings_global_settings = Impostazioni Globali\nsettings_dark_theme = Tema scuro\nsettings_show_only_icons = Mostra solo icone\nsettings_excluded_items = Voce esclusa:\nsettings_allowed_extensions = Estensioni ammesse:\nsettings_excluded_extensions = Estensioni escluse:\nsettings_file_size = Dimensione File (Kilobyte)\nsettings_minimum_file_size = Min.:\nsettings_maximum_file_size = Massimo:\nsettings_recursive_search = Ricerca ricorsiva\nsettings_use_cache = Usa cache\nsettings_save_as_json = Salva anche la cache come file JSON\nsettings_move_to_trash = Sposta i file eliminati nel cestino\nsettings_ignore_other_filesystems = Ignora altri filesystem (solo Linux)\nsettings_delete_outdated_cache_entries = Elimina automaticamente le voci di cache obsolete\nsettings_delete_outdated_cache_entries_hint = Quando abilitato, l'app verificherà durante il caricamento della cache (al massimo una volta a settimana) se i record memorizzati in cache puntano ancora a file/dati esistenti e non modificati\nsettings_hide_hard_links = Nascondi collegamenti duri\nsettings_hide_hard_links_hint = Nascondi collegamenti duri a file identici nei risultati\nsettings_thread_number = Numero del thread\nsettings_restart_required = ---È necessario riavviare l'applicazione per applicare le modifiche al numero della conversazione---\nsettings_duplicate_image_preview = Anteprima immagine\nsettings_duplicate_minimal_hash_cache_size = Dimensione minima dei file memorizzati nella cache - Hash (KB)\nsettings_duplicate_use_prehash = Usa prehash\nsettings_duplicate_minimal_prehash_cache_size = Dimensione minima dei file memorizzati nella cache - Prehash (KB)\nsettings_similar_images_show_image_preview = Anteprima immagine\nsettings_application_scale_text = Scala dell'applicazione\nsettings_application_scale_hint_text = Quando è abilitata la scala manuale, ciò consente di scegliere un fattore di scala personalizzato, ma disabilita completamente la scalatura automatica in base al DPI del monitor.\nsettings_restart_required_scale_text = ---Devi riavviare l'app per applicare le modifiche alla scala---\nsettings_use_manual_application_scale_text = Utilizza scala di applicazione manuale\nsettings_video_thumbnails_preview = Anteprima immagine\nsettings_open_config_folder = Apri cartella di configurazione\nsettings_open_cache_folder = Apri cartella cache\nsettings_language = Lingua\nsettings_current_preset = Preset Attuale:\nsettings_edit_name = Modifica nome\nsettings_choose_name_for_prefix = Scegli il nome per il prefisso\nsettings_save = Salva\nsettings_load = Carica\nsettings_reset = Ripristina\nsettings_similar_videos_tool = Strumento video simile\nsettings_video_thumbnails_clear_unused_thumbnails = Elimina miniature video inutilizzate più vecchie di 7 giorni all'avvio dell'app\nsettings_video_thumbnails_header = Immagini miniatura dei video\nsettings_video_thumbnails_generate = Genera miniature\nsettings_video_thumbnails_position = Posizione miniatura in video (%)\nsettings_video_thumbnails_generate_grid = Genera griglia miniature invece di singola immagine\nsettings_video_thumbnails_generate_grid_hint = Generare più immagini in griglia è molto più lento rispetto alla generazione di un singolo miniatura\nsettings_video_thumbnails_grid_tiles_per_side = Numero di mattoni per lato nella griglia in miniatura\nsettings_video_thumbnails_grid_tiles_per_side_hint = Numero di mattoncini miniature per lato nella griglia. Ad esempio, selezionando 2 crea una griglia 2 x 2, risultando in un singolo mattoncino composto da 4 immagini.\nsettings_similar_images_tool = Strumento immagini simili\nsettings_general_settings = Impostazioni Generali\nsettings_cache_header_text = Impostazioni Cache\nsettings_clean_cache_button_text = Pulisci cache obsoleta\nsettings_settings = Impostazioni\nsettings_load_tabs_sizes_at_startup = Carica le dimensioni delle schede all'avvio\nsettings_load_windows_size_at_startup = Carica la dimensione delle finestre all'avvio\nsettings_limit_lines_of_messages = Limita i messaggi a 500 righe (workaround per il widget di modifica di testo lenta)\nsettings_play_audio_on_scan_completion_text = Riproduci suono quando la scansione completa con successo\nsettings_audio_feature_hint_text = Disponibile solo quando si compila con la funzione audio\nsettings_audio_env_variable_hint_text = Il suono può essere modificato, impostando la variabile d'ambiente KROKIET_AUDIO_STOP_FILE a un percorso file audio valido\npopup_save_title = Salvataggio risultati\npopup_save_message = Questo salverà i risultati in 3 file diversi\npopup_rename_title = Rinomina file\npopup_new_paths_title = Si prega di aggiungere i percorsi uno per riga\npopup_move_title = Spostamento file\npopup_move_copy_checkbox = Copia i file invece di spostare\npopup_move_preserve_folder_checkbox = Conserva la struttura delle cartelle\nmove_confirmation_text = Sei sicuro di voler spostare gli elementi selezionati?\nrename_confirmation_text = Sei sicuro di voler rinominare gli elementi selezionati?\ndelete = Elimina elementi\nstopping_scan = Interruzione della scansione, attendere prego...\nsearching = Ricerca...\nsubsettings_videos_crop_detect = Metodo di rilevamento ritaglio\nsubsettings_videos_skip_forward_amount = Salta durata [s]\nsubsettings_videos_vid_hash_duration = Durata hash video\nsettings_cache_number_size_text = Dimensione dei file cache: { $size }, numero di file: { $number }\nsettings_video_thumbnails_number_size_text = Dimensioni miniature video: { $size }, numero di file: { $number }\nsettings_log_number_size_text = Dimensione file di registro: { $size }, numero di file: { $number }\npopup_clean_cache_title_text = Svuota Cache Obsoleta\npopup_clean_cache_confirmation_text = Sei sicuro di voler eliminare le voci di cache obsolete? Ciò rimuoverà le voci di cache per i file che non esistono più o che sono stati modificati.\npopup_clean_cache_progress_text = Elaborazione file cache:\npopup_clean_cache_current_file_text = File corrente:\npopup_clean_cache_file_progress_text = Progresso file corrente:\npopup_clean_cache_overall_progress_text = Progresso complessivo:\npopup_clean_cache_stopped_by_user_text = Pulizia cache interrotta dall'utente\npopup_clean_cache_finished_text = Pulizia cache completata con successo!\npopup_clean_cache_error_details_text = Dettagli errore:\npopup_clean_cache_files_with_errors = File con errori:\nsubsettings_video_optimizer_mode = Modalità\nsubsettings_video_optimizer_crop_type = Tipo di coltura\nsubsettings_video_optimizer_black_pixel_threshold = Soglia Pixel Nero\nsubsettings_video_optimizer_black_pixel_threshold_hint = Il valore RGB massimo per ogni canale di pixel da considerare nero (0-128). Valore predefinito: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Barra nera percentuale minima\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimo percentuale di pixel neri in una riga/colonna da considerare una barra nera (50-100). Valore predefinito: 90\nsubsettings_video_optimizer_max_samples = Massimo campioni\nsubsettings_video_optimizer_max_samples_hint = Numero massimo di fotogrammi da analizzare per video (5-1000). Predefinito: 60\nsubsettings_video_optimizer_min_crop_size = Dimensione Minima dell'Immagine\nsubsettings_video_optimizer_min_crop_size_hint = Minimo numero di pixel da ritagliare su qualsiasi lato (1-1000). I ritagli più piccoli vengono ignorati. Valore predefinito: 5\nsubsettings_video_optimizer_video_codec = Codifica video\nsubsettings_video_optimizer_excluded_codecs = Escluso codec\nsubsettings_video_optimizer_video_quality = Qualità video (CRF)\nsubsettings_reset = Reimposta\nsubsettings_exif_ignored_tags_text = Ignorato tag:\nsubsettings_exif_ignored_tags_hint_text = Elenco separato da virgole di tag da escludere dallo scansione (ad esempio, GPS, Miniature). Alcuni tag, come ImageWidth nei file TIFF, sono nascosti per evitare di interrompere l'immagine.\nclean_button_text = Pulisci\nclean_text = Dati EXIF puliti\nclean_confirmation_text = Sei sicuro di voler rimuovere i dati EXIF dagli elementi selezionati?\ncrop_videos_text = Tagliare video\ncrop_video_confirmation_text = Sei sicuro di voler ritagliare i video selezionati?\ncrop_reencode_video_text = Re-codifica video\nreencode_videos_text = Re-codificare video\noptimize_button_text = Ottimizza\noptimize_confirmation_text = Sei sicuro di voler re-codificare i video selezionati?\noptimize_fail_if_bigger_text = Fallire se il file ottimizzato è più grande\noptimize_overwrite_files_text = Sovrascrivi file\noptimize_limit_video_size_text = Limita dimensione video\noptimize_max_width_text = Massimo larghezza:\noptimize_max_height_text = Altezza massima:\nhardlink_button_text = Collegamento duro\nhardlink_text = Crea hardlink\nhardlink_confirmation_text = Sei sicuro di voler creare collegamenti duri per gli elementi selezionati?\nsoftlink_button_text = Softlink\nsoftlink_text = Crea collegamenti simbolici\nsoftlink_confirmation_text = Sei sicuro di voler creare collegamenti simbolici (symlink) per gli elementi selezionati?\n"
  },
  {
    "path": "krokiet/i18n/ja/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = アプリの起動中に致命的なエラーが発生しました\nrust_init_error_message = \n        アプリケーションの起動中に、次のエラーが発生しました：\n \n        { $error_message }\n \n        これは、OpenGL/Vulkanドライバが不足しているか、または動作していないこと、仮想マシンでアプリケーションを実行していること、またはKrokietまたはそのライブラリ内のバグによって引き起こされた可能性があります。\n \n        問題が解決するかどうかを確認するために、異なるビルド（skia_opengl、skia_vulkan、femtovg_opengl - デフォルト）を実行するか、ソフトウェアレンダラーを使用してみてください。.\nrust_loaded_preset = プリセット { $preset_idx }を読み込みました\nrust_file_already_exists = ファイル \"{ $file }\" は既に存在し、上書きされません。\nrust_error_removing_file_after_copy = ファイル \"{ $file }\" の削除中にエラー (別のパーティションにコピーされた後)、理由: { $reason }\nrust_error_copying_file = \"{ $input }\" を \"{ $output }\" にコピー中にエラーが発生しました、理由: { $reason }\nrust_loading_tags_cache = タグキャッシュを読み込み中\nrust_loading_fingerprints_cache = フィンガープリントキャッシュを読み込み中\nrust_saving_tags_cache = タグキャッシュを保存中\nrust_saving_fingerprints_cache = フィンガープリントキャッシュを保存中\nrust_loading_prehash_cache = プレハッシュキャッシュを読み込み中\nrust_saving_prehash_cache = プレハッシュキャッシュを保存中\nrust_loading_hash_cache = ハッシュキャッシュを読み込み中\nrust_saving_hash_cache = ハッシュキャッシュを保存中\nrust_loading_exif_cache = 読み込み EXIF キャッシュ\nrust_saving_exif_cache = EXIF キャッシュを保存\nrust_scanning_name = { $entries_checked } ファイルの名前をスキャンしています\nrust_scanning_size_name = { $entries_checked } ファイルのサイズと名前をスキャンしています\nrust_scanning_size = { $entries_checked } ファイルのサイズをスキャンしています\nrust_scanning_file = { $entries_checked } ファイルをスキャン中\nrust_scanning_folder = { $entries_checked } フォルダをスキャン中\nrust_checked_tags = { $items_stats } のチェックされたタグ\nrust_checked_content = { $items_stats } のチェックされたコンテンツ ({ $size_stats })\nrust_compared_tags = { $items_stats } のタグの比較\nrust_compared_content = { $items_stats } の比較コンテンツ\nrust_hashed_images = ハッシュ化された { $items_stats } 画像 ({ $size_stats })\nrust_compared_image_hashes = { $items_stats } のイメージハッシュの比較\nrust_hashed_videos = ハッシュ化された { $items_stats } ビデオ\nrust_created_thumbnails = { $items_stats } ビデオ用のサムネイルを作成しました\nrust_checked_files = チェックされた { $items_stats } ファイル ({ $size_stats })\nrust_checked_files_bad_extensions = チェックされた { $items_stats } ファイル\nrust_checked_files_bad_names = { $items_stats } ファイルを確認しました\nrust_checked_videos = チェック済み { $items_stats } ビデオ ({ $size_stats })\nrust_analyzed_partial_hash = { $items_stats } ファイルの部分ハッシュを分析しました ({ $size_stats })\nrust_analyzed_full_hash = { $items_stats } ファイルの完全ハッシュを分析しました ({ $size_stats })\nrust_failed_to_rename_file = ファイル { $old_path } を { $new_path }にリネームできませんでした。エラー: { $error }\nrust_no_included_paths = パスが設定されていませんので、スキャンを開始できません。.\nrust_all_paths_referenced = すべての含まれるパスを参照パスとして設定するとスキャンを開始できません。入力パスの横にある参照チェックボックスを無効にする必要があります。.\nrust_found_empty_folders = 空のフォルダが { $items_found } 個見つかりました ({ $time })\nrust_found_empty_files = 空のファイルが { $items_found } 個見つかりました ({ $time })\nrust_found_similar_images = { $items_found } 同様の画像ファイルが { $groups } グループ内の { $time } で見つかりました\nrust_found_similar_videos = { $items_found }件の類似動画ファイルを{ $groups }グループで{ $time }に見つけました\nrust_found_similar_music_files =\n    同じ調子とスタイルを保ち、特別なフォーマットやプレースホルダーも保持します。\n    翻訳後のテキスト：\n    同じ調子とスタイルを保ち、特別なフォーマットやプレースホルダーも保持します。\n    見つかった { $items_found } 類似音楽ファイルは { $groups } レコードにまたがり、時間は { $time } です。\nrust_found_invalid_symlinks = 無効なシンボリックリンクが { $items_found } 個見つかりました ({ $time })\nrust_found_temporary_files = 一時ファイルが { $items_found } 個見つかりました ({ $time })\nrust_no_file_type_selected = 選択したファイルタイプがない壊れたファイルを見つけることができません。.\nrust_found_broken_files = 見つかった壊れたファイル数は{ $items_found }で、サイズは{ $size }で、時間は{ $time }took\nrust_found_bad_extensions = 無効な拡張子を持つファイルが { $items_found } 個見つかりました ({ $time })\nrust_found_bad_names = { $items_found } ファイルを { $time } で見つけました。\nrust_found_video_optimizer = { $items_found } ファイルを { $time } で最適化しました。\nrust_found_duplicate_files = { $items_found } 重複したファイルを { $groups } グループで { $size } を { $time } で見つけました\nrust_found_duplicate_files_no_lost_space = { $items_found } 重複したファイルが { $groups } グループの { $time } で見つかりました\nrust_found_big_files = 見つけた/bigファイル{ $items_found }個はサイズ{ $size }で{ $time }にあります\nrust_found_exif_files = ファイルを { $time } に { $items_found } 件見つけました。\nrust_cannot_load_preset = プリセット { $preset_idx } - 理由 { $reason }を変更して読み込むことができません。代わりにデフォルト設定を使用してください。\nrust_saved_preset = プリセット { $preset_idx }を保存しました\nrust_cannot_save_preset = プリセット { $preset_idx } を保存できません - 理由 { $reason }\nrust_reset_preset = 予め設定{ $preset_idx }をリセット\nrust_cannot_create_output_folder = 出力フォルダ { $output_folder }を作成できません、理由： { $error }\nrust_delete_summary = { $deleted } アイテムを削除しました。 { $failed } アイテムの削除に失敗しました。 { $total } アイテムのうち\nrust_rename_summary = 再命名{ $renamed }項目、再命名失敗{ $failed }項目、総数{ $total }項目\nrust_move_summary = { $moved } アイテムを移動しました、 { $failed } アイテムの移動に失敗しました、 { $total } アイテムのうち\nrust_hardlink_summary = ハードリンクされた { $hardlinked } アイテム、{ $failed } アイテムへのハードリンクに失敗、{ $total } アイテムのうち\nrust_symlink_summary = シンクリリンク { $symlinked } アイテム、シンクリリンク { $failed } アイテムに失敗、{ $total } アイテムのうち\nrust_optimize_video_summary = 最適化 { $optimized } ビデオ、最適化失敗 { $failed } ビデオ、総数 { $total } ビデオ\nrust_clean_exif_summary = { $cleaned } ファイルの EXIF をクリーンアップし、{ $failed } ファイルのクリーンアップに失敗し、{ $total } ファイルのうちで\nrust_deleting_files = { $items_stats } ファイル ({ $size_stats }) を削除しています\nrust_deleting_no_size_files = { $items_stats } ファイルを削除中\nrust_renaming_files = { $items_stats } ファイルの名前を変更中\nrust_moving_files = { $items_stats } ファイルの移動 ({ $size_stats })\nrust_moving_no_size_files = { $items_stats } ファイルを移動中\nrust_hardlinking_files = ハードリンク { $items_stats } ファイル ({ $size_stats })\nrust_hardlinking_no_size_files = ハードリンク { $items_stats } ファイル\nrust_symlinking_files = シンボリックリンク { $items_stats } ファイル ({ $size_stats })\nrust_symlinking_no_size_files = Symlinking { $items_stats } ファイル\nrust_optimizing_videos = 最適化 { $items_stats } ビデオ ({ $size_stats })\nrust_optimizing_no_size_videos = 最適化 { $items_stats } ビデオ\nrust_cleaning_exif = { $items_stats }ファイルからEXIFをクリーンアップする({ $size_stats })\nrust_cleaning_no_size_exif = { $items_stats } ファイルから EXIF をクリーンアップする\nrust_no_files_deleted = 削除するファイルまたはフォルダが選択されていません\nrust_no_files_renamed = 名前を変更するファイルまたはフォルダが選択されていません\nrust_no_files_moved = 移動するファイルまたはフォルダが選択されていません\nrust_no_files_hardlinked = ファイルまたはフォルダがハードリンク用に選択されていません\nrust_no_files_symlinked = シンボリックリンク用のファイルまたはフォルダが選択されていません\nrust_no_videos_optimized = 動画の最適化には選択された動画がありません\nrust_no_exif_cleaned = EXIFクリーニング用のファイルが選択されていません\nrust_extracted_exif_tags = 抽出 { $items_stats } ファイル ({ $size_stats }) から EXIF タグ\nrust_delete_confirmation = 選択したアイテムを削除してもよろしいですか？\nrust_delete_confirmation_number_simple = { $items } アイテムを選択しました。.\nrust_delete_confirmation_number_groups = { $items } 個のグループで { $groups } 個のアイテムが選択されました。.\nrust_delete_confirmation_selected_all_in_group = { $groups } グループで選択されたすべてのアイテム。.\nrust_move_confirmation = 選択した項目を移動しますか？\nrust_move_confirmation_number_simple = { $items } アイテムが選択されました。.\nrust_clean_exif_confirmation = 選択した項目からEXIFデータを削除しますか？\nrust_clean_exif_confirmation_number_simple = { $items } アイテムが選択されました。.\nclean_exif_overwrite_files_text = ファイルを上書きする\nrust_optimize_video_confirmation = 選択したビデオを最適化しますか？\nrust_optimize_video_confirmation_number_simple = { $items } アイテムが選択されました。.\nrust_hardlink_confirmation = 選択した項目に対してハードリンクを作成しますか？\nrust_hardlink_confirmation_number_simple = { $items } アイテムが選択されました。.\nrust_symlink_confirmation = 選択した項目に対してシンボリックリンクを作成しますか？\nrust_symlink_confirmation_number_simple = { $items } アイテムが選択されました。.\nrust_rename_confirmation = 選択した項目をリネームしますか？\nrust_rename_confirmation_number_simple = { $items } アイテムが選択されました。.\nrust_cache_processed_files = 処理済み { $files } キャッシュファイル\nrust_cache_entries_stats = 削除 { $removed } エントリのうち、{ $all } のうち、{ $left } が残る\nrust_cache_size_reduced = キャッシュファイルのサイズを{ $size }に削減しました。\nrust_cache_time_elapsed = 経過時間: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = { $name } を { $target } へのハードリンクに失敗しました、理由 { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = 選択\ncolumn_size = サイズ\ncolumn_file_name = ファイル名\ncolumn_path = パス\ncolumn_modification_date = 変更日\ncolumn_similarity = 類似度\ncolumn_dimensions = 寸法\ncolumn_new_dimensions = 新たな次元\ncolumn_title = タイトル\ncolumn_artist = アーティスト\ncolumn_year = 年\ncolumn_bitrate = ビットレート\ncolumn_length = 長さ\ncolumn_genre = ジャンル\ncolumn_type_of_error = エラーの種類\ncolumn_symlink_name = シンボリックリンク名\ncolumn_symlink_folder = シンボリックリンクフォルダ\ncolumn_destination_path = 宛先パス\ncolumn_current_extension = 現在の拡張子\ncolumn_proper_extension = 適切な拡張子\ncolumn_fps = FPS\ncolumn_codec = コーデック：\ncolumn_duration = 期間\ncolumn_exif_tags = EXIFタグ\ncolumn_new_name = 新しい名前\n# Slint translations\nok_button = OK\ncancel_button = キャンセル\ndo_you_want_to_continue = 続行しますか？\nmain_window_title = Krokiet - データクリーナー\nscan_button = スキャン\nstop_button = 停止\nstop_text = 停止\nselect_button = 選択\nmove_button = 移動\ndelete_button = 削除\nsave_button = 保存\nsort_button = 並べ替え\nrename_button = 名前の変更\nmotto = \\n詳細については、MIT/GPL ライセンスをご覧ください。.\nunicorn = ユニコーンを見てはいけませんが、ユニコーンはいつもあなたを見ています。.\nrepository = リポジトリ\ninstruction = 説明\ndonation = 寄付\ntranslation = 翻訳\nincluded_paths = 含まれるパス\nexcluded_paths = 除外パス\nref = 参照\npath = パス\ntool_duplicate_files = 重複ファイル\ntool_empty_folders = 空のフォルダ\ntool_big_files = 大きなファイル\ntool_empty_files = 空のファイル\ntool_temporary_files = 一時ファイル\ntool_similar_images = 類似する画像\ntool_similar_videos = 類似する動画\ntool_music_duplicates = 重複する音楽\ntool_invalid_symlinks = 無効なシンボリックリンク\ntool_broken_files = 壊れたファイル\ntool_bad_extensions = 不正な拡張子\ntool_bad_names = 悪い名前\ntool_video_optimizer = ビデオ最適化\ntool_exif_remover = Exif 情報削除ツール\nsort_by_full_name = 氏名でソート\nsort_by_selection = 選択でソート\nsort_reverse = 順序を逆にする\nselection_all = すべて選択\nselection_deselect_all = すべての選択を解除\nselection_invert_selection = 選択を反転\nselection_the_biggest_size = 最大サイズを選択\nselection_the_biggest_resolution = 最大解像度を選択\nselection_the_smallest_size = 最小サイズを選択\nselection_the_smallest_resolution = 最小解像度を選択\nselection_newest = 最新のものを選択\nselection_oldest = 最古のものを選択\nselection_shortest_path = 最短経路を選択する\nselection_longest_path = 選択肢の中で最も長い経路を選択してください\nstage_current = 現在のステージ:\nstage_all = すべてのステージ:\nsubsettings = サブ設定\nsubsettings_images_hash_size = ハッシュサイズ\nsubsettings_images_resize_algorithm = アルゴリズムのサイズ変更\nsubsettings_images_ignore_same_size = 同じサイズの画像を無視\nsubsettings_images_max_difference = 最大差\nsubsettings_images_duplicates_hash_type = ハッシュタイプ\nsubsettings_duplicates_check_method = メソッドのチェック\nsubsettings_duplicates_name_case_sensitive = ケースセンシティブ(名前モードのみ)\nsubsettings_biggest_files_sub_method = 方法\nsubsettings_biggest_files_sub_number_of_files = ファイル数\nsubsettings_videos_max_difference = 最大差\nsubsettings_videos_ignore_same_size = 同じサイズのビデオを無視\nsubsettings_music_audio_check_type = 音声チェックの種類\nsubsettings_music_approximate_comparison = おおよそのタグ比較\nsubsettings_music_compared_tags = 比較されたタグ\nsubsettings_music_title = タイトル\nsubsettings_music_artist = アーティスト\nsubsettings_music_bitrate = ビットレート\nsubsettings_music_genre = ジャンル\nsubsettings_music_year = 年\nsubsettings_music_length = 長さ\nsubsettings_music_max_difference = 最大差\nsubsettings_music_minimal_fragment_duration = フラグメントの最小期間\nsubsettings_music_compare_fingerprints_only_with_similar_titles = 類似したタイトルのグループ内で比較\nsubsettings_broken_files_type = チェックするファイルの種類\nsubsettings_broken_files_audio = オーディオ\nsubsettings_broken_files_pdf = pdf\nsubsettings_broken_files_archive = アーカイブ\nsubsettings_broken_files_image = 画像\nsubsettings_broken_files_video = ビデオ\nsubsettings_broken_files_video_info = ffmpeg/ffprobe を使用します。遅く、ファイルが正常に再生されていても、煩雑なエラーを検出することがあります。.\nsubsettings_bad_names_issues = ファイル名チェック\nsubsettings_bad_names_uppercase_extension = 大文字拡張\nsubsettings_bad_names_uppercase_extension_hint = 大文字の拡張子（例：.JPG、.Mp3）を持つファイルを検索し、小文字版を提案します。\nsubsettings_bad_names_emoji_used = 絵文字を含む名前\nsubsettings_bad_names_emoji_used_hint = ファイル名に絵文字文字（😀、🎉など）を含むファイルを検索し、削除を提案します。\nsubsettings_bad_names_space_at_start_end = 前後の空白\nsubsettings_bad_names_space_at_start_end_hint = スペースで始まるか終わるファイルを見つけ、トリミングを提案します。\nsubsettings_bad_names_non_ascii = 非ASCII文字\nsubsettings_bad_names_non_ascii_hint = 非ASCII文字（ą、ć、ñなど）を見つけ、ASCII相当文字（a、c、n）で置き換えたり、マッピングが存在しない場合は削除を提案します。\nsubsettings_bad_names_restricted_charset = 限定文字セット\nsubsettings_bad_names_restricted_charset_hint = 非ASCII文字をASCIIに転写し、0-9a-zA-Zおよびユーザー定義の許可文字外の文字を含むファイルを見つけ出す。\nsubsettings_bad_names_allowed_chars = 許可文字\nsubsettings_bad_names_remove_duplicated = 重複した文字\nsubsettings_bad_names_remove_duplicated_hint = 連続した重複した非英数字文字（例：「file---name..txt」）を見つけ、重複を削除することを提案します。\nsettings_global_settings = グローバル設定\nsettings_dark_theme = ダークテーマ\nsettings_show_only_icons = アイコンのみ表示\nsettings_excluded_items = 除外されたアイテム:\nsettings_allowed_extensions = 許可される拡張子:\nsettings_excluded_extensions = 除外する拡張子:\nsettings_file_size = ファイルサイズ(キロバイト)\nsettings_minimum_file_size = 最小:\nsettings_maximum_file_size = 最大：\nsettings_recursive_search = 再帰的な検索\nsettings_use_cache = キャッシュを使用\nsettings_save_as_json = また、キャッシュをJSONファイルとして保存\nsettings_move_to_trash = 削除したファイルをゴミ箱に移動する\nsettings_ignore_other_filesystems = 他のファイルシステムを無視（Linuxのみ）\nsettings_delete_outdated_cache_entries = 自動的に古いキャッシュエントリを削除する\nsettings_delete_outdated_cache_entries_hint = 有効になっている場合、アプリはキャッシュの読み込み中に（1週間に最大1回まで）キャッシュされたレコードが既存で変更されていないファイル/データにまだ参照しているかどうかを確認します。\nsettings_hide_hard_links = 隠すハードリンク\nsettings_hide_hard_links_hint = 同じファイルのハードリンクを結果に隠す\nsettings_thread_number = スレッド数\nsettings_restart_required = ---スレッド数の変更を適用するにはアプリを再起動する必要があります ---\nsettings_duplicate_image_preview = 画像のプレビュー\nsettings_duplicate_minimal_hash_cache_size = キャッシュされたファイルの最小サイズ - ハッシュ (KB)\nsettings_duplicate_use_prehash = 事前ハッシュを使用\nsettings_duplicate_minimal_prehash_cache_size = キャッシュされたファイルの最小サイズ - Prehash (KB)\nsettings_similar_images_show_image_preview = 画像のプレビュー\nsettings_application_scale_text = アプリケーション規模\nsettings_application_scale_hint_text = 手動スケーリングが有効になっていると、カスタムのスケーリングファクタを選択できますが、モニターのDPIに基づく自動スケーリングを完全に無効にします。.\nsettings_restart_required_scale_text = ---アプリを再起動して、スケール変更を適用する必要があります---\nsettings_use_manual_application_scale_text = 手動アプリケーションスケールを使用する\nsettings_video_thumbnails_preview = 画像プレビュー\nsettings_open_config_folder = 設定フォルダを開く\nsettings_open_cache_folder = キャッシュフォルダを開く\nsettings_language = 言語\nsettings_current_preset = 現在のプリセット:\nsettings_edit_name = 名前を編集\nsettings_choose_name_for_prefix = プレフィックスの名前を選択\nsettings_save = 保存\nsettings_load = 読み込み\nsettings_reset = リセット\nsettings_similar_videos_tool = 類似の動画ツール\nsettings_video_thumbnails_clear_unused_thumbnails = 起動時に7日以上経過した未使用のビデオサムネイルを削除する\nsettings_video_thumbnails_header = 動画サムネイル\nsettings_video_thumbnails_generate = サムネイルを生成する\nsettings_video_thumbnails_position = 動画のサムネイル位置（%）\nsettings_video_thumbnails_generate_grid = サムネイルグリッドを単一画像ではなく生成する\nsettings_video_thumbnails_generate_grid_hint = 複数の画像をグリッドで生成することは、単一のサムネイルを生成するよりもずっと遅いです。\nsettings_video_thumbnails_grid_tiles_per_side = サムネイルグリッドの一辺当たりのタイル数\nsettings_video_thumbnails_grid_tiles_per_side_hint = グリッドの各辺のサムネイルタイル数。たとえば、2を選択すると2 x 2グリッドが作成され、4枚の画像を含む単一のサムネイルが生成されます。.\nsettings_similar_images_tool = 類似の画像ツール\nsettings_general_settings = 全般設定\nsettings_cache_header_text = キャッシュ設定\nsettings_clean_cache_button_text = 古いキャッシュをクリアする\nsettings_settings = 設定\nsettings_load_tabs_sizes_at_startup = 起動時にタブのサイズを読み込む\nsettings_load_windows_size_at_startup = 起動時にウィンドウサイズを読み込む\nsettings_limit_lines_of_messages = メッセージを 500 行に制限します(テキスト編集の遅いウィジェットの回避策)\nsettings_play_audio_on_scan_completion_text = スキャンが正常に完了したときに音を再生する\nsettings_audio_feature_hint_text = オーディオ機能でコンパイルした場合のみ利用可能\nsettings_audio_env_variable_hint_text = サウンドは、KROKIET_AUDIO_STOP_FILE環境変数を有効なオーディオファイルパスに設定することで変更できます。\npopup_save_title = 結果を保存しています\npopup_save_message = 結果を3つの異なるファイルに保存します\npopup_rename_title = ファイル名の変更\npopup_new_paths_title = パスを1行に記述してください\npopup_move_title = ファイルを移動中\npopup_move_copy_checkbox = ファイルを移動する代わりにコピー\npopup_move_preserve_folder_checkbox = フォルダ構造を保持する\nmove_confirmation_text = 選択した項目を移動しますか？\nrename_confirmation_text = 選択した項目をリネームしますか？\ndelete = アイテムを削除\nstopping_scan = スキャンを停止しています。お待ちください...\nsearching = 検索中...\nsubsettings_videos_crop_detect = トリミング検出方法\nsubsettings_videos_skip_forward_amount = スキップ時間 [s]\nsubsettings_videos_vid_hash_duration = ビデオハッシュ時間\nsettings_cache_number_size_text = キャッシュファイルサイズ: { $size }, ファイル数: { $number }\nsettings_video_thumbnails_number_size_text = 動画のサムネイルサイズ: { $size }, ファイル数: { $number }\nsettings_log_number_size_text = ログ ファイル サイズ: { $size }、ファイル数: { $number }\npopup_clean_cache_title_text = 古いキャッシュをクリアする\npopup_clean_cache_confirmation_text = 古いキャッシュエントリを削除しますか？ これは、存在しなくなったファイルまたは変更されたファイルに対するキャッシュエントリを削除します。.\npopup_clean_cache_progress_text = 処理キャッシュファイル:\npopup_clean_cache_current_file_text = 現在のファイル:\npopup_clean_cache_file_progress_text = 現在のファイル進捗:\npopup_clean_cache_overall_progress_text = 全体進捗：\npopup_clean_cache_stopped_by_user_text = キャッシュのクリアがユーザーによって停止されました\npopup_clean_cache_finished_text = キャッシュのクリアが正常に完了しました！\npopup_clean_cache_error_details_text = エラー詳細:\npopup_clean_cache_files_with_errors = エラーを含むファイル:\nsubsettings_video_optimizer_mode = モード\nsubsettings_video_optimizer_crop_type = 作物タイプ\nsubsettings_video_optimizer_black_pixel_threshold = ブラックピクセル閾値\nsubsettings_video_optimizer_black_pixel_threshold_hint = 各ピクセルチャンネルの最大RGB値を黒（0-128）とみなす。デフォルト：20\nsubsettings_video_optimizer_black_bar_min_percentage = ブラックバー最小パーセント\nsubsettings_video_optimizer_black_bar_min_percentage_hint = 最小の黒ピクセルの行/列の割合が黒い帯と見なされる（50-100）。デフォルト：90\nsubsettings_video_optimizer_max_samples = マックスサンプルズ\nsubsettings_video_optimizer_max_samples_hint = 最大分析フレーム数（5-1000）。デフォルト：60\nsubsettings_video_optimizer_min_crop_size = 最小の作物サイズ\nsubsettings_video_optimizer_min_crop_size_hint = 最小のピクセルをどの辺でもトリミングする（1-1000）。より小さいトリミングは無視されます。デフォルト：5\nsubsettings_video_optimizer_video_codec = ビデオコーデック\nsubsettings_video_optimizer_excluded_codecs = 除外コーデック\nsubsettings_video_optimizer_video_quality = ビデオ品質（CRF）\nsubsettings_reset = リセット\nsubsettings_exif_ignored_tags_text = 無視されたタグ：\nsubsettings_exif_ignored_tags_hint_text = カンマ区切りのスキャンから除外するタグのリスト（例：GPS、サムネイル）。一部のタグ、例えばTIFFファイルのImageWidthは、画像の破損を防ぐために非表示になっています。.\nclean_button_text = きれい\nclean_text = クリーン EXIF データ\nclean_confirmation_text = 選択した項目からEXIFデータを削除しますか？\ncrop_videos_text = 動画をトリミングする\ncrop_video_confirmation_text = 選択したビデオをトリミングしますか？\ncrop_reencode_video_text = 動画を再エンコードする\nreencode_videos_text = 動画を再エンコードする\noptimize_button_text = 最適化\noptimize_confirmation_text = 選択したビデオを再エンコードしますか？\noptimize_fail_if_bigger_text = 最適化されたファイルが大きすぎると失敗します\noptimize_overwrite_files_text = ファイルを上書きする\noptimize_limit_video_size_text = 動画サイズを制限\noptimize_max_width_text = 最大幅：\noptimize_max_height_text = 最大 高さ:\nhardlink_button_text = ハードリンク\nhardlink_text = ハードリンクを作成\nhardlink_confirmation_text = 選択した項目に対してハードリンクを作成しますか？\nsoftlink_button_text = ソフトリンク\nsoftlink_text = 作成してリンク\nsoftlink_confirmation_text = 選択した項目に対して、シンボリックリンク（symlinks）を作成しますか？\n"
  },
  {
    "path": "krokiet/i18n/ko/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = 앱 시작 중 심각한 오류\nrust_init_error_message = \n        애플리케이션 시작 중 오류가 발생했습니다:\n\n        { $error_message }\n\n        이는 OpenGL/Vulkan 드라이버가 누락되었거나 제대로 작동하지 않아서, 가상 머신에서 애플리케이션을 실행하거나 Krokiet 또는 해당 라이브러리의 버그 때문일 수 있습니다.\n\n        다른 빌드(skia_opengl, skia_vulkan, femtovg_opengl - 기본)를 실행하거나 소프트웨어 렌더러를 사용하여 문제가 해결되는지 확인해 볼 수 있습니다.\nrust_loaded_preset = 로드된 프리셋 { $preset_idx }\nrust_file_already_exists = 파일 \"{ $file }\" 이 이미 존재하며 덮어쓰이지 않습니다\nrust_error_removing_file_after_copy = 파일 \"{ $file }\" 삭제 중 오류 발생 (다른 파티션으로 복사 후), 이유: { $reason }\nrust_error_copying_file = 오류가 \"{ $input }\"를 \"{ $output }\"로 복사하는 동안 발생했습니다, 이유: { $reason }\nrust_loading_tags_cache = 태그 캐시 로드 중\nrust_loading_fingerprints_cache = 지문 캐시 로드 중\nrust_saving_tags_cache = 태그 캐시 저장 중\nrust_saving_fingerprints_cache = 지문 캐시 저장 중\nrust_loading_prehash_cache = PreHash 캐시 로드 중\nrust_saving_prehash_cache = PreHash 캐시 저장 중\nrust_loading_hash_cache = 해시 캐시 로드 중\nrust_saving_hash_cache = 해시 캐시 저장 중\nrust_loading_exif_cache = 로딩 EXIF 캐시\nrust_saving_exif_cache = 저장 EXIF 캐시\nrust_scanning_name = { $entries_checked }개 파일 이름 스캔 중\nrust_scanning_size_name = { $entries_checked }개 파일의 크기 및 이름 스캔 중\nrust_scanning_size = { $entries_checked }개 파일 크기 스캔 중\nrust_scanning_file = { $entries_checked }개 파일 스캔 중\nrust_scanning_folder = { $entries_checked }개 폴더 스캔 중\nrust_checked_tags = { $items_stats } 항목의 태그 확인 완료\nrust_checked_content = { $items_stats } 항목의 내용 확인 완료 ({ $size_stats })\nrust_compared_tags = { $items_stats } 항목의 태그 비교 완료\nrust_compared_content = { $items_stats } 항목의 내용 비교 완료\nrust_hashed_images = 해싱된 { $items_stats } 이미지들 ({ $size_stats })\nrust_compared_image_hashes = { $items_stats }의 비교 이미지 해시를 보존하세요\nrust_hashed_videos = 해싱된 { $items_stats } 비디오들\nrust_created_thumbnails = { $items_stats } 비디오를 미리보기로 만들었습니다\nrust_checked_files = { $items_stats }개의 파일 확인 완료 ({ $size_stats })\nrust_checked_files_bad_extensions = 부적절한 확장자를 가진 { $items_stats }개의 파일 확인 완료\nrust_checked_files_bad_names = 부적절한 확장자를 가진 { $items_stats }개의 파일 확인 완료\nrust_checked_videos = 확인됨 { $items_stats } 비디오 ({ $size_stats })\nrust_analyzed_partial_hash = { $items_stats }개의 파일 부분 해시 분석 완료 ({ $size_stats })\nrust_analyzed_full_hash = { $items_stats }개의 파일 전체 해시 분석 완료 ({ $size_stats })\nrust_failed_to_rename_file = 파일 { $old_path }를 { $new_path }로 이름변경에 실패하였습니다. 오류: { $error }\nrust_no_included_paths = 검색을 시작할 수 없습니다. 포함된 경로가 설정되지 않았기 때문입니다.\nrust_all_paths_referenced = 모든 포함된 경로가 참조 경로로 설정되면 스캔을 시작할 수 없습니다. 입력 경로 옆의 참조 확인란을 해제해야 합니다.\nrust_found_empty_folders = { $items_found }개의 폴더가 비어 있어 { $time }에 발견되었습니다\nrust_found_empty_files = { $items_found }개의 파일이 { $time }에 비어 있습니다\nrust_found_similar_images = { $items_found }개의 유사한 이미지 파일을 { $groups } 그룹에서 { $time }에 찾았습니다\nrust_found_similar_videos = { $groups }개의 그룹에서 비디오 파일 { $items_found }개를 { $time }에 찾았습니다\nrust_found_similar_music_files = { $items_found }개의 유사 음악 파일을 { $groups } 그룹에서 { $time }에 찾았습니다\nrust_found_invalid_symlinks = { $items_found }개의 잘못된 심볼릭 링크를 찾았습니다.{ $time }\nrust_found_temporary_files = { $items_found }분의 일시 파일을 { $time }에서 찾았습니다\nrust_no_file_type_selected = 선택된 파일 유형 없이 손상된 파일을 찾을 수 없습니다.\nrust_found_broken_files = 발견된 { $items_found } 개의 깨진 파일이 { $size }에 차지하고 있습니다 { $time }동안\nrust_found_bad_extensions = 발견된 { $items_found }개의 파일 중 { $time }에 잘못된 확장자를 가진 파일이 있습니다\nrust_found_bad_names = 발견된 { $items_found }개의 파일 중 { $time }에서 이름이 잘못된 것들이 있습니다\nrust_found_video_optimizer = 찾은 { $items_found }개의 파일을 { $time }에 최적화했습니다\nrust_found_duplicate_files = { $groups } 그룹에서 중복 파일 { $items_found }개를 발견하여 { $size }의 용량을 { $time }에 차지하고 있습니다\nrust_found_duplicate_files_no_lost_space = { $items_found }개의 중복 파일을 { $groups } 그룹에서 { $time }에 찾았습니다\nrust_found_big_files = 找到 크기 { $size }의 { $items_found }대 큰 파일 { $time } 내에\nrust_found_exif_files = 발견된 { $items_found }개의 파일과 { $time }에 대한 EXIF 데이터를 찾았습니다\nrust_cannot_load_preset = 프리셋 { $preset_idx }을(를) 변경하거나 로드할 수 없습니다: { $reason }. 기본 설정을 사용합니다\nrust_saved_preset = 프리셋 { $preset_idx }이(가) 저장되었습니다\nrust_cannot_save_preset = 프리셋 { $preset_idx }을(를) 저장할 수 없습니다: { $reason }\nrust_reset_preset = 프리셋 { $preset_idx }이(가) 재설정되었습니다\nrust_cannot_create_output_folder = 출력 폴더 { $output_folder }을(를) 생성할 수 없습니다: { $error }\nrust_delete_summary = 삭제된 { $deleted } 항목, 삭제 실패한 { $failed } 항목, 총 { $total } 항목 중\nrust_rename_summary = 이름을 변경한 { $renamed } 항목, 이름 변경에 실패한 { $failed } 항목, 총 { $total } 항목 중\nrust_move_summary = 이동한 아이템 수: { $moved }, 이동 실패한 아이템 수: { $failed }, 총 아이템 수: { $total }\nrust_hardlink_summary = 하드링크된 { $hardlinked } 항목, 하드링크에 실패한 { $failed } 항목, 총 { $total } 항목 중\nrust_symlink_summary = 심볼릭 링크 { $symlinked } 항목, 심볼릭 링크 { $failed } 항목 실패, 총 { $total } 항목 중\nrust_optimize_video_summary = 최적화됨 { $optimized } 비디오, 최적화 실패 { $failed } 비디오, 총 { $total } 비디오 중\nrust_clean_exif_summary = 정리된 EXIF를 { $cleaned } 파일에서, { $failed } 파일에서 정리 실패, 총 { $total } 파일 중 입니다\nrust_deleting_files = { $items_stats }개의 파일 삭제 중 ({ $size_stats })\nrust_deleting_no_size_files = { $items_stats }개의 파일 삭제 중\nrust_renaming_files = { $items_stats }개의 파일 이름 변경 중\nrust_moving_files = { $items_stats }개의 파일 이동 중 ({ $size_stats })\nrust_moving_no_size_files = { $items_stats }개의 파일 이동 중\nrust_hardlinking_files = 하드 링크 { $items_stats } 파일 ({ $size_stats })\nrust_hardlinking_no_size_files = 하드 링크 { $items_stats } 파일\nrust_symlinking_files = 심링크 { $items_stats } 파일 ({ $size_stats })\nrust_symlinking_no_size_files = 심링크 { $items_stats } 파일\nrust_optimizing_videos = 최적화됨 { $items_stats } 비디오 ({ $size_stats })\nrust_optimizing_no_size_videos = 최적화된 { $items_stats } 비디오\nrust_cleaning_exif = Cleaning EXIF from { $items_stats } 파일 ({ $size_stats })\nrust_cleaning_no_size_exif = { $items_stats } 파일에서 EXIF 정리\nrust_no_files_deleted = 삭제할 파일이나 폴더가 선택되지 않았습니다\nrust_no_files_renamed = 변경할 파일이나 폴더가 선택되지 않았습니다\nrust_no_files_moved = 이동할 파일이나 폴더가 선택되지 않았습니다\nrust_no_files_hardlinked = 파일 또는 폴더가 하드 링크용으로 선택되지 않았습니다\nrust_no_files_symlinked = 파일 또는 폴더가 심볼릭 링크용으로 선택되지 않았습니다\nrust_no_videos_optimized = 선택된 비디오가 최적화되지 않았습니다\nrust_no_exif_cleaned = 선택된 파일이 없습니다. EXIF 정리용\nrust_extracted_exif_tags = 추출된 EXIF 태그에서 { $items_stats } 파일 ({ $size_stats })\nrust_delete_confirmation = 선택한 항목을 정말 삭제하시겠습니까?\nrust_delete_confirmation_number_simple = { $items } 아이템 선택되었습니다.\nrust_delete_confirmation_number_groups = { $items } 개가 선택되어 있는 { $groups } 그룹입니다.\nrust_delete_confirmation_selected_all_in_group = { $groups } 그룹에서 선택된 모든 항목.\nrust_move_confirmation = 선택한 항목을 이동하시겠습니까?\nrust_move_confirmation_number_simple = { $items } 항목 선택됨.\nrust_clean_exif_confirmation = 선택한 항목에서 EXIF 데이터 삭제하시겠습니까?\nrust_clean_exif_confirmation_number_simple = { $items } 항목 선택됨.\nclean_exif_overwrite_files_text = 덮어쓰기 파일\nrust_optimize_video_confirmation = 선택한 비디오를 최적화하시겠습니까?\nrust_optimize_video_confirmation_number_simple = { $items } 항목 선택됨.\nrust_hardlink_confirmation = 선택한 항목에 대해 하드 링크를 생성하시겠습니까?\nrust_hardlink_confirmation_number_simple = { $items } 항목 선택됨.\nrust_symlink_confirmation = 선택한 항목에 대한 심볼릭 링크를 생성하시겠습니까?\nrust_symlink_confirmation_number_simple = { $items } 항목 선택됨.\nrust_rename_confirmation = 선택한 항목을 이름을 변경하시겠습니까?\nrust_rename_confirmation_number_simple = { $items } 항목 선택됨.\nrust_cache_processed_files = 처리됨 { $files } 캐시 파일\nrust_cache_entries_stats = 제거됨 { $removed } 항목 중 { $all }, { $left } 항목이 남아 있음\nrust_cache_size_reduced = 캐시 파일 크기 { $size } 줄임\nrust_cache_time_elapsed = 시간 경과: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = 하드 링크를 생성하지 못했습니다 { $name }을 { $target }으로, 이유는 { $reason }입니다\n\n# Slint translations, but in arrays\n\ncolumn_selection = 선택\ncolumn_size = 파일 크기\ncolumn_file_name = 파일명\ncolumn_path = 경로\ncolumn_modification_date = 수정한 날짜\ncolumn_similarity = 유사도\ncolumn_dimensions = 크기\ncolumn_new_dimensions = 새로운 차원\ncolumn_title = 제목\ncolumn_artist = 아티스트\ncolumn_year = 연도\ncolumn_bitrate = 비트레이트\ncolumn_length = 길이\ncolumn_genre = 장르\ncolumn_type_of_error = 오류 유형\ncolumn_symlink_name = 심볼릭 링크 이름\ncolumn_symlink_folder = 심볼릭 링크 폴더\ncolumn_destination_path = 대상 경로\ncolumn_current_extension = 현재 확장자\ncolumn_proper_extension = 올바른 확장자\ncolumn_fps = FPS\ncolumn_codec = 코덱\ncolumn_duration = 기간\ncolumn_exif_tags = EXIF 태그\ncolumn_new_name = 새 이름\n# Slint translations\nok_button = 확인\ncancel_button = 취소\ndo_you_want_to_continue = 계속하시겠습니까?\nmain_window_title = 크로키에트 - 데이터 클리너\nscan_button = 스캔\nstop_button = 정지\nstop_text = 정지\nselect_button = 선택\nmove_button = 이동\ndelete_button = 삭제\nsave_button = 저장\nsort_button = 종류\nrename_button = 이름 바꾸기\nmotto = \n    이 프로그램은 무료로 사용할 수 있으며 항상如此翻译结果：\n    这个程序是免费使用的，而且将会一直如此。\n    请参见MIT/MPL许可证以获取详细信息。.\nunicorn = 🦄을 보지 못하더라도 Unicorn은 항상 당신을 바라봅니다.\nrepository = 리포지토리\ninstruction = 사용방법\ndonation = 기부\ntranslation = 번역\nincluded_paths = 포함된 경로\nexcluded_paths = 제외 경로\nref = 레퍼런스\npath = 경로\ntool_duplicate_files = 중복 파일\ntool_empty_folders = 비어있는 폴더들\ntool_big_files = 큰 파일\ntool_empty_files = 빈 파일\ntool_temporary_files = 임시 파일\ntool_similar_images = 유사한 이미지\ntool_similar_videos = 비슷한 영상\ntool_music_duplicates = 중복 음악\ntool_invalid_symlinks = 잘못된 심볼릭 링크\ntool_broken_files = 손상된 파일\ntool_bad_extensions = 잘못된 확장자\ntool_bad_names = 나쁜 이름\ntool_video_optimizer = 비디오 최적화\ntool_exif_remover = Exif 제거\nsort_by_full_name = 전체 이름순 정렬\nsort_by_selection = 선택 상태순 정렬\nsort_reverse = 역순 정렬\nselection_all = 전체 선택\nselection_deselect_all = 전체 선택 해제\nselection_invert_selection = 선택 반전\nselection_the_biggest_size = 가장 큰 파일 선택\nselection_the_biggest_resolution = 가장 큰 해상도 선택\nselection_the_smallest_size = 가장 작은 파일 선택\nselection_the_smallest_resolution = 가장 작은 해상도 선택\nselection_newest = 최신 순 선택\nselection_oldest = 오래된 순 선택\nselection_shortest_path = 최단 경로 선택\nselection_longest_path = 가장 긴 경로 선택\nstage_current = 현재 스테이지:\nstage_all = 모든 스테이지:\nsubsettings = 부가설정들\nsubsettings_images_hash_size = 해시 크기\nsubsettings_images_resize_algorithm = 리사이즈 알고리즘\nsubsettings_images_ignore_same_size = 같은 크기의 이미지 무시하기\nsubsettings_images_max_difference = 최대 차이값\nsubsettings_images_duplicates_hash_type = 해시 종류\nsubsettings_duplicates_check_method = 확인 방법\nsubsettings_duplicates_name_case_sensitive = 대소문자 구분(이름 모드에서만)\nsubsettings_biggest_files_sub_method = 방식\nsubsettings_biggest_files_sub_number_of_files = 파일 수\nsubsettings_videos_max_difference = 최대 차이값\nsubsettings_videos_ignore_same_size = 같은 크기의 비디오 무시하기\nsubsettings_music_audio_check_type = 오디오 체크 타입\nsubsettings_music_approximate_comparison = 근사 태그 비교\nsubsettings_music_compared_tags = 비교된 태그\nsubsettings_music_title = 제목\nsubsettings_music_artist = 아티스트\nsubsettings_music_bitrate = 비트레이트\nsubsettings_music_genre = 장르\nsubsettings_music_year = 연도\nsubsettings_music_length = 길이\nsubsettings_music_max_difference = 최대 차이값\nsubsettings_music_minimal_fragment_duration = 최소 조각 길이\nsubsettings_music_compare_fingerprints_only_with_similar_titles = 유사한 제목들의 그룹 내에서 비교\nsubsettings_broken_files_type = 검사할 파일 유형\nsubsettings_broken_files_audio = 오디오 파일\nsubsettings_broken_files_pdf = PDF 파일\nsubsettings_broken_files_archive = 아카이브 파일\nsubsettings_broken_files_image = 이미지 파일\nsubsettings_broken_files_video = 비디오\nsubsettings_broken_files_video_info = ffmpeg/ffprobe 사용합니다. 상당히 느리고 파일이 잘 재생되더라도 엄격한 오류를 감지할 수 있습니다.\nsubsettings_bad_names_issues = 파일 이름 확인\nsubsettings_bad_names_uppercase_extension = 대문자 확장\nsubsettings_bad_names_uppercase_extension_hint = 대문자 확장자(예: .JPG, .Mp3)를 포함하는 파일을 찾고 소문자 버전 제안\nsubsettings_bad_names_emoji_used = 이모지 이름에\nsubsettings_bad_names_emoji_used_hint = 이름에 이모지 문자(😀, 🎉 등)가 포함된 파일을 찾고 제거를 제안합니다\nsubsettings_bad_names_space_at_start_end = 앞/뒤 공백\nsubsettings_bad_names_space_at_start_end_hint = 공백이 있는 이름을 가진 파일을 찾고 이름의 양쪽 끝에 공백을 제거하는 것을 제안합니다\nsubsettings_bad_names_non_ascii = Non-ASCII 문자\nsubsettings_bad_names_non_ascii_hint = 유니코드 이외의 문자(ą, ć, ñ 등)를 찾고 ASCII에 해당하는 문자(a, c, n)로 대체하거나 매핑이 없는 경우 제거를 제안합니다\nsubsettings_bad_names_restricted_charset = 제한된 문자 집합\nsubsettings_bad_names_restricted_charset_hint = 비-ASCII 문자들을 ASCII로 변환한 다음, 0-9, a-zA-Z 및 사용자 정의 허용 문자 외의 파일들을 찾습니다\nsubsettings_bad_names_allowed_chars = 허용 문자\nsubsettings_bad_names_remove_duplicated = 중복 문자\nsubsettings_bad_names_remove_duplicated_hint = 연속된 중복된 비알파벳 문자를 찾고(예: \"file---name..txt\") 중복을 제거하는 것을 제안합니다\nsettings_global_settings = 전역 설정\nsettings_dark_theme = 다크 테마\nsettings_show_only_icons = 아이콘만 표시\nsettings_excluded_items = 제외된 항목:\nsettings_allowed_extensions = 허용된 확장자:\nsettings_excluded_extensions = 제외할 확장자:\nsettings_file_size = 파일 크기(킬로바이트)\nsettings_minimum_file_size = 최소:\nsettings_maximum_file_size = 최대:\nsettings_recursive_search = 재귀 검색\nsettings_use_cache = 캐시 사용\nsettings_save_as_json = 캐시를 JSON으로 저장\nsettings_move_to_trash = 삭제된 파일을 휴지통으로 이동\nsettings_ignore_other_filesystems = 다른 파일시스템 무시하기(리눅스 한정)\nsettings_delete_outdated_cache_entries = 자동으로 오래된 캐시 항목 삭제\nsettings_delete_outdated_cache_entries_hint = 활성화되면, 앱은 캐시 로딩 중에 (주당 최대 한 번) 캐시 기록이 기존 및 수정되지 않은 파일/데이터를 가리키고 있는지 확인합니다\nsettings_hide_hard_links = 하드 링크 숨기기\nsettings_hide_hard_links_hint = 숨겨 동일 파일의 하드 링크를 결과에\nsettings_thread_number = 스레드 갯수\nsettings_restart_required = ---스레드 갯수 설정을 변경하려면 앱을 재시작 해야해요---\nsettings_duplicate_image_preview = 이미지 미리보기\nsettings_duplicate_minimal_hash_cache_size = 캐시된 파일 최소 크기 - 해시(KB)\nsettings_duplicate_use_prehash = 프리해시 사용하기\nsettings_duplicate_minimal_prehash_cache_size = 캐시된 파일 최소 크기 - 프리해시(KB)\nsettings_similar_images_show_image_preview = 이미지 미리보기\nsettings_application_scale_text = 신청 규모\nsettings_application_scale_hint_text = 수동 스케일이 활성화되면, 이를 통해 사용자 지정 스케일 요인을 선택할 수 있지만, 모니터의 DPI에 따른 자동 스케일링을 완전히 비활성화합니다.\nsettings_restart_required_scale_text = ---앱을 다시 시작하여 스케일 변경 사항을 적용해야 합니다---\nsettings_use_manual_application_scale_text = 수동 적용 척도 사용\nsettings_video_thumbnails_preview = 이미지 미리보기\nsettings_open_config_folder = 설정 폴더 열기\nsettings_open_cache_folder = 캐시 폴더 열기\nsettings_language = 언어\nsettings_current_preset = 현재 프리셋:\nsettings_edit_name = 이름 수정하기\nsettings_choose_name_for_prefix = 접두어 이름 선택\nsettings_save = 저장\nsettings_load = 로드\nsettings_reset = 재설정\nsettings_similar_videos_tool = 유사한 동영상 도구\nsettings_video_thumbnails_clear_unused_thumbnails = 앱 시작 시 7일 이상 사용되지 않은 비디오 썸네일 삭제\nsettings_video_thumbnails_header = 비디오 썸네일\nsettings_video_thumbnails_generate = 생성 썸네일\nsettings_video_thumbnails_position = 비디오 내 썸네일 위치 (%)\nsettings_video_thumbnails_generate_grid = 썸네일 그리드 생성 대신 단일 이미지 생성\nsettings_video_thumbnails_generate_grid_hint = 여러 개의 이미지를 격자로 생성하는 것은 단일 작은 미리보기 이미지를 생성하는 것보다 훨씬 느립니다\nsettings_video_thumbnails_grid_tiles_per_side = 썸네일 그리드 당 측면 타일 수\nsettings_video_thumbnails_grid_tiles_per_side_hint = 그리드 내 각 측면의 작은 이미지 타일 수. 예를 들어 2를 선택하면 2x2 그리드가 생성되어 4개의 이미지가 포함된 단일 작은 이미지가 됩니다.\nsettings_similar_images_tool = 유사한 이미지 도구\nsettings_general_settings = 일반 설정\nsettings_cache_header_text = 캐시 설정\nsettings_clean_cache_button_text = 오래된 캐시 삭제\nsettings_settings = 설정\nsettings_load_tabs_sizes_at_startup = 시작시 탭 크기 불러오기\nsettings_load_windows_size_at_startup = 시작시 윈도우 크기 불러오기\nsettings_limit_lines_of_messages =\n    메시지를 500줄以内我无法继续使用韩语翻译这个句子，因为提供的文本片段突然包含了中文。不过，前部分内容的正确韩语翻译是：\n    \n    메시지를 500줄로 제한하세요(TextEdit 위젯이 느릴 때의 대안)\nsettings_play_audio_on_scan_completion_text = 스캔 완료 성공 시 음성 재생\nsettings_audio_feature_hint_text = 오디오 기능으로 컴파일할 때만 사용 가능\nsettings_audio_env_variable_hint_text = 소리 는 KROKIET_AUDIO_STOP_FILE 환경 변수를 유효한 오디오 파일 경로 로 설정 함으로써 변경 될 수 있습니다\npopup_save_title = 결과 저장\npopup_save_message = 결과를 3개의 파일로 저장합니다\npopup_rename_title = 파일 이름 변경중\npopup_new_paths_title = 경로를 한 줄에 하나씩 추가하십시오\npopup_move_title = 항목 이동\npopup_move_copy_checkbox = 이동 대신 복사\npopup_move_preserve_folder_checkbox = 폴더 구조 유지\nmove_confirmation_text = 선택한 항목을 이동하시겠습니까?\nrename_confirmation_text = 선택한 항목을 이름을 변경하시겠습니까?\ndelete = 항목 삭제\nstopping_scan = 스캔 중단 중, 잠시만 기다려 주세요….\nsearching = 검색 중….\nsubsettings_videos_crop_detect = 크롭 감지 방법\nsubsettings_videos_skip_forward_amount = 건너뛸 길이 [들]\nsubsettings_videos_vid_hash_duration = 영상 해시 길이\nsettings_cache_number_size_text = 캐시 파일 크기: { $size }, 파일 수: { $number }\nsettings_video_thumbnails_number_size_text = 비디오 탭형 이미지 크기: { $size }, 파일 개수: { $number }\nsettings_log_number_size_text = 로그 파일 크기: { $size }, 파일 수: { $number }\npopup_clean_cache_title_text = 삭제된 캐시 정리\npopup_clean_cache_confirmation_text = 삭제하시려는 오래된 캐시 항목이 맞습니까? 이는 더 이상 존재하지 않거나 수정된 파일에 대한 캐시 항목을 제거합니다.\npopup_clean_cache_progress_text = 처리 캐시 파일:\npopup_clean_cache_current_file_text = 현재 파일:\npopup_clean_cache_file_progress_text = 현재 파일 진행률:\npopup_clean_cache_overall_progress_text = 전체 진행 상황:\npopup_clean_cache_stopped_by_user_text = 사용자가 캐시 정리 중단을 중단했습니다\npopup_clean_cache_finished_text = 캐시 정리 완료되었습니다!\npopup_clean_cache_error_details_text = 오류 상세 정보:\npopup_clean_cache_files_with_errors = 오류가 있는 파일들:\nsubsettings_video_optimizer_mode = 모드\nsubsettings_video_optimizer_crop_type = 작물 유형\nsubsettings_video_optimizer_black_pixel_threshold = 검정 픽셀 임계값\nsubsettings_video_optimizer_black_pixel_threshold_hint = 각 픽셀 채널의 최대 RGB 값은 검정으로 간주됩니다(0-128). 기본값: 20\nsubsettings_video_optimizer_black_bar_min_percentage = 블랙 바 민 퍼센트\nsubsettings_video_optimizer_black_bar_min_percentage_hint = 최소 행/열 내 검은 픽셀 비율이 검은 막으로 간주되는 값 (50-100). 기본값: 90\nsubsettings_video_optimizer_max_samples = 맥스 샘플스\nsubsettings_video_optimizer_max_samples_hint = 최대 분석 프레임 수 (5-1000). 기본값: 60\nsubsettings_video_optimizer_min_crop_size = 최소 크롭 크기\nsubsettings_video_optimizer_min_crop_size_hint = 최소 잘라낼 픽셀 수 (1-1000). 더 작은 잘림은 무시됩니다. 기본값: 5\nsubsettings_video_optimizer_video_codec = 비디오 코덱\nsubsettings_video_optimizer_excluded_codecs = 제외 코덱\nsubsettings_video_optimizer_video_quality = 비디오 품질 (CRF)\nsubsettings_reset = 초기화\nsubsettings_exif_ignored_tags_text = 무시된 태그:\nsubsettings_exif_ignored_tags_hint_text = 쉼표로 구분된 스캔에서 제외할 태그 목록(예: GPS, 썸네일). 일부 태그, 예를 들어 TIFF 파일의 ImageWidth와 같이 이미지를 깨뜨리는 것을 방지하기 위해 숨겨집니다.\nclean_button_text = 깨끗하다\nclean_text = 클린 EXIF 데이터\nclean_confirmation_text = 선택한 항목에서 EXIF 데이터 삭제하시겠습니까?\ncrop_videos_text = 자르기 영상\ncrop_video_confirmation_text = 선택한 비디오를 잘라서 괜찮으십니까?\ncrop_reencode_video_text = 재인코딩 비디오\nreencode_videos_text = 재인코딩 비디오\noptimize_button_text = 최적화\noptimize_confirmation_text = 선택한 비디오를 다시 인코딩하시겠습니까?\noptimize_fail_if_bigger_text = 최적화된 파일이 더 크면 실패합니다\noptimize_overwrite_files_text = 덮어쓰기 파일\noptimize_limit_video_size_text = 비디오 크기 제한\noptimize_max_width_text = 최대 너비:\noptimize_max_height_text = 최대 높이:\nhardlink_button_text = 하드 링크\nhardlink_text = 하드 링크 생성\nhardlink_confirmation_text = 선택한 항목에 대해 하드 링크를 생성하시겠습니까?\nsoftlink_button_text = 소프트링크\nsoftlink_text = 생성 softlinks\nsoftlink_confirmation_text = 선택한 항목에 대한 심볼릭 링크(symlink)를 생성하시겠습니까?\n"
  },
  {
    "path": "krokiet/i18n/nl/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Critische fout tijdens app opstarten\nrust_init_error_message = \n        Een kritische fout is opgetreden bij het starten van de applicatie:\n\n        { $error_message }\n\n        Dit kan worden veroorzaakt door ontbrekende of defecte OpenGL/Vulkan drivers, het uitvoeren van de applicatie in een virtuele machine of een bug in Krokiet of een van zijn libraries.\nrust_loaded_preset = Voorinstelling geladen { $preset_idx }\nrust_file_already_exists = Bestand \"{ $file }\" bestaat al en zal niet worden overschreven\nrust_error_removing_file_after_copy = Fout bij het verwijderen van bestand \"{ $file }\" (na kopiëren naar een andere partitie), reden: { $reason }\nrust_error_copying_file = Fout bij kopiëren \"{ $input }\" naar \"{ $output }\", reden: { $reason }\nrust_loading_tags_cache = Labels cache laden\nrust_loading_fingerprints_cache = Vingerafdruk cache laden\nrust_saving_tags_cache = Tags cache opslaan\nrust_saving_fingerprints_cache = Vingerafdruk cache opslaan\nrust_loading_prehash_cache = Prehash cache laden\nrust_saving_prehash_cache = Opslaan van prehash cache\nrust_loading_hash_cache = hash-cache laden\nrust_saving_hash_cache = hash cache opslaan\nrust_loading_exif_cache = Laad EXIF cache\nrust_saving_exif_cache = Opslaan EXIF cache\nrust_scanning_name = Scannen van naam { $entries_checked } bestand\nrust_scanning_size_name = Scannen grootte en naam van { $entries_checked } bestand\nrust_scanning_size = Scannen grootte van { $entries_checked } bestand\nrust_scanning_file = Scannen van { $entries_checked } bestand\nrust_scanning_folder = Scannen { $entries_checked } map\nrust_checked_tags = Gecontroleerde tags van { $items_stats }\nrust_checked_content = Gecontroleerde inhoud van { $items_stats } ({ $size_stats })\nrust_compared_tags = Vergeleken tags van { $items_stats }\nrust_compared_content = Vergeleken inhoud van { $items_stats }\nrust_hashed_images = Hashed { $items_stats } afbeeldingen ({ $size_stats })\nrust_compared_image_hashes = Vergeleken afbeeldingshashes van { $items_stats }\nrust_hashed_videos = Gehashte { $items_stats } video's\nrust_created_thumbnails = Gemaakte miniaturen voor { $items_stats } video's\nrust_checked_files = Gecontroleerd { $items_stats } bestand ({ $size_stats })\nrust_checked_files_bad_extensions = Gecontroleerd { $items_stats } bestand\nrust_checked_files_bad_names = Gevalideerd { $items_stats } bestand\nrust_checked_videos = Gecontroleerd { $items_stats } video’s ({ $size_stats })\nrust_analyzed_partial_hash = Geanalyseerde gedeeltelijke hash van { $items_stats } bestanden ({ $size_stats })\nrust_analyzed_full_hash = Volledige hash van { $items_stats } bestanden ({ $size_stats } ) geanalyseerd\nrust_failed_to_rename_file = Kan bestand { $old_path } niet hernoemen naar { $new_path }, fout: { $error }\nrust_no_included_paths = Kan geen scan starten wanneer geen ingesloten paden zijn ingesteld.\nrust_all_paths_referenced = Kan geen scan starten wanneer alle opgenomen paden als verwijzen paden zijn ingesteld, u moet het checkbox-vak naast het invoerpad uitschakelen.\nrust_found_empty_folders = Gevonden { $items_found } lege mappen in { $time }\nrust_found_empty_files = Gevonden { $items_found } lege bestanden in { $time }\nrust_found_similar_images = { $items_found } vergelijkbare afbeeldingsbestanden gevonden in { $groups } groepen in { $time }\nrust_found_similar_videos = { $items_found } vergelijkbare videobestanden gevonden in { $groups } groepen in { $time }\nrust_found_similar_music_files = Gevonden { $items_found } soortgelijke muziekbestanden in { $groups } groepen in { $time }\nrust_found_invalid_symlinks = Gevonden { $items_found } ongeldige symlinks in { $time }\nrust_found_temporary_files = Gevonden { $items_found } tijdelijke bestanden in { $time }\nrust_no_file_type_selected = Kan defecte bestanden niet vinden zonder geselecteerde bestandstype.\nrust_found_broken_files = Gevonden { $items_found } defecte bestanden in { $size } in { $time }\nrust_found_bad_extensions = { $items_found } bestanden met ongeldige extensies gevonden in { $time }\nrust_found_bad_names = Vond { $items_found } bestanden met slechte namen in { $time}\nrust_found_video_optimizer = Vond { $items_found } bestanden om te optimaliseren in { $time }\nrust_found_duplicate_files = Gevonden { $items_found } dubbele bestanden in { $groups } groepen die { $size } in { $time } nemen\nrust_found_duplicate_files_no_lost_space = Gevonden { $items_found } dubbele bestanden in { $groups } groepen in { $time }\nrust_found_big_files = Gevonden { $items_found } grote bestanden met grootte { $size } in { $time }\nrust_found_exif_files = Vond { $items_found } bestanden met exif data in { $time }\nrust_cannot_load_preset = Kan vooraf ingestelde { $preset_idx } niet wijzigen en laden - reden { $reason }, met behulp van standaardinstellingen\nrust_saved_preset = Opgeslagen voorinstelling { $preset_idx }\nrust_cannot_save_preset = Kan voorinstelling { $preset_idx } niet opslaan - reden { $reason }\nrust_reset_preset = Reset vooraf ingestelde { $preset_idx }\nrust_cannot_create_output_folder = Kan de uitvoermap { $output_folder }niet aanmaken, reden: { $error }\nrust_delete_summary = { $deleted } items verwijderd, { $failed } items kunnen niet verwijderd worden, uit { $total } items\nrust_rename_summary = { $renamed } items hernoemd, kan { $failed } items niet hernoemen, uit { $total } items\nrust_move_summary = { $moved } items verplaatst, { $failed } items konden niet verplaatst worden, van { $total } items\nrust_hardlink_summary = Hardgelinkte { $hardlinked } items, faalde om { $failed } items te hardlinken, van { $total } items\nrust_symlink_summary = Verkooppunten { $symlinked } , geen koppelingen { $failed } , van { $total } items\nrust_optimize_video_summary = Geoptimaliseerd { $optimized } video's, mislukte optimalisatie van { $failed } video's, van { $total } video's\nrust_clean_exif_summary = Gepureerde EXIF van { $cleaned } bestanden, mislukt bij het zuiveren van { $failed } bestanden, van { $total } bestanden\nrust_deleting_files = Verwijderen { $items_stats } bestand ({ $size_stats })\nrust_deleting_no_size_files = { $items_stats } bestand verwijderen\nrust_renaming_files = { $items_stats } bestand hernoemen\nrust_moving_files = Verplaatsen van { $items_stats } bestand ({ $size_stats })\nrust_moving_no_size_files = Verplaatsen { $items_stats } bestand\nrust_hardlinking_files = Hardlinking { $items_stats } bestand ({ $size_stats })\nrust_hardlinking_no_size_files = Hardlinking { $items_stats } bestand\nrust_symlinking_files = Symlinking { $items_stats } bestand ({ $size_stats })\nrust_symlinking_no_size_files = Symlinking { $items_stats } bestand\nrust_optimizing_videos = Geoptimaliseerd { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Geoptimaliseerd { $items_stats } video\nrust_cleaning_exif = Reinigen EXIF van { $items_stats } bestand ({ $size_stats })\nrust_cleaning_no_size_exif = Reinigen EXIF van { $items_stats } bestand\nrust_no_files_deleted = Geen bestanden of mappen geselecteerd voor verwijdering\nrust_no_files_renamed = Geen bestanden of mappen geselecteerd voor hernoemen\nrust_no_files_moved = Geen bestanden of mappen geselecteerd om te verplaatsen\nrust_no_files_hardlinked = Geen bestanden of mappen geselecteerd voor hardlinken\nrust_no_files_symlinked = Geen bestanden of mappen geselecteerd voor symbolische links\nrust_no_videos_optimized = Geen video's geselecteerd voor optimalisatie\nrust_no_exif_cleaned = Geen bestanden geselecteerd voor EXIF reiniging\nrust_extracted_exif_tags = Geëxtraheerde EXIF-tags van { $items_stats } bestanden ({ $size_stats })\nrust_delete_confirmation = Weet u zeker dat u de geselecteerde items wilt verwijderen?\nrust_delete_confirmation_number_simple = { $items } items geselecteerd.\nrust_delete_confirmation_number_groups = { $items } items geselecteerd in { $groups } groepen.\nrust_delete_confirmation_selected_all_in_group = Alle items geselecteerd in { $groups } groepen.\nrust_move_confirmation = Zijnu er zeker van dat u de geselecteerde items wilt verplaatsen?\nrust_move_confirmation_number_simple = { $items } items geselecteerd.\nrust_clean_exif_confirmation = Zijnu er zeker van dat u de EXIF-gegevens uit de geselecteerde items wilt verwijderen?\nrust_clean_exif_confirmation_number_simple = { $items } items geselecteerd.\nclean_exif_overwrite_files_text = Schrijf bestanden over\nrust_optimize_video_confirmation = Ben je zeker dat je de geselecteerde video's wilt optimaliseren?\nrust_optimize_video_confirmation_number_simple = { $items } items geselecteerd.\nrust_hardlink_confirmation = Ben je er zeker van dat je hardlinks wilt aanmaken voor de geselecteerde items?\nrust_hardlink_confirmation_number_simple = { $items } items geselecteerd.\nrust_symlink_confirmation = Ben je er zeker van dat je symbolische links wilt aanmaken voor de geselecteerde items?\nrust_symlink_confirmation_number_simple = { $items } items geselecteerd.\nrust_rename_confirmation = Zijnu zeker dat u de geselecteerde items wilt hernoemen?\nrust_rename_confirmation_number_simple = { $items } items geselecteerd.\nrust_cache_processed_files = Verwerkt { $files } cache bestanden\nrust_cache_entries_stats = Verwijderd { $removed } entries uit alle { $all }, { $left } overgebleven\nrust_cache_size_reduced = Verkleinde cachebestandsgrootte met { $size }\nrust_cache_time_elapsed = Tijd verstreken: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Faalde bij het hardlinken van { $name } naar { $target }, reden { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Selectie\ncolumn_size = Grootte\ncolumn_file_name = Bestandsnaam\ncolumn_path = Pad\ncolumn_modification_date = Wijziging datum\ncolumn_similarity = Vergelijkbaarheid\ncolumn_dimensions = Mål\ncolumn_new_dimensions = Nieuwe Dimensies\ncolumn_title = Aanspreektitel\ncolumn_artist = Kunstenaar\ncolumn_year = jaar\ncolumn_bitrate = Bitsnelheid\ncolumn_length = longueur\ncolumn_genre = genre\ncolumn_type_of_error = Type fout\ncolumn_symlink_name = Symlink Naam\ncolumn_symlink_folder = Symlink map\ncolumn_destination_path = Bestemming pad\ncolumn_current_extension = Huidige extensie\ncolumn_proper_extension = Proper Extensie\ncolumn_fps = FPS\ncolumn_codec = Codec\ncolumn_duration = Duur\ncolumn_exif_tags = EXIF tags\ncolumn_new_name = Nieuwe Naam\n# Slint translations\nok_button = OK\ncancel_button = annuleren\ndo_you_want_to_continue = Wilt u doorgaan?\nmain_window_title = Krokiet - Opschoner gegevens\nscan_button = Scannen\nstop_button = Stoppen\nstop_text = Stop\nselect_button = Selecteren\nmove_button = Verplaatsen\ndelete_button = Verwijderen\nsave_button = Opslaan\nsort_button = Sorteren\nrename_button = Hernoem\nmotto = Dit programma is gratis te gebruiken en zal dat altijd zijn.\\nZie de MIT/GPL Licentie voor meer informatie.\nunicorn = Je mag niet naar een eenhoorn kijken, maar de eenhoorn kijkt altijd naar je.\nrepository = Bewaarplaats\ninstruction = Instructie\ndonation = Donatie\ntranslation = Vertaling\nincluded_paths = Inclusieve Paden\nexcluded_paths = Uitgesloten Paden\nref = Ref:\npath = Pad\ntool_duplicate_files = Dupliceer Bestanden\ntool_empty_folders = Lege mappen\ntool_big_files = Grote bestanden\ntool_empty_files = Lege bestanden\ntool_temporary_files = Tijdelijke bestanden\ntool_similar_images = Vergelijkbare afbeeldingen\ntool_similar_videos = Soortgelijke video's\ntool_music_duplicates = Muziek duplicaten\ntool_invalid_symlinks = Ongeldige Symlinks\ntool_broken_files = Kapotte Bestanden\ntool_bad_extensions = Slechte extensies\ntool_bad_names = Slechte Namen\ntool_video_optimizer = Video optimalisatie\ntool_exif_remover = Exif Verwijderaar\nsort_by_full_name = Sorteren op volledige naam\nsort_by_selection = Sorteren op selectie\nsort_reverse = Omgekeerde volgorde\nselection_all = Alles selecteren\nselection_deselect_all = Deselecteer alles\nselection_invert_selection = Selectie omkeren\nselection_the_biggest_size = Selecteer de grootste grootte\nselection_the_biggest_resolution = Selecteer de grootste resolutie\nselection_the_smallest_size = Selecteer de kleinste grootte\nselection_the_smallest_resolution = Selecteer de kleinste resolutie\nselection_newest = Selecteer nieuwste\nselection_oldest = Selecteer oudste\nselection_shortest_path = Select de kortste route\nselection_longest_path = Select de langste route\nstage_current = Huidige fase:\nstage_all = Alle fasen:\nsubsettings = Subinstellingen\nsubsettings_images_hash_size = Hash grootte\nsubsettings_images_resize_algorithm = Algoritme verkleinen\nsubsettings_images_ignore_same_size = Negeer afbeeldingen met dezelfde grootte\nsubsettings_images_max_difference = Max verschil\nsubsettings_images_duplicates_hash_type = Hash-Type\nsubsettings_duplicates_check_method = Controleer methode\nsubsettings_duplicates_name_case_sensitive = Hoofdletters (alleen naam modus)\nsubsettings_biggest_files_sub_method = Methode\nsubsettings_biggest_files_sub_number_of_files = Aantal bestanden\nsubsettings_videos_max_difference = Max verschil\nsubsettings_videos_ignore_same_size = Negeer video's met dezelfde grootte\nsubsettings_music_audio_check_type = Type audio-controle\nsubsettings_music_approximate_comparison = Geschatte Tag vergelijking\nsubsettings_music_compared_tags = Vergeleken labels\nsubsettings_music_title = Aanspreektitel\nsubsettings_music_artist = Kunstenaar\nsubsettings_music_bitrate = Bitsnelheid\nsubsettings_music_genre = genre\nsubsettings_music_year = jaar\nsubsettings_music_length = longueur\nsubsettings_music_max_difference = Max verschil\nsubsettings_music_minimal_fragment_duration = Minimale fragment duur\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Vergelijk binnen groepen van vergelijkbare titels\nsubsettings_broken_files_type = Type bestanden om te controleren\nsubsettings_broken_files_audio = Geluid\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archief\nsubsettings_broken_files_image = Afbeelding\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Gebruikt ffmpeg/ffprobe. Zeer traag en kan pedantische fouten detecteren, zelfs als het bestand prima afspeelt.\nsubsettings_bad_names_issues = Bestand controles\nsubsettings_bad_names_uppercase_extension = Hoofdletteruitbreiding\nsubsettings_bad_names_uppercase_extension_hint = Vindt bestanden met hoofdletters in de extensie (bijv. .JPG, .Mp3) en suggereert de kleine letters versie\nsubsettings_bad_names_emoji_used = Emoji in naam\nsubsettings_bad_names_emoji_used_hint = Vindt bestanden met emoji-tekens (😀, 🎉, enz.) in de naam en suggereert ze te verwijderen\nsubsettings_bad_names_space_at_start_end = Voorexpandende/achterwaartse ruimtes\nsubsettings_bad_names_space_at_start_end_hint = Vindt bestanden met spaties aan het begin of aan het einde van de naam en suggereert deze te trimmen\nsubsettings_bad_names_non_ascii = Niet-ASCII tekens\nsubsettings_bad_names_non_ascii_hint = Vindt niet-ASCII tekens (ą, ć, ñ, enz.) en suggereert ze te vervangen door ASCII equivalenten (a, c, n) of verwijderen indien er geen mapping bestaat\nsubsettings_bad_names_restricted_charset = Beperkte tekencode\nsubsettings_bad_names_restricted_charset_hint = Transcode niet-ASCII tekens naar ASCII, vervolgens worden bestanden gevonden die karakters buiten 0-9a-zA-Z en toegestane karakters die door de gebruiker zijn gedefinieerd\nsubsettings_bad_names_allowed_chars = Toegestane tekens\nsubsettings_bad_names_remove_duplicated = Verdupte tekens\nsubsettings_bad_names_remove_duplicated_hint = Vindt opeenvolgende verdubbelde niet-alfanumerieke tekens (bijv. \"file---name..txt\") en suggereert het verwijderen van duplicaten\nsettings_global_settings = Globale instellingen\nsettings_dark_theme = Donker thema\nsettings_show_only_icons = Alleen pictogrammen weergeven\nsettings_excluded_items = Uitgesloten item:\nsettings_allowed_extensions = Toegestane extensies:\nsettings_excluded_extensions = Uitgesloten extensies:\nsettings_file_size = Bestandsomvang (Kilobaten)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Max:\nsettings_recursive_search = Recursieve zoekopdracht\nsettings_use_cache = Gebruik cache\nsettings_save_as_json = Sla ook de cache op als JSON-bestand\nsettings_move_to_trash = Verwijderde bestanden verplaatsen naar prullenbak\nsettings_ignore_other_filesystems = Negeer andere bestandssystemen (alleen Linux)\nsettings_delete_outdated_cache_entries = Verwijder automatisch verouderde cache-entries\nsettings_delete_outdated_cache_entries_hint = Wanneer ingeschakeld, zal de app tijdens het laden van de cache (maximaal één keer per week) controleren of de opgeslagen records nog steeds naar bestaande en ongewijzigde bestanden/gegevens wijzen\nsettings_hide_hard_links = Verberg hardkoppelingen\nsettings_hide_hard_links_hint = Verberg hard links naar dezelfde bestanden in de resultaten\nsettings_thread_number = Thread nummer\nsettings_restart_required = ---U moet de app herstarten om wijzigingen in thread number--- toe te passen\nsettings_duplicate_image_preview = Voorvertoning afbeelding\nsettings_duplicate_minimal_hash_cache_size = Minimale grootte van gecachete bestanden - Hash (KB)\nsettings_duplicate_use_prehash = Gebruik prehash\nsettings_duplicate_minimal_prehash_cache_size = Minimale grootte van gecachete bestanden - Prehash (KB)\nsettings_similar_images_show_image_preview = Voorvertoning afbeelding\nsettings_application_scale_text = Toepassingsschaal\nsettings_application_scale_hint_text = Wanneer handmatige schaal is ingeschakeld, staat dit toe dat u een aangepaste schaalfactor kiest, maar schakelt het automatisch schalen op basis van de DPI van het scherm volledig uit.\nsettings_restart_required_scale_text = ---Je moet de app opnieuw starten om wijzigingen in de schaal toe te passen---\nsettings_use_manual_application_scale_text = Gebruik handmatige toepassingsschaal\nsettings_video_thumbnails_preview = Voorbeeldweergave\nsettings_open_config_folder = Open configuratiemap\nsettings_open_cache_folder = Open cachemap\nsettings_language = Taal\nsettings_current_preset = Huidige voorkeursinstelling:\nsettings_edit_name = Naam bewerken\nsettings_choose_name_for_prefix = Kies een naam voor de prefix\nsettings_save = Opslaan\nsettings_load = Belasting\nsettings_reset = Herstel\nsettings_similar_videos_tool = Soortgelijke video's gereedschap\nsettings_video_thumbnails_clear_unused_thumbnails = Verwijder ongebruikte videobeschrijvingen ouder dan 7 dagen bij het opstarten van de app\nsettings_video_thumbnails_header = Video Miniatuurfoto's\nsettings_video_thumbnails_generate = Genereer miniaturen\nsettings_video_thumbnails_position = Thumbnail positie in video (%)\nsettings_video_thumbnails_generate_grid = Maak een thumbnail-raster in plaats van één afbeelding genereren\nsettings_video_thumbnails_generate_grid_hint = Het genereren van meerdere afbeeldingen in een raster is veel langzamer dan het genereren van een enkele miniaturen\nsettings_video_thumbnails_grid_tiles_per_side = Aantal tegels per zijde in de miniaturen raster\nsettings_video_thumbnails_grid_tiles_per_side_hint = Aantal miniaturen tiles per kant in het raster. Bijvoorbeeld, het selecteren van 2 creëert een 2 x 2 raster, wat resulteert in een enkele miniatuur bestaande uit 4 afbeeldingen.\nsettings_similar_images_tool = Vergelijkbare afbeeldingen tool\nsettings_general_settings = Algemene instellingen\nsettings_cache_header_text = Cache Instellingen\nsettings_clean_cache_button_text = Maak verouderde cache opslag\nsettings_settings = Instellingen\nsettings_load_tabs_sizes_at_startup = Laad tabbladen grootte bij het opstarten\nsettings_load_windows_size_at_startup = Laad venstergrootte bij het opstarten\nsettings_limit_lines_of_messages = Berichten beperken tot 500 regels(workaround voor langzame TextEdit widget)\nsettings_play_audio_on_scan_completion_text = Speel geluid wanneer scan succesvol is voltooid\nsettings_audio_feature_hint_text = Alleen beschikbaar bij het compileren met de audio-functie\nsettings_audio_env_variable_hint_text = Kan worden veranderd, door de KROKIET_AUDIO_STOP_FILE omgevingvariabele in te stellen op een geldige audiobestandspad\npopup_save_title = Resultaten opslaan\npopup_save_message = Dit zal resultaten opslaan naar 3 verschillende bestanden\npopup_rename_title = Bestanden hernoemen\npopup_new_paths_title = Geef de pad één per regel toe\npopup_move_title = Bestanden verplaatsen\npopup_move_copy_checkbox = Bestanden kopiëren in plaats van verplaatsen\npopup_move_preserve_folder_checkbox = Mapstructuur behouden\nmove_confirmation_text = Zijnu er zeker van dat u de geselecteerde items wilt verplaatsen?\nrename_confirmation_text = Zijnu zeker dat u de geselecteerde items wilt hernoemen?\ndelete = Artikelen verwijderen\nstopping_scan = Scannen stoppen, even geduld...\nsearching = Zoeken...\nsubsettings_videos_crop_detect = Gewas detecteerde methode\nsubsettings_videos_skip_forward_amount = Overslaan duur [s]\nsubsettings_videos_vid_hash_duration = Video hash duur\nsettings_cache_number_size_text = Cache bestanden grootte: { $size }, aantal bestanden: { $number }\nsettings_video_thumbnails_number_size_text = Video miniatuurgrootte: { $size }, aantal bestanden: { $number }\nsettings_log_number_size_text = Log bestanden grootte: { $size }, aantal bestanden: { $number }\npopup_clean_cache_title_text = Schoon Cache Op\npopup_clean_cache_confirmation_text = Ben je zeker dat je verouderde cache-items wilt opschonen? Dit verwijdert cache-items voor bestanden die niet meer bestaan ​​of zijn gewijzigd.\npopup_clean_cache_progress_text = Verwerking cachebestand:\npopup_clean_cache_current_file_text = Huidige bestand:\npopup_clean_cache_file_progress_text = Huidige bestandsgang:\npopup_clean_cache_overall_progress_text = Algemeen vooruitgangsrapport:\npopup_clean_cache_stopped_by_user_text = Cache opschonen is gestopt door gebruiker\npopup_clean_cache_finished_text = Cache opschonen voltooid met succes!\npopup_clean_cache_error_details_text = Foutdetails:\npopup_clean_cache_files_with_errors = Bestanden met fouten:\nsubsettings_video_optimizer_mode = Modus\nsubsettings_video_optimizer_crop_type = Type van gewassen\nsubsettings_video_optimizer_black_pixel_threshold = Zwarte Pixel Drempel\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maxima RGB-waarde voor elke kleurkanal om als zwart (0-128) te worden beschouwd. Standaard: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Zwarte Balk Min Percentage\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimum percentage van zwarte pixels in een rij/kolom om als een zwarte balk te worden beschouwd (50-100). Standaard: 90\nsubsettings_video_optimizer_max_samples = Maximaal aantal samples\nsubsettings_video_optimizer_max_samples_hint = Maxima aantal frames te analyseren per video (5-1000). Standaard: 60\nsubsettings_video_optimizer_min_crop_size = Min Afbeelding Grootte\nsubsettings_video_optimizer_min_crop_size_hint = Minimale pixels om te knippen aan elke zijde (1-1000). Kleinere knipsels worden genegeerd. Standaard: 5\nsubsettings_video_optimizer_video_codec = Videocodec\nsubsettings_video_optimizer_excluded_codecs = Uitgesloten codecs\nsubsettings_video_optimizer_video_quality = Video kwaliteit (CRF)\nsubsettings_reset = Herstel\nsubsettings_exif_ignored_tags_text = Verwaarloosde tags:\nsubsettings_exif_ignored_tags_hint_text = Comma-gescheiden lijst van tags om uit te sluiten van het scannen (bijv. GPS, Thumbnail). Sommige tags, zoals ImageWidth in TIFF-bestanden, zijn verborgen om te voorkomen dat het beeld wordt verbroken.\nclean_button_text = Schoon\nclean_text = Schoon EXIF data\nclean_confirmation_text = Zijnu er zeker van dat u de EXIF-gegevens uit de geselecteerde items wilt verwijderen?\ncrop_videos_text = Knip video's\ncrop_video_confirmation_text = Ben je er zeker van dat je de geselecteerde video's wilt inzoomen?\ncrop_reencode_video_text = Her-encode video\nreencode_videos_text = Her-encode video’s\noptimize_button_text = Optimaliseer\noptimize_confirmation_text = Ben je zeker dat je de geselecteerde video's opnieuw wilt coderen?\noptimize_fail_if_bigger_text = Faal als de geoptimaliseerde bestandsgrootte groter is\noptimize_overwrite_files_text = Schrijf bestanden over\noptimize_limit_video_size_text = Beperk videogrootte\noptimize_max_width_text = Max breedte:\noptimize_max_height_text = Maximale hoogte:\nhardlink_button_text = Hardlink\nhardlink_text = Maak hardlinks\nhardlink_confirmation_text = Ben je er zeker van dat je hardlinks wilt aanmaken voor de geselecteerde items?\nsoftlink_button_text = Softlink\nsoftlink_text = Maak softlinks\nsoftlink_confirmation_text = Ben je zeker dat je softlinks (symlinks) wilt maken voor de geselecteerde items?\n"
  },
  {
    "path": "krokiet/i18n/no/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Kritisk feil under applikasjonsstart\nrust_init_error_message = \n        En kritisk feil oppstod under oppstart av applikasjonen:\n\n        { $error_message }\n\n        Dette kan skyldes manglende eller defekte OpenGL/Vulkan-drivere, kjøring av applikasjonen i en virtuell maskin eller en feil i Krokiet eller en av dets biblioteker.\n\n        Du kan prøve å kjøre forskjellige bygg (skia_opengl, skia_vulkan, femtovg_opengl - standard) eller med programvarerenderer for å se om det løser problemet.\nrust_loaded_preset = Lastet inn forhåndsinnstilling { $preset_idx }\nrust_file_already_exists = Fil \"{ $file }\" eksisterer allerede, og vil ikke bli overskrevet\nrust_error_removing_file_after_copy = Feil ved sletting av fil \"{ $file }\" (etter kopiert til annen partisjon), årsak: { $reason }\nrust_error_copying_file = Feil ved kopiering av \"{ $input }\" til \"{ $output }\", årsak: { $reason }\nrust_loading_tags_cache = Laster tagger cache\nrust_loading_fingerprints_cache = Laster fingeravtrykks-cache\nrust_saving_tags_cache = Lagrer merkelapper\nrust_saving_fingerprints_cache = Lagrer fingeravtrykksbufferen\nrust_loading_prehash_cache = Laster prehash cache\nrust_saving_prehash_cache = Lagrer prehash-cache\nrust_loading_hash_cache = Laster hash-cache\nrust_saving_hash_cache = Lagrer hurtigbufferen for hash\nrust_loading_exif_cache = Laster EXIF-cache\nrust_saving_exif_cache = Lagre EXIF-minnebank\nrust_scanning_name = Søker etter navn på { $entries_checked } fil\nrust_scanning_size_name = Søker etter størrelse og navn på { $entries_checked } fil\nrust_scanning_size = Skanner størrelse på { $entries_checked } fil\nrust_scanning_file = Skanner { $entries_checked } fil\nrust_scanning_folder = Søker etter { $entries_checked } mappe\nrust_checked_tags = Avviste tagger av { $items_stats }\nrust_checked_content = Merket innhold i { $items_stats } ({ $size_stats })\nrust_compared_tags = Sammenlignet tagger med { $items_stats }\nrust_compared_content = Sammenlignet innhold av { $items_stats }\nrust_hashed_images = Hashed { $items_stats } bilder ({ $size_stats })\nrust_compared_image_hashes = Sammenlignet bilde-hasher av { $items_stats }\nrust_hashed_videos = Kastet { $items_stats } videoer\nrust_created_thumbnails = Lagede miniatyrbilder for { $items_stats } videoer\nrust_checked_files = Merket { $items_stats } fil ({ $size_stats })\nrust_checked_files_bad_extensions = Merket { $items_stats } fil\nrust_checked_files_bad_names = Sjekket { $items_stats } fil\nrust_checked_videos = Sjekket { $items_stats } videoer ({ $size_stats })\nrust_analyzed_partial_hash = Analyserte delvis hash av { $items_stats } filer ({ $size_stats })\nrust_analyzed_full_hash = Analyserte fullhash av { $items_stats } filer ({ $size_stats })\nrust_failed_to_rename_file = Klarte ikke å endre navn på filen { $old_path } til { $new_path }, feil: { $error }\nrust_no_included_paths = Kan ikke starte skanning når ingen inkluderte stier er satt.\nrust_all_paths_referenced = Kan ikke starte skanning når alle inkluderte stier er satt som refererte stier, må du deaktivere avmerkingsboksen ved siden av inngangsstien.\nrust_found_empty_folders = Fant { $items_found } tomme mapper i { $time }\nrust_found_empty_files = Fant { $items_found } tomme filer i { $time }\nrust_found_similar_images = Fant { $items_found } lignende bildefiler i { $groups } grupper i { $time }\nrust_found_similar_videos = Fant { $items_found } lignende videofiler i { $groups } grupper i { $time }\nrust_found_similar_music_files = Fant { $items_found } lignende musikkfiler i { $groups } grupper i { $time }\nrust_found_invalid_symlinks = Fant { $items_found } ugyldige symlinker i { $time }\nrust_found_temporary_files = Fant { $items_found } midlertidige filer i { $time }\nrust_no_file_type_selected = Finner ikke ødelagte filer uten valgt filtype.\nrust_found_broken_files = Fant { $items_found } ødelagte filer som tar { $size } i { $time }\nrust_found_bad_extensions = Fant { $items_found } filer med feilaktige utvidelser i { $time }\nrust_found_bad_names = Fant { $items_found } filer med dårlige navn i { $time}\nrust_found_video_optimizer = Fant { $items_found } filer å optimalisere i { $time }\nrust_found_duplicate_files = Fant { $items_found } dupliserte filer i { $groups } grupper som tar { $size } i { $time }\nrust_found_duplicate_files_no_lost_space = Fant { $items_found } dupliserte filer i { $groups } grupper i { $time }\nrust_found_big_files = Fant { $items_found } store filer med størrelse { $size } i { $time }\nrust_found_exif_files = Fant { $items_found } filer med exif-data i { $time }\nrust_cannot_load_preset = Kan ikke endre eller laste inn forhåndsinnstilling { $preset_idx } - årsak { $reason }, bruker standardinnstillinger i stedet\nrust_saved_preset = Lagret forhåndsinnstilling { $preset_idx }\nrust_cannot_save_preset = Kan ikke lagre forhåndsinnstilling { $preset_idx } - årsak { $reason }\nrust_reset_preset = Nullstill predisetting { $preset_idx }\nrust_cannot_create_output_folder = Kan ikke opprette utdatamappen { $output_folder }, grunnen: { $error }\nrust_delete_summary = Slettet { $deleted } elementer, feilet i å fjerne { $failed } elementer, av totalt { $total } elementer\nrust_rename_summary = Endret navn på { $renamed } elementer, mislyktes å endre navn på { $failed } elementer, ikke av { $total } objekter\nrust_move_summary = Flyttet { $moved } elementer, mislyktes i å flytte { $failed } elementer, uten { $total } elementer\nrust_hardlink_summary = Hardlinkt { $hardlinked } elementer, mislyktes med å hardlinke { $failed } elementer, av { $total } elementer\nrust_symlink_summary = Symlink { $symlinked } elementer, mislyktes med å symlinke { $failed } elementer, av { $total } elementer\nrust_optimize_video_summary = Optimaliserte { $optimized } videoer, mislykkede optimaliseringer av { $failed } videoer, ut av { $total } videoer\nrust_clean_exif_summary = Renset EXIF fra { $cleaned } filer, mislyktes med å rense { $failed } filer, av { $total } filer\nrust_deleting_files = Sletter { $items_stats } filen ({ $size_stats })\nrust_deleting_no_size_files = Sletter { $items_stats } fil\nrust_renaming_files = Endrer { $items_stats } fil\nrust_moving_files = Flytter { $items_stats } fil ({ $size_stats })\nrust_moving_no_size_files = Flytter { $items_stats } fil\nrust_hardlinking_files = Hardlinking { $items_stats } filen ({ $size_stats })\nrust_hardlinking_no_size_files = Hardlinking { $items_stats } fil\nrust_symlinking_files = Symlinking { $items_stats } filen ({ $size_stats })\nrust_symlinking_no_size_files = Symlinking { $items_stats } fil\nrust_optimizing_videos = Optimalisert { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimalisert { $items_stats } video\nrust_cleaning_exif = Rens EXIF fra { $items_stats } filen ({ $size_stats })\nrust_cleaning_no_size_exif = Rens EXIF fra { $items_stats } fil\nrust_no_files_deleted = Ingen filer eller mapper valgt for sletting\nrust_no_files_renamed = Ingen filer eller mapper valgt for navneendring\nrust_no_files_moved = Ingen filer eller mapper valgt for flytting\nrust_no_files_hardlinked = Ingen filer eller mapper valgt for hardlenking\nrust_no_files_symlinked = Ingen filer eller mapper valgt for symlinking\nrust_no_videos_optimized = Ingen videoer valgt for optimalisering\nrust_no_exif_cleaned = Ingen filer valgt for EXIF rengjøring\nrust_extracted_exif_tags = Uthentet EXIF-tagger fra { $items_stats } filer ({ $size_stats })\nrust_delete_confirmation = Er du sikker på at du vil slette de valgte elementene?\nrust_delete_confirmation_number_simple = { $items } artikler valgt.\nrust_delete_confirmation_number_groups = { $items } elementer valgt i { $groups } grupper.\nrust_delete_confirmation_selected_all_in_group = Alle elementer er valgt i { $groups } grupper.\nrust_move_confirmation = Er du sikker på at du vil flytte de valgte elementene?\nrust_move_confirmation_number_simple = { $items } elementer valgt.\nrust_clean_exif_confirmation = Er du sikker på at du vil fjerne EXIF-data fra de valgte elementene?\nrust_clean_exif_confirmation_number_simple = { $items } elementer valgt.\nclean_exif_overwrite_files_text = OverSkriv filer\nrust_optimize_video_confirmation = Er du sikker på at du vil optimalisere de valgte videoene?\nrust_optimize_video_confirmation_number_simple = { $items } elementer valgt.\nrust_hardlink_confirmation = Er du sikker på at du vil opprette hardkoblinger for de valgte elementene?\nrust_hardlink_confirmation_number_simple = { $items } elementer valgt.\nrust_symlink_confirmation = Er du sikker på at du vil opprette symlinker for de valgte elementene?\nrust_symlink_confirmation_number_simple = { $items } elementer valgt.\nrust_rename_confirmation = Er du sikker på at du vil omdøpe de valgte elementene?\nrust_rename_confirmation_number_simple = { $items } elementer valgt.\nrust_cache_entries_stats = Fjernet { $removed } oppføringer ut av alle { $all }, { $left } igjen\nrust_cache_size_reduced = Redusert cachefilstørrelse med { $size }\nrust_cache_time_elapsed = Tid som er gått: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Feilet med å hardlenke { $name } til { $target }, årsak { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Utvalg\ncolumn_size = Størrelse\ncolumn_file_name = Fil Navn\ncolumn_path = Sti\ncolumn_modification_date = Endret dato\ncolumn_similarity = Likhet\ncolumn_dimensions = Dimensjoner\ncolumn_new_dimensions = Nye dimensjoner\ncolumn_title = Tittel\ncolumn_artist = Kunstner\ncolumn_year = År\ncolumn_bitrate = Bithastighet\ncolumn_length = Lengde\ncolumn_genre = Sjanger\ncolumn_type_of_error = Type feil\ncolumn_symlink_name = Symlink navn\ncolumn_symlink_folder = Mappe for Symlink\ncolumn_destination_path = Destinasjons sti\ncolumn_current_extension = Gjeldende utvidelse\ncolumn_proper_extension = Riktig utvidelse\ncolumn_fps = BPS\ncolumn_codec = Kodek\ncolumn_duration = Varighet\ncolumn_exif_tags = EXIF merker\ncolumn_new_name = Nytt Navn\n# Slint translations\nok_button = OK\ncancel_button = Avbryt\ndo_you_want_to_continue = Ønsker du å fortsette?\nmain_window_title = Krokiet - Datatoftrensing\nscan_button = Skann\nstop_button = Stopp\nstop_text = Stopp\nselect_button = Velg\nmove_button = Flytt\ndelete_button = Slett\nsave_button = Lagre\nsort_button = Sorter\nrename_button = Omdøp\nmotto = Dette programmet brukes fritt og vil alltid være.\\nSe MIT/GPL lisensen for detaljer.\nunicorn = Du kan ikke se på en enhjørning, men enhjørningen ser alltid på deg.\nrepository = Kodelager\ninstruction = Instruksjon\ndonation = Donasjon\ntranslation = Oversettelse\nincluded_paths = Inkluderte Stier\nexcluded_paths = Uekskluderte stier\nref = Ref:\npath = Sti\ntool_duplicate_files = Dupliser filer\ntool_empty_folders = Tomme mapper\ntool_big_files = Store filer\ntool_empty_files = Tomme filer\ntool_temporary_files = Midlertidige filer\ntool_similar_images = Lignende bilder\ntool_similar_videos = Lignende videoer\ntool_music_duplicates = Musikk dupliserer\ntool_invalid_symlinks = Ugyldige Symlinks\ntool_broken_files = Ødelagte filer\ntool_bad_extensions = Feil utvidelser\ntool_bad_names = Dårlige navn\ntool_video_optimizer = Video Optimaliser\ntool_exif_remover = Exif Fjerner\nsort_by_full_name = Sorter etter fullt navn\nsort_by_selection = Sorter etter utvalg\nsort_reverse = Motsatt rekkefølge\nselection_all = Velg alle\nselection_deselect_all = Fjern all merking\nselection_invert_selection = Inverter merking\nselection_the_biggest_size = Velg den største størrelsen\nselection_the_biggest_resolution = Velg den største oppløsningen\nselection_the_smallest_size = Velg den minste størrelsen\nselection_the_smallest_resolution = Velg den minste oppløsningen\nselection_newest = Velg nyeste\nselection_oldest = Velg eldste\nselection_shortest_path = Velg den korteste veien\nselection_longest_path = Velg den lengste veien\nstage_current = Gjeldende side:\nstage_all = Alle stadier:\nsubsettings = Underinnstillinger\nsubsettings_images_hash_size = Hash størrelse\nsubsettings_images_resize_algorithm = Endre algoritmen\nsubsettings_images_ignore_same_size = Ignorer bilder med samme størrelse\nsubsettings_images_max_difference = Maks. differanse\nsubsettings_images_duplicates_hash_type = Type Hash\nsubsettings_duplicates_check_method = Sjekkmetode\nsubsettings_duplicates_name_case_sensitive = Saksfølsomhet (bare navnemodus)\nsubsettings_biggest_files_sub_method = Metode\nsubsettings_biggest_files_sub_number_of_files = Antall filer\nsubsettings_videos_max_difference = Maks. differanse\nsubsettings_videos_ignore_same_size = Ignorer videoer med samme størrelse\nsubsettings_music_audio_check_type = Lyd avkryssingstype\nsubsettings_music_approximate_comparison = Omtrentlig sammenligning av tagg\nsubsettings_music_compared_tags = Sammenlignede tagger\nsubsettings_music_title = Tittel\nsubsettings_music_artist = Kunstner\nsubsettings_music_bitrate = Bithastighet\nsubsettings_music_genre = Sjanger\nsubsettings_music_year = År\nsubsettings_music_length = Lengde\nsubsettings_music_max_difference = Maks. differanse\nsubsettings_music_minimal_fragment_duration = Minimal fragmenterings varighet\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Sammenlign innenfor grupper av lignende titler\nsubsettings_broken_files_type = Type filer for kontroll\nsubsettings_broken_files_audio = Lyd\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Arkiv\nsubsettings_broken_files_image = Bilde\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Bruker ffmpeg/ffprobe. Veldig treg og kan oppdage pedantiske feil selv om filen spilles fint.\nsubsettings_bad_names_issues = Filnavisjoner\nsubsettings_bad_names_uppercase_extension = Store utvidelse\nsubsettings_bad_names_uppercase_extension_hint = Finner filer med store bokstaver i utvidelse (f.eks. .JPG, .Mp3) og foreslår nedrestilte versjon\nsubsettings_bad_names_emoji_used = Emoji i navn\nsubsettings_bad_names_emoji_used_hint = Finner filer med emoji-tegn (😀, 🎉, etc.) i navn og foreslår å fjerne dem\nsubsettings_bad_names_space_at_start_end = Ledende/følgende mellomrom\nsubsettings_bad_names_space_at_start_end_hint = Finner filer med mellomrom i begynnelsen eller slutten av navnet og foreslår å kutte dem\nsubsettings_bad_names_non_ascii = Ikke-ASCII tegn\nsubsettings_bad_names_non_ascii_hint = Finner ikke-ASCII-tegn (ą, ć, ñ, osv.) og foreslår å erstatte dem med ASCII-ekvivalenter (a, c, n) eller fjerne dem hvis ingen mapping finnes\nsubsettings_bad_names_restricted_charset = Begrenset tegnsett\nsubsettings_bad_names_restricted_charset_hint = Transkriberer ikke-ASCII tegn til ASCII, deretter finner filer som inneholder tegn utenfor 0-9a-zA-Z og brukerdefinerte tillatte tegn\nsubsettings_bad_names_allowed_chars = Tillatt tegn\nsubsettings_bad_names_remove_duplicated = Dupliserte tegn\nsubsettings_bad_names_remove_duplicated_hint = Finner sammenhengende dupliserte ikke-alfanumeriske tegn (f.eks. \"fil---navn..txt\") og foreslår å fjerne duplikater\nsettings_global_settings = Globale innstillinger\nsettings_dark_theme = Mørkt tema\nsettings_show_only_icons = Vis bare ikoner\nsettings_excluded_items = Ekskluderte elementer:\nsettings_allowed_extensions = Tillatte utvidelser:\nsettings_excluded_extensions = Ekskluderte utvidelser:\nsettings_file_size = Filstørrelse(Kilobyte)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Maks:\nsettings_recursive_search = Rekursivt søk\nsettings_use_cache = Bruk buffer\nsettings_save_as_json = Lagre også mellomlager som JSON-fil\nsettings_move_to_trash = Flytt slettede filer til papirkurv\nsettings_ignore_other_filesystems = Ignorer andre filsystemer (bare Linux)\nsettings_delete_outdated_cache_entries = Slett automatisk utdaterte cache-oppføringer\nsettings_delete_outdated_cache_entries_hint = Når det er aktivert, vil appen verifisere under cachelasting (maks én gang per uke) om de cachelagrede oppføringene fortsatt peker til eksisterende og uendrede filer/data\nsettings_hide_hard_links = Skjul hard lenker\nsettings_hide_hard_links_hint = Skjul harde lenker til samme filer i resultatene\nsettings_thread_number = Tråd nummer\nsettings_restart_required = ---Du må starte appen på nytt for å aktivere endringene i trådnummer---\nsettings_duplicate_image_preview = Forhåndsvisning av bilde\nsettings_duplicate_minimal_hash_cache_size = Minimal størrelse på bufrede filer - Hash (KB)\nsettings_duplicate_use_prehash = Bruk prehash\nsettings_duplicate_minimal_prehash_cache_size = Minimal størrelse på bufrede filer - Prehash (KB)\nsettings_similar_images_show_image_preview = Forhåndsvisning av bilde\nsettings_application_scale_text = Søknadsomfang\nsettings_application_scale_hint_text = Når manuell skala er aktivert, gir dette deg muligheten til å velge en tilpasset skala faktor, men deaktiverer helt automatisk skalering basert på skjermens DPI.\nsettings_restart_required_scale_text = ---Du må starte appen på nytt for å anvende endringer i skala---\nsettings_use_manual_application_scale_text = Bruk manuell applikasjonsskala\nsettings_video_thumbnails_preview = Bildeforhåndsvisning\nsettings_open_config_folder = Åpne konfigurasjonsmappen\nsettings_open_cache_folder = Åpne mappe for hurtigbuffer\nsettings_language = Språk\nsettings_current_preset = Gjeldende Forhåndsinnstilling:\nsettings_edit_name = Endre navn\nsettings_choose_name_for_prefix = Velg navn på prefiks\nsettings_save = Lagre\nsettings_load = Last\nsettings_reset = Nullstill\nsettings_similar_videos_tool = Lignende videoverktøy\nsettings_video_thumbnails_clear_unused_thumbnails = Slett ubrukte videobilder som er eldre enn 7 dager ved appstart\nsettings_video_thumbnails_header = Video Miniatyrbilder\nsettings_video_thumbnails_generate = Generer miniatyrbilder\nsettings_video_thumbnails_position = Miniatyrposisjon i video (%)\nsettings_video_thumbnails_generate_grid = Generer miniatyrrutenett i stedet for enkelt bilde\nsettings_video_thumbnails_generate_grid_hint = Generere flere bilder i rutenett er mye tregere enn å generere en enkelt miniatyrbilde\nsettings_video_thumbnails_grid_tiles_per_side = Antall fliser per side i miniatyrnettet\nsettings_video_thumbnails_grid_tiles_per_side_hint = Antall miniatyrfliser per side i rutenettet. For eksempel, ved å velge 2, opprettes et 2 x 2 rutenett, som resulterer i en enkelt miniatyr bilde bestående av 4 bilder.\nsettings_similar_images_tool = Lignende bildesverktøy\nsettings_general_settings = Generelle innstillinger\nsettings_cache_header_text = Cache Innstillinger\nsettings_clean_cache_button_text = Rydd utdatert cache\nsettings_settings = Innstillinger\nsettings_load_tabs_sizes_at_startup = Last inn fanestørrelse ved oppstart\nsettings_load_windows_size_at_startup = Last inn vindusstørrelse ved oppstart\nsettings_limit_lines_of_messages = Begrens meldinger til 500 linker (slisset for treg TextEdit widget)\nsettings_play_audio_on_scan_completion_text = Spill lyd når skanning fullføres vellykket\nsettings_audio_feature_hint_text = Kun tilgjengelig ved kompilering med lydfunksjon\nsettings_audio_env_variable_hint_text = Lyden kan endres, ved å sette KROKIET_AUDIO_STOP_FILE miljøvariabelen til en gyldig lydfilsti\npopup_save_title = Lagrer resultater\npopup_save_message = Dette vil lagre resultater til 3 forskjellige filer\npopup_rename_title = Endrer filer\npopup_new_paths_title = Vennligst legg til stier én per linje\npopup_move_title = Flytter filer\npopup_move_copy_checkbox = Kopier filer i stedet for å flytte\npopup_move_preserve_folder_checkbox = Behold mappestruktur\nmove_confirmation_text = Er du sikker på at du vil flytte de valgte elementene?\nrename_confirmation_text = Er du sikker på at du vil omdøpe de valgte elementene?\ndelete = Slett elementer\nstopping_scan = Stopper skanning, vennligst vent...\nsearching = Søker...\nsubsettings_videos_crop_detect = Beskjær oppdaget metode\nsubsettings_videos_skip_forward_amount = Hopp over varighet [s]\nsubsettings_videos_vid_hash_duration = Video hash varighet\nsettings_cache_number_size_text = Cachefilstørrelse: { $size }, antall filer: { $number }\nsettings_video_thumbnails_number_size_text = Miniatyrbilder størrelse: { $size }, antall filer: { $number }\nsettings_log_number_size_text = Loggfilstørrelse: { $size }, antall filer: { $number }\npopup_clean_cache_title_text = Rydd utdatert cache\npopup_clean_cache_confirmation_text = Er du sikker på at du vil slette utdaterte cache-innlegg? Dette vil fjerne cache-innlegg for filer som ikke lenger eksisterer eller er endret.\npopup_clean_cache_progress_text = Behandler cachefil:\npopup_clean_cache_current_file_text = Nåværende fil:\npopup_clean_cache_file_progress_text = Nåværende fil fremdrift:\npopup_clean_cache_overall_progress_text = Samlet fremgang:\npopup_clean_cache_stopped_by_user_text = Cache rengjøring ble stoppet av bruker\npopup_clean_cache_finished_text = Cache rengjøring fullført med suksess!\npopup_clean_cache_error_details_text = Feildetaljer:\npopup_clean_cache_files_with_errors = Filer med feil:\nsubsettings_video_optimizer_mode = Modus\nsubsettings_video_optimizer_crop_type = Bildekategori\nsubsettings_video_optimizer_black_pixel_threshold = Svart Piksell Terskel\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maksimal RGB-verdi for hver pikselkanal som skal anses som svart (0-128). Standard: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Svart Bari Min Prosentandel\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimum prosentandel av svarte piksler i en rad/kolonne som skal anses som en svart stripe (50-100). Standard: 90\nsubsettings_video_optimizer_max_samples = Maksimalt antall prøver\nsubsettings_video_optimizer_max_samples_hint = Maksimalt antall bilder å analysere per video (5-1000). Standard: 60\nsubsettings_video_optimizer_min_crop_size = Min Crop Størrelse\nsubsettings_video_optimizer_min_crop_size_hint = Minimum piksler å beskjære på noen side (1-1000). Mindre beskjæringer blir ignorert. Standard: 5\nsubsettings_video_optimizer_video_codec = Videokodek\nsubsettings_video_optimizer_excluded_codecs = Uekskluderte kodeker\nsubsettings_video_optimizer_video_quality = Video kvalitet (CRF)\nsubsettings_reset = Tilbakestill\nsubsettings_exif_ignored_tags_text = Ignorert tagger:\nsubsettings_exif_ignored_tags_hint_text = Kommaseparert liste over tagger å utelate fra skanning (f.eks. GPS, Thumbnail). Noen tagger, som ImageWidth i TIFF-filer, er skjult for å hindre at bildet blir ødelagt.\nclean_button_text = Rengjør\nclean_text = Rens EXIF-data\nclean_confirmation_text = Er du sikker på at du vil fjerne EXIF-data fra de valgte elementene?\ncrop_videos_text = Klipp videoer\ncrop_video_confirmation_text = Er du sikker på at du vil beskjære de valgte videoene?\ncrop_reencode_video_text = Re-kodér video\nreencode_videos_text = Re-kodere videoer\noptimize_button_text = Optimaliser\noptimize_confirmation_text = Er du sikker på at du vil rekomprimere de valgte videoene?\noptimize_fail_if_bigger_text = Feil hvis optimalisert fil er større\noptimize_overwrite_files_text = OverSkriv filer\noptimize_limit_video_size_text = Begrens videostørrelse\noptimize_max_width_text = Maks bredde:\noptimize_max_height_text = Maks høyde:\nhardlink_button_text = Hardlenke\nhardlink_text = Opprett hardlinks\nhardlink_confirmation_text = Er du sikker på at du vil opprette hardkoblinger for de valgte elementene?\nsoftlink_button_text = Programlenke\nsoftlink_text = Opprett mylkoblinger\nsoftlink_confirmation_text = Er du sikker på at du vil opprette softlenker (symlinks) for de valgte elementene?\n\nrust_cache_processed_files = Behandlet { $files } cache-filer"
  },
  {
    "path": "krokiet/i18n/pl/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Krytyczny błąd podczas uruchamiania aplikacji\nrust_init_error_message =\n        Wystąpił krytyczny błąd podczas uruchamiania aplikacji:\n\n        { $error_message }\n\n        Może to być spowodowane brakiem lub uszkodzeniem sterowników OpenGL/Vulkan, uruchamianiem aplikacji w maszynie wirtualnej lub błędem w Krokiecie albo jednej z używanych bibliotek.\n\n        Możesz spróbować uruchomić różne wersje renderera (skia_opengl, skia_vulkan, femtovg_opengl — domyślny) lub użyć renderera programowego, aby sprawdzić, czy to rozwiąże problem.\nrust_loaded_preset = Wczytano preset { $preset_idx }\nrust_file_already_exists = Plik \"{ $file }\" już istnieje i nie zostanie nadpisany\nrust_error_removing_file_after_copy = Błąd podczas usuwania pliku \"{ $file }\" (po skopiowaniu na inną partycję), powód: { $reason }\nrust_error_copying_file = Błąd podczas kopiowania \"{ $input }\" do \"{ $output }\", powód: { $reason }\nrust_loading_tags_cache = Ładowanie pamięci podręcznej tagów\nrust_loading_fingerprints_cache = Ładowanie pamięci podręcznej odcisków palców\nrust_saving_tags_cache = Zapisywanie pamięci podręcznej tagów\nrust_saving_fingerprints_cache = Zapisywanie pamięci podręcznej odcisków palców\nrust_loading_prehash_cache = Ładowanie pamięci podręcznej wstępnych hashy\nrust_saving_prehash_cache = Zapisywanie pamięci podręcznej wstępnych hashy\nrust_loading_hash_cache = Ładowanie pamięci podręcznej hashy\nrust_saving_hash_cache = Zapisywanie pamięci podręcznej hashy\nrust_loading_exif_cache = Ładowanie pamięci podręcznej EXIF\nrust_saving_exif_cache = Zapisywanie pamięci podręcznej EXIF\nrust_scanning_name = Skanowanie nazw { $entries_checked } plików\nrust_scanning_size_name = Skanowanie rozmiaru i nazw { $entries_checked } plików\nrust_scanning_size = Skanowanie rozmiaru { $entries_checked } plików\nrust_scanning_file = Skanowanie { $entries_checked } plików\nrust_scanning_folder = Skanowanie { $entries_checked } folderów\nrust_checked_tags = Sprawdzono tagi: { $items_stats }\nrust_checked_content = Sprawdzono zawartość: { $items_stats } ({ $size_stats })\nrust_compared_tags = Porównano tagi: { $items_stats }\nrust_compared_content = Porównano zawartość: { $items_stats }\nrust_hashed_images = Obliczono hashe obrazów dla { $items_stats } pozycji ({ $size_stats })\nrust_compared_image_hashes = Porównano hashe obrazów: { $items_stats }\nrust_hashed_videos = Obliczono hashe wideo dla { $items_stats } pozycji\nrust_created_thumbnails = Utworzono miniatury dla wideo: { $items_stats }\nrust_checked_files = Sprawdzono { $items_stats } plików ({ $size_stats })\nrust_checked_files_bad_extensions = Sprawdzono { $items_stats } plików (z nieprawidłowymi rozszerzeniami)\nrust_checked_files_bad_names = Sprawdzono { $items_stats } plików (z nieprawidłowymi nazwami)\nrust_checked_videos = Sprawdzono { $items_stats } wideo ({ $size_stats })\nrust_analyzed_partial_hash = Przeanalizowano częściowy hash dla { $items_stats } plików ({ $size_stats })\nrust_analyzed_full_hash = Przeanalizowano pełny hash dla { $items_stats } plików ({ $size_stats })\nrust_failed_to_rename_file = Nie udało się zmienić nazwy pliku \"{ $old_path }\" na \"{ $new_path }\", błąd: { $error }\nrust_no_included_paths = Nie można uruchomić skanu, ponieważ nie ustawiono żadnych ścieżek.\nrust_all_paths_referenced = Nie można uruchomić skanu, gdy wszystkie ścieżki są oznaczone jako ścieżki referencyjne (używane tylko do odczytu). Wyłącz oznaczenie przynajmniej jednej takiej ścieżki.\nrust_found_empty_folders = Znaleziono { $items_found } pustych folderów w { $time }\nrust_found_empty_files = Znaleziono { $items_found } pustych plików w { $time }\nrust_found_similar_images = Znaleziono { $items_found } podobnych obrazów w { $groups } grupach w { $time }\nrust_found_similar_videos = Znaleziono { $items_found } podobnych plików wideo w { $groups } grupach w { $time }\nrust_found_similar_music_files = Znaleziono { $items_found } podobnych plików muzycznych w { $groups } grupach w { $time }\nrust_found_invalid_symlinks = Znaleziono { $items_found } niepoprawnych dowiązań symbolicznych w { $time }\nrust_found_temporary_files = Znaleziono { $items_found } plików tymczasowych w { $time }\nrust_no_file_type_selected = Nie można przeprowadzić wyszukiwania uszkodzonych plików bez wybranego typu pliku.\nrust_found_broken_files = Znaleziono { $items_found } uszkodzonych plików o łącznym rozmiarze { $size } w { $time }\nrust_found_bad_extensions = Znaleziono { $items_found } plików z nieprawidłowymi rozszerzeniami w { $time }\nrust_found_bad_names = Znaleziono { $items_found } plików o błędnych nazwach w { $time }\nrust_found_video_optimizer = Znaleziono { $items_found } plików do zoptymalizowania w { $time }\nrust_found_duplicate_files = Znaleziono { $items_found } duplikatów w { $groups } grupach w { $time }, zajmujących { $size }\nrust_found_duplicate_files_no_lost_space = Znaleziono duplikaty: { $items_found } w { $groups } grupach w { $time }\nrust_found_big_files = Znaleziono { $items_found } dużych plików o rozmiarze { $size } w { $time }\nrust_found_exif_files = Znaleziono { $items_found } plików z danymi EXIF w { $time }\nrust_cannot_load_preset = Nie można wczytać presetu { $preset_idx } — powód: { $reason }. Zastosowano ustawienia domyślne\nrust_saved_preset = Zapisano preset { $preset_idx }\nrust_cannot_save_preset = Nie można zapisać presetu { $preset_idx } — powód: { $reason }\nrust_reset_preset = Zresetowano preset { $preset_idx }\nrust_cannot_create_output_folder = Nie można utworzyć folderu wyjściowego \"{ $output_folder }\", powód: { $error }\nrust_delete_summary = Usunięto { $deleted } elementów, nie udało się usunąć { $failed } elementów spośród { $total }\nrust_rename_summary = Zmieniono nazwę { $renamed } elementów, nie udało się zmienić nazwy { $failed } elementów spośród { $total }\nrust_move_summary = Przeniesiono { $moved } elementów, nie udało się przenieść { $failed } spośród { $total }\nrust_hardlink_summary = Utworzono twarde dowiązania dla { $hardlinked } elementów, nie udało się utworzyć dowiązań dla { $failed } spośród { $total } plików\nrust_symlink_summary = Utworzono dowiązania symboliczne dla { $symlinked } elementów, nie udało się utworzyć dowiązań dla { $failed } spośród { $total } plików\nrust_optimize_video_summary = Zoptymalizowano { $optimized } wideo, nie udało się zoptymalizować { $failed } spośród { $total } plików\nrust_clean_exif_summary = Usunięto dane EXIF z { $cleaned } plików, nie udało się usunąć danych EXIF z { $failed } spośród { $total } plików\nrust_deleting_files = Usuwanie plików: { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Usuwanie plików: { $items_stats }\nrust_renaming_files = Zmiana nazw plików: { $items_stats }\nrust_moving_files = Przenoszenie plików: { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = Przenoszenie plików: { $items_stats }\nrust_hardlinking_files = Tworzenie twardych dowiązań dla { $items_stats } plików ({ $size_stats })\nrust_hardlinking_no_size_files = Tworzenie twardych dowiązań dla { $items_stats } plików\nrust_symlinking_files = Tworzenie dowiązań symbolicznych dla { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Tworzenie dowiązań symbolicznych dla { $items_stats } plików\nrust_optimizing_videos = Optymalizacja wideo: { $items_stats } ({ $size_stats })\nrust_optimizing_no_size_videos = Optymalizacja wideo: { $items_stats }\nrust_cleaning_exif = Czyszczenie EXIF: { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = Czyszczenie EXIF: { $items_stats }\nrust_no_files_deleted = Nie wybrano plików ani folderów do usunięcia\nrust_no_files_renamed = Nie wybrano plików ani folderów do zmiany nazw\nrust_no_files_moved = Nie wybrano plików ani folderów do przeniesienia\nrust_no_files_hardlinked = Nie wybrano plików ani folderów do tworzenia twardych dowiązań\nrust_no_files_symlinked = Nie wybrano plików ani folderów do tworzenia dowiązań symbolicznych\nrust_no_videos_optimized = Nie wybrano filmów do optymalizacji\nrust_no_exif_cleaned = Nie wybrano plików do usunięcia danych EXIF\nrust_extracted_exif_tags = Wyodrębnianie tagów EXIF z plików: { $items_stats } ({ $size_stats })\nrust_delete_confirmation = Czy na pewno chcesz usunąć wybrane elementy?\nrust_delete_confirmation_number_simple = Wybrano { $items } elementów.\nrust_delete_confirmation_number_groups = Wybrano { $items } elementów w { $groups } grupach.\nrust_delete_confirmation_selected_all_in_group = Wszystkie elementy wybrane we { $groups } grupach.\nrust_move_confirmation = Czy na pewno chcesz przenieść wybrane elementy?\nrust_move_confirmation_number_simple = Wybrano { $items } elementów.\nrust_clean_exif_confirmation = Czy na pewno chcesz usunąć dane EXIF z wybranych elementów?\nrust_clean_exif_confirmation_number_simple = Wybrano { $items } elementów.\nclean_exif_overwrite_files_text = Nadpisz pliki\nrust_optimize_video_confirmation = Czy na pewno chcesz zoptymalizować wybrane filmy?\nrust_optimize_video_confirmation_number_simple = Wybrano { $items } elementów.\nrust_hardlink_confirmation = Czy na pewno chcesz utworzyć twarde linki dla wybranych elementów?\nrust_hardlink_confirmation_number_simple = Wybrano { $items } elementów.\nrust_symlink_confirmation = Czy na pewno chcesz utworzyć dowiązania symboliczne dla wybranych elementów?\nrust_symlink_confirmation_number_simple = Wybrano { $items } elementów.\nrust_rename_confirmation = Czy na pewno chcesz zmienić nazwę wybranych elementów?\nrust_rename_confirmation_number_simple = Wybrano { $items } elementów.\nrust_cache_entries_stats = Usunięto { $removed } wpisów z { $all }, { $left } pozostało\nrust_cache_size_reduced = Zmniejszono rozmiar pamięci podręcznej o { $size }\nrust_cache_time_elapsed = Upłynęło: { $time }\nrust_symlink_failed = Nie udało się utworzyć dowiązania symbolicznego \"{ $name }\" do \"{ $target }\", powód: { $reason }\nrust_hardlink_failed = Nie udało się utworzyć twardego dowiązania \"{ $name }\" do \"{ $target }\", powód: { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Zaznaczenie\ncolumn_size = Rozmiar\ncolumn_file_name = Nazwa pliku\ncolumn_path = Ścieżka\ncolumn_modification_date = Data modyfikacji\ncolumn_similarity = Podobieństwo\ncolumn_dimensions = Wymiary\ncolumn_new_dimensions = Nowe wymiary\ncolumn_title = Tytuł\ncolumn_artist = Artysta\ncolumn_year = Rok\ncolumn_bitrate = Przepływność\ncolumn_length = Długość\ncolumn_genre = Gatunek\ncolumn_type_of_error = Typ błędu\ncolumn_symlink_name = Nazwa dowiązania symbolicznego\ncolumn_symlink_folder = Folder dowiązania symbolicznego\ncolumn_destination_path = Ścieżka docelowa\ncolumn_current_extension = Obecne rozszerzenie\ncolumn_proper_extension = Właściwe rozszerzenie\ncolumn_fps = Klatki na sekundę\ncolumn_codec = Kodek\ncolumn_duration = Czas trwania\ncolumn_exif_tags = Tagi EXIF\ncolumn_new_name = Nowa nazwa\n# Slint translations\nok_button = OK\ncancel_button = Anuluj\ndo_you_want_to_continue = Czy chcesz kontynuować?\nmain_window_title = Krokiet — Czyścioch Danych\nscan_button = Skanuj\nstop_button = Zatrzymaj\nstop_text = Stop\nselect_button = Wybierz\nmove_button = Przenieś\ndelete_button = Usuń\nsave_button = Zapisz\nsort_button = Sortuj\nrename_button = Zmień nazwę\nmotto = Ten program jest darmowy i zawsze pozostanie wolny.\\nZobacz licencję MIT/GPL po szczegóły.\nunicorn = Może nie widzisz jednorożca, ale jednorożec zawsze patrzy na ciebie.\nrepository = Repozytorium\ninstruction = Instrukcja\ndonation = Darowizna\ntranslation = Tłumaczenie\nincluded_paths = Wybrane ścieżki\nexcluded_paths = Wykluczone ścieżki\nref = Ref\npath = Ścieżka\ntool_duplicate_files = Zduplikowane pliki\ntool_empty_folders = Puste foldery\ntool_big_files = Duże pliki\ntool_empty_files = Puste pliki\ntool_temporary_files = Pliki tymczasowe\ntool_similar_images = Podobne obrazy\ntool_similar_videos = Podobne wideo\ntool_music_duplicates = Zduplikowana muzyka\ntool_invalid_symlinks = Błędne dowiązania\ntool_broken_files = Uszkodzone pliki\ntool_bad_extensions = Nieprawidłowe rozszerzenia\ntool_bad_names = Błędne nazwy\ntool_video_optimizer = Optymalizator wideo\ntool_exif_remover = Usuwanie EXIF\nsort_by_full_name = Sortuj według pełnej nazwy\nsort_by_selection = Sortuj według zaznaczenia\nsort_reverse = Odwróć kolejność\nselection_all = Zaznacz wszystko\nselection_deselect_all = Odznacz wszystko\nselection_invert_selection = Odwróć zaznaczenie\nselection_the_biggest_size = Wybierz największe pliki\nselection_the_biggest_resolution = Wybierz największą rozdzielczość\nselection_the_smallest_size = Wybierz najmniejsze pliki\nselection_the_smallest_resolution = Wybierz najmniejszą rozdzielczość\nselection_newest = Wybierz najnowsze\nselection_oldest = Wybierz najstarsze\nselection_shortest_path = Wybierz najkrótszą ścieżkę\nselection_longest_path = Wybierz najdłuższą ścieżkę\nstage_current = Bieżący etap:\nstage_all = Wszystkie etapy:\nsubsettings = Ustawienia szczegółowe\nsubsettings_images_hash_size = Rozmiar hasha\nsubsettings_images_resize_algorithm = Algorytm zmiany rozmiaru\nsubsettings_images_ignore_same_size = Ignoruj obrazy o tych samych wymiarach\nsubsettings_images_max_difference = Maksymalna różnica\nsubsettings_images_duplicates_hash_type = Typ hasha\nsubsettings_duplicates_check_method = Metoda sprawdzania\nsubsettings_duplicates_name_case_sensitive = Uwzględniaj wielkość liter (tylko tryby nazw)\nsubsettings_biggest_files_sub_method = Metoda\nsubsettings_biggest_files_sub_number_of_files = Liczba plików\nsubsettings_videos_max_difference = Maksymalna różnica\nsubsettings_videos_ignore_same_size = Ignoruj filmy o tych samych wymiarach\nsubsettings_music_audio_check_type = Typ sprawdzania audio\nsubsettings_music_approximate_comparison = Przybliżone porównywanie tagów\nsubsettings_music_compared_tags = Porównywane tagi\nsubsettings_music_title = Tytuł\nsubsettings_music_artist = Artysta\nsubsettings_music_bitrate = Przepływność\nsubsettings_music_genre = Gatunek\nsubsettings_music_year = Rok\nsubsettings_music_length = Długość\nsubsettings_music_max_difference = Maksymalna różnica\nsubsettings_music_minimal_fragment_duration = Minimalny czas trwania fragmentu\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Porównuj w grupach o podobnych tytułach\nsubsettings_broken_files_type = Typ plików do sprawdzenia\nsubsettings_broken_files_audio = Dźwięk\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Archiwum\nsubsettings_broken_files_image = Obraz\nsubsettings_broken_files_video = Wideo\nsubsettings_broken_files_video_info = Wykorzystuje ffmpeg/ffprobe. Jest to dość wolne i może wykrywać pedantyczne błędy, nawet jeśli plik odtwarza się poprawnie.\nsubsettings_bad_names_issues = Sprawdzanie nazw plików\nsubsettings_bad_names_uppercase_extension = Wersja rozszerzenia z wielkimi literami\nsubsettings_bad_names_uppercase_extension_hint = Znajduje pliki z wielkimi literami w rozszerzeniu (np. .JPG, .Mp3) i sugeruje wersję z małymi literami\nsubsettings_bad_names_emoji_used = Emoji w nazwie\nsubsettings_bad_names_emoji_used_hint = Znajduje pliki zawierające emoji (😀, 🎉 itd.) w nazwie i sugeruje ich usunięcie\nsubsettings_bad_names_space_at_start_end = Spacje na początku/końcu\nsubsettings_bad_names_space_at_start_end_hint = Znajduje pliki z odstępami na początku lub końcu nazwy i sugeruje ich usunięcie\nsubsettings_bad_names_non_ascii = Znaki spoza ASCII\nsubsettings_bad_names_non_ascii_hint = Znajduje znaki niebędące ASCII (ą, ć, ñ itd.) i sugeruje zastąpienie ich odpowiednikami ASCII (a, c, n) lub usunięcie, jeśli nie istnieje mapa transliteracji\nsubsettings_bad_names_restricted_charset = Ograniczony zestaw znaków\nsubsettings_bad_names_restricted_charset_hint = Transliteruje znaki spoza ASCII na ASCII, a następnie znajduje pliki zawierające znaki niedozwolone (po transliteracji) i proponuje ich zmianę\nsubsettings_bad_names_allowed_chars = Dozwolone znaki\nsubsettings_bad_names_remove_duplicated = Powtarzające się znaki\nsubsettings_bad_names_remove_duplicated_hint = Znajduje kolejne powtarzające się niealfanumeryczne znaki (np. \"file---name..txt\") i sugeruje usunięcie duplikatów\nsettings_global_settings = Ustawienia globalne\nsettings_dark_theme = Ciemny motyw\nsettings_show_only_icons = Pokaż tylko ikony\nsettings_excluded_items = Wykluczone elementy\nsettings_allowed_extensions = Dozwolone rozszerzenia:\nsettings_excluded_extensions = Wykluczone rozszerzenia:\nsettings_file_size = Rozmiar pliku (KB)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Maks.:\nsettings_recursive_search = Wyszukiwanie rekurencyjne\nsettings_use_cache = Użyj pamięci podręcznej\nsettings_save_as_json = Zapisz pamięć podręczną także jako plik JSON\nsettings_move_to_trash = Przenieś usunięte pliki do kosza\nsettings_ignore_other_filesystems = Ignoruj inne systemy plików (tylko Linux)\nsettings_delete_outdated_cache_entries = Automatycznie usuń nieaktualne wpisy pamięci podręcznej\nsettings_delete_outdated_cache_entries_hint = Po włączeniu aplikacja podczas ładowania pamięci podręcznej (maksymalnie raz na tydzień) sprawdzi, czy wpisy nadal wskazują na istniejące i niezmienione pliki\nsettings_hide_hard_links = Ukryj twarde dowiązania\nsettings_hide_hard_links_hint = Ukryj twarde linki do tych samych plików w wynikach\nsettings_thread_number = Liczba wątków\nsettings_restart_required = ---Musisz ponownie uruchomić aplikację, aby zastosować zmiany w liczbie wątków---\nsettings_duplicate_image_preview = Podgląd obrazu\nsettings_duplicate_minimal_hash_cache_size = Minimalny rozmiar pliku do użycia w pamięci podręcznej hasha (KB)\nsettings_duplicate_use_prehash = Użyj wstępnego hasha\nsettings_duplicate_minimal_prehash_cache_size = Minimalny rozmiar pliku do użycia we wstępnym hashu (KB)\nsettings_similar_images_show_image_preview = Podgląd obrazu\nsettings_application_scale_text = Skala aplikacji\nsettings_application_scale_hint_text = Gdy włączysz ręczną skalę, możesz wybrać niestandardowy współczynnik, ale zostanie wyłączone automatyczne skalowanie oparte na DPI monitora.\nsettings_restart_required_scale_text = ---Należy ponownie uruchomić aplikację, aby zastosować zmiany w skali---\nsettings_use_manual_application_scale_text = Użyj ręcznego skalowania aplikacji\nsettings_video_thumbnails_preview = Podgląd miniatur wideo\nsettings_open_config_folder = Otwórz folder konfiguracji\nsettings_open_cache_folder = Otwórz folder pamięci podręcznej\nsettings_language = Język\nsettings_current_preset = Bieżący preset:\nsettings_edit_name = Edytuj nazwę\nsettings_choose_name_for_prefix = Wybierz nazwę prefiksu\nsettings_save = Zapisz\nsettings_load = Wczytaj\nsettings_reset = Resetuj\nsettings_similar_videos_tool = Narzędzie do podobnych wideo\nsettings_similar_videos_preview_hint = Podgląd będzie widoczny tylko, gdy opcja „Generuj miniatury” jest włączona, lub jeśli miniatura została już wygenerowana.\nsettings_video_thumbnails_clear_unused_thumbnails = Usuń nieużywane miniatury wideo starsze niż 7 dni przy uruchamianiu aplikacji\nsettings_video_thumbnails_header = Miniatury wideo\nsettings_video_thumbnails_generate = Generuj miniatury\nsettings_video_thumbnails_position = Pozycja miniatury wideo (%)\nsettings_video_thumbnails_generate_grid = Generuj siatkę miniatur zamiast pojedynczej miniatury\nsettings_video_thumbnails_generate_grid_hint = Generowanie siatki jest znacznie wolniejsze niż generowanie pojedynczej miniatury\nsettings_video_thumbnails_grid_tiles_per_side = Liczba płytek na bok siatki miniatur\nsettings_video_thumbnails_grid_tiles_per_side_hint = Liczba miniatur w rzędzie siatki. Na przykład wybranie 2 tworzy siatkę 2x2, co daje 4 obrazy w pojedynczej miniaturze.\nsettings_similar_images_tool = Narzędzie do podobnych obrazów\nsettings_general_settings = Ustawienia ogólne\nsettings_cache_header_text = Ustawienia pamięci podręcznej\nsettings_clean_cache_button_text = Wyczyść przestarzałą pamięć podręczną\nsettings_settings = Ustawienia\nsettings_load_tabs_sizes_at_startup = Załaduj rozmiary kart przy starcie\nsettings_load_windows_size_at_startup = Załaduj rozmiar okien przy starcie\nsettings_limit_lines_of_messages = Ogranicz wiadomości do 500 linii (dotyczy wolnego widżetu TextEdit)\nsettings_play_audio_on_scan_completion_text = Odtwórz dźwięk po zakończeniu skanu\nsettings_audio_feature_hint_text = Dostępne tylko, gdy aplikacja skompilowana została z funkcją audio\nsettings_audio_env_variable_hint_text = Dźwięk można zmienić ustawiając zmienną środowiskową KROKIET_AUDIO_STOP_FILE na ścieżkę do pliku audio\npopup_save_title = Zapisywanie wyników\npopup_save_message = To zapisze wyniki do 3 różnych plików\npopup_rename_title = Zmienianie nazw plików\npopup_new_paths_title = Proszę dodawać ścieżki po jednej na linię\npopup_move_title = Przenoszenie plików\npopup_move_copy_checkbox = Kopiuj pliki zamiast przenosić\npopup_move_preserve_folder_checkbox = Zachowaj strukturę folderów\nmove_confirmation_text = Czy na pewno chcesz przenieść wybrane elementy?\nrename_confirmation_text = Czy na pewno chcesz zmienić nazwę wybranych elementów?\ndelete = Usuń elementy\nstopping_scan = Zatrzymywanie skanowania, proszę czekać...\nsearching = Wyszukiwanie...\nsubsettings_videos_crop_detect = Metoda wykrywania przycinania\nsubsettings_videos_skip_forward_amount = Pomiń czas [s]\nsubsettings_videos_vid_hash_duration = Czas trwania hashu wideo\nsettings_cache_number_size_text = Rozmiar pamięci podręcznej: { $size }, liczba plików: { $number }\nsettings_video_thumbnails_number_size_text = Rozmiar miniatur wideo: { $size }, liczba plików: { $number }\nsettings_log_number_size_text = Rozmiar plików dziennika: { $size }, liczba plików: { $number }\npopup_clean_cache_title_text = Wyczyść przestarzałą pamięć podręczną\npopup_clean_cache_confirmation_text = Czy na pewno chcesz wyczyścić przestarzałe wpisy pamięci podręcznej? To usunie wpisy dla plików, które nie istnieją lub zostały zmodyfikowane.\npopup_clean_cache_progress_text = Przetwarzanie pliku pamięci podręcznej:\npopup_clean_cache_current_file_text = Obecny plik:\npopup_clean_cache_file_progress_text = Postęp pliku:\npopup_clean_cache_overall_progress_text = Postęp całościowy:\npopup_clean_cache_stopped_by_user_text = Czyszczenie pamięci podręcznej zostało zatrzymane przez użytkownika\npopup_clean_cache_finished_text = Czyszczenie pamięci podręcznej zakończone pomyślnie!\npopup_clean_cache_error_details_text = Szczegóły błędu:\npopup_clean_cache_files_with_errors = Pliki z błędami:\nsubsettings_video_optimizer_mode = Tryb\nsubsettings_video_optimizer_crop_type = Typ przycinania\nsubsettings_video_optimizer_black_pixel_threshold = Próg dla czarnego piksela\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maksymalna wartość RGB dla każdego kanału, aby piksel był traktowany jako czarny (0-128). Domyślnie: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Minimalny procent czarnego paska\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimalny procent czarnych pikseli w wierszu/kolumnie, aby uznać ją za czarną linię (50-100). Domyślnie: 90\nsubsettings_video_optimizer_max_samples = Maksymalna liczba próbek\nsubsettings_video_optimizer_max_samples_hint = Maksymalna liczba klatek do analizy dla każdego wideo (5-1000). Domyślnie: 60\nsubsettings_video_optimizer_min_crop_size = Minimalny rozmiar przycięcia\nsubsettings_video_optimizer_min_crop_size_hint = Minimalna liczba pikseli do przycięcia po dowolnej stronie (1-1000). Mniejsze przycięcia są ignorowane. Domyślnie: 5\nsubsettings_video_optimizer_video_codec = Kodek wideo\nsubsettings_video_optimizer_excluded_codecs = Wykluczone kodeki\nsubsettings_video_optimizer_video_quality = Jakość wideo (CRF)\nsubsettings_reset = Resetuj\nsubsettings_exif_ignored_tags_text = Zignorowane tagi:\nsubsettings_exif_ignored_tags_hint_text = Lista tagów do wykluczenia ze skanowania, oddzielona przecinkami (np. GPS, Thumbnail). Niektóre tagi, jak ImageWidth w plikach TIFF, są ukrywane, by zapobiec uszkodzeniu obrazu.\nclean_button_text = Wyczyść\nclean_text = Usuń dane EXIF\nclean_confirmation_text = Czy na pewno chcesz usunąć dane EXIF z wybranych elementów?\ncrop_videos_text = Przycinanie wideo\ncrop_video_confirmation_text = Czy na pewno chcesz przyciąć wybrane filmy?\ncrop_reencode_video_text = Ponowne kodowanie wideo po przycięciu\nreencode_videos_text = Ponowne kodowanie wideo\noptimize_button_text = Optymalizuj\noptimize_confirmation_text = Czy na pewno chcesz ponownie zakodować wybrane wideo?\noptimize_fail_if_bigger_text = Pomiń, jeśli zoptymalizowany plik jest większy\noptimize_overwrite_files_text = Nadpisz pliki\noptimize_limit_video_size_text = Ogranicz rozmiar wideo\noptimize_max_width_text = Maksymalna szerokość:\noptimize_max_height_text = Maksymalna wysokość:\nhardlink_button_text = Dowiązanie twarde\nhardlink_text = Utwórz dowiązanie twarde\nhardlink_confirmation_text = Czy na pewno chcesz utworzyć twarde dowiązania dla wybranych elementów?\nsoftlink_button_text = Dowiązanie symboliczne\nsoftlink_text = Utwórz dowiązania symboliczne\nsoftlink_confirmation_text = Czy na pewno chcesz utworzyć dowiązania symboliczne dla wybranych elementów?\n\nrust_cache_processed_files = Przetworzono { $files } plików pamięci podręcznej"
  },
  {
    "path": "krokiet/i18n/pt-BR/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Ocorreu um erro crítico durante a inicialização do programa\nrust_init_error_message =\n    Ocorreu um erro crítico ao iniciar o programa:\n    \n    ‘{ $error_message }’\n    \n    O erro pode ter sido causado pela ausência ou pelo mau funcionamento dos controladores do OpenGL/Vulkan, pela execução do programa em uma máquina virtual ou por um problema no Krokiet ou em uma das suas bibliotecas.\nrust_loaded_preset = A predefinição ‘{ $preset_idx }’ foi carregada\nrust_file_already_exists = O arquivo ‘{ $file }’ já existe e não será sobrescrito\nrust_error_removing_file_after_copy = Ocorreu um erro ao remover o arquivo ‘{ $file }’ (depois de copiá-lo para uma outra partição), por causa de ‘{ $reason }’\nrust_error_copying_file = Ocorreu um erro ao copiar o ‘{ $input }’ para o ‘{ $output }’, por causa de ‘{ $reason }’\nrust_loading_tags_cache = Carregando as informações do ‘cache’\nrust_loading_fingerprints_cache = Carregando as impressões digitais do ‘cache’\nrust_saving_tags_cache = Salvando as informações no ‘cache’\nrust_saving_fingerprints_cache = Salvando as impressões digitais no ‘cache’\nrust_loading_prehash_cache = Carregando o ‘hash’ parcial do ‘cache’\nrust_saving_prehash_cache = Salvando o ‘hash’ parcial no ‘cache’\nrust_loading_hash_cache = Carregando o ‘hash’ do ‘cache’\nrust_saving_hash_cache = Salvando o ‘hash’ no ‘cache’\nrust_loading_exif_cache = Carregando o ‘EXIF’ do ‘cache’\nrust_saving_exif_cache = Salvando o ‘EXIF’ no ‘cache’\nrust_scanning_name = Pesquisando por nome do arquivo nos ‘{ $entries_checked }’\nrust_scanning_size_name = Pesquisando por nome e por tamanho do arquivo nos ‘{ $entries_checked }’\nrust_scanning_size = Pesquisando por tamanho do arquivo nos ‘{ $entries_checked }’\nrust_scanning_file = Pesquisando o arquivo ‘{ $entries_checked }’\nrust_scanning_folder = Pesquisando a pasta ‘{ $entries_checked }’\nrust_checked_tags = Verificando as informações de ‘{ $items_stats }’\nrust_checked_content = Verificando o conteúdo ‘{ $items_stats }’ de ‘{ $size_stats }’\nrust_compared_tags = Comparando as informações de ‘{ $items_stats }’\nrust_compared_content = Comparando o conteúdo de ‘{ $items_stats }’\nrust_hashed_images = As informações de ‘{ $items_stats }’ arquivos ‘hash’ das imagens ‘{ $size_stats }’\nrust_compared_image_hashes = Comparando ‘{ $items_stats }’ código ‘hash’ dos arquivos de imagem\nrust_hashed_videos = Foram criados ‘{ $items_stats }’ do código ‘hash’ dos arquivos de vídeo\nrust_created_thumbnails = Foram criados ‘{ $items_stats }’ miniaturas dos arquivos de vídeo\nrust_checked_files = Verificando ‘{ $items_stats }’ informações dos ‘{ $size_stats }’ arquivos\nrust_checked_files_bad_extensions = Verificando as informações do arquivo ‘{ $items_stats }’\nrust_checked_files_bad_names = Verificando as informações do arquivo ‘{ $items_stats }’\nrust_checked_videos = Verificando ‘{ $items_stats }’ informações dos ‘{ $size_stats }’ arquivos de vídeo\nrust_analyzed_partial_hash = Analisando ‘{ $items_stats }’ código ‘hash’ parcial dos ‘{ $size_stats }’ arquivos\nrust_analyzed_full_hash = Analisando ‘{ $items_stats }’ código ‘hash’ completo dos ‘{ $size_stats }’ arquivos\nrust_failed_to_rename_file = Ocorreu uma falha ao renomear o arquivo ‘{ $old_path }’ para ‘{ $new_path }’, por causa do erro ‘{ $error }’\nrust_no_included_paths = Não é possível iniciar a verificação até que algum diretório seja incluído.\nrust_all_paths_referenced = Não é possível iniciar a verificação quando todos os diretórios incluídos estão definidos como pastas referenciadas.\nrust_found_empty_folders = Foram encontradas ‘{ $items_found }’ pastas vazias. A verificação durou ‘{ $time }’\nrust_found_empty_files = Foram encontrados ‘{ $items_found }’ arquivos vazios. A verificação durou ‘{ $time }’\nrust_found_similar_images = Foram encontrados ‘{ $items_found }’ arquivos de imagem equivalentes nos ‘{ $groups }’ grupos. A verificação durou ‘{ $time }’\nrust_found_similar_videos = Foram encontrados ‘{ $items_found }’ arquivos de vídeo equivalentes nos ‘{ $groups }’ grupos. A verificação durou ‘{ $time }’\nrust_found_similar_music_files = Foram encontrados ‘{ $items_found }’ arquivos de música equivalentes nos ‘{ $groups }’ grupos. A verificação durou ‘{ $time }’\nrust_found_invalid_symlinks = Foram encontradas ‘{ $items_found }’ ligações simbólicas que não são válidas. A verificação durou ‘{ $time }’\nrust_found_temporary_files = Foram encontrados ‘{ $items_found }’ arquivos temporários. A verificação durou ‘{ $time }’\nrust_no_file_type_selected = Não é possível encontrar os arquivos danificados sem que o tipo do arquivo seja selecionado.\nrust_found_broken_files = Foram encontrados ‘{ $items_found }’ arquivos danificados que ocupam ‘{ $size }’ de espaço. A verificação durou ‘{ $time }’\nrust_found_bad_extensions = Foram encontrados ‘{ $items_found }’ arquivos com extensões problemáticas. A verificação durou ‘{ $time }’\nrust_found_bad_names = Foram encontrados ‘{ $items_found }’ arquivos com nomes problemáticas. A verificação durou ‘{ $time }’\nrust_found_video_optimizer = Foram encontrados ‘{ $items_found }’ arquivos para serem otimizados. A verificação durou ‘{ $time }’\nrust_found_duplicate_files = Foram encontrados ‘{ $items_found }’ arquivos duplicados nos ‘{ $groups }’ grupos que ocupam ‘{ $size }’. A verificação durou ‘{ $time }’\nrust_found_duplicate_files_no_lost_space = Foram encontrados ‘{ $items_found }’ arquivos duplicados nos ‘{ $groups }’ grupos. A verificação durou ‘{ $time }’\nrust_found_big_files = Foram encontrados ‘{ $items_found }’ arquivos grandes com tamanho de ‘{ $size }’. A verificação durou ‘{ $time }’\nrust_found_exif_files = Foram encontrados ‘{ $items_found }’ arquivos com dados do tipo ‘EXIF’. A verificação durou ‘{ $time }’\nrust_cannot_load_preset = Não foi possível carregar e alterar a predefinição ‘{ $preset_idx }’ por causa de ‘{ $reason }’, por tanto, será utilizado as configurações padrão\nrust_saved_preset = A predefinição ‘{ $preset_idx }’ foi salva com sucesso\nrust_cannot_save_preset = Não foi possível salvar a predefinição ‘{ $preset_idx }’, por causa de ‘{ $reason }’\nrust_reset_preset = Redefinir a predefinição ‘{ $preset_idx }’\nrust_cannot_create_output_folder = Não foi possível criar a pasta de saída ‘{ $output_folder }’, por causa do erro ‘{ $error }’\nrust_delete_summary = Foram excluídos ‘{ $deleted }’ itens, ocorreu uma falha ao remover ‘{ $failed }’ itens de ‘{ $total }’ itens\nrust_rename_summary = Foram renomeados ‘{ $renamed }’ itens, ocorreu uma falha ao renomear ‘{ $failed }’ itens de ‘{ $total }’ itens\nrust_move_summary = Foram movidos ‘{ $moved }’ itens, ocorreu uma falha ao mover ‘{ $failed }’ itens  para fora de ‘{ $total }’ itens\nrust_hardlink_summary = Foram criadas ‘{ $hardlinked }’ ligações rígidas, ocorreu uma falha ao ligar ou vincular ‘{ $failed }’ itens de ‘{ $total }’ itens\nrust_symlink_summary = Foram criadas ‘{ $symlinked }’ ligações simbólicas, ocorreu uma falha ao ligar ou vincular ‘{ $failed }’ itens de ‘{ $total }’ itens\nrust_optimize_video_summary = Foram otimizados ‘{ $optimized }’ vídeos, ocorreu uma falha ao otimizar ‘{ $failed }’ vídeos de ‘{ $total }’ vídeos\nrust_clean_exif_summary = Foram removidos ‘{ $cleaned }’ arquivos com o ‘EXIF’, ocorreu uma falha ao remover ‘{ $failed }’ arquivos de ‘{ $total }’ arquivos\nrust_deleting_files = Excluindo ‘{ $items_stats }’ arquivo(s) de ‘{ $size_stats }’\nrust_deleting_no_size_files = Excluindo o arquivo ‘{ $items_stats }’\nrust_renaming_files = Renomeando o arquivo ‘{ $items_stats }’\nrust_moving_files = Movendo ‘{ $items_stats }’ arquivos de ‘{ $size_stats }’\nrust_moving_no_size_files = Movendo o arquivo ‘{ $items_stats }’\nrust_hardlinking_files = Criando ‘{ $items_stats }’ ligações rígidas de ‘{ $size_stats }’ arquivos\nrust_hardlinking_no_size_files = Criando ‘{ $items_stats }’ ligações rígidas dos arquivos\nrust_symlinking_files = Criando ‘{ $items_stats }’ ligações simbólicas de ‘{ $size_stats }’ arquivos\nrust_symlinking_no_size_files = Criando ‘{ $items_stats }’ ligações simbólicas dos arquivos\nrust_optimizing_videos = Foram otimizados ‘{ $items_stats }’ vídeos de ‘{ $size_stats }’ arquivos\nrust_optimizing_no_size_videos = Foram otimizados ‘{ $items_stats }’ vídeos\nrust_cleaning_exif = Removendo o ‘EXIF’ dos ‘{ $items_stats }’ arquivos de ‘{ $size_stats }’ arquivos\nrust_cleaning_no_size_exif = Removendo o ‘EXIF’ do arquivo ‘{ $items_stats }’\nrust_no_files_deleted = Nenhum arquivo ou pasta foi selecionado para ser excluído\nrust_no_files_renamed = Nenhum arquivo ou pasta foi selecionado para ser renomeado\nrust_no_files_moved = Nenhum arquivo ou pasta foi selecionado para ser movido\nrust_no_files_hardlinked = Nenhum arquivo ou pasta foi selecionado para a criação da ligação rígida\nrust_no_files_symlinked = Nenhum arquivo ou pasta foi selecionado para a criação da ligação simbólica\nrust_no_videos_optimized = Nenhum arquivo de vídeo foi selecionado para ser otimizado\nrust_no_exif_cleaned = Nenhum arquivo foi selecionado para ser removido os ‘EXIF’\nrust_extracted_exif_tags = Os ‘EXIF’ foram extraídos de ‘{ $items_stats }’ arquivos de ‘{ $size_stats }’ arquivos\nrust_delete_confirmation = Você tem certeza de que quer excluir os itens que foram selecionados?\nrust_delete_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nrust_delete_confirmation_number_groups = Foram selecionados ‘{ $items }’ itens nos ‘{ $groups }’ grupos.\nrust_delete_confirmation_selected_all_in_group = Todos os itens foram selecionados nos ‘{ $groups }’ grupos.\nrust_move_confirmation = Você tem certeza de que quer mover os itens que foram selecionados?\nrust_move_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nrust_clean_exif_confirmation = Você tem certeza de que quer remover os dados ‘EXIF’ dos itens que foram selecionados?\nrust_clean_exif_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nclean_exif_overwrite_files_text = Sobrescrever os arquivos\nrust_optimize_video_confirmation = Você tem certeza de que quer otimizar os arquivos de vídeos que foram selecionados?\nrust_optimize_video_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nrust_hardlink_confirmation = Você tem certeza de que quer criar as ligações rígidas para os itens que foram selecionados?\nrust_hardlink_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nrust_symlink_confirmation = Você tem certeza de que quer criar as ligações simbólicas para os itens que foram selecionados?\nrust_symlink_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nrust_rename_confirmation = Você tem certeza de que quer renomear os itens que foram selecionados?\nrust_rename_confirmation_number_simple = Foram selecionados ‘{ $items }’ itens.\nrust_cache_processed_files = Processados { $files } arquivos em cache\nrust_cache_entries_stats = Foram removidas ‘{ $removed }’ entradas de um total de ‘{ $all }’ e ‘{ $left }’ restantes\nrust_cache_size_reduced = O tamanho dos arquivos do ‘cache’ foi reduzido para ‘{ $size }’\nrust_cache_time_elapsed = O tempo decorrido foi de ‘{ $time }’\nrust_symlink_failed = Ocorreu uma falha ao criar a ligação simbólica ‘{ $name }’ para ‘{ $target }’, por causa de ‘{ $reason }’\nrust_hardlink_failed = Ocorreu uma falha ao criar a ligação rígida ‘{ $name }’ para ‘{ $target }’, por causa de ‘{ $reason }’\n\n# Slint translations, but in arrays\n\ncolumn_selection = Seleção\ncolumn_size = Tamanho\ncolumn_file_name = Nome do arquivo\ncolumn_path = Caminho\ncolumn_modification_date = Data da modificação\ncolumn_similarity = Equivalentes\ncolumn_dimensions = Dimensões\ncolumn_new_dimensions = Novas Dimensões\ncolumn_title = Título\ncolumn_artist = Artista\ncolumn_year = Ano\ncolumn_bitrate = Taxa de bits\ncolumn_length = Comprimento\ncolumn_genre = Gênero\ncolumn_type_of_error = Tipo do erro\ncolumn_symlink_name = Nome da ligação simbólica\ncolumn_symlink_folder = Pasta da ligação simbólica\ncolumn_destination_path = Caminho do destino\ncolumn_current_extension = Extensão atual\ncolumn_proper_extension = Extensão corrompida\ncolumn_fps = FPS\ncolumn_codec = Códigocx\ncolumn_duration = Duração\ncolumn_exif_tags = Identificadores EXIF\ncolumn_new_name = Novo Nome\n# Slint translations\nok_button = Tudo bem\ncancel_button = Cancelar\ndo_you_want_to_continue = Você quer continuar?\nmain_window_title = Limpador de Dados do Krokiet\nscan_button = Verificar\nstop_button = Interromper\nstop_text = Interromper\nselect_button = Selecionar\nmove_button = Mover\ndelete_button = Excluir\nsave_button = Salvar\nsort_button = Ordenar\nrename_button = Renomear\nmotto = Este programa é e sempre será de código aberto e de uso gratuito.\\nPor favor, consulte a licença do MIT/GPL para obter mais informações.\nunicorn = Você pode até não olhar para o unicórnio, mas o unicórnio sempre está olhando para você.\nrepository = Repositório\ninstruction = Instruções\ndonation = Faça uma doação\ntranslation = Tradução\nincluded_paths = Diretórios incluídos\nexcluded_paths = Diretórios excluídos\nref = Referência\npath = Caminho\ntool_duplicate_files = Arquivos duplicados\ntool_empty_folders = Pastas vazias\ntool_big_files = Arquivos grandes\ntool_empty_files = Arquivos vazios\ntool_temporary_files = Arquivos temporários\ntool_similar_images = Imagens equivalentes\ntool_similar_videos = Vídeos equivalentes\ntool_music_duplicates = Músicas duplicadas\ntool_invalid_symlinks = Ligações simbólicas inválidas\ntool_broken_files = Arquivos corrompidos\ntool_bad_extensions = Extensões que não são válidas\ntool_bad_names = Nomes inválidos\ntool_video_optimizer = Otimizador de vídeo\ntool_exif_remover = Removedor de EXIF\nsort_by_full_name = Ordenar por nome completo\nsort_by_selection = Ordenar por seleção\nsort_reverse = Ordenar inversamente\nselection_all = Selecionar todos\nselection_deselect_all = Desselecionar todos\nselection_invert_selection = Inverter a seleção\nselection_the_biggest_size = Selecionar o maior tamanho\nselection_the_biggest_resolution = Selecionar a maior resolução\nselection_the_smallest_size = Selecionar o menor tamanho\nselection_the_smallest_resolution = Selecionar a menor resolução\nselection_newest = Selecionar o mais recente\nselection_oldest = Selecionar o mais antigo\nselection_shortest_path = Selecionar o caminho mais curto\nselection_longest_path = Selecionar o caminho mais longo\nstage_current = Etapa atual:\nstage_all = Todas as etapas:\nsubsettings = Subconfigurações\nsubsettings_images_hash_size = Tamanho do ‘hash’\nsubsettings_images_resize_algorithm = Redimensionar o algoritmo\nsubsettings_images_ignore_same_size = Ignorar as imagens com o mesmo tamanho\nsubsettings_images_max_difference = Diferença máxima\nsubsettings_images_duplicates_hash_type = Tipo do ‘hash’\nsubsettings_duplicates_check_method = Método de verificação\nsubsettings_duplicates_name_case_sensitive = Diferenciar as letras maiúsculas das minúsculas (somente para o modo por nome)\nsubsettings_biggest_files_sub_method = Método\nsubsettings_biggest_files_sub_number_of_files = Quantidade de arquivos\nsubsettings_videos_max_difference = Diferença máxima\nsubsettings_videos_ignore_same_size = Ignorar os vídeos com o mesmo tamanho\nsubsettings_music_audio_check_type = Tipo de verificação por áudio\nsubsettings_music_approximate_comparison = Comparação aproximada das informações dos arquivos\nsubsettings_music_compared_tags = As informações dos arquivos foram comparadas\nsubsettings_music_title = Título\nsubsettings_music_artist = Artista\nsubsettings_music_bitrate = Taxa de bits\nsubsettings_music_genre = Gênero\nsubsettings_music_year = Ano\nsubsettings_music_length = Comprimento\nsubsettings_music_max_difference = Diferença máxima\nsubsettings_music_minimal_fragment_duration = Duração mínima do fragmento\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Comparar nos grupos por títulos equivalentes\nsubsettings_broken_files_type = Tipo dos arquivos a serem verificados\nsubsettings_broken_files_audio = Áudio\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Arquivo\nsubsettings_broken_files_image = Imagem\nsubsettings_broken_files_video = Vídeo\nsubsettings_broken_files_video_info = Utilizar o ‘ffmpeg’ ou o ‘ffprobe’. Esta opção é bastante lenta e pode detectar alguns erros insignificantes mesmo que o arquivo seja reproduzido corretamente.\nsubsettings_bad_names_issues = Verificações por nome do arquivo\nsubsettings_bad_names_uppercase_extension = Extensão de maiúsculas\nsubsettings_bad_names_uppercase_extension_hint = Localizar os arquivos que contenham as letras maiúsculas na suas extensão, por exemplo, .JPG, .MP3, .MP4, etc. e sugerir a alteração por letras minúsculas\nsubsettings_bad_names_emoji_used = Nome com caracteres de emoji\nsubsettings_bad_names_emoji_used_hint = Localizar os arquivos que contenham em seu nome os caracteres de emoji (😀, 🎉, etc.) e sugerir a sua remoção\nsubsettings_bad_names_space_at_start_end = Espaços no início ou no fim\nsubsettings_bad_names_space_at_start_end_hint = Localizar os arquivos que contenham espaços no início ou no fim do seu nome e sugerir a sua remoção\nsubsettings_bad_names_non_ascii = Caracteres não ASCII\nsubsettings_bad_names_non_ascii_hint = Localizar os caracteres que não são do tipo ASCII (ą, ć, ñ, etc.) e sugerir a substituição por caracteres equivalentes do tipo ASCII (a, c, n) ou removê-los caso não exista um caractere correspondente\nsubsettings_bad_names_restricted_charset = Conjunto de caracteres limitado\nsubsettings_bad_names_restricted_charset_hint = Transliterar os caracteres que não são do tipo ASCII para ASCII e, em seguida, localizar os arquivos que contenham os caracteres que estão fora do intervalo de ‘0’ a ‘9’ ou de ‘a’ a ‘z’ ou de ‘A’ a ‘Z’ e permitir os caracteres definidos pelo usuário\nsubsettings_bad_names_allowed_chars = Caracteres permitidos\nsubsettings_bad_names_remove_duplicated = Caracteres duplicados\nsubsettings_bad_names_remove_duplicated_hint = Localizar os caracteres duplicados que não são alfanuméricos que sejam consecutivos (por exemplo, \"nome---arquivo..txt\") e sugerir a remoção dos caracteres que estão duplicados\nsettings_global_settings = Configurações globais\nsettings_dark_theme = Tema escuro\nsettings_show_only_icons = Exibir somente os ícones\nsettings_excluded_items = Item que foi excluído:\nsettings_allowed_extensions = Extensões permitidas:\nsettings_excluded_extensions = Extensões excluídas:\nsettings_file_size = Tamanho do arquivo (em quilobytes)\nsettings_minimum_file_size = Mínimo:\nsettings_maximum_file_size = Máximo:\nsettings_recursive_search = Pesquisa recursiva\nsettings_use_cache = Utilizar o ‘cache’\nsettings_save_as_json = Salvar o ‘cache’ como um arquivo JSON\nsettings_move_to_trash = Excluir permanentemente os arquivos que foram movidos para a lixeira\nsettings_ignore_other_filesystems = Ignorar os outros sistemas de arquivos (somente para o GNU/Linux)\nsettings_delete_outdated_cache_entries = Excluir automaticamente as entradas que estão desatualizadas no ‘cache’\nsettings_delete_outdated_cache_entries_hint = Quando esta opção está ativada, o programa verificará durante o carregamento do ‘cache’ (no máximo uma vez por semana) se os registros que estão no ‘cache’ ainda apontam para arquivos/dados que ainda existem e não foram modificados\nsettings_hide_hard_links = Ocultar as ligações rígidas\nsettings_hide_hard_links_hint = Ocultar as ligações rígidas para os mesmos arquivos nos resultados\nsettings_thread_number = Número do tópico\nsettings_restart_required = --- É necessário reiniciar o programa para aplicar as alterações no número do tópico ---\nsettings_duplicate_image_preview = Pré-visualizar a imagem\nsettings_duplicate_minimal_hash_cache_size = Tamanho mínimo (KB) do código ‘hash’ dos arquivos no ‘cache’\nsettings_duplicate_use_prehash = Utilizar o ‘hash’ parcial\nsettings_duplicate_minimal_prehash_cache_size = Tamanho mínimo (KB) do código ‘hash’ parcial dos arquivos no ‘cache’\nsettings_similar_images_show_image_preview = Pré-visualizar a imagem\nsettings_application_scale_text = Tamanho do programa\nsettings_application_scale_hint_text = Quando a opção do ‘Tamanho do programa’ está ativado, permite que você escolha um fator da escala personalizado, mas desativa completamente a escala automática com base no DPI (‘Dots Per Inch’ que significa ‘pontos por polegada’) do monitor.\nsettings_restart_required_scale_text = --- É necessário reiniciar o programa para aplicar as alterações do tamanho ---\nsettings_use_manual_application_scale_text = Utilizar o tamanho manual do programa\nsettings_video_thumbnails_preview = Pré-visualizar a imagem\nsettings_open_config_folder = Abrir a pasta das configurações\nsettings_open_cache_folder = Abrir a pasta do ‘cache’\nsettings_language = Idioma\nsettings_current_preset = Predefinição atual:\nsettings_edit_name = Editar o nome\nsettings_choose_name_for_prefix = Escolha o nome para o prefixo\nsettings_save = Salvar\nsettings_load = Carregar\nsettings_reset = Redefinir\nsettings_similar_videos_tool = Ferramenta dos vídeos equivalentes\nsettings_video_thumbnails_clear_unused_thumbnails = Excluir as miniaturas dos vídeos que não são utilizadas por mais de sete dias ao iniciar o programa\nsettings_video_thumbnails_header = Miniaturas dos vídeos\nsettings_video_thumbnails_generate = Gerar as miniaturas\nsettings_video_thumbnails_position = Posição da miniatura no vídeo (%)\nsettings_video_thumbnails_generate_grid = Gerar uma grade de miniaturas em vez de uma única imagem\nsettings_video_thumbnails_generate_grid_hint = A ação de gerar várias imagens em uma grade é muito mais lenta do que gerar uma única miniatura\nsettings_video_thumbnails_grid_tiles_per_side = Número de azulejos por lado na grade de miniatura\nsettings_video_thumbnails_grid_tiles_per_side_hint = Número de miniaturas por lado na grade. Por exemplo, selecionar 2 cria uma grade 2 x 2, resultando em uma única miniatura composta por 4 imagens.\nsettings_similar_images_tool = Ferramenta das imagens equivalentes\nsettings_general_settings = Configurações gerais\nsettings_cache_header_text = Configurações do ‘cache’\nsettings_clean_cache_button_text = Remover os dados do ‘cache’ que estão desatualizados\nsettings_settings = Configurações\nsettings_load_tabs_sizes_at_startup = Carregar por tamanho as abas ou guias na inicialização\nsettings_load_windows_size_at_startup = Carregar por tamanho as janelas na inicialização\nsettings_limit_lines_of_messages = Limitar o tamanho das mensagens em 500 linhas é uma solução alternativa para a lentidão do Editor de Texto\nsettings_play_audio_on_scan_completion_text = Reproduzir um efeito sonoro quando uma pequisa for concluída com sucesso\nsettings_audio_feature_hint_text = Está opção está disponível somente ao compilar com a funcionalidade de áudio\nsettings_audio_env_variable_hint_text = O efeito sonoro pode ser alterado definindo a variável de ambiente ‘KROKIET_AUDIO_STOP_FILE’ para o caminho válido de um arquivo de áudio\npopup_save_title = Salvando os resultados\npopup_save_message = Esta ação irá salvar os resultados em três arquivos diferentes\npopup_rename_title = Renomeando os arquivos\npopup_new_paths_title = Por favor, adicione os diretórios um em cada linha\npopup_move_title = Movendo os arquivos\npopup_move_copy_checkbox = Copiar os arquivos em vez de movê-los\npopup_move_preserve_folder_checkbox = Preservar a estrutura das pastas\nmove_confirmation_text = Você tem certeza de que quer mover os itens que foram selecionados?\nrename_confirmation_text = Você tem certeza de que quer renomear os itens que foram selecionados?\ndelete = Remover os itens\nstopping_scan = Por favor, aguarde a interrupção da pesquisa.\nsearching = Pesquisando...\nsubsettings_videos_crop_detect = Método de detecção do corte\nsubsettings_videos_skip_forward_amount = Pular a duração [s]\nsubsettings_videos_vid_hash_duration = Duração do ‘hash’ de vídeo\nsettings_cache_number_size_text = Tamanho dos arquivos do ‘cache’: ‘{ $size }’, quantidade de arquivos: ‘{ $number }’\nsettings_video_thumbnails_number_size_text = Tamanho das miniaturas dos vídeo: ‘{ $size }’, quantidade de arquivos: ‘{ $number }’\nsettings_log_number_size_text = Tamanho dos arquivos do registro das alterações: ‘{ $size }’, quantidade de arquivos: ‘{ $number }’\npopup_clean_cache_title_text = Remover os dados do ‘cache’ que estão desatualizados\npopup_clean_cache_confirmation_text = Você tem certeza de que quer limpar as entradas dos dados do ‘cache’ que estão desatualizadas? Esta ação removerá as entradas do ‘cache’ dos arquivos que não existem mais ou que foram modificados.\npopup_clean_cache_progress_text = Processando o arquivo do ‘cache’:\npopup_clean_cache_current_file_text = Arquivo atual:\npopup_clean_cache_file_progress_text = Progresso do arquivo atual:\npopup_clean_cache_overall_progress_text = Progresso geral:\npopup_clean_cache_stopped_by_user_text = A limpeza do ‘cache’ foi interrompida pelo usuário\npopup_clean_cache_finished_text = O ‘cache’ foi limpo com sucesso\npopup_clean_cache_error_details_text = Informações do erro:\npopup_clean_cache_files_with_errors = Arquivos com erros:\nsubsettings_video_optimizer_mode = Modo\nsubsettings_video_optimizer_crop_type = Tipo do corte\nsubsettings_video_optimizer_black_pixel_threshold = Limiar do pixel em cor preta\nsubsettings_video_optimizer_black_pixel_threshold_hint = O valor máximo do RGB (vermelho, verde e azul) para cada canal de pixel a ser considerado como cor preta (de 0 a 128). O padrão é 20\nsubsettings_video_optimizer_black_bar_min_percentage = Porcentagem mínima da barra preta\nsubsettings_video_optimizer_black_bar_min_percentage_hint = A porcentagem mínima de pixels em cor preta em uma linha/coluna para ser considerada uma barra em cor preta (de 50 a 100%). O padrão é 90\nsubsettings_video_optimizer_max_samples = Amostras máximas\nsubsettings_video_optimizer_max_samples_hint = Quantidade máxima de quadros a serem analisados por vídeo (de 5 a 1000). O padrão é 60\nsubsettings_video_optimizer_min_crop_size = Tamanho mínimo do corte\nsubsettings_video_optimizer_min_crop_size_hint = Quantidade mínima de pixels para recortar em qualquer lado (de 1 a 1000). Os recortes menores são ignorados. O padrão é 5\nsubsettings_video_optimizer_video_codec = Codec do vídeo\nsubsettings_video_optimizer_excluded_codecs = Codecs excluídos\nsubsettings_video_optimizer_video_quality = Qualidade do vídeo (CRF)\nsubsettings_reset = Redefinir\nsubsettings_exif_ignored_tags_text = Informações ignoradas:\nsubsettings_exif_ignored_tags_hint_text = Lista das informações dos arquivos que foram separados por vírgulas para excluir da verificação (por exemplo, GPS, Thumbnail ou Miniatura). Algumas informações dos arquivos, como ‘ImageWidth’ (largura da imagem) nos arquivos TIFF, são ocultadas para evitar que o arquivo da imagem seja corrompido.\nclean_button_text = Remover\nclean_text = Remover os dados EXIF\nclean_confirmation_text = Você tem certeza de que quer remover os dados ‘EXIF’ dos itens que foram selecionados?\ncrop_videos_text = Recortar o vídeo\ncrop_video_confirmation_text = Você tem certeza de que quer recortar os vídeos que foram selecionados?\ncrop_reencode_video_text = Recodificar o vídeo\nreencode_videos_text = Recodificar os vídeos\noptimize_button_text = Otimizar\noptimize_confirmation_text = Você tem certeza de que quer recodificar os vídeos que foram selecionados?\noptimize_fail_if_bigger_text = Ignorar se o arquivo otimizado for maior\noptimize_overwrite_files_text = Sobrescrever os arquivos\noptimize_limit_video_size_text = Limitar o tamanho do vídeo\noptimize_max_width_text = Largura máxima:\noptimize_max_height_text = Altura máxima:\nhardlink_button_text = Ligação simbólica rígida\nhardlink_text = Criar as ligações simbólicas rígidas\nhardlink_confirmation_text = Você tem certeza de que quer criar a ligação rígida (hardlinks) para os itens que foram selecionados?\nsoftlink_button_text = Ligação simbólica\nsoftlink_text = Criar a ligação simbólica\nsoftlink_confirmation_text = Você tem certeza de que quer criar a ligação simbólica (softlinks ou symlinks) para os itens que foram selecionados?\n"
  },
  {
    "path": "krokiet/i18n/pt-PT/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Erro Crítico Durante a Inicialização do Aplicativo\nrust_init_error_message = \n        Ocorreu um erro crítico ao iniciar a aplicação:\n\n        { $error_message }\n\n        Isto pode ser causado por drivers OpenGL/Vulkan ausentes ou com defeito, executando a aplicação em uma máquina virtual ou por um bug no Krokiet ou em uma de suas bibliotecas.\n\n        Você pode tentar executar diferentes builds (skia_opengl, skia_vulkan, femtovg_opengl - o padrão) ou com o renderizador de software para ver se isso resolve o problema.\nrust_loaded_preset = Predefinição carregada { $preset_idx }\nrust_file_already_exists = Arquivo \"{ $file }\" já existe e não será sobrescrito\nrust_error_removing_file_after_copy = Erro ao remover o arquivo \"{ $file }\" (após copiar para outra partição), motivo: { $reason }\nrust_error_copying_file = Erro ao copiar \"{ $input }\" para \"{ $output }\", motivo: { $reason }\nrust_loading_tags_cache = Carregando cache de tags\nrust_loading_fingerprints_cache = Carregando cache de impressões digitais\nrust_saving_tags_cache = Salvando cache de tags\nrust_saving_fingerprints_cache = Salvando cache de impressões digitais\nrust_loading_prehash_cache = Carregando cache de pré-hash\nrust_saving_prehash_cache = Salvando cache pré-hash\nrust_loading_hash_cache = Carregando cache de hash\nrust_saving_hash_cache = Salvando cache de hash\nrust_loading_exif_cache = Carregando cache EXIF\nrust_saving_exif_cache = Salvar cache EXIF\nrust_scanning_name = Verificando nome do arquivo { $entries_checked }\nrust_scanning_size_name = Verificando tamanho e nome do arquivo { $entries_checked }\nrust_scanning_size = Verificando o tamanho do arquivo { $entries_checked }\nrust_scanning_file = Escaneando arquivo { $entries_checked }\nrust_scanning_folder = Verificando pasta { $entries_checked }\nrust_checked_tags = Tags verificadas de { $items_stats }\nrust_checked_content = Conteúdo verificado de { $items_stats } ({ $size_stats })\nrust_compared_tags = Marcadores comparados de { $items_stats }\nrust_compared_content = Conteúdo comparado de { $items_stats }\nrust_hashed_images = Hash de { $items_stats } imagens ({ $size_stats })\nrust_compared_image_hashes = Comparados a imagens de hashes de { $items_stats }\nrust_hashed_videos = Vídeos de { $items_stats } Hashou\nrust_created_thumbnails = Miniaturas criadas para vídeos de { $items_stats }\nrust_checked_files = Verificado { $items_stats } arquivo ({ $size_stats })\nrust_checked_files_bad_extensions = Verificado o arquivo { $items_stats }\nrust_checked_files_bad_names = Verificado { $items_stats } arquivo\nrust_checked_videos = Verificados { $items_stats } vídeos ({ $size_stats })\nrust_analyzed_partial_hash = Hash parcial analisado de arquivos { $items_stats } ({ $size_stats })\nrust_analyzed_full_hash = Hash completo de { $items_stats } arquivos ({ $size_stats } ) analisado\nrust_failed_to_rename_file = Falha ao renomear o arquivo { $old_path } para { $new_path }, erro: { $error }\nrust_no_included_paths = Não é possível iniciar a varredura quando nenhum caminho incluído estiver definido.\nrust_all_paths_referenced = Não é possível iniciar a varredura quando todas as caminhos incluídos estão definidos como caminhos referenciados, você precisa desmarcar a caixa de seleção ao lado do caminho de entrada.\nrust_found_empty_folders = Encontradas pastas { $items_found } vazias em { $time }\nrust_found_empty_files = Encontrados { $items_found } arquivos vazios em { $time }\nrust_found_similar_images = Encontrados { $items_found } arquivos de imagem similares em { $groups } grupos em { $time }\nrust_found_similar_videos = Encontrados { $items_found } arquivos de vídeo similares em { $groups } grupos em { $time }\nrust_found_similar_music_files = Encontrados { $items_found } arquivos de música similares em { $groups } grupos em { $time }\nrust_found_invalid_symlinks = Encontrado { $items_found } links simbólicos inválidos em { $time }\nrust_found_temporary_files = { $items_found } arquivos temporários encontrados em { $time }\nrust_no_file_type_selected = Não é possível encontrar arquivos quebrados sem o tipo de arquivo selecionado.\nrust_found_broken_files = Encontrados { $items_found } arquivos quebrados que levam { $size } em { $time }\nrust_found_bad_extensions = Encontrados { $items_found } arquivos com extensões ruins em { $time }\nrust_found_bad_names = Encontrados { $items_found } arquivos com nomes ruins em { $time}\nrust_found_video_optimizer = Encontrados { $items_found } arquivos para otimizar em { $time }\nrust_found_duplicate_files = Encontrados { $items_found } arquivos duplicados em { $groups } grupos que levam { $size } em { $time }\nrust_found_duplicate_files_no_lost_space = Encontrados { $items_found } arquivos duplicados em { $groups } grupos em { $time }\nrust_found_big_files = Encontrados { $items_found } arquivos grandes com tamanho { $size } em { $time }\nrust_found_exif_files = Encontrados { $items_found } arquivos com exif data em { $time }\nrust_cannot_load_preset = Não é possível alterar e carregar a predefinição { $preset_idx } - motivo { $reason }, usando as configurações padrão em vez disso\nrust_saved_preset = Predefinição salva { $preset_idx }\nrust_cannot_save_preset = Não é possível salvar predefinição { $preset_idx } - razão { $reason }\nrust_reset_preset = Resetar pré-configuração { $preset_idx }\nrust_cannot_create_output_folder = Não é possível criar a pasta de saída { $output_folder }, motivo: { $error }\nrust_delete_summary = { $deleted } itens excluídos, falhou em remover os itens de { $failed } , de { $total } itens\nrust_rename_summary = Renomeado { $renamed } itens, falhou em renomear itens de { $failed } , de { $total } itens\nrust_move_summary = Movido itens de { $moved } , falhou em mover itens de { $failed } para fora de { $total } itens\nrust_hardlink_summary = Itens com link rígido { $hardlinked }, falharam em criar link rígido { $failed }, de um total de { $total } itens\nrust_symlink_summary = Simlink { $symlinked } itens, falhou em simlink { $failed } itens, de { $total } itens\nrust_optimize_video_summary = Vídeos otimizados { $optimized }, vídeos que não otimizaram { $failed }, fora de { $total } vídeos\nrust_clean_exif_summary = Limpo EXIF de { $cleaned } arquivos, falhou em limpar { $failed } arquivos, de { $total } arquivos\nrust_deleting_files = Excluindo { $items_stats } arquivo ({ $size_stats })\nrust_deleting_no_size_files = Excluindo arquivo { $items_stats }\nrust_renaming_files = Renomeando arquivo { $items_stats }\nrust_moving_files = Movendo { $items_stats } arquivo ({ $size_stats })\nrust_moving_no_size_files = Movendo arquivo { $items_stats }\nrust_hardlinking_files = Linkagem rígida { $items_stats } arquivo ({ $size_stats })\nrust_hardlinking_no_size_files = Linkagem rígida { $items_stats } arquivo\nrust_symlinking_files = Simlinkando o arquivo { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Simlink { $items_stats } arquivo\nrust_optimizing_videos = Vídeo otimizado { $items_stats } ({ $size_stats })\nrust_optimizing_no_size_videos = Vídeo otimizado { $items_stats }\nrust_cleaning_exif = Limpar EXIF de { $items_stats } arquivo ({ $size_stats })\nrust_cleaning_no_size_exif = Limpar EXIF de { $items_stats } arquivo\nrust_no_files_deleted = Nenhum arquivo ou pasta selecionados para exclusão\nrust_no_files_renamed = Nenhum arquivo ou pasta selecionados para renomear\nrust_no_files_moved = Não há arquivos ou pastas selecionados para mover\nrust_no_files_hardlinked = Nenhum arquivo ou pasta selecionado para hardlinking\nrust_no_files_symlinked = Nenhum arquivo ou pasta selecionado para simlink\nrust_no_videos_optimized = Nenhum vídeo selecionado para otimização\nrust_no_exif_cleaned = Nenhuma arquivo selecionado para limpeza EXIF\nrust_extracted_exif_tags = Extraído tags EXIF de { $items_stats } arquivos ({ $size_stats })\nrust_delete_confirmation = Você tem certeza que deseja excluir os itens selecionados?\nrust_delete_confirmation_number_simple = { $items } itens selecionados.\nrust_delete_confirmation_number_groups = { $items } itens selecionados em grupos { $groups }.\nrust_delete_confirmation_selected_all_in_group = Todos os itens selecionados em { $groups } grupos.\nrust_move_confirmation = Tem certeza de que deseja mover os itens selecionados?\nrust_move_confirmation_number_simple = { $items } itens selecionados.\nrust_clean_exif_confirmation = Tem certeza de que deseja remover os dados EXIF dos itens selecionados?\nrust_clean_exif_confirmation_number_simple = { $items } itens selecionados.\nclean_exif_overwrite_files_text = Substituir arquivos\nrust_optimize_video_confirmation = Tem certeza que deseja otimizar os vídeos selecionados?\nrust_optimize_video_confirmation_number_simple = { $items } itens selecionados.\nrust_hardlink_confirmation = Tem certeza de que deseja criar hardlinks para os itens selecionados?\nrust_hardlink_confirmation_number_simple = { $items } itens selecionados.\nrust_symlink_confirmation = Tem certeza de que deseja criar links simbólicos para os itens selecionados?\nrust_symlink_confirmation_number_simple = { $items } itens selecionados.\nrust_rename_confirmation = Tem certeza que deseja renomear os itens selecionados?\nrust_rename_confirmation_number_simple = { $items } itens selecionados.\nrust_cache_processed_files = Arquivados { $files } arquivos em cache\nrust_cache_entries_stats = Removidas { $removed } entradas de todas as { $all }, { $left } restantes\nrust_cache_size_reduced = Reduziu o tamanho dos arquivos de cache por { $size }\nrust_cache_time_elapsed = Tempo decorrido: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Falhou em criar um link rígido { $name } para { $target }, motivo { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Seleção\ncolumn_size = Tamanho\ncolumn_file_name = Nome do arquivo\ncolumn_path = Caminho\ncolumn_modification_date = Data de modificação\ncolumn_similarity = Similaridade\ncolumn_dimensions = cotas\ncolumn_new_dimensions = Novas Dimensões\ncolumn_title = Título\ncolumn_artist = Artista\ncolumn_year = ano\ncolumn_bitrate = Taxa de bitrates\ncolumn_length = Comprimento\ncolumn_genre = gênero\ncolumn_type_of_error = Tipo de erro\ncolumn_symlink_name = Nome do link simbólico\ncolumn_symlink_folder = Pasta Symlink\ncolumn_destination_path = Caminho de Destino\ncolumn_current_extension = Extensão atual\ncolumn_proper_extension = Extensão adequada\ncolumn_fps = FPS\ncolumn_codec = Codificador\ncolumn_duration = Duração\ncolumn_exif_tags = Tags EXIF\ncolumn_new_name = Novo Nome\n# Slint translations\nok_button = OK\ncancel_button = cancelar\ndo_you_want_to_continue = Você quer continuar?\nmain_window_title = Krokiet - Limpador de Dados\nscan_button = Escanear\nstop_button = Interromper\nstop_text = Parar\nselect_button = Selecionar\nmove_button = Mover-se\ndelete_button = excluir\nsave_button = Guardar\nsort_button = Ordenar\nrename_button = Renomear\nmotto = Este programa é gratuito e sempre será.\\nConsulte a Licença MIT/GPL para obter detalhes.\nunicorn = Você pode não olhar para um unicórnio, mas o unicórnio sempre olha para você.\nrepository = Repositório\ninstruction = Instrução\ndonation = Doação\ntranslation = Tradução\nincluded_paths = Caminhos Incluídos\nexcluded_paths = Caminhos Excluídos\nref = Ref\npath = Caminho\ntool_duplicate_files = Arquivos duplicados\ntool_empty_folders = Pastas vazias\ntool_big_files = Arquivos grandes\ntool_empty_files = Arquivos vazios\ntool_temporary_files = Arquivos Temporários\ntool_similar_images = Imagens semelhantes\ntool_similar_videos = Vídeos similares\ntool_music_duplicates = Músicas duplicadas\ntool_invalid_symlinks = Links simbólicos inválidos\ntool_broken_files = Arquivos quebrados\ntool_bad_extensions = Extensões inválidas\ntool_bad_names = Nomes Ruins\ntool_video_optimizer = Otimizador de Vídeo\ntool_exif_remover = Remover Exif\nsort_by_full_name = Classificar por nome completo\nsort_by_selection = Ordenar por seleção\nsort_reverse = Ordem inversa\nselection_all = Selecionar todos\nselection_deselect_all = Desmarcar todos\nselection_invert_selection = Inverter seleção\nselection_the_biggest_size = Selecione o maior tamanho\nselection_the_biggest_resolution = Selecione a maior resolução\nselection_the_smallest_size = Selecione o menor tamanho\nselection_the_smallest_resolution = Selecione a menor resolução\nselection_newest = Selecionar mais recente\nselection_oldest = Selecionar mais antigo\nselection_shortest_path = Selecione o caminho mais curto\nselection_longest_path = Selecione o caminho mais longo\nstage_current = Etapa atual:\nstage_all = Todos os Stages:\nsubsettings = Subconfigurações\nsubsettings_images_hash_size = Tamanho do hash\nsubsettings_images_resize_algorithm = Redimensionar Algoritmo\nsubsettings_images_ignore_same_size = Ignorar imagens com mesmo tamanho\nsubsettings_images_max_difference = Diferença máxima\nsubsettings_images_duplicates_hash_type = Tipo de hash\nsubsettings_duplicates_check_method = Método de verificação\nsubsettings_duplicates_name_case_sensitive = Caso Sensível (apenas modos de nome)\nsubsettings_biggest_files_sub_method = Método\nsubsettings_biggest_files_sub_number_of_files = Número de arquivos\nsubsettings_videos_max_difference = Diferença máxima\nsubsettings_videos_ignore_same_size = Ignorar vídeos com mesmo tamanho\nsubsettings_music_audio_check_type = Tipo de verificação por áudio\nsubsettings_music_approximate_comparison = Comparação de Tag aproximada\nsubsettings_music_compared_tags = Tags comparadas\nsubsettings_music_title = Título\nsubsettings_music_artist = Artista\nsubsettings_music_bitrate = Taxa de bitrates\nsubsettings_music_genre = gênero\nsubsettings_music_year = ano\nsubsettings_music_length = Comprimento\nsubsettings_music_max_difference = Diferença máxima\nsubsettings_music_minimal_fragment_duration = Duração mínima do fragmento\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Comparar dentro de grupos de títulos similares\nsubsettings_broken_files_type = Tipo de arquivos para verificar\nsubsettings_broken_files_audio = Áudio\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Arquivo\nsubsettings_broken_files_image = Imagem:\nsubsettings_broken_files_video = Vídeo\nsubsettings_broken_files_video_info = Usa ffmpeg/ffprobe. Muito lento e pode detectar erros pedantes mesmo se o arquivo reproduzir bem.\nsubsettings_bad_names_issues = Verificações de nome de arquivo\nsubsettings_bad_names_uppercase_extension = Extensão Maiúscula\nsubsettings_bad_names_uppercase_extension_hint = Encontra arquivos com letras maiúsculas no nome da extensão (por exemplo, .JPG, .Mp3) e sugere a versão em letras minúsculas\nsubsettings_bad_names_emoji_used = Emoji no nome\nsubsettings_bad_names_emoji_used_hint = Encontra arquivos com caracteres emoji (😀, 🎉, etc.) no nome e sugere removê-los\nsubsettings_bad_names_space_at_start_end = espaços iniciais/terminais\nsubsettings_bad_names_space_at_start_end_hint = Encontra arquivos com espaços no início ou no fim do nome e sugere que sejam removidos\nsubsettings_bad_names_non_ascii = caracteres não-ASCII\nsubsettings_bad_names_non_ascii_hint = Encontra caracteres não-ASCII (ą, ć, ñ, etc.) e sugere substituí-los por equivalentes ASCII (a, c, n) ou removê-los se não existir mapeamento\nsubsettings_bad_names_restricted_charset = Conjunto de caracteres limitado\nsubsettings_bad_names_restricted_charset_hint = Transliterar caracteres não-ASCII para ASCII, então encontra arquivos contendo caracteres fora de 0-9a-zA-Z e caracteres permitidos definidos pelo usuário\nsubsettings_bad_names_allowed_chars = Caracteres permitidos\nsubsettings_bad_names_remove_duplicated = Caracteres duplicados\nsubsettings_bad_names_remove_duplicated_hint = Encontra caracteres não alfanuméricos duplicados consecutivos (por exemplo, \"file---name..txt\") e sugere a remoção de duplicados\nsettings_global_settings = Configurações Globais\nsettings_dark_theme = Tema escuro\nsettings_show_only_icons = Mostrar somente ícones\nsettings_excluded_items = Item excluído:\nsettings_allowed_extensions = Extensões permitidas:\nsettings_excluded_extensions = Extensões excluídas:\nsettings_file_size = Tamanho do Arquivo(Kilobytes)\nsettings_minimum_file_size = Mín:\nsettings_maximum_file_size = Máx:\nsettings_recursive_search = Pesquisa recursiva\nsettings_use_cache = Usar cache\nsettings_save_as_json = Também salvar o cache como arquivo JSON\nsettings_move_to_trash = Mover os arquivos apagados para a lixeira\nsettings_ignore_other_filesystems = Ignorar outros sistemas de arquivos (somente Linux)\nsettings_delete_outdated_cache_entries = Excluir automaticamente entradas de cache desatualizadas\nsettings_delete_outdated_cache_entries_hint = Quando habilitado, o aplicativo verificará durante o carregamento em cache (no máximo uma vez por semana) se os registros em cache ainda apontam para arquivos/dados existentes e não modificados\nsettings_hide_hard_links = Esconder links rígidos\nsettings_hide_hard_links_hint = Esconder hard links para os mesmos arquivos nos resultados\nsettings_thread_number = Número do tópico\nsettings_restart_required = ---Você precisa reiniciar o aplicativo para aplicar as alterações no número da thread ---\nsettings_duplicate_image_preview = Pré-visualização da imagem\nsettings_duplicate_minimal_hash_cache_size = Tamanho mínimo dos arquivos em cache - Hash (KB)\nsettings_duplicate_use_prehash = Usar pré-hash\nsettings_duplicate_minimal_prehash_cache_size = Tamanho mínimo dos arquivos em cache - Prehash (KB)\nsettings_similar_images_show_image_preview = Pré-visualização da imagem\nsettings_application_scale_text = Escala de aplicação\nsettings_application_scale_hint_text = Quando a escala manual está habilitada, isso permite que você escolha um fator de escala personalizado, mas desabilita completamente a escala automática com base no DPI do monitor.\nsettings_restart_required_scale_text = —Você precisa reiniciar o aplicativo para aplicar as alterações de escala—\nsettings_use_manual_application_scale_text = Use escala manual de aplicação\nsettings_video_thumbnails_preview = Prévia da imagem\nsettings_open_config_folder = Abrir pasta de configuração\nsettings_open_cache_folder = Abrir pasta cache\nsettings_language = IDIOMA\nsettings_current_preset = Predefinição atual:\nsettings_edit_name = Editar Nome\nsettings_choose_name_for_prefix = Escolha o nome para o prefixo\nsettings_save = Guardar\nsettings_load = Carregar\nsettings_reset = Reiniciar\nsettings_similar_videos_tool = Ferramenta de vídeos similares\nsettings_video_thumbnails_clear_unused_thumbnails = Apagar miniaturas de vídeo não utilizadas com mais de 7 dias no início do aplicativo\nsettings_video_thumbnails_header = Miniaturas de Vídeo\nsettings_video_thumbnails_generate = Gerar miniaturas\nsettings_video_thumbnails_position = Posição da miniatura no vídeo (%)\nsettings_video_thumbnails_generate_grid = Gerar grade de miniaturas em vez de uma única imagem\nsettings_video_thumbnails_generate_grid_hint = Gerar múltiplas imagens em grade é muito mais lento do que gerar uma miniatura única\nsettings_video_thumbnails_grid_tiles_per_side = Número de azulejos por lado na grade de miniatura\nsettings_video_thumbnails_grid_tiles_per_side_hint = Número de pequenos ícones de imagem por lado na grade. Por exemplo, selecionar 2 cria uma grade 2 x 2, resultando em um único pequeno ícone composto por 4 imagens.\nsettings_similar_images_tool = Ferramenta de imagens similares\nsettings_general_settings = Configurações Gerais\nsettings_cache_header_text = Configurações de Cache\nsettings_clean_cache_button_text = Limpar cache desatualizada\nsettings_settings = Confirgurações\nsettings_load_tabs_sizes_at_startup = Carregar os tamanhos das abas na inicialização\nsettings_load_windows_size_at_startup = Carregar o tamanho das janelas na inicialização\nsettings_limit_lines_of_messages = Limitar mensagens a 500 linhas (resolver soluções para o widget lento de Edição de Texto)\nsettings_play_audio_on_scan_completion_text = Reproduzir som quando a digitalização é concluída com sucesso\nsettings_audio_feature_hint_text = Disponível somente ao compilar com o recurso de áudio\nsettings_audio_env_variable_hint_text = O som pode ser alterado, definindo a variável de ambiente KROKIET_AUDIO_STOP_FILE para um caminho de arquivo de áudio válido\npopup_save_title = Salvando resultados\npopup_save_message = Isto salvará os resultados em 3 arquivos diferentes\npopup_rename_title = Renomeando arquivos\npopup_new_paths_title = Por favor, adicione caminhos uma por linha\npopup_move_title = Movendo arquivos\npopup_move_copy_checkbox = Copie arquivos em vez de se mover\npopup_move_preserve_folder_checkbox = Preservar estrutura de pastas\nmove_confirmation_text = Tem certeza de que deseja mover os itens selecionados?\nrename_confirmation_text = Tem certeza que deseja renomear os itens selecionados?\ndelete = Remover itens\nstopping_scan = Parando a verificação, por favor aguarde...\nsearching = Buscando...\nsubsettings_videos_crop_detect = Método de detecção de corte\nsubsettings_videos_skip_forward_amount = Pular duração [s]\nsubsettings_videos_vid_hash_duration = Duração hash de vídeo\nsettings_cache_number_size_text = Tamanho do arquivo de cache: { $size }, número de arquivos: { $number }\nsettings_video_thumbnails_number_size_text = Tamanho das miniaturas de vídeo: { $size }, número de arquivos: { $number }\nsettings_log_number_size_text = Tamanho de arquivos de log: { $size }, número de arquivos: { $number }\npopup_clean_cache_title_text = Limpar Cache Desatualizada\npopup_clean_cache_confirmation_text = Tem certeza de que deseja limpar entradas de cache desatualizadas? Isso removerá entradas de cache para arquivos que não existem mais ou foram modificados.\npopup_clean_cache_progress_text = Processando arquivo de cache:\npopup_clean_cache_current_file_text = Arquivo atual:\npopup_clean_cache_file_progress_text = Progresso do arquivo atual:\npopup_clean_cache_overall_progress_text = Progresso geral:\npopup_clean_cache_stopped_by_user_text = A limpeza da cache foi interrompida pelo utilizador\npopup_clean_cache_finished_text = Limpeza de cache concluída com sucesso!\npopup_clean_cache_error_details_text = Detalhes do erro:\npopup_clean_cache_files_with_errors = Arquivos com erros:\nsubsettings_video_optimizer_mode = Modo\nsubsettings_video_optimizer_crop_type = Tipo de Cultivo\nsubsettings_video_optimizer_black_pixel_threshold = Limite de Pixel Preto\nsubsettings_video_optimizer_black_pixel_threshold_hint = O valor RGB máximo para cada canal de pixel a ser considerado preto (0-128). Padrão: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Barra Preta Mínimo Percentagem\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Mínimo percentual de pixels pretos em uma linha/coluna para ser considerado uma barra preta (50-100). Padrão: 90\nsubsettings_video_optimizer_max_samples = Máx Samples\nsubsettings_video_optimizer_max_samples_hint = Número máximo de quadros para analisar por vídeo (5-1000). Padrão: 60\nsubsettings_video_optimizer_min_crop_size = Tamanho Mínimo da Cultura\nsubsettings_video_optimizer_min_crop_size_hint = Mínimo de pixels para cortar em qualquer lado (1-1000). Cortes menores são ignorados. Padrão: 5\nsubsettings_video_optimizer_video_codec = Codec de vídeo\nsubsettings_video_optimizer_excluded_codecs = Códecs excluídos\nsubsettings_video_optimizer_video_quality = Qualidade de vídeo (CRF)\nsubsettings_reset = Redefinir\nsubsettings_exif_ignored_tags_text = Ignorados tags:\nsubsettings_exif_ignored_tags_hint_text = Lista separada por vírgulas de tags a excluir da varredura (por exemplo, GPS, Miniatura). Algumas tags, como ImageWidth em arquivos TIFF, estão ocultas para evitar quebrar a imagem.\nclean_button_text = Limpo\nclean_text = Dados EXIF limpos\nclean_confirmation_text = Tem certeza de que deseja remover os dados EXIF dos itens selecionados?\ncrop_videos_text = Cortar vídeos\ncrop_video_confirmation_text = Tem certeza que deseja recortar os vídeos selecionados?\ncrop_reencode_video_text = Re-codificar vídeo\nreencode_videos_text = Re-codificar vídeos\noptimize_button_text = Otimizar\noptimize_confirmation_text = Tem certeza de que deseja re-codificar os vídeos selecionados?\noptimize_fail_if_bigger_text = Falhar se o arquivo otimizado for maior\noptimize_overwrite_files_text = Substituir arquivos\noptimize_limit_video_size_text = Limite o tamanho do vídeo\noptimize_max_width_text = Máximo largura:\noptimize_max_height_text = Máximo da altura:\nhardlink_button_text = Link duro\nhardlink_text = Criar hardlinks\nhardlink_confirmation_text = Tem certeza de que deseja criar hardlinks para os itens selecionados?\nsoftlink_button_text = Link\nsoftlink_text = Criar softlinks\nsoftlink_confirmation_text = Tem certeza de que deseja criar links simbólicos (softlinks) para os itens selecionados?\n"
  },
  {
    "path": "krokiet/i18n/ro/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Eroare critică în timpul pornirii aplicației\nrust_init_error_message = \n        A eroare critică a apărut în timpul lansării aplicației:\n\n        { $error_message }\n\n        Aceasta poate fi cauzată de drivere OpenGL/Vulkan lipsă sau defectuoase, de rularea aplicației într-o mașină virtuală sau de o eroare în Krokiet sau într-una dintre bibliotecile sale.\n\n        Puteți încerca să rulați diferite versiuni (skia_opengl, skia_vulkan, femtovg_opengl - implicit) sau cu renderer software pentru a vedea dacă acest lucru rezolvă problema.\nrust_loaded_preset = Presetare încărcată { $preset_idx }\nrust_file_already_exists = Fișierul \"{ $file }\" există deja și nu va fi suprascris\nrust_error_removing_file_after_copy = Eroare la ștergerea fișierului \"{ $file }\" (după copierea în altă partiție), motiv: { $reason }\nrust_error_copying_file = Eroare la copierea \"{ $input }\" în \"{ $output }\", motiv: { $reason }\nrust_loading_tags_cache = Se încarcă cache-ul etichetelor\nrust_loading_fingerprints_cache = Se încarcă cache-ul amprentelor\nrust_saving_tags_cache = Salvare cache etichete\nrust_saving_fingerprints_cache = Salvare cache amprente\nrust_loading_prehash_cache = Se încarcă cache-ul prehash\nrust_saving_prehash_cache = Salvare cache prehash\nrust_loading_hash_cache = Încărcare cache hash\nrust_saving_hash_cache = Salvare cache hash\nrust_loading_exif_cache = Încărcare cache EXIF\nrust_saving_exif_cache = Salvare cache EXIF\nrust_scanning_name = Se scanează numele fișierului { $entries_checked }\nrust_scanning_size_name = Se scanează dimensiunea și numele fișierului { $entries_checked }\nrust_scanning_size = Dimensiune scanare fișier { $entries_checked }\nrust_scanning_file = Se scanează fişierul { $entries_checked }\nrust_scanning_folder = Se scanează directorul { $entries_checked }\nrust_checked_tags = Tag-uri verificate de { $items_stats }\nrust_checked_content = Conținut verificat de { $items_stats } ({ $size_stats })\nrust_compared_tags = Tag-uri comparate de { $items_stats }\nrust_compared_content = Conținut comparat cu { $items_stats }\nrust_hashed_images = Imagini Hashed { $items_stats } ({ $size_stats })\nrust_compared_image_hashes = Hash-uri imagine comparate de { $items_stats }\nrust_hashed_videos = Videoclipuri Hashed { $items_stats }\nrust_created_thumbnails = Miniaturi create pentru videoclipuri { $items_stats }\nrust_checked_files = Fişier { $items_stats } ({ $size_stats } ) verificat\nrust_checked_files_bad_extensions = Fişier { $items_stats } verificat\nrust_checked_files_bad_names = Verificat { $items_stats } fișier\nrust_checked_videos = Verificat { $items_stats } videoclipuri ({ $size_stats })\nrust_analyzed_partial_hash = S-a analizat hash-ul parțial al fișierelor { $items_stats } ({ $size_stats })\nrust_analyzed_full_hash = S-a analizat hash complet al fişierelor { $items_stats } ({ $size_stats })\nrust_failed_to_rename_file = Nu s-a putut redenumi fișierul { $old_path } în { $new_path }, eroare: { $error }\nrust_no_included_paths = Nu se poate începe scanarea când nu sunt setate căi incluse.\nrust_all_paths_referenced = Nu se poate începe scanarea când toate căile incluse sunt setate ca căi referențiale, trebuie să dezactivați caseta de selectare de lângă calea de intrare.\nrust_found_empty_folders = Folderele goale { $items_found } au fost găsite în { $time }\nrust_found_empty_files = Fișiere goale { $items_found } găsite în { $time }\nrust_found_similar_images = Am găsit { $items_found } fişiere de imagine similare în { $groups } grupuri { $time }\nrust_found_similar_videos = Am găsit { $items_found } fişiere video similare în { $groups } grupuri { $time }\nrust_found_similar_music_files = Am găsit { $items_found } fişiere muzicale similare în { $groups } grupuri { $time }\nrust_found_invalid_symlinks = { $items_found } link-uri simboluri invalide găsite în { $time }\nrust_found_temporary_files = Fișiere temporare { $items_found } găsite în { $time }\nrust_no_file_type_selected = Fișierele defecte nu pot fi găsite fără niciun tip de fișier selectat.\nrust_found_broken_files = Am găsit { $items_found } fişiere defecte luând { $size } în { $time }\nrust_found_bad_extensions = Fisiere { $items_found } cu extensii gresite in { $time }\nrust_found_bad_names = Găsite { $items_found } fișiere cu nume defectuos în { $time}\nrust_found_video_optimizer = Găsite { $items_found } fișiere de optimizat în { $time }\nrust_found_duplicate_files = Am găsit { $items_found } fișiere duplicate în { $groups } grupuri care iau { $size } în { $time }\nrust_found_duplicate_files_no_lost_space = Fişierele duplicate { $items_found } au fost găsite în grupurile { $groups } în { $time }\nrust_found_big_files = Fişiere mari { $items_found } cu dimensiunea { $size } în { $time }\nrust_found_exif_files = Găsite { $items_found } fișiere cu date EXIF în { $time }\nrust_cannot_load_preset = Nu se poate schimba și încărca presetarea { $preset_idx } - motivul { $reason }, folosind setările implicite în schimb\nrust_saved_preset = Presetare salvată { $preset_idx }\nrust_cannot_save_preset = Nu se poate salva presetarea { $preset_idx } - motivul { $reason }\nrust_reset_preset = Reinitializare predefinire { $preset_idx }\nrust_cannot_create_output_folder = Nu se poate crea dosarul de ieșire { $output_folder }, motivul: { $error }\nrust_delete_summary = Elemente { $deleted } șterse, au eșuat ștergerea articolelor { $failed } , din { $total } elemente\nrust_rename_summary = Redenumite elementele { $renamed } , nu s-au putut redenumi elementele { $failed } , din { $total } elemente\nrust_move_summary = Mutat elementele { $moved } , nu s-a putut muta elementele { $failed } , din { $total } elemente\nrust_hardlink_summary = Elemente cu linkuri dure { $hardlinked }, nu au putut face linkuri dure { $failed } din { $total } elemente\nrust_symlink_summary = Simbolizate { $symlinked } elemente, nu s-au putut simbola { $failed } elemente, din total { $total } elemente\nrust_optimize_video_summary = Videoclipuri optimizate { $optimized }, videoclipuri care nu au fost optimizate { $failed }, din total { $total } videoclipuri\nrust_clean_exif_summary = Curățat EXIF din { $cleaned } fișiere, nu a putut curăța { $failed } fișiere, din { $total } fișiere\nrust_deleting_files = Ștergere fișier { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Ștergere fișier { $items_stats }\nrust_renaming_files = Redenumire fișier { $items_stats }\nrust_moving_files = Se mută fişierul { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = Se mută fişierul { $items_stats }\nrust_hardlinking_files = Hardlinking { $items_stats } fișier ({ $size_stats })\nrust_hardlinking_no_size_files = Hardlinking { $items_stats } fișier\nrust_symlinking_files = Simulare de legătură către fișier { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Simulare a fiuncționalului fișierul { $items_stats }\nrust_optimizing_videos = Video optimizat { $items_stats } ({ $size_stats })\nrust_optimizing_no_size_videos = Video optimizat { $items_stats }\nrust_cleaning_exif = Curățare EXIF din fișierul { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = Curăță EXIF din fișierul { $items_stats }\nrust_no_files_deleted = Niciun fișier sau dosar selectat pentru ștergere\nrust_no_files_renamed = Nu sunt fișiere sau dosare selectate pentru redenumire\nrust_no_files_moved = Nu sunt fișiere sau dosare selectate pentru mutare\nrust_no_files_hardlinked = Niciun fișier sau folder selectat pentru hardlinking\nrust_no_files_symlinked = Niciun fișier sau folder selectat pentru symlinking\nrust_no_videos_optimized = Niciun video selectat pentru optimizare\nrust_no_exif_cleaned = Niciun fișier selectat pentru curățarea EXIF\nrust_extracted_exif_tags = Extragerea etichetelor EXIF din fișierele { $items_stats } ({ $size_stats })\nrust_delete_confirmation = Sunteţi sigur că doriţi să ştergeţi elementele selectate?\nrust_delete_confirmation_number_simple = { $items } elemente selectate.\nrust_delete_confirmation_number_groups = { $items } articole selectate în grupurile { $groups }.\nrust_delete_confirmation_selected_all_in_group = Toate articolele selectate în grupurile { $groups }.\nrust_move_confirmation = Sunteți sigur(ă) că doriți să mutați elementele selectate?\nrust_move_confirmation_number_simple = { $items } articole selectate.\nrust_clean_exif_confirmation = Sunteți sigur(ă) că doriți să eliminați datele EXIF din elementele selectate?\nrust_clean_exif_confirmation_number_simple = { $items } articole selectate.\nclean_exif_overwrite_files_text = SupraScrie fișiere\nrust_optimize_video_confirmation = Ești sigur că dorești să optimizezi videoclipurile selectate?\nrust_optimize_video_confirmation_number_simple = { $items } articole selectate.\nrust_hardlink_confirmation = Ești sigur că dorești să creezi hardlink-uri pentru elementele selectate?\nrust_hardlink_confirmation_number_simple = { $items } articole selectate.\nrust_symlink_confirmation = Ești sigur că dorești să creezi linkuri simbolice pentru elementele selectate?\nrust_symlink_confirmation_number_simple = { $items } articole selectate.\nrust_rename_confirmation = Ești sigur(ă) că dorești să redenumești elementele selectate?\nrust_rename_confirmation_number_simple = { $items } articole selectate.\nrust_cache_processed_files = Fișierele cache { $files } au fost procesate\nrust_cache_entries_stats = Eliminate { $removed } înregistrări din toate { $all }, { $left} au rămas\nrust_cache_size_reduced = Diminuat dimensiunea fișierelor cache cu { $size }\nrust_cache_time_elapsed = Timp scurs: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Eșec la crearea unui hardlink { $name } către { $target }, motiv { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Selecţie\ncolumn_size = Dimensiune\ncolumn_file_name = Numele fișierului\ncolumn_path = Cale\ncolumn_modification_date = Data modificării\ncolumn_similarity = Similaritate\ncolumn_dimensions = Dimensiuni\ncolumn_new_dimensions = Nouă Dimensiuni\ncolumn_title = Titlu\ncolumn_artist = Artist\ncolumn_year = An\ncolumn_bitrate = Rată de bitrate\ncolumn_length = Lungime\ncolumn_genre = Gen\ncolumn_type_of_error = Tipul de eroare\ncolumn_symlink_name = Nume Symlink\ncolumn_symlink_folder = Dosar Symlink\ncolumn_destination_path = Calea destinației\ncolumn_current_extension = Extensia curentă\ncolumn_proper_extension = Extensie corectă\ncolumn_fps = FPS\ncolumn_codec = Codecul\ncolumn_duration = Durată\ncolumn_exif_tags = Etichete EXIF\ncolumn_new_name = Noul Nume\n# Slint translations\nok_button = Ok\ncancel_button = Anulează\ndo_you_want_to_continue = Vrei să continui?\nmain_window_title = Krokiet - Curățător de date\nscan_button = Scanare\nstop_button = Oprește\nstop_text = Opriți\nselect_button = Selectare\nmove_button = Mutare\ndelete_button = Ștergere\nsave_button = Salvează\nsort_button = Sortează\nrename_button = Redenumire\nmotto = Acest program este liber și va fi întotdeauna folosit.\\nVezi licența MIT/GPL pentru detalii.\nunicorn = Poate că nu vă uitaţi la un unicorn, dar unicornul vă priveşte întotdeauna.\nrepository = Depozit\ninstruction = Instrucțiuni\ndonation = Donație\ntranslation = Traducere\nincluded_paths = Include Puteți\nexcluded_paths = Puteți exclude căile\nref = Ref\npath = Cale\ntool_duplicate_files = Fișiere duplicate\ntool_empty_folders = Golire dosare\ntool_big_files = Fișiere mari\ntool_empty_files = Fișiere goale\ntool_temporary_files = Fișiere temporare\ntool_similar_images = Imagini similare\ntool_similar_videos = Video similare\ntool_music_duplicates = Duplicate Muzică\ntool_invalid_symlinks = Simboluri invalide\ntool_broken_files = Fișiere defecte\ntool_bad_extensions = Extensii rele\ntool_bad_names = Nume Proaste\ntool_video_optimizer = Optimizator Video\ntool_exif_remover = Eliminator Exif\nsort_by_full_name = Sortează după numele complet\nsort_by_selection = Sortează după selecţie\nsort_reverse = Ordinea inversă\nselection_all = Selectează tot\nselection_deselect_all = Deselectează tot\nselection_invert_selection = Inversează selecția\nselection_the_biggest_size = Selectaţi cea mai mare dimensiune\nselection_the_biggest_resolution = Selectează cea mai mare rezoluție\nselection_the_smallest_size = Selectaţi dimensiunea cea mai mică\nselection_the_smallest_resolution = Selectați cea mai mică rezoluție\nselection_newest = Selectează cele mai noi\nselection_oldest = Selectează cel mai vechi\nselection_shortest_path = Selectează cea mai scurtă cale\nselection_longest_path = Selectează cea mai lungă cale\nstage_current = Etapa curentă:\nstage_all = Toate etapele:\nsubsettings = Subsetări\nsubsettings_images_hash_size = Dimensiune Hash\nsubsettings_images_resize_algorithm = Redimensionează algoritmul\nsubsettings_images_ignore_same_size = Ignoră imaginile cu aceeași dimensiune\nsubsettings_images_max_difference = Diferență maximă\nsubsettings_images_duplicates_hash_type = Tip Hash\nsubsettings_duplicates_check_method = Metoda de verificare\nsubsettings_duplicates_name_case_sensitive = Caz Senitive(doar moduri de nume)\nsubsettings_biggest_files_sub_method = Metodă\nsubsettings_biggest_files_sub_number_of_files = Numărul de fișiere\nsubsettings_videos_max_difference = Diferență maximă\nsubsettings_videos_ignore_same_size = Ignoră videoclipuri cu aceeași dimensiune\nsubsettings_music_audio_check_type = Tip verificare audio\nsubsettings_music_approximate_comparison = Comparație aproximativă de etichete\nsubsettings_music_compared_tags = Tag-uri comparate\nsubsettings_music_title = Titlu\nsubsettings_music_artist = Artist\nsubsettings_music_bitrate = Rată de bitrate\nsubsettings_music_genre = Gen\nsubsettings_music_year = An\nsubsettings_music_length = Lungime\nsubsettings_music_max_difference = Diferență maximă\nsubsettings_music_minimal_fragment_duration = Durata minimă a fragmentului\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Compară în cadrul grupurilor de titluri similare\nsubsettings_broken_files_type = Tipul fişierelor de verificat\nsubsettings_broken_files_audio = Sunet\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Arhivează\nsubsettings_broken_files_image = Imagine\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Folosește ffmpeg/ffprobe. Foarte lent și poate detecta erori pedantice chiar dacă fișierul rulează bine.\nsubsettings_bad_names_issues = Verificări nume fișiere\nsubsettings_bad_names_uppercase_extension = Extensie majuscule\nsubsettings_bad_names_uppercase_extension_hint = Găsește fișiere cu litere mari în extensie (de exemplu, .JPG, .Mp3) și sugerează versiunea cu litere mici\nsubsettings_bad_names_emoji_used = Emoji în nume\nsubsettings_bad_names_emoji_used_hint = Găsește fișiere cu caractere emoji (😀, 🎉, etc.) în nume și sugerează eliminarea lor\nsubsettings_bad_names_space_at_start_end = Spații de început/sfârșit\nsubsettings_bad_names_space_at_start_end_hint = Găsește fișiere cu spații la început sau la sfârșit al numelui și sugerează tăierea acestora\nsubsettings_bad_names_non_ascii = caractere non-ASCII\nsubsettings_bad_names_non_ascii_hint = Găsește caractere non-ASCII (ą, ć, ñ, etc.) și sugerează înlocuirea lor cu echivalente ASCII (a, c, n) sau eliminarea dacă nu există o mapare\nsubsettings_bad_names_restricted_charset = Caracter set limitat\nsubsettings_bad_names_restricted_charset_hint = Transliterază caractere non-ASCII în ASCII, apoi găsește fișiere care conțin caractere în afara intervalului 0-9a-zA-Z și a caracterelor permise de utilizator\nsubsettings_bad_names_allowed_chars = Permite caractere\nsubsettings_bad_names_remove_duplicated = Caractere duplicate\nsubsettings_bad_names_remove_duplicated_hint = Găsește caractere consecutive duplicate non-alfanumerice (de ex., \"file---name..txt\") și sugerează eliminarea duplicatelor\nsettings_global_settings = Setări globale\nsettings_dark_theme = Temă întunecată\nsettings_show_only_icons = Arată doar pictograme\nsettings_excluded_items = Articol exclus:\nsettings_allowed_extensions = Extensii permise:\nsettings_excluded_extensions = Extensii excluse:\nsettings_file_size = Dimensiune fișier (Kilobțiți)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Max:\nsettings_recursive_search = Căutare Recursivă\nsettings_use_cache = Utilizare geocutie\nsettings_save_as_json = De asemenea, salvează cache-ul ca fișier JSON\nsettings_move_to_trash = Mută fișierele șterse în gunoi\nsettings_ignore_other_filesystems = Ignorați alte sisteme de fișiere (doar Linux)\nsettings_delete_outdated_cache_entries = Șterge automat înregistrările de cache depășite\nsettings_delete_outdated_cache_entries_hint = Când este activat, aplicația va verifica în timpul încărcării din cache (cel mult o dată pe săptămână) dacă înregistrările din cache indică încă fișiere/date existente și nemodificate\nsettings_hide_hard_links = Ascunde legături hard\nsettings_hide_hard_links_hint = Ascunde legături hard către aceleași fișiere în rezultate\nsettings_thread_number = Număr subiect\nsettings_restart_required = ---Trebuie să reporniți aplicația pentru a aplica modificările în numărul de discuție----\nsettings_duplicate_image_preview = Previzualizare imagine\nsettings_duplicate_minimal_hash_cache_size = Dimensiunea minimă a fişierelor cache - Hash (KB)\nsettings_duplicate_use_prehash = Folosește prehash\nsettings_duplicate_minimal_prehash_cache_size = Dimensiunea minimă a fişierelor cache - Prehash (KB)\nsettings_similar_images_show_image_preview = Previzualizare imagine\nsettings_application_scale_text = Scala aplicației\nsettings_application_scale_hint_text = Când este activat manualul de scară, acest lucru vă permite să alegeți un factor de scară personalizat, dar dezactivează complet scalarea automată bazată pe DPI-ul monitorului.\nsettings_restart_required_scale_text = ---Trebuie să reporniți aplicația pentru a aplica modificările de scară---\nsettings_use_manual_application_scale_text = Folosește scară de aplicare manuală\nsettings_video_thumbnails_preview = Previzualizare imagine\nsettings_open_config_folder = Deschide folderul de configurare\nsettings_open_cache_folder = Deschide dosarul cache\nsettings_language = Limba\nsettings_current_preset = Presetare curentă:\nsettings_edit_name = Editează numele\nsettings_choose_name_for_prefix = Alegeți numele pentru prefix\nsettings_save = Salvează\nsettings_load = Încărcare\nsettings_reset = Rezetează\nsettings_similar_videos_tool = Instrument video similar\nsettings_video_thumbnails_clear_unused_thumbnails = Șterge miniaturile video neutilizate mai vechi de 7 zile la pornirea aplicației\nsettings_video_thumbnails_header = Miniaturi Video\nsettings_video_thumbnails_generate = Generează miniaturile\nsettings_video_thumbnails_position = Poziția miniaturii în video (%)\nsettings_video_thumbnails_generate_grid = Generează grilă de miniatură în loc de imagine unică\nsettings_video_thumbnails_generate_grid_hint = Generarea mai multor imagini în grilă este mult mai lentă decât generarea unei singure miniaturi\nsettings_video_thumbnails_grid_tiles_per_side = Numărul de dale pe fiecare latură în grilă thumbnail\nsettings_video_thumbnails_grid_tiles_per_side_hint = Numărul de dale thumbnail pe fiecare parte din grilă. De exemplu, selectând 2 creează o grilă 2 x 2, rezultând într-o singură dale thumbnail compusă din 4 imagini.\nsettings_similar_images_tool = Unealta de imagini similare\nsettings_general_settings = Setări generale\nsettings_cache_header_text = Setări Cache\nsettings_clean_cache_button_text = Șterge cache-ul vechi\nsettings_settings = Setări\nsettings_load_tabs_sizes_at_startup = Încarcă dimensiunile filelor la pornire\nsettings_load_windows_size_at_startup = Încarcă dimensiunea ferestrelor la pornire\nsettings_limit_lines_of_messages = Limitează mesajele la 500 de linii (lucrează pentru widget-ul TextEdit lent)\nsettings_play_audio_on_scan_completion_text = Redă sunetul când scanarea se finalizează cu succes\nsettings_audio_feature_hint_text = Disponibil doar când se compilează cu funcția audio\nsettings_audio_env_variable_hint_text = Sunetul poate fi modificat, prin setarea variabilei de mediu KROKIET_AUDIO_STOP_FILE la o cale validă de fișier audio\npopup_save_title = Se salvează rezultatele\npopup_save_message = Aceasta va salva rezultatele la 3 fişiere diferite\npopup_rename_title = Redenumire fişiere\npopup_new_paths_title = Vă rugăm să adăugați căile pe câte o linie\npopup_move_title = Mutarea fişierelor\npopup_move_copy_checkbox = Copiați fișierele în loc să mutați\npopup_move_preserve_folder_checkbox = Păstrează structura de dosare\nmove_confirmation_text = Sunteți sigur(ă) că doriți să mutați elementele selectate?\nrename_confirmation_text = Ești sigur(ă) că dorești să redenumești elementele selectate?\ndelete = Ștergere elemente\nstopping_scan = Oprire scanare, vă rugăm așteptați...\nsearching = Căutare...\nsubsettings_videos_crop_detect = Decupare metodă detectare\nsubsettings_videos_skip_forward_amount = Omite durata [s]\nsubsettings_videos_vid_hash_duration = Durată hash video\nsettings_cache_number_size_text = Dimensiunea fişierelor cache: { $size }, număr de fişiere: { $number }\nsettings_video_thumbnails_number_size_text = Dimensiune miniaturi video: { $size }, număr de fişiere: { $number }\nsettings_log_number_size_text = Dimensiune fişiere jurnal: { $size }, număr de fişiere: { $number }\npopup_clean_cache_title_text = Goliți Cache-ul Vechi\npopup_clean_cache_confirmation_text = Ești sigur(ă) că dorești să ștergi intrările de cache depășite? Acest lucru va elimina intrările de cache pentru fișierele care nu mai există sau au fost modificate.\npopup_clean_cache_progress_text = Procesează fișierul cache:\npopup_clean_cache_current_file_text = Fișier curent:\npopup_clean_cache_file_progress_text = Progresul fișierului curent:\npopup_clean_cache_overall_progress_text = Progres general:\npopup_clean_cache_stopped_by_user_text = Curățarea cache-ului a fost oprită de utilizator\npopup_clean_cache_finished_text = Curățarea cache-ului a fost finalizată cu succes!\npopup_clean_cache_error_details_text = Detalii eroare:\npopup_clean_cache_files_with_errors = Fișiere cu erori:\nsubsettings_video_optimizer_mode = Mod\nsubsettings_video_optimizer_crop_type = Tip de cultură\nsubsettings_video_optimizer_black_pixel_threshold = Pragul de intensitate pentru pixelii negri\nsubsettings_video_optimizer_black_pixel_threshold_hint = Valoarea RGB maximă pentru fiecare canal de culoare să fie considerată negru (0-128). Implicit: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Bară Neagră Min Procent\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimum procentaj de pixeli negri într-o linie/coloană pentru a fi considerată o bară neagră (50-100). Implicit: 90\nsubsettings_video_optimizer_max_samples = Max Sampole\nsubsettings_video_optimizer_max_samples_hint = Numărul maxim de cadre de analizat per video (5-1000). Implicit: 60\nsubsettings_video_optimizer_min_crop_size = Dimensiune minimă a culturii\nsubsettings_video_optimizer_min_crop_size_hint = Minim pixeli de tăiat pe orice latură (1-1000). Tăieturile mai mici sunt ignorate. Implicit: 5\nsubsettings_video_optimizer_video_codec = Codecul video\nsubsettings_video_optimizer_excluded_codecs = Codcuce excluse\nsubsettings_video_optimizer_video_quality = Calitatea video (CRF)\nsubsettings_reset = Resetează\nsubsettings_exif_ignored_tags_text = Ignorate etichete:\nsubsettings_exif_ignored_tags_hint_text = Listă separată prin virgulă de etichete de exclus din scanare (de exemplu, GPS, Miniatură). Unele etichete, cum ar fi ImageWidth în fișierele TIFF, sunt ascunse pentru a preveni deteriorarea imaginii.\nclean_button_text = Curat\nclean_text = Datele EXIF curate\nclean_confirmation_text = Sunteți sigur(ă) că doriți să eliminați datele EXIF din elementele selectate?\ncrop_videos_text = Taie videoclipuri\ncrop_video_confirmation_text = Ești sigur că vrei să decupezi videoclipurile selectate?\ncrop_reencode_video_text = Re-encodează videoclipul\nreencode_videos_text = Re-encode videoclipuri\noptimize_button_text = Optimizare\noptimize_confirmation_text = Ești sigur că dorești să re-codifici videoclipurile selectate?\noptimize_fail_if_bigger_text = Eșuează dacă fișierul optimizat este mai mare\noptimize_overwrite_files_text = SupraScrie fișiere\noptimize_limit_video_size_text = Limitează dimensiunea video\noptimize_max_width_text = Max lățime:\noptimize_max_height_text = Max înălțime:\nhardlink_button_text = Hardlink\nhardlink_text = Creează hardlink-uri\nhardlink_confirmation_text = Ești sigur că dorești să creezi hardlink-uri pentru elementele selectate?\nsoftlink_button_text = Softlink\nsoftlink_text = Creează link-uri simbolice\nsoftlink_confirmation_text = Ești sigur că dorești să creezi link-uri simbolice (symlinks) pentru elementele selectate?\n"
  },
  {
    "path": "krokiet/i18n/ru/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Критическая ошибка при запуске приложения\nrust_init_error_message = \n        Произошла критическая ошибка при запуске приложения:\n\n        { $error_message }\n\n        Это может быть вызвано отсутствием или неисправностью драйверов OpenGL/Vulkan, запуском приложения в виртуальной машине или ошибкой в Krokiet или одной из его библиотек.\n\n        Вы можете попробовать запустить разные сборки (skia_opengl, skia_vulkan, femtovg_opengl - по умолчанию) или с программным рендерером, чтобы посмотреть, решит ли это проблему.\nrust_loaded_preset = Загружен пресет { $preset_idx }\nrust_file_already_exists = Файл \"{ $file }\" уже существует и не будет перезаписан\nrust_error_removing_file_after_copy = Ошибка при удалении файла \"{ $file }\" (после копирования на другой раздел), причина: { $reason }\nrust_error_copying_file = Ошибка при копировании \"{ $input }\" в \"{ $output }\", причина: { $reason }\nrust_loading_tags_cache = Загрузка кэша тегов\nrust_loading_fingerprints_cache = Загрузка кэша отпечатков\nrust_saving_tags_cache = Сохранение кэша тегов\nrust_saving_fingerprints_cache = Сохранение кэша отпечатков\nrust_loading_prehash_cache = Загрузка кэша предварительного хеширования\nrust_saving_prehash_cache = Сохранение кэша предварительного хеширования\nrust_loading_hash_cache = Загрузка хеш-кэша\nrust_saving_hash_cache = Сохранение хэш-кеша\nrust_loading_exif_cache = Загрузка кэша EXIF\nrust_saving_exif_cache = Сохранение кэша EXIF\nrust_scanning_name = Сканирование имени { $entries_checked } файла\nrust_scanning_size_name = Сканирование размера и имени файла { $entries_checked }\nrust_scanning_size = Сканирование размера { $entries_checked } файла\nrust_scanning_file = Сканирование { $entries_checked } файла\nrust_scanning_folder = Сканирование папки { $entries_checked }\nrust_checked_tags = Проверенные теги { $items_stats }\nrust_checked_content = Проверенное содержимое { $items_stats } ({ $size_stats })\nrust_compared_tags = По сравнению тегов { $items_stats }\nrust_compared_content = По сравнению содержания { $items_stats }\nrust_hashed_images = Хэшированные { $items_stats } изображения ({ $size_stats })\nrust_compared_image_hashes = Хэш изображений { $items_stats }\nrust_hashed_videos = Хэшировано { $items_stats } видео\nrust_created_thumbnails = Создал миниатюры для { $items_stats } видео\nrust_checked_files = Проверено { $items_stats } файл ({ $size_stats })\nrust_checked_files_bad_extensions = Проверено { $items_stats } файл\nrust_checked_files_bad_names = Проверена { $items_stats } файл\nrust_checked_videos = Проверено { $items_stats } видео ({ $size_stats })\nrust_analyzed_partial_hash = Частичный хэш { $items_stats } файлов ({ $size_stats })\nrust_analyzed_full_hash = Полный хэш { $items_stats } файлов ({ $size_stats })\nrust_failed_to_rename_file = Не удалось переименовать файл { $old_path } в { $new_path }, ошибка: { $error }\nrust_no_included_paths = Невозможно начать сканирование, когда не указаны включенные пути.\nrust_all_paths_referenced = Невозможно начать сканирование, когда все включенные пути установлены как пути, ссылающиеся, необходимо отключить флажок рядом с путем ввода.\nrust_found_empty_folders = Найдено { $items_found } пустых директории в { $time }\nrust_found_empty_files = Найдено { $items_found } пустых файлов в { $time }\nrust_found_similar_images = Найдено { $items_found } похожих изображений в { $groups } группах за { $time }\nrust_found_similar_videos = Найдено { $items_found } схожих видеофайлов в { $groups } группах за { $time }\nrust_found_similar_music_files = Найдено { $items_found } похожих музыкальных файлов в { $groups } группах за { $time }\nrust_found_invalid_symlinks = Найдено { $items_found } неверных симлинков за { $time }\nrust_found_temporary_files = Найдено { $items_found } временных файлов за { $time }\nrust_no_file_type_selected = Не удается найти поврежденные файлы без какого-либо типа файла.\nrust_found_broken_files = Найдено { $items_found } сломанных файлов, взявших { $size } в { $time }\nrust_found_bad_extensions = Найдено { $items_found } файлов с плохими расширениями в { $time }\nrust_found_bad_names = Найдено { $items_found } файлов с плохими именами в { $time }\nrust_found_video_optimizer = Найдено { $items_found } файлов для оптимизации в { $time }\nrust_found_duplicate_files = Найдено { $items_found } дублирующих файла в { $groups } группах размером { $size } за { $time }\nrust_found_duplicate_files_no_lost_space = Найдено { $items_found } дублирующих файлов в { $groups } группах за { $time }\nrust_found_big_files = Найдено { $items_found } больших файлов с размером { $size } за { $time }\nrust_found_exif_files = Найдено { $items_found } файлов с EXIF данными в { $time }\nrust_cannot_load_preset = Невозможно изменить и загрузить пресет { $preset_idx } - причина { $reason }, вместо этого используйте настройки по умолчанию\nrust_saved_preset = Сохраненный пресет { $preset_idx }\nrust_cannot_save_preset = Невозможно сохранить шаблон { $preset_idx } - причина { $reason }\nrust_reset_preset = Сброс предварительной настройки { $preset_idx }\nrust_cannot_create_output_folder = Невозможно создать выходную папку { $output_folder }, причина: { $error }\nrust_delete_summary = Удаленные { $deleted } элементы, не удалось удалить { $failed } элементы из { $total }\nrust_rename_summary = Переименовано { $renamed } элементов, не удалось переименовать { $failed } элементов из общего числа { $total } элементов\nrust_move_summary = Перемещено { $moved } предметов, не удалось переместить { $failed } предметов из { $total }\nrust_hardlink_summary = Связанные жестко { $hardlinked } элементы, не удалось связать жестко { $failed } элементов, из { $total } элементов\nrust_symlink_summary = Симлинковано { $symlinked } элементов, не удалось симлинковать { $failed } элементов, из { $total } элементов\nrust_optimize_video_summary = Оптимизированные { $optimized } видео, не оптимизированные { $failed } видео, из { $total } видео\nrust_clean_exif_summary = Очищено EXIF из { $cleaned } файлов, не удалось очистить { $failed } файлов, всего { $total } файлов\nrust_deleting_files = Удаление файла { $items_stats } ({ $size_stats })\nrust_deleting_no_size_files = Удаление { $items_stats } файла\nrust_renaming_files = Переименование { $items_stats } файла\nrust_moving_files = Перемещение файла { $items_stats } ({ $size_stats })\nrust_moving_no_size_files = Перемещение файла { $items_stats }\nrust_hardlinking_files = Hardlinking { $items_stats } файл ({ $size_stats })\nrust_hardlinking_no_size_files = Жесткая ссылка { $items_stats } файл\nrust_symlinking_files = Симлинковка { $items_stats } файла ({ $size_stats })\nrust_symlinking_no_size_files = Симлинковка { $items_stats } файл\nrust_optimizing_videos = Оптимизированное { $items_stats } видео ({ $size_stats })\nrust_optimizing_no_size_videos = Оптимизирован { $items_stats } видео\nrust_cleaning_exif = Очистка EXIF из файла { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = Очистка EXIF из файла { $items_stats }\nrust_no_files_deleted = Не выбраны файлы или папки для удаления\nrust_no_files_renamed = Не выбраны файлы или папки для переименования\nrust_no_files_moved = Не выбраны файлы или папки для перемещения\nrust_no_files_hardlinked = Нет выбранных файлов или папок для жесткого ссылки\nrust_no_files_symlinked = Нет выбранных файлов или папок для символической ссылки\nrust_no_videos_optimized = Нет выбранных видео для оптимизации\nrust_no_exif_cleaned = Нет выбранных файлов для очистки EXIF\nrust_extracted_exif_tags = Извлечены EXIF теги из { $items_stats } файлов ({ $size_stats })\nrust_delete_confirmation = Вы уверены, что хотите удалить выбранные элементы?\nrust_delete_confirmation_number_simple = { $items } выбранных элементов.\nrust_delete_confirmation_number_groups = { $items } элементов выбрано в { $groups } группах.\nrust_delete_confirmation_selected_all_in_group = Все элементы выбраны в группах { $groups }.\nrust_move_confirmation = Вы уверены, что хотите переместить выбранные элементы?\nrust_move_confirmation_number_simple = { $items } элементов выбрано.\nrust_clean_exif_confirmation = Вы уверены, что хотите удалить EXIF данные из выбранных элементов?\nrust_clean_exif_confirmation_number_simple = { $items } элементов выбрано.\nclean_exif_overwrite_files_text = Перезаписать файлы\nrust_optimize_video_confirmation = Вы уверены, что хотите оптимизировать выбранные видео?\nrust_optimize_video_confirmation_number_simple = { $items } элементов выбрано.\nrust_hardlink_confirmation = Вы уверены, что хотите создать жёсткие ссылки для выбранных элементов?\nrust_hardlink_confirmation_number_simple = { $items } элементов выбрано.\nrust_symlink_confirmation = Вы уверены, что хотите создать символические ссылки для выбранных элементов?\nrust_symlink_confirmation_number_simple = { $items } элементов выбрано.\nrust_rename_confirmation = Вы уверены, что хотите переименовать выбранные элементы?\nrust_rename_confirmation_number_simple = { $items } элементов выбрано.\nrust_cache_processed_files = Обработаны { $files } кэш-файлы\nrust_cache_entries_stats = Удалены { $removed } записей из всех { $all }, { $left } осталось\nrust_cache_size_reduced = Уменьшено размер файлов кэша на { $size }\nrust_cache_time_elapsed = Время прошло: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Не удалось создать жёсткую ссылку { $name } на { $target }, причина { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Выбор\ncolumn_size = Размер\ncolumn_file_name = Имя файла\ncolumn_path = Путь\ncolumn_modification_date = Дата изменения\ncolumn_similarity = Схожесть\ncolumn_dimensions = Размеры\ncolumn_new_dimensions = Новые измерения\ncolumn_title = Заголовок\ncolumn_artist = Художник\ncolumn_year = Год\ncolumn_bitrate = Битрейт\ncolumn_length = Длина\ncolumn_genre = Жанр\ncolumn_type_of_error = Тип ошибки\ncolumn_symlink_name = Название символической ссылки\ncolumn_symlink_folder = Папка Symlink\ncolumn_destination_path = Путь назначения\ncolumn_current_extension = Текущее расширение\ncolumn_proper_extension = Правильное расширение\ncolumn_fps = КПД\ncolumn_codec = Кодек\ncolumn_duration = Продолжительность\ncolumn_exif_tags = EXIF теги\ncolumn_new_name = Новое имя\n# Slint translations\nok_button = Ок\ncancel_button = Отмена\ndo_you_want_to_continue = Вы хотите продолжить?\nmain_window_title = Крокиет - Очистка данных\nscan_button = Сканировать\nstop_button = Остановить\nstop_text = Остановить\nselect_button = Выбрать\nmove_button = Переместить\ndelete_button = Удалить\nsave_button = Сохранить\nsort_button = Сортировка\nrename_button = Переименовать\nmotto = Этот программный код бесплатен для использования и всегда будет miễnеспособным.\\nПосмотрите на лицензию MIT/GPL для получения дополнительной информации.\nunicorn = Вы можете не смотреть на единорога, но единорог всегда смотрит на вас.\nrepository = Репозиторий\ninstruction = Инструкция\ndonation = Пожертвование\ntranslation = Перевод\nincluded_paths = Включенные пути\nexcluded_paths = Исключенные пути\nref = Эталон\npath = Путь\ntool_duplicate_files = Дублировать файлы\ntool_empty_folders = Пустые папки\ntool_big_files = Большие файлы\ntool_empty_files = Пустые файлы\ntool_temporary_files = Временные файлы\ntool_similar_images = Похожие изображения\ntool_similar_videos = Похожие видео\ntool_music_duplicates = Музыкальные дубликаты\ntool_invalid_symlinks = Битые симв. ссылки\ntool_broken_files = Сломанные файлы\ntool_bad_extensions = Плохие расширения\ntool_bad_names = Плохие имена\ntool_video_optimizer = Видео Оптимизатор\ntool_exif_remover = Exif Восстановление\nsort_by_full_name = Сортировать по полному имени\nsort_by_selection = Сортировать по выбору\nsort_reverse = Обратный порядок\nselection_all = Выбрать все\nselection_deselect_all = Отменить выбор\nselection_invert_selection = Инвертировать выделение\nselection_the_biggest_size = Выберите наибольший размер\nselection_the_biggest_resolution = Выберите наибольшее разрешение\nselection_the_smallest_size = Выберите наименьший размер\nselection_the_smallest_resolution = Выберите наименьшее разрешение\nselection_newest = Выберите новейшие\nselection_oldest = Выбрать старые\nselection_shortest_path = Выберите кратчайший путь\nselection_longest_path = Выберите самый длинный путь\nstage_current = Текущая стадия:\nstage_all = Все стадии:\nsubsettings = Поднастройки\nsubsettings_images_hash_size = Размер хэша\nsubsettings_images_resize_algorithm = Изменение размера алгоритма\nsubsettings_images_ignore_same_size = Игнорировать изображения с одинаковым размером\nsubsettings_images_max_difference = Макс. разница\nsubsettings_images_duplicates_hash_type = Тип хэша\nsubsettings_duplicates_check_method = Метод проверки\nsubsettings_duplicates_name_case_sensitive = Чувствительно к регистру (только режим имени)\nsubsettings_biggest_files_sub_method = Метод\nsubsettings_biggest_files_sub_number_of_files = Количество файлов\nsubsettings_videos_max_difference = Макс. разница\nsubsettings_videos_ignore_same_size = Игнорировать видео с одинаковым размером\nsubsettings_music_audio_check_type = Тип проверки аудио\nsubsettings_music_approximate_comparison = Приблизительное сравнение тегов\nsubsettings_music_compared_tags = По сравнению тегов\nsubsettings_music_title = Заголовок\nsubsettings_music_artist = Художник\nsubsettings_music_bitrate = Битрейт\nsubsettings_music_genre = Жанр\nsubsettings_music_year = Год\nsubsettings_music_length = Длина\nsubsettings_music_max_difference = Макс. разница\nsubsettings_music_minimal_fragment_duration = Минимальная длительность фрагмента\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Сравнить внутри групп с одинаковыми названиями\nsubsettings_broken_files_type = Тип файлов для проверки\nsubsettings_broken_files_audio = Аудио\nsubsettings_broken_files_pdf = Пdf\nsubsettings_broken_files_archive = Архивировать\nsubsettings_broken_files_image = Изображение\nsubsettings_broken_files_video = Видео\nsubsettings_broken_files_video_info = Использует ffmpeg/ffprobe. Очень медленно и может обнаруживать педантичные ошибки, даже если файл воспроизводится нормально.\nsubsettings_bad_names_issues = Проверка файлов\nsubsettings_bad_names_uppercase_extension = Верхнее расширение\nsubsettings_bad_names_uppercase_extension_hint = Находит файлы с заглавными буквами в расширении (например, .JPG, .Mp3) и предлагает их строчную версию\nsubsettings_bad_names_emoji_used = Эмодзи в имени\nsubsettings_bad_names_emoji_used_hint = Находит файлы с символами эмодзи (😀, 🎉 и т.д.) в имени и предлагает их удалять\nsubsettings_bad_names_space_at_start_end = Отступы в начале/конце строки\nsubsettings_bad_names_space_at_start_end_hint = Находит файлы с пробелами в начале или в конце имени и предлагает обрезать их\nsubsettings_bad_names_non_ascii = Не-ASCII символы\nsubsettings_bad_names_non_ascii_hint = Находит не-ASCII символы (ą, ć, ñ и т.д.) и предлагает заменить их на ASCII эквиваленты (a, c, n) или удалить, если не существует сопоставления\nsubsettings_bad_names_restricted_charset = Ограниченный набор символов\nsubsettings_bad_names_restricted_charset_hint = Транслитерирует не-ASCII символы в ASCII, затем находит файлы, содержащие символы вне диапазона 0-9a-zA-Z и разрешенных пользователем символов\nsubsettings_bad_names_allowed_chars = Разрешенные символы\nsubsettings_bad_names_remove_duplicated = Дублированные символы\nsubsettings_bad_names_remove_duplicated_hint = Находит последовательные дубликаты неалфанумеровых символов (например, \"file---name..txt\") и предлагает удалять дубликаты\nsettings_global_settings = Глобальные настройки\nsettings_dark_theme = Темная тема\nsettings_show_only_icons = Показывать только иконки\nsettings_excluded_items = Исключенный предмет:\nsettings_allowed_extensions = Допустимые расширения:\nsettings_excluded_extensions = Исключенные расширения:\nsettings_file_size = Размер файла (килобайты)\nsettings_minimum_file_size = Мин:\nsettings_maximum_file_size = Макс:\nsettings_recursive_search = Рекурсивный поиск\nsettings_use_cache = Использовать кэш\nsettings_save_as_json = Также сохранить кэш как файл JSON\nsettings_move_to_trash = Переместить удаленные файлы в корзину\nsettings_ignore_other_filesystems = Игнорировать другие файловые системы (только Linux)\nsettings_delete_outdated_cache_entries = Удалить автоматически устаревшие записи кэша\nsettings_delete_outdated_cache_entries_hint = Когда включено, приложение будет проверять во время загрузки кэша (не более одного раза в неделю), действительно ли записи в кэше указывают на существующие и неизмененные файлы/данные\nsettings_hide_hard_links = Скрыть жёсткие ссылки\nsettings_hide_hard_links_hint = Скрыть жесткие ссылки на одинаковые файлы в результатах\nsettings_thread_number = Номер потока\nsettings_restart_required = ---Вам нужно перезапустить приложение, чтобы применить изменения в номер потока---\nsettings_duplicate_image_preview = Предпросмотр изображения\nsettings_duplicate_minimal_hash_cache_size = Минимальный размер кэшированных файлов - Хэш (KB)\nsettings_duplicate_use_prehash = Использовать prehash\nsettings_duplicate_minimal_prehash_cache_size = Минимальный размер кэшированных файлов - Prehash (KB)\nsettings_similar_images_show_image_preview = Предпросмотр изображения\nsettings_application_scale_text = Масштаб заявки\nsettings_application_scale_hint_text = Когда ручной масштаб включен, это позволяет вам выбрать пользовательский коэффициент масштабирования, но полностью отключает автоматическое масштабирование на основе DPI монитора.\nsettings_restart_required_scale_text = ---Вам необходимо перезапустить приложение, чтобы применить изменения в масштабе---\nsettings_use_manual_application_scale_text = Использовать ручной масштаб применения\nsettings_video_thumbnails_preview = Предпросмотр изображения\nsettings_open_config_folder = Открыть папку конфигурации\nsettings_open_cache_folder = Открыть папку кэша\nsettings_language = Язык\nsettings_current_preset = Текущая настройка:\nsettings_edit_name = Изменить имя\nsettings_choose_name_for_prefix = Выберите имя для префикса\nsettings_save = Сохранить\nsettings_load = Нагрузка\nsettings_reset = Сброс\nsettings_similar_videos_tool = Похожие видео инструменты\nsettings_video_thumbnails_clear_unused_thumbnails = Удалить неиспользуемые видеопревью старше 7 дней при запуске приложения\nsettings_video_thumbnails_header = Видеообложки\nsettings_video_thumbnails_generate = Сгенерировать миниатюры\nsettings_video_thumbnails_position = Миниатюра позиция в видео (%)\nsettings_video_thumbnails_generate_grid = Сгенерировать сетку миниатюр вместо одного изображения\nsettings_video_thumbnails_generate_grid_hint = Генерация нескольких изображений в виде сетки происходит значительно медленнее, чем генерация одного миниатюрного изображения\nsettings_video_thumbnails_grid_tiles_per_side = Количество плиток на стороне в миниатюрной сетке\nsettings_video_thumbnails_grid_tiles_per_side_hint = Количество миниатюрных плиток с одной стороны сетки. Например, выбор 2 создает сетку 2 x 2, в результате чего получается одна миниатюра, состоящая из 4 изображений.\nsettings_similar_images_tool = Похожие изображения\nsettings_general_settings = Общие настройки\nsettings_cache_header_text = Настройки кэша\nsettings_clean_cache_button_text = Очистить устаревшее кэширование\nsettings_settings = Настройки\nsettings_load_tabs_sizes_at_startup = Размер вкладок при запуске\nsettings_load_windows_size_at_startup = Загружать размер окон при запуске\nsettings_limit_lines_of_messages = Ограничение сообщений до 500 строк(общение для виджета медленного TextEdit)\nsettings_play_audio_on_scan_completion_text = Воспроизвести звук при успешном сканировании\nsettings_audio_feature_hint_text = Доступно только при компиляции с аудио-функцией\nsettings_audio_env_variable_hint_text = Звук можно изменить, установив переменную среды KROKIET_AUDIO_STOP_FILE в действительный путь к аудиофайлу\npopup_save_title = Сохранение результатов\npopup_save_message = Это сохранит результаты в 3 разных файла\npopup_rename_title = Переименование файлов\npopup_new_paths_title = Пожалуйста, добавьте пути по одному на строку\npopup_move_title = Перемещение файлов\npopup_move_copy_checkbox = Копировать файлы вместо перемещения\npopup_move_preserve_folder_checkbox = Сохранить структуру папок\nmove_confirmation_text = Вы уверены, что хотите переместить выбранные элементы?\nrename_confirmation_text = Вы уверены, что хотите переименовать выбранные элементы?\ndelete = Удалить элементы\nstopping_scan = Остановка сканирования, пожалуйста, подождите...\nsearching = Поиск...\nsubsettings_videos_crop_detect = Метод обнаружения обрезания\nsubsettings_videos_skip_forward_amount = Пропустить [сек]\nsubsettings_videos_vid_hash_duration = Длительность хэша видео\nsettings_cache_number_size_text = Размер кэша: { $size }, количество файлов: { $number }\nsettings_video_thumbnails_number_size_text = Размер миниатюр видео: { $size }, количество файлов: { $number }\nsettings_log_number_size_text = Размер файлов: { $size }, количество файлов: { $number }\npopup_clean_cache_title_text = Очистить устаревшее кэширование\npopup_clean_cache_confirmation_text = Вы уверены, что хотите очистить устаревшие записи кэша? Это удалит записи кэша для файлов, которые больше не существуют или были изменены.\npopup_clean_cache_progress_text = Обработка кэш-файла:\npopup_clean_cache_current_file_text = Текущий файл:\npopup_clean_cache_file_progress_text = Текущий прогресс файла:\npopup_clean_cache_overall_progress_text = Общий прогресс:\npopup_clean_cache_stopped_by_user_text = Очистка кэша была остановлена пользователем\npopup_clean_cache_finished_text = Очистка кэша завершена успешно!\npopup_clean_cache_error_details_text = Ошибка деталей:\npopup_clean_cache_files_with_errors = Файлы с ошибками:\nsubsettings_video_optimizer_mode = Режим\nsubsettings_video_optimizer_crop_type = Тип культуры\nsubsettings_video_optimizer_black_pixel_threshold = Чёрный Пиксельный Порог\nsubsettings_video_optimizer_black_pixel_threshold_hint = Максимальное значение RGB для каждого цветового канала, которое будет считаться черным (0-128). По умолчанию: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Черная полоса минимального процента\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Минимальный процент черных пикселей в строке/столбце, который должен быть признан черной полосой (50-100). По умолчанию: 90\nsubsettings_video_optimizer_max_samples = Макс Сэмплз\nsubsettings_video_optimizer_max_samples_hint = Максимальное количество кадров для анализа на видео (5-1000). По умолчанию: 60\nsubsettings_video_optimizer_min_crop_size = Минимальный размер обрезки\nsubsettings_video_optimizer_min_crop_size_hint = Минимальное количество пикселей для обрезки с любой стороны (1-1000). Меньшие обрезки игнорируются. Значение по умолчанию: 5\nsubsettings_video_optimizer_video_codec = Видео кодек\nsubsettings_video_optimizer_excluded_codecs = Исключены кодеки\nsubsettings_video_optimizer_video_quality = Видео качество (CRF)\nsubsettings_reset = Сбросить\nsubsettings_exif_ignored_tags_text = Игнорируемые теги:\nsubsettings_exif_ignored_tags_hint_text = Список тегов, разделенных запятыми, для исключения из сканирования (например, GPS, Превью). Некоторые теги, такие как ImageWidth в файлах TIFF, скрыты, чтобы предотвратить повреждение изображения.\nclean_button_text = Чистый\nclean_text = Очистить EXIF данные\nclean_confirmation_text = Вы уверены, что хотите удалить EXIF данные из выбранных элементов?\ncrop_videos_text = Обрезать видео\ncrop_video_confirmation_text = Вы уверены, что хотите обрезать выбранные видео?\ncrop_reencode_video_text = Перекодировать видео\nreencode_videos_text = Перекодировать видео\noptimize_button_text = Оптимизировать\noptimize_confirmation_text = Вы уверены, что хотите перекодировать выбранные видео?\noptimize_fail_if_bigger_text = Не удается, если оптимизированный файл больше\noptimize_overwrite_files_text = Перезаписать файлы\noptimize_limit_video_size_text = Ограничьте размер видео\noptimize_max_width_text = Максимальная ширина:\noptimize_max_height_text = Максимальная высота:\nhardlink_button_text = Жёсткая ссылка\nhardlink_text = Создать жёсткие ссылки\nhardlink_confirmation_text = Вы уверены, что хотите создать жёсткие ссылки для выбранных элементов?\nsoftlink_button_text = Софтссылка\nsoftlink_text = Создать символические ссылки\nsoftlink_confirmation_text = Вы уверены, что хотите создать символические ссылки (symlinks) для выбранных элементов?\n"
  },
  {
    "path": "krokiet/i18n/sv-SE/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Kritisk Fel Vid Applikationsstart\nrust_init_error_message = \n        Ett allvarligt fel inträffade vid start av applikationen:\n\n        { $error_message }\n\n        Detta kan bero på saknade eller felaktiga OpenGL/Vulkan-drivrutiner, att applikationen körs i en virtuell maskin eller ett fel i Krokiet eller någon av dess bibliotek.\n\n        Du kan försöka köra olika byggen (skia_opengl, skia_vulkan, femtovg_opengl - standard) eller med en renderingsmotor för att se om det löser problemet.\nrust_loaded_preset = Laddad förinställning { $preset_idx }\nrust_file_already_exists = Fil \"{ $file }\" finns redan och kommer inte att skrivas över\nrust_error_removing_file_after_copy = Fel vid borttagning av fil \"{ $file }\" (efter kopiering till en annan partition), anledning: { $reason }\nrust_error_copying_file = Fel vid kopiering av \"{ $input }\" till \"{ $output }\", anledning: { $reason }\nrust_loading_tags_cache = Laddar taggar cache\nrust_loading_fingerprints_cache = Laddar fingeravtryck cache\nrust_saving_tags_cache = Sparar cache för taggar\nrust_saving_fingerprints_cache = Sparar cache för fingeravtryck\nrust_loading_prehash_cache = Laddar prehash cache\nrust_saving_prehash_cache = Sparar Omfattande cache\nrust_loading_hash_cache = Laddar hash-cache\nrust_saving_hash_cache = Sparar hash-cache\nrust_loading_exif_cache = Ladda EXIF cache\nrust_saving_exif_cache = Spara EXIF cache\nrust_scanning_name = Skannar namnet på { $entries_checked } fil\nrust_scanning_size_name = Skannar storlek och namn på { $entries_checked } fil\nrust_scanning_size = Skannar storlek på { $entries_checked } fil\nrust_scanning_file = Skannar { $entries_checked } fil\nrust_scanning_folder = Skannar { $entries_checked } mapp\nrust_checked_tags = Kontrollerade taggar av { $items_stats }\nrust_checked_content = Kontrollerat innehåll i { $items_stats } ({ $size_stats })\nrust_compared_tags = Jämförda taggar av { $items_stats }\nrust_compared_content = Jämfört innehåll i { $items_stats }\nrust_hashed_images = Hashade { $items_stats } bilder ({ $size_stats })\nrust_compared_image_hashes = Jämfört bildhashen av { $items_stats }\nrust_hashed_videos = Hashade { $items_stats } videor\nrust_created_thumbnails = Skapade miniatyrer för { $items_stats } videor\nrust_checked_files = Kontrollerad { $items_stats } fil ({ $size_stats })\nrust_checked_files_bad_extensions = Kontrollerad { $items_stats } fil\nrust_checked_files_bad_names = Kontrollerad { $items_stats } fil\nrust_checked_videos = Kontrollerat { $items_stats } videor ({ $size_stats })\nrust_analyzed_partial_hash = Analyserad partiell hash av { $items_stats } filer ({ $size_stats })\nrust_analyzed_full_hash = Analyserad full hash av { $items_stats } filer ({ $size_stats })\nrust_failed_to_rename_file = Det gick inte att byta namn på filen { $old_path } till { $new_path }, fel: { $error }\nrust_no_included_paths = Kan inte starta skanning när inga inkluderade sökvägar är inställda.\nrust_all_paths_referenced = Kan inte starta skanning när alla inkluderade sökvägar är inställda som referenssökvägar, du måste inaktivera kryssrutan bredvid ingångssökväg.\nrust_found_empty_folders = Hittade { $items_found } tomma mappar i { $time }\nrust_found_empty_files = Hittade { $items_found } tomma filer i { $time }\nrust_found_similar_images = Hittade { $items_found } liknande bildfiler i { $groups } grupper i { $time }\nrust_found_similar_videos = Hittade { $items_found } liknande videofiler i { $groups } grupper i { $time }\nrust_found_similar_music_files = Hittade { $items_found } liknande musikfiler i { $groups } grupper i { $time }\nrust_found_invalid_symlinks = Hittade { $items_found } ogiltiga symlänkar i { $time }\nrust_found_temporary_files = Hittade { $items_found } temporära filer i { $time }\nrust_no_file_type_selected = Kan inte hitta trasiga filer utan någon vald filtyp.\nrust_found_broken_files = Hittade { $items_found } trasiga filer som tog { $size } i { $time }\nrust_found_bad_extensions = Hittade { $items_found } filer med dåliga tillägg i { $time }\nrust_found_bad_names = Hittade { $items_found } filer med dåliga namn i { $time}\nrust_found_video_optimizer = Hittade { $items_found } filer att optimera i { $time }\nrust_found_duplicate_files = Hittade { $items_found } duplicerade filer i { $groups } grupper som tog { $size } i { $time }\nrust_found_duplicate_files_no_lost_space = Hittade { $items_found } dubblettfiler i { $groups } grupper i { $time }\nrust_found_big_files = Hittade { $items_found } stora filer med storlek { $size } i { $time }\nrust_found_exif_files = Hittade { $items_found } filer med exif-data i { $time }\nrust_cannot_load_preset = Kan inte ändra och ladda förinställda { $preset_idx } - anledning { $reason }, med standardinställningar istället\nrust_saved_preset = Sparad förinställning { $preset_idx }\nrust_cannot_save_preset = Kan inte spara förinställda { $preset_idx } - anledning { $reason }\nrust_reset_preset = Resettkrets { $preset_idx }\nrust_cannot_create_output_folder = Kan inte skapa utdatamappen { $output_folder }anledning: { $error }\nrust_delete_summary = Borttagna { $deleted } objekt, misslyckades med att ta bort { $failed } objekt, av { $total } objekt\nrust_rename_summary = Döp om { $renamed } objekt, kunde inte döpa om { $failed } objekt, av { $total } objekt\nrust_move_summary = Flyttade { $moved } objekt, kunde inte flytta { $failed } objekt, av { $total } objekt\nrust_hardlink_summary = Hardlänkade { $hardlinked } objekt, misslyckades med att hardlänka { $failed } objekt, av { $total } objekt\nrust_symlink_summary = Symboliska länkade { $symlinked } objekt, misslyckades med att symboliska länkade { $failed } objekt, av { $total } objekt\nrust_optimize_video_summary = Optimerade { $optimized } videor, misslyckades att optimera { $failed } videor, av { $total } videor\nrust_clean_exif_summary = Rengjord EXIF från { $cleaned } filer, misslyckades med att rengöra { $failed } filer, av { $total } filer\nrust_deleting_files = Raderar { $items_stats } fil ({ $size_stats })\nrust_deleting_no_size_files = Raderar { $items_stats } fil\nrust_renaming_files = Döper om { $items_stats } fil\nrust_moving_files = Flyttar { $items_stats } fil ({ $size_stats })\nrust_moving_no_size_files = Flyttar { $items_stats } fil\nrust_hardlinking_files = Hardlinking { $items_stats } fil ({ $size_stats })\nrust_hardlinking_no_size_files = Hardlinking { $items_stats } fil\nrust_symlinking_files = Symlänka { $items_stats } filen ({ $size_stats })\nrust_symlinking_no_size_files = Symlänka { $items_stats } fil\nrust_optimizing_videos = Optimerad { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimerad { $items_stats } video\nrust_cleaning_exif = Rensa EXIF från { $items_stats } filen ({ $size_stats })\nrust_cleaning_no_size_exif = Rensa EXIF från { $items_stats } fil\nrust_no_files_deleted = Inga filer eller mappar valda för borttagning\nrust_no_files_renamed = Inga filer eller mappar valda för att byta namn\nrust_no_files_moved = Inga filer eller mappar valda för att flytta\nrust_no_files_hardlinked = Inga filer eller mappar har valts för hårdlänkning\nrust_no_files_symlinked = Inga filer eller mappar har valts för symbolisk länkning\nrust_no_videos_optimized = Inga videor valdes för optimering\nrust_no_exif_cleaned = Inga filer valdes för EXIF-rengöring\nrust_extracted_exif_tags = Extraherade EXIF-taggar från { $items_stats } filer ({ $size_stats })\nrust_delete_confirmation = Är du säker på att du vill ta bort de markerade objekten?\nrust_delete_confirmation_number_simple = { $items } objekt valda.\nrust_delete_confirmation_number_groups = { $items } objekt valda i { $groups } grupper.\nrust_delete_confirmation_selected_all_in_group = Alla objekt valda i { $groups } grupper.\nrust_move_confirmation = Är du säker på att du vill flytta de valda objekten?\nrust_move_confirmation_number_simple = { $items } föremål valda.\nrust_clean_exif_confirmation = Är du säker på att du vill ta bort EXIF-data från de valda objekten?\nrust_clean_exif_confirmation_number_simple = { $items } föremål valda.\nclean_exif_overwrite_files_text = Överskriv filer\nrust_optimize_video_confirmation = Är du säker på att du vill optimera de valda videorna?\nrust_optimize_video_confirmation_number_simple = { $items } föremål valda.\nrust_hardlink_confirmation = Är du säker på att du vill skapa hårda länkar för de valda objekten?\nrust_hardlink_confirmation_number_simple = { $items } föremål valda.\nrust_symlink_confirmation = Är du säker på att du vill skapa symboliska länkar för de valda objekten?\nrust_symlink_confirmation_number_simple = { $items } föremål valda.\nrust_rename_confirmation = Är du säker på att du vill omdöpa de valda objekten?\nrust_rename_confirmation_number_simple = { $items } föremål valda.\nrust_cache_entries_stats = Borttagna { $removed } posteringar ut av alla { $all }, { $left } kvar\nrust_cache_size_reduced = Minskade cachefilers storlek med { $size }\nrust_cache_time_elapsed = Tid som gått: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Misslyckades med att skapa hårdlänk { $name } till { $target }, anledning { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Urval\ncolumn_size = Storlek\ncolumn_file_name = Filnamn\ncolumn_path = Sökväg\ncolumn_modification_date = Ändrad datum\ncolumn_similarity = Likhet\ncolumn_dimensions = Dimensioner\ncolumn_new_dimensions = Nya dimensioner\ncolumn_title = Titel\ncolumn_artist = Konstnären\ncolumn_year = År\ncolumn_bitrate = Bithastighet\ncolumn_length = Längd\ncolumn_genre = Genrer\ncolumn_type_of_error = Typ av fel\ncolumn_symlink_name = Symlink namn\ncolumn_symlink_folder = Symlink mapp\ncolumn_destination_path = Sökväg till destination\ncolumn_current_extension = Nuvarande tillägg\ncolumn_proper_extension = Rätt tillägg\ncolumn_fps = FPS\ncolumn_codec = Kodek\ncolumn_duration = Varaktighet\ncolumn_exif_tags = EXIF-taggar\ncolumn_new_name = Nytt Namn\n# Slint translations\nok_button = OK\ncancel_button = Avbryt\ndo_you_want_to_continue = Vill du fortsätta?\nmain_window_title = Krokiet - Datarensare\nscan_button = Skanna\nstop_button = Stoppa\nstop_text = Stoppa\nselect_button = Välj\nmove_button = Flytta\ndelete_button = Radera\nsave_button = Spara\nsort_button = Sortera\nrename_button = Döp om\nmotto = Detta program är gratis att använda och kommer alltid att vara.\\nSe MIT/GPL-licensen för detaljer.\nunicorn = Du får inte titta på en enhörning, men enhörningen alltid tittar på dig.\nrepository = Utveckling\ninstruction = Instruktion\ndonation = Donation\ntranslation = Översättning\nincluded_paths = Inkluderade Sökvägar\nexcluded_paths = Exkluderade Sökvägar\nref = Referens\npath = Sökväg\ntool_duplicate_files = Duplicera filer\ntool_empty_folders = Tomma mappar\ntool_big_files = Stora filer\ntool_empty_files = Tomma filer\ntool_temporary_files = Tillfälliga filer\ntool_similar_images = Liknande bilder\ntool_similar_videos = Liknande videor\ntool_music_duplicates = Musik Duplicerar\ntool_invalid_symlinks = Ogiltiga Symlinks\ntool_broken_files = Trasiga filer\ntool_bad_extensions = Dåliga tillägg\ntool_bad_names = Dåliga namn\ntool_video_optimizer = Videooptimerare\ntool_exif_remover = Exif Ta bort\nsort_by_full_name = Sortera efter fullständigt namn\nsort_by_selection = Sortera efter urval\nsort_reverse = Omvänd ordning\nselection_all = Markera alla\nselection_deselect_all = Avmarkera alla\nselection_invert_selection = Invertera markering\nselection_the_biggest_size = Välj den största storleken\nselection_the_biggest_resolution = Välj den största upplösningen\nselection_the_smallest_size = Välj den minsta storleken\nselection_the_smallest_resolution = Välj den minsta upplösning\nselection_newest = Välj nyaste\nselection_oldest = Välj äldsta\nselection_shortest_path = Välj den kortaste vägen\nselection_longest_path = Välj den längsta vägen\nstage_current = Nuvarande stadium:\nstage_all = Alla etapper:\nsubsettings = Underinställningar\nsubsettings_images_hash_size = Hashstorlek\nsubsettings_images_resize_algorithm = Ändra storlek på algoritmen\nsubsettings_images_ignore_same_size = Ignorera bilder med samma storlek\nsubsettings_images_max_difference = Max skillnad\nsubsettings_images_duplicates_hash_type = Hash typ\nsubsettings_duplicates_check_method = Kontrollera metod\nsubsettings_duplicates_name_case_sensitive = Case Sensitive(endast namnlägen)\nsubsettings_biggest_files_sub_method = Metod\nsubsettings_biggest_files_sub_number_of_files = Antal filer\nsubsettings_videos_max_difference = Max skillnad\nsubsettings_videos_ignore_same_size = Ignorera videor med samma storlek\nsubsettings_music_audio_check_type = Typ av ljudkontroll\nsubsettings_music_approximate_comparison = Ungefärlig Tag Jämförelse\nsubsettings_music_compared_tags = Jämförda taggar\nsubsettings_music_title = Titel\nsubsettings_music_artist = Konstnären\nsubsettings_music_bitrate = Bithastighet\nsubsettings_music_genre = Genrer\nsubsettings_music_year = År\nsubsettings_music_length = Längd\nsubsettings_music_max_difference = Max skillnad\nsubsettings_music_minimal_fragment_duration = Minimal längd på fragment\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Jämför inom grupper med liknande titlar\nsubsettings_broken_files_type = Typ av filer att kontrollera\nsubsettings_broken_files_audio = Ljud\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Arkivera\nsubsettings_broken_files_image = Bild\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = Använder ffmpeg/ffprobe. Ganska långsam och kan upptäcka pedantiska fel även om filen spelas fint.\nsubsettings_bad_names_issues = Filnamnkontroller\nsubsettings_bad_names_uppercase_extension = Stora utökning\nsubsettings_bad_names_uppercase_extension_hint = Hittar filer med versaler i filändelse (t.ex. .JPG, .Mp3) och föreslår en nedtryckt version\nsubsettings_bad_names_emoji_used = Emoji i namn\nsubsettings_bad_names_emoji_used_hint = Hittar filer med emoji-tecken (😀, 🎉, etc.) i namnet och föreslår att de tas bort\nsubsettings_bad_names_space_at_start_end = Ledande/efterföljande blanksteg\nsubsettings_bad_names_space_at_start_end_hint = Hittar filer med blanksteg i början eller slutet av namnet och föreslår att de klipps bort\nsubsettings_bad_names_non_ascii = Ej-ASCII tecken\nsubsettings_bad_names_non_ascii_hint = Hittar icke-ASCII-tecken (ą, ć, ñ, etc.) och föreslår att de ersätts med ASCII-ekvivalenter (a, c, n) eller tas bort om ingen mappning finns\nsubsettings_bad_names_restricted_charset = Begränsat teckenuppsättning\nsubsettings_bad_names_restricted_charset_hint = Transkriberar icke-ASCII tecken till ASCII, sedan söker efter filer som innehåller tecken utanför 0-9a-zA-Z och användardefinierade tillåtna tecken\nsubsettings_bad_names_allowed_chars = Tillåtna tecken\nsubsettings_bad_names_remove_duplicated = Duplicerade tecken\nsubsettings_bad_names_remove_duplicated_hint = Hittar sekventiella dubblerade icke-alfanumeriska tecken (t.ex. \"fil---namn..txt\") och föreslår att dubblerna tas bort\nsettings_global_settings = Globala inställningar\nsettings_dark_theme = Mörkt tema\nsettings_show_only_icons = Visa endast ikoner\nsettings_excluded_items = Exkluderat objekt:\nsettings_allowed_extensions = Tillåtna tillägg:\nsettings_excluded_extensions = Uteslutna tillägg:\nsettings_file_size = Filstorlek (Kilobytar)\nsettings_minimum_file_size = Min:\nsettings_maximum_file_size = Max:\nsettings_recursive_search = Rekursiv sökning\nsettings_use_cache = Använd cache\nsettings_save_as_json = Spara även cache som JSON-fil\nsettings_move_to_trash = Flytta raderade filer till papperskorgen\nsettings_ignore_other_filesystems = Ignorera andra filsystem (endast Linux)\nsettings_delete_outdated_cache_entries = Radera automatiskt utdaterade cacheposter\nsettings_delete_outdated_cache_entries_hint = När det är aktiverat kommer appen att verifiera under cache-laddning (högst en gång per vecka) om de cachelagrade posterna fortfarande pekar på befintliga och oförändrade filer/data\nsettings_hide_hard_links = Dölj hårda länkar\nsettings_hide_hard_links_hint = Dölj hårda länkar till samma filer i resultaten\nsettings_thread_number = Tråd nummer\nsettings_restart_required = ---Du måste starta om appen för att tillämpa ändringar i trådens nummer---\nsettings_duplicate_image_preview = Förhandsgranskning av bild\nsettings_duplicate_minimal_hash_cache_size = Minimal storlek på cachade filer - Hash (KB)\nsettings_duplicate_use_prehash = Använd prehash\nsettings_duplicate_minimal_prehash_cache_size = Minimal storlek på cachade filer - Prehash (KB)\nsettings_similar_images_show_image_preview = Förhandsgranskning av bild\nsettings_application_scale_text = Ansökningsomfattning\nsettings_application_scale_hint_text = När manuell skala är aktiverad, vilket gör att du kan välja en anpassad skalningsfaktor, men helt inaktiverar automatisk skalning baserat på monitorns DPI.\nsettings_restart_required_scale_text = ---Du måste starta om appen för att tillämpa ändringar i skala---\nsettings_use_manual_application_scale_text = Använd manuell applikationsskala\nsettings_video_thumbnails_preview = Bildförhandsvisning\nsettings_open_config_folder = Öppna konfigurationsmappen\nsettings_open_cache_folder = Öppna cache-mapp\nsettings_language = Språk\nsettings_current_preset = Nuvarande förinställning:\nsettings_edit_name = Redigera namn\nsettings_choose_name_for_prefix = Välj namn för prefix\nsettings_save = Spara\nsettings_load = Ladda\nsettings_reset = Genomstart\nsettings_similar_videos_tool = Liknande Videor verktyg\nsettings_video_thumbnails_clear_unused_thumbnails = Radera oanvända videobilder större än 7 dagar vid appstart\nsettings_video_thumbnails_header = Videominiatyrer\nsettings_video_thumbnails_generate = Generera miniatyrbilder\nsettings_video_thumbnails_position = Miniatyrposition i video (%)\nsettings_video_thumbnails_generate_grid = Generera miniatyr-grid istället för enstaka bild\nsettings_video_thumbnails_generate_grid_hint = Generera flera bilder i rutnät är mycket långsammare än att generera enskilda miniatyrbilder\nsettings_video_thumbnails_grid_tiles_per_side = Antal klossar per sida i miniatyrnätet\nsettings_video_thumbnails_grid_tiles_per_side_hint = Antalet miniatyrrutor per sida i rutnätet. Till exempel, genom att välja 2 skapas ett 2 x 2 rutnät, vilket resulterar i en enda miniatyrbild som består av 4 bilder.\nsettings_similar_images_tool = Liknande bilder verktyg\nsettings_general_settings = Allmänna inställningar\nsettings_cache_header_text = Cacheinställningar\nsettings_clean_cache_button_text = Rensa gammal cache\nsettings_settings = Inställningar\nsettings_load_tabs_sizes_at_startup = Ladda flikar storlekar vid start\nsettings_load_windows_size_at_startup = Ladda fönsterstorlek vid uppstart\nsettings_limit_lines_of_messages = Begränsa meddelanden till 500 rader (workaround för långsam TextEdit widget)\nsettings_play_audio_on_scan_completion_text = Spela upp ljud när skanningen slutförs framgångsrikt\nsettings_audio_feature_hint_text = Tillgängligt endast vid kompilering med ljudfunktion\nsettings_audio_env_variable_hint_text = Ljud kan ändras, genom att ställa KROKIET_AUDIO_STOP_FILE miljövariabeln till en giltig sökväg till en ljudfil\npopup_save_title = Sparar resultat\npopup_save_message = Detta kommer att spara resultat till 3 olika filer\npopup_rename_title = Byter namn på filer\npopup_new_paths_title = Vänligen lägg till sökvägar en per rad\npopup_move_title = Flyttar filer\npopup_move_copy_checkbox = Kopiera filer istället för att flytta\npopup_move_preserve_folder_checkbox = Bevara mappstruktur\nmove_confirmation_text = Är du säker på att du vill flytta de valda objekten?\nrename_confirmation_text = Är du säker på att du vill omdöpa de valda objekten?\ndelete = Ta bort objekt\nstopping_scan = Slutar söka, vänligen vänta...\nsearching = Söker...\nsubsettings_videos_crop_detect = Beskär detekteringsmetod\nsubsettings_videos_skip_forward_amount = Hoppa över varaktighet [s]\nsubsettings_videos_vid_hash_duration = Video hash-varaktighet\nsettings_cache_number_size_text = Cache-filstorlek: { $size }, antal filer: { $number }\nsettings_video_thumbnails_number_size_text = Storlek på video-miniatyrbilder: { $size }, antal filer: { $number }\nsettings_log_number_size_text = Storlek på loggfiler: { $size }, antal filer: { $number }\npopup_clean_cache_title_text = Rensa Utanförgiltig Cache\npopup_clean_cache_confirmation_text = Är du säker på att du vill rensa gamla cacheposter? Detta kommer att ta bort cacheposter för filer som inte längre finns eller har ändrats.\npopup_clean_cache_progress_text = Bearbeta cachefil:\npopup_clean_cache_current_file_text = Aktuell fil:\npopup_clean_cache_file_progress_text = Nuvarande filens framsteg:\npopup_clean_cache_overall_progress_text = Övergripande framsteg:\npopup_clean_cache_stopped_by_user_text = Cache rensning stoppades av användare\npopup_clean_cache_finished_text = Cache rensning slutförd framgångsrikt!\npopup_clean_cache_error_details_text = Feldetaljer:\npopup_clean_cache_files_with_errors = Felaktiga filer:\nsubsettings_video_optimizer_mode = Läge\nsubsettings_video_optimizer_crop_type = Skördetyp\nsubsettings_video_optimizer_black_pixel_threshold = Svart Pixeltillförändring\nsubsettings_video_optimizer_black_pixel_threshold_hint = Maximal RGB-värde för varje färgkanal att betraktas som svart (0-128). Standard: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Svart Balk Min Procent\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Minimum procentandel av svarta pixlar i en rad/kolumn som ska anses vara en svart balk (50-100). Standard: 90\nsubsettings_video_optimizer_max_samples = Maximalt antal sampel\nsubsettings_video_optimizer_max_samples_hint = Maximalt antal bilder att analysera per video (5-1000). Standard: 60\nsubsettings_video_optimizer_min_crop_size = Min beskärningsstorlek\nsubsettings_video_optimizer_min_crop_size_hint = Minimum pixlar att beskära på någon sida (1-1000). Mindre beskäringar ignoreras. Standard: 5\nsubsettings_video_optimizer_video_codec = Video-codec\nsubsettings_video_optimizer_excluded_codecs = Exkluderade codecs\nsubsettings_video_optimizer_video_quality = Video kvalitet (CRF)\nsubsettings_reset = Återställ\nsubsettings_exif_ignored_tags_text = Ignorera taggar:\nsubsettings_exif_ignored_tags_hint_text = Komma-separerad lista med taggar att exkludera från skanning (t.ex. GPS, Thumbnail). Vissa taggar, såsom ImageWidth i TIFF-filer, är dolda för att förhindra att bilden går sönder.\nclean_button_text = Rensa\nclean_text = Rensa EXIF-data\nclean_confirmation_text = Är du säker på att du vill ta bort EXIF-data från de valda objekten?\ncrop_videos_text = Klipp videor\ncrop_video_confirmation_text = Är du säker på att du vill beskära de valda videorna?\ncrop_reencode_video_text = Omkoda video\nreencode_videos_text = Omkoda videor\noptimize_button_text = Optimera\noptimize_confirmation_text = Är du säker på att du vill omkoda de valda videorna?\noptimize_fail_if_bigger_text = Misslyckas om det optimerade filen är större\noptimize_overwrite_files_text = Överskriv filer\noptimize_limit_video_size_text = Begränsa videokvalitet\noptimize_max_width_text = Max bredd:\noptimize_max_height_text = Max höjd:\nhardlink_button_text = Hårdlänk\nhardlink_text = Skapa hålänkar\nhardlink_confirmation_text = Är du säker på att du vill skapa hårda länkar för de valda objekten?\nsoftlink_button_text = Softlink\nsoftlink_text = Skapa symboliska länkar\nsoftlink_confirmation_text = Är du säker på att du vill skapa symboliska länkar (symlinks) för de valda objekten?\n\nrust_cache_processed_files = Bearbetade { $files } cache-filer"
  },
  {
    "path": "krokiet/i18n/tr/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Uygulama Başlangıç Sırasında Kritik Hata\nrust_init_error_message = \n        Uygulamayı başlatırken kritik bir hata oluştu:\n\n        { $error_message }\n\n        Bu, eksik veya arızalı OpenGL/Vulkan sürücüleri, bir sanal makinede uygulama çalıştırma veya Krokiet veya kütüphanelerinden birinde bir hata nedeniyle oluşmuş olabilir.\n\n        Sorunu çözmek için farklı sürümler (skia_opengl, skia_vulkan, femtovg_opengl - varsayılan) veya yazılım render'ı kullanarak denemeyi deneyebilirsiniz.\nrust_loaded_preset = Yüklenen ön ayar { $preset_idx }\nrust_file_already_exists = Dosya \"{ $file }\" zaten mevcut ve geçersiz kılınmayacak\nrust_error_removing_file_after_copy = Dosya \"{ $file }\" silinirken hata oluştu (farklı bir bölüme kopyalandıktan sonra), sebep: { $reason }\nrust_error_copying_file = \"{ $input }\" değerini \"{ $output }\"’a kopyalamada hata, nedeni: { $reason }\nrust_loading_tags_cache = Etiket önbelleği yükleniyor\nrust_loading_fingerprints_cache = Parmak izi önbelleği yükleniyor\nrust_saving_tags_cache = Etiket önbelleği kaydediliyor\nrust_saving_fingerprints_cache = Parmak izi önbelleği kaydediliyor\nrust_loading_prehash_cache = Prehash önbelleği yükleniyor\nrust_saving_prehash_cache = Prehash önbelleği kaydediliyor\nrust_loading_hash_cache = Hash önbelleği yükleniyor\nrust_saving_hash_cache = Hash önbelleği kaydediliyor\nrust_loading_exif_cache = Yükleniyor EXIF önbelleği\nrust_saving_exif_cache = EXIF önbelleğini kaydetme\nrust_scanning_name = { $entries_checked } dosyanın adı taranıyor\nrust_scanning_size_name = { $entries_checked } dosyanın boyutu ve adı taranıyor\nrust_scanning_size = { $entries_checked } dosyanın boyutu taranıyor\nrust_scanning_file = { $entries_checked } dosya taranıyor\nrust_scanning_folder = { $entries_checked } klasör taranıyor\nrust_checked_tags = { $items_stats } öğesinin etiketleri kontrol edildi\nrust_checked_content = { $items_stats } öğesinin içeriği kontrol edildi ({ $size_stats })\nrust_compared_tags = { $items_stats } öğesinin etiketleri karşılaştırıldı\nrust_compared_content = { $items_stats } öğesinin içeriği karşılaştırıldı\nrust_hashed_images = Hashed { $items_stats } resimler({ $size_stats })\nrust_compared_image_hashes = { $items_stats } karşılaştırmalı görüntü-hash'leri\nrust_hashed_videos = Hashlanmış { $items_stats } videolar\nrust_created_thumbnails = { $items_stats } video için küçük resimler oluşturduğun\nrust_checked_files = { $items_stats } dosya kontrol edildi ({ $size_stats })\nrust_checked_files_bad_extensions = Yanlış uzantılı { $items_stats } dosya kontrol edildi\nrust_checked_files_bad_names = Yanlış uzantılı { $items_stats } dosya kontrol edildi\nrust_checked_videos = Kontrol edildi { $items_stats } videoları ({ $size_stats })\nrust_analyzed_partial_hash = { $items_stats } dosyanın kısmi hash’i analiz edildi ({ $size_stats })\nrust_analyzed_full_hash = { $items_stats } dosyanın tam hash’i analiz edildi ({ $size_stats })\nrust_failed_to_rename_file = Dosya adı değiştirme başarısız { $old_path } ile { $new_path }, hata: { $error }\nrust_no_included_paths = Başarısızlıkla tarama başlatılamıyor, dahil edilmemiş yollar ayarlanmamış olduğu için.\nrust_all_paths_referenced = Tüm dahil yollar referans yollar olarak ayarlandığında tarama başlatılamaz, giriş yolünün yanındaki referans kutucuğu devre dışı bırakılmalıdır.\nrust_found_empty_folders = { $items_found } boş klasörünü { $time } buldum\nrust_found_empty_files = { $items_found } boş dosya { $time } içinde bulundu\nrust_found_similar_images = { $items_found } benzer resim dosyası { $groups } gruplarında { $time } süre içinde bulundu\nrust_found_similar_videos = { $items_found } benzer videodosyası { $groups } grubunda { $time } zamanında bulunmuştur\nrust_found_similar_music_files = { $items_found } benzer müzik dosyası { $groups } grup içinde { $time } zamanında buldunuz\nrust_found_invalid_symlinks = { $items_found } geçersiz simgeLink bulunmuştur { $time }\nrust_found_temporary_files = { $items_found } geçici dosya found { $time } sürede bulunmuştur\nrust_no_file_type_selected = Seçili herhangi bir dosya türü olmaksızın bozuk dosyaları bulamıyoruz.\nrust_found_broken_files = Bulunan { $items_found } bozuk dosya { $size }'i alıyor { $time }'da\nrust_found_bad_extensions = { $items_found } kötü uzantılı dosya bulundu { $time } içinde\nrust_found_bad_names = { $items_found } dosyası ile { $time } içinde kötü isimli bulundu\nrust_found_video_optimizer = { $items_found } dosyası optimize edildi { $time }\nrust_found_duplicate_files = { $items_found } tane tekrarlı dosya { $groups } grup içinde { $size } boyutda { $time } zamanında bulunmuştur\nrust_found_duplicate_files_no_lost_space = { $items_found } kopyalı dosya { $groups } gruba ve { $time } içinde bulundu\nrust_found_big_files = { $items_found } büyük dosyayı { $size } boyutuyla { $time } zamanında buldum\nrust_found_exif_files = { $items_found } dosyasıyla { $time } içinde exif verisi bulundu\nrust_cannot_load_preset = { $preset_idx } ön ayar yüklenemedi veya değiştirilemedi: { $reason }. Varsayılan ayarlar kullanılıyor\nrust_saved_preset = { $preset_idx } ön ayar kaydedildi\nrust_cannot_save_preset = { $preset_idx } ön ayar kaydedilemedi: { $reason }\nrust_reset_preset = { $preset_idx } ön ayar sıfırlandı\nrust_cannot_create_output_folder = \"{ $output_folder }\" çıkış klasörü oluşturulamadı: { $error }\nrust_delete_summary = Silinen { $deleted } öğe, { $failed } öğeyi kaldıramadım, toplamda { $total } öğeden\nrust_rename_summary = { $renamed } adet öğe yeniden adlandırıldı, { $failed } adet öğe yeniden adlandırılamadı, toplamda { $total } adet öğe bulunmaktadır\nrust_move_summary = { $moved } öğeyi taşındı, { $failed } öğeyi taşınamadi, toplamda { $total } öğeden\nrust_hardlink_summary = Sert bağlantılı { $hardlinked } öğeler, { $failed } öğeyi sert bağlayamadı, toplam { $total } öğe içerisinden\nrust_symlink_summary = Bağlantılı { $symlinked } öğeler, bağlantılı { $failed } öğe başarısız oldu, toplam { $total } öğeden\nrust_optimize_video_summary = Optimize edilmiş { $optimized } videolar, optimize edilemeyen { $failed } videolar, toplamda { $total } video\nrust_clean_exif_summary = Temizlenen EXIF { $cleaned } dosyalarından, { $failed } dosyaların temizlenemedi, toplamda { $total } dosyadandı\nrust_deleting_files = { $items_stats } dosyalar siliniyor ({ $size_stats })\nrust_deleting_no_size_files = { $items_stats } dosyalar siliniyor\nrust_renaming_files = { $items_stats } dosyalar yeniden adlandırılıyor\nrust_moving_files = { $items_stats } dosyalar taşınıyor ({ $size_stats })\nrust_moving_no_size_files = { $items_stats } dosyalar taşınıyor\nrust_hardlinking_files = Hardlinking { $items_stats } dosyası ({ $size_stats })\nrust_hardlinking_no_size_files = Sert Bağlantı { $items_stats } dosyası\nrust_symlinking_files = Symlinking { $items_stats } dosyası ({ $size_stats })\nrust_symlinking_no_size_files = Symlinking { $items_stats } dosyasını\nrust_optimizing_videos = Optimize edilmiş { $items_stats } video ({ $size_stats })\nrust_optimizing_no_size_videos = Optimize Edilmiş { $items_stats } videosu\nrust_cleaning_exif = Temizleme EXIF'i { $items_stats } dosyasından ({ $size_stats })\nrust_cleaning_no_size_exif = Temizleme EXIF'i { $items_stats } dosyasından\nrust_no_files_deleted = Silmek için herhangi bir dosya veya klasör seçilmedi\nrust_no_files_renamed = Değiştirilecek herhangi bir dosya veya klasör seçilmedi\nrust_no_files_moved = Taşınmak için herhangi bir dosya veya klasör seçilmedi\nrust_no_files_hardlinked = Hiç seçilen dosya veya klasör yok hard bağlantısı için\nrust_no_files_symlinked = Hiç seçilen dosya veya klasör simyolu için yok\nrust_no_videos_optimized = Hiç optimizasyon için seçilen video yok\nrust_no_exif_cleaned = Seçilen hiçbir dosya EXIF temizleme için\nrust_extracted_exif_tags = Çıkarılan EXIF etiketleri { $items_stats } dosyalarından ({ $size_stats })\nrust_delete_confirmation = Seçili öğeleri gerçekten silmek istiyor musunuz?\nrust_delete_confirmation_number_simple = { $items } öğe seçildi.\nrust_delete_confirmation_number_groups = { $items } öğe({ $groups } gruptan seçildi).\nrust_delete_confirmation_selected_all_in_group = { $groups } gruptaki tüm öğeler seçildi.\nrust_move_confirmation = Seçilen öğeleri taşımaya emin misiniz?\nrust_move_confirmation_number_simple = { $items } öğeler seçildi.\nrust_clean_exif_confirmation = Seçilen öğelerden EXIF verilerini silmekten emin misiniz?\nrust_clean_exif_confirmation_number_simple = { $items } öğeler seçildi.\nclean_exif_overwrite_files_text = Dosyaları geçersiz kıl\nrust_optimize_video_confirmation = Seçilen videoları optimize etmekten emin misiniz?\nrust_optimize_video_confirmation_number_simple = { $items } öğeler seçildi.\nrust_hardlink_confirmation = Seçilen öğeler için sert bağlantılar oluşturmaya emin misiniz?\nrust_hardlink_confirmation_number_simple = { $items } öğeler seçildi.\nrust_symlink_confirmation = Seçilen öğeler için simgesel bağlantılar oluşturmaya emin misiniz?\nrust_symlink_confirmation_number_simple = { $items } öğeler seçildi.\nrust_rename_confirmation = Seçilen öğeleri yeniden adlandırmak eminsiniz mi?\nrust_rename_confirmation_number_simple = { $items } öğeler seçildi.\nrust_cache_processed_files = İşlenmiş { $files } önbellek dosyaları\nrust_cache_entries_stats = Silindi { $removed } girdislerinden toplam { $all }, { $left } tanesi kaldı\nrust_cache_size_reduced = Azaltılmış önbellek dosyası boyutu { $size }\nrust_cache_time_elapsed = Zaman geçti: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = { $name }'yi { $target }'a hafıza.linklemek başarısız oldu, sebep { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Seçim\ncolumn_size = Boyut\ncolumn_file_name = Dosya Adı\ncolumn_path = Yol\ncolumn_modification_date = Düzenleme Tarihi\ncolumn_similarity = Benzerlik\ncolumn_dimensions = En x Boy\ncolumn_new_dimensions = Yeni Boyutlar\ncolumn_title = Başlık\ncolumn_artist = Sanatçı\ncolumn_year = Yıl\ncolumn_bitrate = Bit-hızı\ncolumn_length = Uzunluk\ncolumn_genre = Müzik Türü\ncolumn_type_of_error = Hata Türü\ncolumn_symlink_name = Sembolik Bağlantı Adı\ncolumn_symlink_folder = Sembolik Bağlantı Klasörü\ncolumn_destination_path = Hedef Yol\ncolumn_current_extension = Geçerli Uzantı\ncolumn_proper_extension = Doğru Uzantı\ncolumn_fps = FPS\ncolumn_codec = Kodçalışma\ncolumn_duration = Süre\ncolumn_exif_tags = EXIF Etiketleri\ncolumn_new_name = Yeni Ad\n# Slint translations\nok_button = Tamam\ncancel_button = İptal\ndo_you_want_to_continue = Devam etmek istiyor musun?\nmain_window_title = Krokiet - Veri Temizleyici\nscan_button = Tarama\nstop_button = Durdur\nstop_text = Durdur\nselect_button = Seç\nmove_button = Taşı\ndelete_button = Sil\nsave_button = Kaydet\nsort_button = Sırala\nrename_button = Yeniden Adlandır\nmotto = Bu program kullanıma özgür ve her zaman özenekle ücretsiz olacak.\\nMIT/GPL Lisansı için ayrıntılar burada.\nunicorn = Bir türbeline bakmayabilirsin, ama türbine her zaman seni bakaçtıkları gibi.\nrepository = Depo\ninstruction = Yönerge\ndonation = Bağış\ntranslation = Çeviri\nincluded_paths = Dahil Edilen Yollar\nexcluded_paths = Hariç Tutulan Yollar\nref = Referans\npath = Yol\ntool_duplicate_files = Eş Dosyalar\ntool_empty_folders = Boş Klasörler\ntool_big_files = Büyük/Küçük Dosyalar\ntool_empty_files = Boş Dosyalar\ntool_temporary_files = Geçici Dosyalar\ntool_similar_images = Benzer Resimler\ntool_similar_videos = Benzer Videolar\ntool_music_duplicates = Müzik Kopyaları\ntool_invalid_symlinks = Geçersiz Sembolik Bağlar\ntool_broken_files = Bozuk Dosyalar\ntool_bad_extensions = Hatalı Uzantılar\ntool_bad_names = Kötü İsimler\ntool_video_optimizer = Video Optimizörü\ntool_exif_remover = Exif Kaldırıcı\nsort_by_full_name = Tam İsimle Sırala\nsort_by_selection = Seçilmişe Göre Sırala\nsort_reverse = Ters Sırala\nselection_all = Tümünü Seç\nselection_deselect_all = Seçili olanları kaldırın\nselection_invert_selection = Seçimi Ters Çevir\nselection_the_biggest_size = En Büyük Büyüklükteki Dosyayı Seç\nselection_the_biggest_resolution = En Yüksek Çözünürlüğü Seç\nselection_the_smallest_size = En Küçük Büyüklükteki Dosyayı Seç\nselection_the_smallest_resolution = En Küçük Çözünürlüğü Seç\nselection_newest = En Yeni Olanı Seç\nselection_oldest = En Eski Olanı Seç\nselection_shortest_path = Kısağına en uygun yolu seç\nselection_longest_path = En uzun yolu seçin\nstage_current = Mevcut Adım:\nstage_all = Tüm Adımlar:\nsubsettings = Alt ayarlar\nsubsettings_images_hash_size = Has Hasızi\nsubsettings_images_resize_algorithm = Önizleme Algortimi\nsubsettings_images_ignore_same_size = Aynı boyutta görseller ihmal et\nsubsettings_images_max_difference = Maksimum Fark\nsubsettings_images_duplicates_hash_type = Sıfırlama Tipi\nsubsettings_duplicates_check_method = Denetim yöntemi:\nsubsettings_duplicates_name_case_sensitive = Büyük küçük harfe duyarlı (sadece isim modunda)\nsubsettings_biggest_files_sub_method = Yöntem\nsubsettings_biggest_files_sub_number_of_files = Dosya Sayısı\nsubsettings_videos_max_difference = Maksimum Fark\nsubsettings_videos_ignore_same_size = Aynı boyutta videoları görmezden geliniz\nsubsettings_music_audio_check_type = Ses Kontrol Türü\nsubsettings_music_approximate_comparison = Yaklaşık Etiket Karşılaştırması\nsubsettings_music_compared_tags = Karşılaştırılan Etiketler\nsubsettings_music_title = Başlık\nsubsettings_music_artist = Sanatçı\nsubsettings_music_bitrate = Bit Hızı\nsubsettings_music_genre = Müzik Türü\nsubsettings_music_year = Yıl\nsubsettings_music_length = Uzunluk\nsubsettings_music_max_difference = Maksimum Fark\nsubsettings_music_minimal_fragment_duration = Minimum Parça Süresi\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Benzer başlıklı gruplar arasında karşılaştırın\nsubsettings_broken_files_type = Kontrol edilecek dosya türü\nsubsettings_broken_files_audio = Ses\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = Arşiv\nsubsettings_broken_files_image = Görsel\nsubsettings_broken_files_video = Video\nsubsettings_broken_files_video_info = ffmpeg/ffprobe'yi kullanır. Çok yavaş ve dosya düzgün çalışsa bile katı hataları bile tespit edebilir.\nsubsettings_bad_names_issues = Dosya adları doğrulamaları\nsubsettings_bad_names_uppercase_extension = BÜYÜK HARF EKLENTİSİ\nsubsettings_bad_names_uppercase_extension_hint = Büyük harfli yazılı olan dosyaları uzantıda bulur (örneğin, .JPG, .Mp3) ve küçük harfli versiyonunu önerir\nsubsettings_bad_names_emoji_used = Emoji'de isim\nsubsettings_bad_names_emoji_used_hint = Emoji karakterleri (😀, 🎉, vb.) adlarında dosyaları bulur ve bunları silmeyi önermeler\nsubsettings_bad_names_space_at_start_end = Önde/arkada boşluklar\nsubsettings_bad_names_space_at_start_end_hint = Boşluklarla başlayan veya biten isimlerdeki dosyaları bulur ve bunları budamayı önerir\nsubsettings_bad_names_non_ascii = Non-ASCII karakterler\nsubsettings_bad_names_non_ascii_hint = ANSI karakterlerini (ą, ć, ñ, vb.) bulur ve bunları ASCII eşdeğerleriyle (a, c, n) değiştirmeyi veya eşleme yoksa kaldırmayı önerir\nsubsettings_bad_names_restricted_charset = Sınırlı karakter kümesi\nsubsettings_bad_names_restricted_charset_hint = ASCII olmayan karakterleri ASCII'ye dönüştürür, ardından 0-9a-zA-Z aralığının ve kullanıcı tarafından tanımlanan izin verilen karakterlerin dışında karakterler içeren dosyaları bulur\nsubsettings_bad_names_allowed_chars = İzin verilen karakterler\nsubsettings_bad_names_remove_duplicated = Çoğaltılmış karakterler\nsubsettings_bad_names_remove_duplicated_hint = Ardışık yinelenen alfanümerik olmayan karakterleri bulur (örneğin, \"file---name..txt\") ve yinelenenleri kaldırmayı önerir\nsettings_global_settings = Global Ayarlar\nsettings_dark_theme = Karanlık thema\nsettings_show_only_icons = Sadece simge göster\nsettings_excluded_items = Hariç Tutulan Öğeler:\nsettings_allowed_extensions = İzin Verilen Uzantılar:\nsettings_excluded_extensions = Hariç Tutulan Uzantılar:\nsettings_file_size = Dosya Boyutu (Kilobayt)\nsettings_minimum_file_size = Dakika:\nsettings_maximum_file_size = Maks:\nsettings_recursive_search = Yordayıcı arama\nsettings_use_cache = Önbellek Kullan\nsettings_save_as_json = Önbelleği JSON dosyası olarak da kaydet\nsettings_move_to_trash = Silinen dosyaları çöp kutusuna taşı\nsettings_ignore_other_filesystems = Başka bir dosya sistemi gözardı edin (sadece Linux)\nsettings_delete_outdated_cache_entries = Sil otomatik olarak eskiyen önbellek girdilerini\nsettings_delete_outdated_cache_entries_hint = Eğer etkinleştirilmişse, uygulama önbellek yükleme sırasında (haftada en fazla bir kez) önbelleğe alınmış kayıtların mevcut ve değiştirilmemiş dosyalara/verilere işaret edip etmediğini doğrulayacaktır\nsettings_hide_hard_links = Zor linkleri gizle\nsettings_hide_hard_links_hint = Aynı dosyalar için zorunlu bağlantıları sonuçlarda gizle\nsettings_thread_number = Süzgeç numarası\nsettings_restart_required = ---Değişiklikleri thread sayıda uygulamak için uygulamayı yeniden başlatmalısınız---\nsettings_duplicate_image_preview = Resim önização\nsettings_duplicate_minimal_hash_cache_size = Temizlenen dosya boyutu minimum - Hash (KB)\nsettings_duplicate_use_prehash = Ön hesh kullanın\nsettings_duplicate_minimal_prehash_cache_size = Yatay önbelleğe alınan dosyaların minimal boyutu - Prehash (KB)\nsettings_similar_images_show_image_preview = Resim önpreview\nsettings_application_scale_text = Uygulama ölçeği\nsettings_application_scale_hint_text = Elle manuel ölçeği etkinleştirildiğinde, özel bir ölçek faktörü seçmenize izin verir, ancak monitörün DPI'sine göre otomatik ölçelemeyi tamamen devre dışı bırakır.\nsettings_restart_required_scale_text = ---Uygulamayı yeniden başlatmanız ölçekteki değişiklikleri uygulamak için gereklidir---\nsettings_use_manual_application_scale_text = Manuel uygulama ölçeği kullanın\nsettings_video_thumbnails_preview = Resim önörüm\nsettings_open_config_folder = Ayar klasörünü aç\nsettings_open_cache_folder = Önbellek klasörünü aç\nsettings_language = Dil\nsettings_current_preset = Mevcut Önyapı:\nsettings_edit_name = İsmi Düzenle\nsettings_choose_name_for_prefix = Önek için isim seçin\nsettings_save = Kaydet\nsettings_load = Yükle\nsettings_reset = Sıfırla\nsettings_similar_videos_tool = Benzer Videolar Aracı\nsettings_video_thumbnails_clear_unused_thumbnails = Kullanılmayan video önizleme dosyalarını 7 günden eski olanları uygulama başlangıcında sil\nsettings_video_thumbnails_header = Video Başlıkları\nsettings_video_thumbnails_generate = Resim önizleme oluştur\nsettings_video_thumbnails_position = Video (%) bünyesinde eklemeyin konumu\nsettings_video_thumbnails_generate_grid = Öz boyutta grid oluşturmak yerine tek resim oluştur\nsettings_video_thumbnails_generate_grid_hint = Çoklu görüntü oluşturmak bir ızgıda tek bir minyatür görüntüyü oluşturmaktan çok daha yavaştır\nsettings_video_thumbnails_grid_tiles_per_side = Miniğrafçık şeritlerde her bir kenarda bulunan seramik sayısı\nsettings_video_thumbnails_grid_tiles_per_side_hint = Griddeki her taraftaki minyatür tuvalet sayısı. Örneğin, 2 seçmek 2x2 bir grid oluşturur, bu da 4 resimden oluşan tek bir minyatür tuvalete sonuçlanır.\nsettings_similar_images_tool = Benzer Görseller Aracı\nsettings_general_settings = Genel Ayarlar\nsettings_cache_header_text = Önbellek Ayarları\nsettings_clean_cache_button_text = Eski güncel önbelleği temizle\nsettings_settings = Ayarlar\nsettings_load_tabs_sizes_at_startup = Başlangıçta sekme boyutlarını yükleyin\nsettings_load_windows_size_at_startup = Başlangıçta window boyutunu yükle\nsettings_limit_lines_of_messages = Mesajları 500 satıra limit et (yavaş TextEdit bileşeni için çözüm)\nsettings_play_audio_on_scan_completion_text = Tarayıcı tamamlandığında sesi çal\nsettings_audio_feature_hint_text = Sadece ses özelliğiyle derlendiğinde kullanılabilir\nsettings_audio_env_variable_hint_text = Ses değiştirilebilir, KROKIET_AUDIO_STOP_FILE ortam değişkenini geçerli bir ses dosyası yoluna ayarlayarak\npopup_save_title = Sonuçları Kaydet\npopup_save_message = Sonuçlar 3 farklı dosyaya kaydedilecek\npopup_rename_title = Dosya isimlerini değiştirme\npopup_new_paths_title = Lütfen yolları birer satırda ekleyin\npopup_move_title = Öğeleri Taşı\npopup_move_copy_checkbox = Taşımak yerine kopyala\npopup_move_preserve_folder_checkbox = Klasör yapısını koru\nmove_confirmation_text = Seçilen öğeleri taşımaya emin misiniz?\nrename_confirmation_text = Seçilen öğeleri yeniden adlandırmak eminsiniz mi?\ndelete = Öğeleri Sil\nstopping_scan = Tarama durduruluyor, lütfen bekleyin….\nsearching = Aranıyor….\nsubsettings_videos_crop_detect = Kazı algılama yöntemi\nsubsettings_videos_skip_forward_amount = Süre atla [s]\nsubsettings_videos_vid_hash_duration = Video hash süresi\nsettings_cache_number_size_text = Cache dosya boyutu: { $size }, dosya sayısı: { $number }\nsettings_video_thumbnails_number_size_text = Video thumbnail boyutu: { $size }, dosya sayısı: { $number }\nsettings_log_number_size_text = Log dosya boyutu: { $size }, dosya sayısı: { $number }\npopup_clean_cache_title_text = Eski T Cutunuğu'nu Temizle\npopup_clean_cache_confirmation_text = Kesin olarak eski önbellek girdilerini temizlemek istiyor musunuz? Bu, artık mevcut olmayan veya değiştirilen dosyalara ait önbellek girdilerini kaldıracaktır.\npopup_clean_cache_progress_text = İşlem önbellek dosyasını:\npopup_clean_cache_current_file_text = Mevcut dosya:\npopup_clean_cache_file_progress_text = Mevcut dosya ilerlemesi:\npopup_clean_cache_overall_progress_text = Genel ilerleme:\npopup_clean_cache_stopped_by_user_text = Önbellek temizleme kullanıcının tarafından durduruldu\npopup_clean_cache_finished_text = Önbellek temizleme başarıyla tamamlandı!\npopup_clean_cache_error_details_text = Hata detayları:\npopup_clean_cache_files_with_errors = Hatalı dosyalar:\nsubsettings_video_optimizer_mode = Mod\nsubsettings_video_optimizer_crop_type = Bitki Türü\nsubsettings_video_optimizer_black_pixel_threshold = Siyah Piksel Seviyesi\nsubsettings_video_optimizer_black_pixel_threshold_hint = Her bir piksel kanal değeri siyah olarak kabul edilmek üzere maksimum RGB değerine (0-128). Varsayılan: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Siyah Çubuk Minimum Yüzdesi\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Satır/sütunda siyah piksel yüzdesi, siyah bir çubuk olarak kabul edilmesi için gereken minimum yüzdesidir (50-100). Varsayılan değer: 90\nsubsettings_video_optimizer_max_samples = Max Örnekler\nsubsettings_video_optimizer_max_samples_hint = Analiz edilecek maksimum kare sayısı (5-1000). Varsayılan değer: 60\nsubsettings_video_optimizer_min_crop_size = Minimum Crop Boyutu\nsubsettings_video_optimizer_min_crop_size_hint = Herhangi bir kenardan kesilecek minimum pikseller (1-1000). Daha küçük kesintiler göz ardı edilir. Varsayılan değer: 5\nsubsettings_video_optimizer_video_codec = Video kodeği\nsubsettings_video_optimizer_excluded_codecs = Hariç bırakılmış codec'ler\nsubsettings_video_optimizer_video_quality = Video kalitesi (CRF)\nsubsettings_reset = Yeniden Başlat\nsubsettings_exif_ignored_tags_text = İhmal edilen etiketler:\nsubsettings_exif_ignored_tags_hint_text = Virgülle ayrılmış listedeki taramadan hariç tutulacak etiketler (örneğin GPS, Küçük Resim). Bazı etiketler, örneğin TIFF dosyalarındaki ImageWidth, görüntüyü bozmamak için gizlenir.\nclean_button_text = Temiz\nclean_text = Temiz EXIF verileri\nclean_confirmation_text = Seçilen öğelerden EXIF verilerini silmekten emin misiniz?\ncrop_videos_text = Video kesme\ncrop_video_confirmation_text = Seçilen videoları kırpmanızdan emin misiniz?\ncrop_reencode_video_text = Video yeniden kodla\nreencode_videos_text = Videoları yeniden kodla\noptimize_button_text = Optimize edin\noptimize_confirmation_text = Seçili videoları yeniden kodlamayı kesin olarak mı istiyorsunuz?\noptimize_fail_if_bigger_text = Optimizasyon sonucu dosya daha büyükse başarısız ol\noptimize_overwrite_files_text = Dosyaları geçersiz kıl\noptimize_limit_video_size_text = Video boyutu sınırlı\noptimize_max_width_text = Максимальная ширина:\noptimize_max_height_text = Максима височина:\nhardlink_button_text = Sert bağlantı\nhardlink_text = hard bağlantılar oluştur\nhardlink_confirmation_text = Seçilen öğeler için sert bağlantılar oluşturmaya emin misiniz?\nsoftlink_button_text = Yumuşak bağlantı\nsoftlink_text = Yumuşak bağlantılar oluştur\nsoftlink_confirmation_text = Seçilen öğeler için yumuşak bağlantılar (sembolik bağlantılar) oluşturmaya emin misiniz?\n"
  },
  {
    "path": "krokiet/i18n/uk/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = Критична помилка під час запуску додатку\nrust_init_error_message = \n        Виникла критична помилка під час запуску програми:\n\n        { $error_message }\n\n        Це може бути спричинено відсутніми або несправними драйверами OpenGL/Vulkan, запуском програми у віртуальній машині або помилкою в Krokiet або одній із його бібліотек.\n\n        Ви можете спробувати запустити різні версії (skia_opengl, skia_vulkan, femtovg_opengl - за замовчуванням) або з програмним рендерером, щоб побачити, чи це вирішить проблему.\nrust_loaded_preset = Завантажено пресет { $preset_idx }\nrust_file_already_exists = Файл \"{ $file }\" вже існує і не буде перезаписано\nrust_error_removing_file_after_copy = Помилка при видаленні файлу \"{ $file }\" (після копіювання на інший розділ), причина: { $reason }\nrust_error_copying_file = Помилка при копіюванні \"{ $input }\" до \"{ $output }\", причина: { $reason }\nrust_loading_tags_cache = Завантаження кешу тегів\nrust_loading_fingerprints_cache = Завантаження кешу відбитків пальців\nrust_saving_tags_cache = Збереження кешу міток\nrust_saving_fingerprints_cache = Збереження кешу відбитків пальців\nrust_loading_prehash_cache = Завантаження цільового кешу\nrust_saving_prehash_cache = Збереження цілковитого кешу\nrust_loading_hash_cache = Завантаження схованки\nrust_saving_hash_cache = Збереження кешу кешу\nrust_loading_exif_cache = Завантаження кешу EXIF\nrust_saving_exif_cache = Збереження EXIF кешу\nrust_scanning_name = Сканування імені файлу { $entries_checked }\nrust_scanning_size_name = Сканування і ім'я файлу { $entries_checked }\nrust_scanning_size = Сканування файлу { $entries_checked }\nrust_scanning_file = Сканування файлу { $entries_checked }\nrust_scanning_folder = Пошук теки { $entries_checked }\nrust_checked_tags = Перевірені теґи { $items_stats }\nrust_checked_content = Перевірений вміст { $items_stats } ({ $size_stats })\nrust_compared_tags = Порівняльні теґи { $items_stats }\nrust_compared_content = Порівняй вміст { $items_stats }\nrust_hashed_images = Приховати зображення { $items_stats } ({ $size_stats })\nrust_compared_image_hashes = Хеші зображень порівняних з { $items_stats }\nrust_hashed_videos = Прибрали відеозаписів { $items_stats }\nrust_created_thumbnails = Створено мініатюри для { $items_stats } відео\nrust_checked_files = Перевірено файл { $items_stats } ({ $size_stats })\nrust_checked_files_bad_extensions = Перевірено файл { $items_stats }\nrust_checked_files_bad_names = Перевірено { $items_stats } файл\nrust_checked_videos = Перевірено { $items_stats } відео ({ $size_stats })\nrust_analyzed_partial_hash = Проаналізовано частковий хеш файлів { $items_stats } ({ $size_stats })\nrust_analyzed_full_hash = Проаналізовано повний хеш файлів { $items_stats } ({ $size_stats })\nrust_failed_to_rename_file = Не вдалося перейменувати файл { $old_path } до { $new_path }, помилка: { $error }\nrust_no_included_paths = Не вдається запустити сканування, коли не встановлено жодні шляхи.\nrust_all_paths_referenced = Не вдається запустити сканування, коли всі включені шляхи встановлено як шляхи, посилання, вам потрібно вимкнути прапорець посилання поруч із шляхом вхідних даних.\nrust_found_empty_folders = Знайдено { $items_found } порожніх папок в { $time }\nrust_found_empty_files = Знайдено { $items_found } порожніх файлів за { $time }\nrust_found_similar_images = Знайдено { $items_found } подібних зображенняв в { $groups } групах за { $time }\nrust_found_similar_videos = Знайдено { $items_found } подібних відеофайлів в { $groups } групах за { $time }\nrust_found_similar_music_files = Знайдено { $items_found } подібних музичних файлів у { $groups } групах за { $time }\nrust_found_invalid_symlinks = Знайдено { $items_found } неприпустимі символьні посилання в { $time }\nrust_found_temporary_files = Знайдено { $items_found } тимчасових файлів в { $time }\nrust_no_file_type_selected = Не вдається знайти пошкоджені файли без будь-якого типу файлу.\nrust_found_broken_files = Знайдено { $items_found } повреждених файлів що заняло { $size } за { $time }\nrust_found_bad_extensions = Знайдено { $items_found } файли з невірними розширеннями в { $time }\nrust_found_bad_names = Знайдено { $items_found } файлів з поганими назвами у { $time }\nrust_found_video_optimizer = Знайдено { $items_found } файлів для оптимізації в { $time }\nrust_found_duplicate_files = Знайдено { $items_found } дубlicitних файлів у { $groups } груп розміром { $size } за { $time }\nrust_found_duplicate_files_no_lost_space = Знайдено { $items_found } дубль файлів в { $groups } групах за { $time }\nrust_found_big_files = Знайдено { $items_found } великих файлів розміром { $size } за { $time }\nrust_found_exif_files = Знайдено { $items_found } файлів з EXIF даними в { $time }\nrust_cannot_load_preset = Не можна змінити та завантажити попереднє встановлення { $preset_idx } - причина { $reason }, будуть використано замовчувальне налаштування замість цього\nrust_saved_preset = Збережено пресет { $preset_idx }\nrust_cannot_save_preset = Неможливо зберегти налаштування { $preset_idx } - причина { $reason }\nrust_reset_preset = Скинути попередній вибір { $preset_idx }\nrust_cannot_create_output_folder = Неможе створити папку виводу { $output_folder }, причина: { $error }\nrust_delete_summary = Видалено об'єкти { $deleted } для видалення { $failed } елементів, з { $total }\nrust_rename_summary = Перейменовано пункти { $renamed } і не вдалося перейменувати { $failed } з об'єктів { $total }\nrust_move_summary = Переміщено пунктів { $moved } , не вдалося перемістити { $failed } з елементів { $total }\nrust_hardlink_summary = Зв’язані посиланням { $hardlinked } елементи, не вдалося зв’язати посиланням { $failed } елементів, з { $total } елементів\nrust_symlink_summary = Зв’язано символічно { $symlinked } елементів, не вдалося зв’язати символічно { $failed } елементів, з { $total } елементів\nrust_optimize_video_summary = Оптимізовані { $optimized } відео, невдало оптимізовані { $failed } відео, з { $total } відео\nrust_clean_exif_summary = Очищено EXIF з файлів { $cleaned }, не вдалося очистити файлів { $failed }, з { $total } файлів\nrust_deleting_files = Видалення { $items_stats } файлу ({ $size_stats })\nrust_deleting_no_size_files = Видалення { $items_stats } файлу\nrust_renaming_files = Перейменування файлу { $items_stats }\nrust_moving_files = Переміщення { $items_stats } файлу ({ $size_stats })\nrust_moving_no_size_files = Переміщення { $items_stats } файлу\nrust_hardlinking_files = Жорстке посилання { $items_stats } файл ({ $size_stats })\nrust_hardlinking_no_size_files = Жорстке посилання { $items_stats } файл\nrust_symlinking_files = Створення символічної посилання на файл { $items_stats } ({ $size_stats })\nrust_symlinking_no_size_files = Симлінку { $items_stats } файл\nrust_optimizing_videos = Оптимізоване { $items_stats } відео ({ $size_stats })\nrust_optimizing_no_size_videos = Оптимізоване { $items_stats } відео\nrust_cleaning_exif = Очищення EXIF з файлу { $items_stats } ({ $size_stats })\nrust_cleaning_no_size_exif = Очищення EXIF з файлу { $items_stats }\nrust_no_files_deleted = Для видалення не вибрано жодного файлу чи теки\nrust_no_files_renamed = Не вибрано жодного файлу чи теки для перейменування\nrust_no_files_moved = Не вибрано жодного файлу чи папок для переміщення\nrust_no_files_hardlinked = Жодно жодних файлів або папок вибрано для жорсткого посилання\nrust_no_files_symlinked = Жодно файлів чи папок не вибрано для символічного посилання\nrust_no_videos_optimized = Не вибрано жодних відео для оптимізації\nrust_no_exif_cleaned = Жоден файл не вибрано для очищення EXIF\nrust_extracted_exif_tags = Витягнуто теги EXIF з { $items_stats } файлів ({ $size_stats })\nrust_delete_confirmation = Ви впевнені, що бажаєте видалити виділені елементи?\nrust_delete_confirmation_number_simple = { $items } елементів вибрано.\nrust_delete_confirmation_number_groups = { $items } об'єктів вибрано в { $groups } групі.\nrust_delete_confirmation_selected_all_in_group = Усі елементи обрані в { $groups } групі.\nrust_move_confirmation = Ви впевнені, що хочете перемістити вибрані елементи?\nrust_move_confirmation_number_simple = { $items } елементи вибрано.\nrust_clean_exif_confirmation = Ви впевнені, що хочете видалити дані EXIF з вибраних елементів?\nrust_clean_exif_confirmation_number_simple = { $items } елементи вибрано.\nclean_exif_overwrite_files_text = Перезаписувати файли\nrust_optimize_video_confirmation = Ви впевнені, що хочете оптимізувати вибрані відео?\nrust_optimize_video_confirmation_number_simple = { $items } елементи вибрано.\nrust_hardlink_confirmation = Ви впевнені, що хочете створити жорсткі посилання для вибраних елементів?\nrust_hardlink_confirmation_number_simple = { $items } елементи вибрано.\nrust_symlink_confirmation = Ви впевнені, що хочете створити символічні посилання для вибраних елементів?\nrust_symlink_confirmation_number_simple = { $items } елементи вибрано.\nrust_rename_confirmation = Ви впевнені, що хочете перейменувати вибрані елементи?\nrust_rename_confirmation_number_simple = { $items } елементи вибрано.\nrust_cache_processed_files = Оброблені { $files } кешовані файли\nrust_cache_entries_stats = Видалено { $removed } записів з усіх { $all }, { $left } залишилось\nrust_cache_size_reduced = Зменшено розмір файлів кешу на { $size }\nrust_cache_time_elapsed = Час, що минув: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = Не вдалося створити символічне посилання { $name } на { $target }, причина { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = Вибір\ncolumn_size = Розмір\ncolumn_file_name = Назва файлу\ncolumn_path = Шлях\ncolumn_modification_date = Дата зміни\ncolumn_similarity = Подібність\ncolumn_dimensions = Розміри\ncolumn_new_dimensions = Нові виміри\ncolumn_title = Найменування\ncolumn_artist = Художник\ncolumn_year = Рік\ncolumn_bitrate = Бітрейт\ncolumn_length = Довжина\ncolumn_genre = Жанр\ncolumn_type_of_error = Тип помилки\ncolumn_symlink_name = Ім'я символьного посилання\ncolumn_symlink_folder = Тека символічного посилання\ncolumn_destination_path = Шлях призначення\ncolumn_current_extension = Поточне розширення\ncolumn_proper_extension = Належне розширення\ncolumn_fps = Кадрів в секунду\ncolumn_codec = Кодек\ncolumn_duration = Тривалість\ncolumn_exif_tags = EXIF мітки\ncolumn_new_name = Нове Ім'я\n# Slint translations\nok_button = Гаразд\ncancel_button = Скасувати\ndo_you_want_to_continue = Бажаєте продовжити?\nmain_window_title = Крокет - Очищувач даних\nscan_button = Сканування товару\nstop_button = Зупинити\nstop_text = Зупинити\nselect_button = Вибрати\nmove_button = Пересунути\ndelete_button = Видалити\nsave_button = Зберегти\nsort_button = Упорядкувати\nrename_button = Перейменувати\nmotto = Цей програмний продукт є вільним для використання та завжди буде щодо використання.\\nПерегляньте ліцензію MIT/GPL для докладів.\nunicorn = Не дивишся на єдинорога, але єдиноріг завжди дивиться на вас.\nrepository = Репозиторій\ninstruction = Інструкція\ndonation = Пожертва\ntranslation = Переклад\nincluded_paths = Включені шляхи\nexcluded_paths = Виключені шляхи\nref = Посилання\npath = Шлях\ntool_duplicate_files = Дублювати файли\ntool_empty_folders = Порожні теки\ntool_big_files = Великі файли\ntool_empty_files = Порожні файли\ntool_temporary_files = Тимчасові файли\ntool_similar_images = Схожі зображення\ntool_similar_videos = Схожі відео\ntool_music_duplicates = Музичні дублікати\ntool_invalid_symlinks = Пошкоджені симв. посилання\ntool_broken_files = Пошкоджені файли\ntool_bad_extensions = Помилкові розширення\ntool_bad_names = Погані імена\ntool_video_optimizer = Оптимізатор відео\ntool_exif_remover = Видаляч EXIF\nsort_by_full_name = Сортувати за іменем\nsort_by_selection = Сортувати за вибором\nsort_reverse = У зворотньому порядку\nselection_all = Виділити все\nselection_deselect_all = Зняти всі виділення\nselection_invert_selection = Інвертувати виділення\nselection_the_biggest_size = Виберіть найбільший розмір\nselection_the_biggest_resolution = Виберіть найбільшу роздільну здатність\nselection_the_smallest_size = Оберіть найменший розмір\nselection_the_smallest_resolution = Виберіть найменшу роздільну здатність\nselection_newest = Виберіть найновіші\nselection_oldest = Виберіть найстаріші\nselection_shortest_path = Оберіть найкоротший шлях\nselection_longest_path = Оберіть найдовший шлях\nstage_current = Поточний етап:\nstage_all = Всі етапи:\nsubsettings = Піднастройки\nsubsettings_images_hash_size = Розмір хешу\nsubsettings_images_resize_algorithm = Змінити розмір алгоритму\nsubsettings_images_ignore_same_size = Ігнорувати зображення з таким самим розміром\nsubsettings_images_max_difference = Макс. різницю\nsubsettings_images_duplicates_hash_type = Тип хешу\nsubsettings_duplicates_check_method = Перевірити метод\nsubsettings_duplicates_name_case_sensitive = Чутливість до регістру(тільки для режимів імен)\nsubsettings_biggest_files_sub_method = Метод\nsubsettings_biggest_files_sub_number_of_files = Кількість файлів\nsubsettings_videos_max_difference = Макс. різницю\nsubsettings_videos_ignore_same_size = Ігнорувати відео з таким самим розміром\nsubsettings_music_audio_check_type = Тип перевірки звуку\nsubsettings_music_approximate_comparison = Приблизне порівняння тегів\nsubsettings_music_compared_tags = Порівнялені мітки\nsubsettings_music_title = Найменування\nsubsettings_music_artist = Художник\nsubsettings_music_bitrate = Бітрейт\nsubsettings_music_genre = Жанр\nsubsettings_music_year = Рік\nsubsettings_music_length = Довжина\nsubsettings_music_max_difference = Макс. різницю\nsubsettings_music_minimal_fragment_duration = Мінімальна тривалість фрагменту\nsubsettings_music_compare_fingerprints_only_with_similar_titles = Порівняйте у групах подібних назвах\nsubsettings_broken_files_type = Тип файлів для перевірки\nsubsettings_broken_files_audio = Аудіо\nsubsettings_broken_files_pdf = Пдф\nsubsettings_broken_files_archive = Архів\nsubsettings_broken_files_image = Зображення\nsubsettings_broken_files_video = Відео\nsubsettings_broken_files_video_info = Використовує ffmpeg/ffprobe. Дуже повільно і може виявляти пунктуалістичні помилки, навіть якщо файл відтворюється нормально.\nsubsettings_bad_names_issues = Перевірка файлів\nsubsettings_bad_names_uppercase_extension = Верхній регістр\nsubsettings_bad_names_uppercase_extension_hint = Знаходить файли з великими літерами в розширенні (наприклад, .JPG, .Mp3) та пропонує їх нижчі регістри\nsubsettings_bad_names_emoji_used = Емодзі в імені\nsubsettings_bad_names_emoji_used_hint = Знаходить файли з емадзі-символами (😀, 🎉, тощо) у назві та пропонує їх видалити\nsubsettings_bad_names_space_at_start_end = Пробіли на початку/в кінці\nsubsettings_bad_names_space_at_start_end_hint = Знаходить файли з пробілами на початку або в кінці назви та пропонує обрізати їх\nsubsettings_bad_names_non_ascii = Не-ASCII символи\nsubsettings_bad_names_non_ascii_hint = Знаходить не-ASCII символи (ą, ć, ñ, тощо) та пропонує замінити їх ASCII еквівалентами (a, c, n) або видалити, якщо не існує відображення\nsubsettings_bad_names_restricted_charset = Обмежений charset\nsubsettings_bad_names_restricted_charset_hint = Транслітує не-ASCII символи в ASCII, а потім шукає файли, що містять символи поза межами 0-9a-zA-Z та дозволених символів, визначених користувачем\nsubsettings_bad_names_allowed_chars = Дозволені символи\nsubsettings_bad_names_remove_duplicated = Дублікатовані літери\nsubsettings_bad_names_remove_duplicated_hint = Знаходить послідовні дублікати неалфавітних символів (наприклад, \"file---name..txt\") та пропонує видалити дублікати\nsettings_global_settings = Глобальні налаштування\nsettings_dark_theme = Темна тема\nsettings_show_only_icons = Показати тільки значки\nsettings_excluded_items = Виключений елемент:\nsettings_allowed_extensions = Дозволені розширення:\nsettings_excluded_extensions = Виключені розширення:\nsettings_file_size = Розмір файлу (Кіlobyтами)\nsettings_minimum_file_size = Мін:\nsettings_maximum_file_size = Максимум:\nsettings_recursive_search = Рекурсивний пошук\nsettings_use_cache = Використання кешу\nsettings_save_as_json = Також зберегти кеш у вигляді файлу JSON\nsettings_move_to_trash = Перемістити видалені файли в смітник\nsettings_ignore_other_filesystems = Ігнорувати інші файлові системи (лише Linux)\nsettings_delete_outdated_cache_entries = Видалити автоматично застарілі записи кешу\nsettings_delete_outdated_cache_entries_hint = Коли увімкнено, додаток буде перевіряти під час завантаження кешу (не більше одного разу на тиждень), чи чинні записи кешу все ще вказують на існуючі та незмінені файли/дані\nsettings_hide_hard_links = Приховати жорсткі посилання\nsettings_hide_hard_links_hint = Приховати складні посилання на однакові файли в результатах\nsettings_thread_number = Номер теми\nsettings_restart_required = ---Вам потрібно перезапустити програму, щоб застосувати зміни в номері тем ---\nsettings_duplicate_image_preview = Попередній перегляд зображення\nsettings_duplicate_minimal_hash_cache_size = Мінімальний розмір кешованих файлів - Hash (Kb)\nsettings_duplicate_use_prehash = Використовувати значення\nsettings_duplicate_minimal_prehash_cache_size = Мінімальний розмір кешованих файлів - Прегеш (Kb)\nsettings_similar_images_show_image_preview = Попередній перегляд зображення\nsettings_application_scale_text = Масштаб застосування\nsettings_application_scale_hint_text = Коли увімкнено ручний масштаб, це дозволяє вибрати власний коефіцієнт масштабування, але повністю вимикає автоматичне масштабування на основі DPI монітора.\nsettings_restart_required_scale_text = ---Вам потрібно перезапустити додаток, щоб застосувати зміни в масштабі---\nsettings_use_manual_application_scale_text = Використовуйте ручний масштаб застосування\nsettings_video_thumbnails_preview = Попередній перегляд зображення\nsettings_open_config_folder = Відкрити папку конфігурації\nsettings_open_cache_folder = Відкрити теку кешу\nsettings_language = Мова:\nsettings_current_preset = Поточний шаблон:\nsettings_edit_name = Змінити ім'я\nsettings_choose_name_for_prefix = Виберіть ім'я для префікса\nsettings_save = Зберегти\nsettings_load = Загрузити\nsettings_reset = Скинути\nsettings_similar_videos_tool = Інструмент типізації відео\nsettings_video_thumbnails_clear_unused_thumbnails = Видалити невикористані мініатюри відео старші за 7 днів при запуску програми\nsettings_video_thumbnails_header = Відео Мікшені\nsettings_video_thumbnails_generate = Згенерувати мініатюри\nsettings_video_thumbnails_position = Місце мініатюри у відео (%)\nsettings_video_thumbnails_generate_grid = Згенерувати сітку мініатюр замість одного зображення\nsettings_video_thumbnails_generate_grid_hint = Генерація кількох зображень у сітці значно повільніша, ніж генерація одного мініатюрного зображення\nsettings_video_thumbnails_grid_tiles_per_side = Кількість плиток на стороні в мініатюрній сітці\nsettings_video_thumbnails_grid_tiles_per_side_hint = Кількість мініатюрних плиток по стороні в сітці. Наприклад, вибір 2 створює сітку 2 x 2, що призводить до однієї мініатюри, що складається з 4 зображень.\nsettings_similar_images_tool = Інструмент типізації зображень\nsettings_general_settings = Загальні налаштування\nsettings_cache_header_text = Налаштування кешу\nsettings_clean_cache_button_text = Очистити застарілі кеші\nsettings_settings = Налаштування\nsettings_load_tabs_sizes_at_startup = Завантажувати розміри вкладок при запуску\nsettings_load_windows_size_at_startup = Розмір вікна при запуску\nsettings_limit_lines_of_messages = Обмежити повідомлення до 500 рядків(працює над повільним віджетом Texted)\nsettings_play_audio_on_scan_completion_text = Відтворити звук при успішному завершенні скану\nsettings_audio_feature_hint_text = Доступно лише при збиранні з функцією аудіо\nsettings_audio_env_variable_hint_text = Можна змінити звук, встановивши змінну середовища KROKIET_AUDIO_STOP_FILE на діючий шлях до аудіофайлу\npopup_save_title = Збереження результатів\npopup_save_message = Це дозволить зберегти результати до 3 різних файлів\npopup_rename_title = Перейменування файлів\npopup_new_paths_title = Будь ласка, додайте шляхи по одному на рядок\npopup_move_title = Переміщення файлів\npopup_move_copy_checkbox = Копіювання файлів замість переміщення\npopup_move_preserve_folder_checkbox = Зберігати структуру папки\nmove_confirmation_text = Ви впевнені, що хочете перемістити вибрані елементи?\nrename_confirmation_text = Ви впевнені, що хочете перейменувати вибрані елементи?\ndelete = Видалити елементи\nstopping_scan = Зупиняю скан, будь ласка, зачекайте...\nsearching = Пошук...\nsubsettings_videos_crop_detect = Метод Обрізання\nsubsettings_videos_skip_forward_amount = Пропустити тривалість [с]\nsubsettings_videos_vid_hash_duration = Тривалість хешу відео\nsettings_cache_number_size_text = КількістьMegabajtів кеш-файлів: { $size }, кількість файлів: { $number }\nsettings_video_thumbnails_number_size_text = Розмір зображків для опису відео: { $size }, кількість файлів: { $number }\nsettings_log_number_size_text = Розмір лог-файлів: { $size }, кількість файлів: { $number }\npopup_clean_cache_title_text = Очистити застарілі кеші\npopup_clean_cache_confirmation_text = Ви впевнені, що хочете очистити застарілі записи кешу? Це видалить записи кешу для файлів, які більше не існують або були змінені.\npopup_clean_cache_progress_text = Обробка кешу файлу:\npopup_clean_cache_current_file_text = Поточна файл:\npopup_clean_cache_file_progress_text = Поточний прогрес файлу:\npopup_clean_cache_overall_progress_text = Загальний прогрес:\npopup_clean_cache_stopped_by_user_text = Очищення кешу було зупинено користувачем\npopup_clean_cache_finished_text = Очищення кешу завершено успішно!\npopup_clean_cache_error_details_text = Помилкові деталі:\npopup_clean_cache_files_with_errors = Файли з помилками:\nsubsettings_video_optimizer_mode = Режим\nsubsettings_video_optimizer_crop_type = Тип обрізки\nsubsettings_video_optimizer_black_pixel_threshold = Чорний піксельний поріг\nsubsettings_video_optimizer_black_pixel_threshold_hint = Максимальне значення RGB для кожного піксельного каналу, яке вважатиметься чорним (0-128). За замовчуванням: 20\nsubsettings_video_optimizer_black_bar_min_percentage = Чорний Порожній Мінімальний Відсоток\nsubsettings_video_optimizer_black_bar_min_percentage_hint = Мінімальний відсоток чорних пікселів у рядку/стовпці, який вважається чорною смугою (50-100). За замовчуванням: 90\nsubsettings_video_optimizer_max_samples = Макс Семпли\nsubsettings_video_optimizer_max_samples_hint = Максимальна кількість кадрів для аналізу на відео (5-1000). За замовчуванням: 60\nsubsettings_video_optimizer_min_crop_size = Мінімальний розмір обрізки\nsubsettings_video_optimizer_min_crop_size_hint = Мінімальна кількість пікселів для обрізки з будь-якого боку (1-1000). Менші обрізки ігноруються. За замовчуванням: 5\nsubsettings_video_optimizer_video_codec = Відео кодек\nsubsettings_video_optimizer_excluded_codecs = Виключені кодеки\nsubsettings_video_optimizer_video_quality = Якість відео (CRF)\nsubsettings_reset = Сброс\nsubsettings_exif_ignored_tags_text = Ігноровані теги:\nsubsettings_exif_ignored_tags_hint_text = Запунктирований список тегів, які слід виключити зі сканування (наприклад, GPS, Мініатюра). Деякі теги, такі як ImageWidth у файлах TIFF, приховані, щоб запобігти пошкодженню зображення.\nclean_button_text = Чистий\nclean_text = Очистити EXIF дані\nclean_confirmation_text = Ви впевнені, що хочете видалити дані EXIF з вибраних елементів?\ncrop_videos_text = Зрізати відео\ncrop_video_confirmation_text = Ви впевнені, що хочете обрізати вибрані відео?\ncrop_reencode_video_text = Перекодувати відео\nreencode_videos_text = Перекодувати відео\noptimize_button_text = Оптимізувати\noptimize_confirmation_text = Ви впевнені, що хочете повторно закодувати вибрані відео?\noptimize_fail_if_bigger_text = Помилка, якщо оптимізований файл більший\noptimize_overwrite_files_text = Перезаписувати файли\noptimize_limit_video_size_text = Обмежте розмір відео\noptimize_max_width_text = Максимальна ширина:\noptimize_max_height_text = Максимальна висота:\nhardlink_button_text = Жорстке посилання\nhardlink_text = Створити жорсткі посилання\nhardlink_confirmation_text = Ви впевнені, що хочете створити жорсткі посилання для вибраних елементів?\nsoftlink_button_text = М'яке посилання\nsoftlink_text = Створити символічні посилання\nsoftlink_confirmation_text = Ви впевнені, що хочете створити символічні посилання (symlinks) для вибраних елементів?\n"
  },
  {
    "path": "krokiet/i18n/zh-CN/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = 应用启动时发生严重错误\nrust_init_error_message = \n        在启动应用程序时发生了一个严重错误：\n \n        { $error_message }\n \n        这可能是由于缺少或损坏的 OpenGL/Vulkan 驱动程序、在虚拟机中运行应用程序或 Krokiet 或其库中的错误所致。\n \n        您可以尝试运行不同的构建（skia_opengl、skia_vulkan、femtovg_opengl - 默认）或使用软件渲染器，看看是否可以解决问题。.\nrust_loaded_preset = 加载预设 { $preset_idx }\nrust_file_already_exists = 文件 \"{ $file }\" 已存在，不会被覆盖\nrust_error_removing_file_after_copy = 删除文件 \"{ $file }\" 时出错 (复制到不同分区后), 原因: { $reason }\nrust_error_copying_file = 复制 \"{ $input }\" 到 \"{ $output }\" 时出错，原因：{ $reason }\nrust_loading_tags_cache = 加载标签缓存\nrust_loading_fingerprints_cache = 正在加载指纹缓存\nrust_saving_tags_cache = 正在保存标签缓存\nrust_saving_fingerprints_cache = 保存指纹缓存\nrust_loading_prehash_cache = 正在加载逮捕缓存\nrust_saving_prehash_cache = 正在保存抓取缓存\nrust_loading_hash_cache = 加载散列缓存\nrust_saving_hash_cache = 保存哈希缓存\nrust_loading_exif_cache = 加载 EXIF 缓存\nrust_saving_exif_cache = 保存 EXIF 缓存\nrust_scanning_name = 正在扫描 { $entries_checked } 文件的名称\nrust_scanning_size_name = 扫描 { $entries_checked } 文件的大小和名称\nrust_scanning_size = 扫描文件大小 { $entries_checked }\nrust_scanning_file = 正在扫描 { $entries_checked } 文件\nrust_scanning_folder = 正在扫描 { $entries_checked } 文件夹\nrust_checked_tags = 已检查的 { $items_stats } 标签\nrust_checked_content = 检查的 { $items_stats } ({ $size_stats })\nrust_compared_tags = 对比的 { $items_stats } 标签\nrust_compared_content = 比较的 { $items_stats } 内容\nrust_hashed_images = 哈希的 { $items_stats } 图像 ({ $size_stats })\nrust_compared_image_hashes = 对比的 { $items_stats } 图像哈希值\nrust_hashed_videos = 哈希的 { $items_stats } 视频\nrust_created_thumbnails = 为 { $items_stats } 视频创建缩略图\nrust_checked_files = 签入 { $items_stats } 文件 ({ $size_stats })\nrust_checked_files_bad_extensions = 已检查 { $items_stats } 文件\nrust_checked_files_bad_names = 检查 { $items_stats } 文件\nrust_checked_videos = 已检查 { $items_stats } 视频 ({ $size_stats })\nrust_analyzed_partial_hash = 分析了 { $items_stats } 文件的部分散列({ $size_stats })\nrust_analyzed_full_hash = 分析了 { $items_stats } 文件的完整哈希({ $size_stats })\nrust_failed_to_rename_file = 无法将文件 { $old_path } 重命名为 { $new_path }, 错误: { $error }\nrust_no_included_paths = 无法在未设置任何包含路径时开始扫描。.\nrust_all_paths_referenced = 无法在所有包含路径都设置为引用路径时开始扫描，您需要禁用输入路径旁边的引用复选框。.\nrust_found_empty_folders = 找到了{ $items_found }个空文件夹在{ $time }时间内\nrust_found_empty_files = 找到了 { $items_found } 个空文件在 { $time }\nrust_found_similar_images = 在 { $groups } 组中找到 { $items_found } 相似的图像文件在 { $time }\nrust_found_similar_videos = 在 { $groups } 组中找到 { $items_found } 类似的视频文件，在 { $time }\nrust_found_similar_music_files = 在 { $groups } 组中找到 { $items_found } 类似的音乐文件在 { $time }\nrust_found_invalid_symlinks = 找到了 { $items_found } 无效的符号链接在 { $time }\nrust_found_temporary_files = 找到了 { $items_found } 个临时文件，在 { $time }\nrust_no_file_type_selected = 找不到没有选定文件类型的损坏文件。.\nrust_found_broken_files = 找到 { $items_found } 个损坏的文件，从 { $size } 到 { $time }\nrust_found_bad_extensions = 在 { $items_found } 文件扩展名有坏的 { $time }\nrust_found_bad_names = 找到 { $items_found } 个名称不正确的文件在 { $time }\nrust_found_video_optimizer = 找到 { $items_found } 个文件进行优化，耗时 { $time }\nrust_found_duplicate_files = 找到 { $items_found } 重复的文件在 { $groups } 组占用了 { $size } 的 { $time }\nrust_found_duplicate_files_no_lost_space = 找到了{ $items_found }个重复文件，分布在{ $groups }组内，耗时{ $time }\nrust_found_big_files = 找到 { $items_found } 大小为 { $size } 的大文件，在 { $time }\nrust_found_exif_files = 找到 { $items_found } 个带有 exif 数据的文件于 { $time }\nrust_cannot_load_preset = 无法更改并加载预设 { $preset_idx } - 原因 { $reason }，使用默认设置\nrust_saved_preset = 保存预设 { $preset_idx }\nrust_cannot_save_preset = 无法保存预设 { $preset_idx } - 原因 { $reason }\nrust_reset_preset = 重置预设 { $preset_idx }\nrust_cannot_create_output_folder = 无法创建输出文件夹 { $output_folder }，原因： { $error }\nrust_delete_summary = 已删除 { $deleted } 项目，未能删除 { $failed } 项目，其中的 { $total } 项\nrust_rename_summary = 已重命名的 { $renamed } 项, 未能重命名了 { $failed } 项, 其中的 { $total } 项\nrust_move_summary = 移动 { $moved } 项目，未能移动 { $failed } 个项目，从 { $total } 个项目\nrust_hardlink_summary = 硬链接 { $hardlinked } 项目，未能硬链接 { $failed } 项目，共 { $total } 个项目\nrust_symlink_summary = 符号链接 { $symlinked } 项目，未能符号链接 { $failed } 个项目，总共有 { $total } 个项目\nrust_optimize_video_summary = 优化 { $optimized } 视频，未优化 { $failed } 视频，总共 { $total } 视频\nrust_clean_exif_summary = 清理了 { $cleaned } 文件中的 EXIF 数据，未能清理 { $failed } 文件，总共有 { $total } 个文件。\nrust_deleting_files = 正在删除 { $items_stats } 文件 ({ $size_stats })\nrust_deleting_no_size_files = 正在删除 { $items_stats } 文件\nrust_renaming_files = 正在重命名 { $items_stats } 文件\nrust_moving_files = 正在移动 { $items_stats } 文件({ $size_stats })\nrust_moving_no_size_files = 正在移动 { $items_stats } 文件\nrust_hardlinking_files = 硬链接 { $items_stats } 文件 ({ $size_stats })\nrust_hardlinking_no_size_files = 硬链接 { $items_stats } 文件\nrust_symlinking_files = 符号链接 { $items_stats } 文件 ({ $size_stats })\nrust_symlinking_no_size_files = 符号链接 { $items_stats } 文件\nrust_optimizing_videos = 优化 { $items_stats } 视频 ({ $size_stats })\nrust_optimizing_no_size_videos = 优化 { $items_stats } 视频\nrust_cleaning_exif = 从 { $items_stats } 文件中清理 EXIF ({ $size_stats })\nrust_cleaning_no_size_exif = 从 { $items_stats } 文件中清理 EXIF\nrust_no_files_deleted = 没有选择要删除的文件或文件夹\nrust_no_files_renamed = 没有选择要重命名的文件或文件夹\nrust_no_files_moved = 没有选择要移动的文件或文件夹\nrust_no_files_hardlinked = 未选择任何文件或文件夹用于硬链接\nrust_no_files_symlinked = 未选择任何文件或文件夹用于符号链接\nrust_no_videos_optimized = 未选择任何视频进行优化\nrust_no_exif_cleaned = 未选择任何文件进行EXIF清理\nrust_extracted_exif_tags = 从 { $items_stats } 文件中提取 EXIF 标签 ({ $size_stats })\nrust_delete_confirmation = 您确定要删除选定的项目吗？\nrust_delete_confirmation_number_simple = 选择了 { $items } 个项目。.\nrust_delete_confirmation_number_groups = { $items } 项目在 { $groups } 组中选定。.\nrust_delete_confirmation_selected_all_in_group = 在 { $groups } 组中选择的所有项目。.\nrust_move_confirmation = 确定您是否要移动所选项目？\nrust_move_confirmation_number_simple = { $items } 项目已选定。.\nrust_clean_exif_confirmation = 您确定要从所选项目删除 EXIF 数据吗？\nrust_clean_exif_confirmation_number_simple = { $items } 项目已选定。.\nclean_exif_overwrite_files_text = 覆盖文件\nrust_optimize_video_confirmation = 您确定要优化所选视频吗？\nrust_optimize_video_confirmation_number_simple = { $items } 项目已选定。.\nrust_hardlink_confirmation = 您确定要为所选项目创建硬链接吗？\nrust_hardlink_confirmation_number_simple = { $items } 项目已选定。.\nrust_symlink_confirmation = 您确定要为所选项目创建符号链接吗？\nrust_symlink_confirmation_number_simple = { $items } 项目已选定。.\nrust_rename_confirmation = 您确定要重命名所选项目吗？\nrust_rename_confirmation_number_simple = { $items } 项目已选定。.\nrust_cache_processed_files = 已处理 { $files } 缓存文件\nrust_cache_entries_stats = 移除了 { $removed } 条目中的所有 { $all }，{ $left } 条还剩\nrust_cache_size_reduced = 减少缓存文件大小为 { $size }\nrust_cache_time_elapsed = 时间耗过: { $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = 未能将 { $name } 硬链接到 { $target }，原因 { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = 选择\ncolumn_size = 大小\ncolumn_file_name = 文件名称\ncolumn_path = 路径\ncolumn_modification_date = 修改日期\ncolumn_similarity = 相似度\ncolumn_dimensions = 尺寸\ncolumn_new_dimensions = 新维度\ncolumn_title = 标题\ncolumn_artist = 艺人\ncolumn_year = 年份\ncolumn_bitrate = 位速率\ncolumn_length = 长度\ncolumn_genre = 流派数\ncolumn_type_of_error = 错误类型\ncolumn_symlink_name = 符号链接名称\ncolumn_symlink_folder = 符号链接文件夹\ncolumn_destination_path = 目标路径\ncolumn_current_extension = 当前扩展\ncolumn_proper_extension = 合适的扩展\ncolumn_fps = 帧率\ncolumn_codec = 编解码器\ncolumn_duration = 期限\ncolumn_exif_tags = EXIF 标签\ncolumn_new_name = 新名称\n# Slint translations\nok_button = 好的\ncancel_button = 取消\ndo_you_want_to_continue = 您想要继续吗？\nmain_window_title = Krokiet - 数据清理\nscan_button = 扫描\nstop_button = 停止\nstop_text = 停止\nselect_button = 选择\nmove_button = 移动\ndelete_button = 删除\nsave_button = 保存\nsort_button = 排序\nrename_button = 重命名：\nmotto = 这个程序可以自由使用，并且将永远是。\\n详情请参阅MIT/GPL许可证。.\nunicorn = 你不能看到独角兽，但独角兽总是看到你。.\nrepository = 存储库\ninstruction = 说明\ndonation = 捐助\ntranslation = 翻译\nincluded_paths = 包含路径\nexcluded_paths = 排除路径\nref = 参考值\npath = 路径\ntool_duplicate_files = 复制文件\ntool_empty_folders = 空文件夹\ntool_big_files = 大文件\ntool_empty_files = 空文件\ntool_temporary_files = 临时文件\ntool_similar_images = 相似图像\ntool_similar_videos = 相似视频\ntool_music_duplicates = 重复音乐\ntool_invalid_symlinks = 无效的符号链接\ntool_broken_files = 损坏的文件\ntool_bad_extensions = 错误的扩展\ntool_bad_names = 糟糕的名字\ntool_video_optimizer = 视频优化器\ntool_exif_remover = Exif移除\nsort_by_full_name = 按全名排序\nsort_by_selection = 按选择排序\nsort_reverse = 反向顺序\nselection_all = 选择所有\nselection_deselect_all = 取消全选\nselection_invert_selection = 反转选择\nselection_the_biggest_size = 选择最大的大小\nselection_the_biggest_resolution = 选择最大的分辨率\nselection_the_smallest_size = 选择最小尺寸\nselection_the_smallest_resolution = 选择最小分辨率\nselection_newest = 选择最新的\nselection_oldest = 选择最旧的\nselection_shortest_path = 选择最短路径\nselection_longest_path = 选择最长的路径\nstage_current = 当前阶段：\nstage_all = 所有阶段：\nsubsettings = 子设置\nsubsettings_images_hash_size = 散列大小\nsubsettings_images_resize_algorithm = 调整算法大小\nsubsettings_images_ignore_same_size = 忽略大小相同的图像\nsubsettings_images_max_difference = 最大差异\nsubsettings_images_duplicates_hash_type = 哈希类型\nsubsettings_duplicates_check_method = 检查方法\nsubsettings_duplicates_name_case_sensitive = 区分大小写(仅名称模式)\nsubsettings_biggest_files_sub_method = 方法\nsubsettings_biggest_files_sub_number_of_files = 文件数\nsubsettings_videos_max_difference = 最大差异\nsubsettings_videos_ignore_same_size = 忽略相同大小的视频\nsubsettings_music_audio_check_type = 音频检查类型\nsubsettings_music_approximate_comparison = 近似标签比较\nsubsettings_music_compared_tags = 比较标签\nsubsettings_music_title = 标题\nsubsettings_music_artist = 艺人\nsubsettings_music_bitrate = 位速率\nsubsettings_music_genre = 流派数\nsubsettings_music_year = 年份\nsubsettings_music_length = 长度\nsubsettings_music_max_difference = 最大差异\nsubsettings_music_minimal_fragment_duration = 最小碎片持续时间\nsubsettings_music_compare_fingerprints_only_with_similar_titles = 在相似标题的组中比较\nsubsettings_broken_files_type = 要检查的文件类型\nsubsettings_broken_files_audio = 音频\nsubsettings_broken_files_pdf = 帕德夫\nsubsettings_broken_files_archive = 存档\nsubsettings_broken_files_image = 图片\nsubsettings_broken_files_video = 视频\nsubsettings_broken_files_video_info = 使用ffmpeg/ffprobe。 速度较慢，并且可能检测到刻板的错误，即使文件播放正常。.\nsubsettings_bad_names_issues = 文件检查\nsubsettings_bad_names_uppercase_extension = 大写延伸\nsubsettings_bad_names_uppercase_extension_hint = 查找包含大写字母的扩展名的文件（例如：.JPG、.Mp3）并建议使用小写版本\nsubsettings_bad_names_emoji_used = 表情符号在名字里\nsubsettings_bad_names_emoji_used_hint = 查找包含表情符号字符（😀、🎉等）在名称中的文件并建议删除它们\nsubsettings_bad_names_space_at_start_end = 首尾空格\nsubsettings_bad_names_space_at_start_end_hint = 查找以空格开头或结尾的文件，并建议将其修剪\nsubsettings_bad_names_non_ascii = 非ASCII字符\nsubsettings_bad_names_non_ascii_hint = 查找非ASCII字符（ą、ć、ñ等）并建议用ASCII等效字符（a、c、n）替换，或如果不存在映射则删除\nsubsettings_bad_names_restricted_charset = 有限字符集\nsubsettings_bad_names_restricted_charset_hint = 转义非ASCII字符为ASCII，然后查找包含不在0-9a-zA-Z范围内以及用户自定义允许字符的文件的\nsubsettings_bad_names_allowed_chars = 允许字符\nsubsettings_bad_names_remove_duplicated = 重复的字符\nsubsettings_bad_names_remove_duplicated_hint = 查找连续重复的非字母数字字符（例如，“file---name..txt”）并建议删除重复项\nsettings_global_settings = 全局设置\nsettings_dark_theme = 暗色主题\nsettings_show_only_icons = 只显示图标\nsettings_excluded_items = 排除的项目：\nsettings_allowed_extensions = 允许的扩展：\nsettings_excluded_extensions = 排除扩展名：\nsettings_file_size = 文件大小(千字节)\nsettings_minimum_file_size = 最小值：\nsettings_maximum_file_size = 最大值：\nsettings_recursive_search = 递归搜索\nsettings_use_cache = 使用缓存\nsettings_save_as_json = 同时将缓存保存为 JSON 文件\nsettings_move_to_trash = 移动已删除的文件到回收站\nsettings_ignore_other_filesystems = 忽略其他文件系统 (仅限Linux)\nsettings_delete_outdated_cache_entries = 删除自动过时缓存条目\nsettings_delete_outdated_cache_entries_hint = 当启用时，应用程序将在缓存加载期间（每周最多一次）验证缓存记录是否仍然指向现有且未修改的文件/数据\nsettings_hide_hard_links = 隐藏硬链接\nsettings_hide_hard_links_hint = 隐藏相同文件的硬链接在结果中\nsettings_thread_number = 线程编号\nsettings_restart_required = ---你需要重新启动应用才能应用线程编号中的更改--\nsettings_duplicate_image_preview = 图像预览\nsettings_duplicate_minimal_hash_cache_size = 缓存文件的最小大小 - 哈希值 (KB)\nsettings_duplicate_use_prehash = 使用预设的\nsettings_duplicate_minimal_prehash_cache_size = 缓存文件最小大小 - Prehash (KB)\nsettings_similar_images_show_image_preview = 图像预览\nsettings_application_scale_text = 应用规模\nsettings_application_scale_hint_text = 当手动缩放启用时，这允许您选择自定义缩放比例，但完全禁用基于显示器 DPI 的自动缩放。.\nsettings_restart_required_scale_text = 您需要重启应用以应用缩放更改\nsettings_use_manual_application_scale_text = 使用手动应用刻度\nsettings_video_thumbnails_preview = 图片预览\nsettings_open_config_folder = 打开配置文件夹\nsettings_open_cache_folder = 打开缓存文件夹\nsettings_language = 语言\nsettings_current_preset = 当前预设：\nsettings_edit_name = 编辑名称\nsettings_choose_name_for_prefix = 选择前缀的名称\nsettings_save = 保存\nsettings_load = 负载\nsettings_reset = 恢复\nsettings_similar_videos_tool = 相似视频工具\nsettings_video_thumbnails_clear_unused_thumbnails = 删除启动时超过7天未使用的视频缩略图\nsettings_video_thumbnails_header = 视频缩图\nsettings_video_thumbnails_generate = 生成缩略图\nsettings_video_thumbnails_position = 视频缩图位置（%）\nsettings_video_thumbnails_generate_grid = 生成缩图网格而不是单个图像\nsettings_video_thumbnails_generate_grid_hint = 生成多个图像在网格中比生成单个缩略图要慢很多\nsettings_video_thumbnails_grid_tiles_per_side = 缩略图网格每侧瓷砖数量\nsettings_video_thumbnails_grid_tiles_per_side_hint = 网格中每侧缩图瓦片的数量。例如，选择 2 会创建一个 2 x 2 网格，从而产生一个由 4 张图像组成的单个缩图。.\nsettings_similar_images_tool = 相似图像工具\nsettings_general_settings = 常规设置\nsettings_cache_header_text = 缓存设置\nsettings_clean_cache_button_text = 清除过时的缓存\nsettings_settings = 设置\nsettings_load_tabs_sizes_at_startup = 启动时加载标签大小\nsettings_load_windows_size_at_startup = 启动时加载窗口大小\nsettings_limit_lines_of_messages = 将消息限制在500行(用于慢速文字编辑部件的工作)\nsettings_play_audio_on_scan_completion_text = 扫描完成成功时播放声音\nsettings_audio_feature_hint_text = 仅在启用音频功能时可用\nsettings_audio_env_variable_hint_text = 声音可以被改变，通过将 KROKIET_AUDIO_STOP_FILE 环境变量设置为一个有效的音频文件路径\npopup_save_title = 保存结果\npopup_save_message = 这将保存结果到3个不同的文件\npopup_rename_title = 重命名文件\npopup_new_paths_title = 请在一行添加路径\npopup_move_title = 正在移动文件\npopup_move_copy_checkbox = 复制文件而不是移动\npopup_move_preserve_folder_checkbox = 保留文件夹结构\nmove_confirmation_text = 确定您是否要移动所选项目？\nrename_confirmation_text = 您确定要重命名所选项目吗？\ndelete = 删除项目\nstopping_scan = 正在停止扫描，请稍候...\nsearching = 搜索中...\nsubsettings_videos_crop_detect = 裁剪检测方法\nsubsettings_videos_skip_forward_amount = 跳过持续时间 [s]\nsubsettings_videos_vid_hash_duration = 视频散列持续时间\nsettings_cache_number_size_text = 缓存文件大小: { $size }, 文件数量: { $number }\nsettings_video_thumbnails_number_size_text = 视频缩略图大小： { $size }，文件数量： { $number }\nsettings_log_number_size_text = 日志文件大小: { $size }, 文件数量: { $number }\npopup_clean_cache_title_text = 清除过时缓存\npopup_clean_cache_confirmation_text = 您确定要清理过时的缓存条目吗？ 这将删除不再存在或已修改的文件缓存条目。.\npopup_clean_cache_progress_text = 处理缓存文件：\npopup_clean_cache_current_file_text = 当前文件：\npopup_clean_cache_file_progress_text = 当前文件进度：\npopup_clean_cache_overall_progress_text = 总进度：\npopup_clean_cache_stopped_by_user_text = 用户已停止清理缓存\npopup_clean_cache_finished_text = 缓存清理成功！\npopup_clean_cache_error_details_text = 错误详情：\npopup_clean_cache_files_with_errors = 错误的文件：\nsubsettings_video_optimizer_mode = 模式\nsubsettings_video_optimizer_crop_type = 作物类型\nsubsettings_video_optimizer_black_pixel_threshold = 黑色像素阈值\nsubsettings_video_optimizer_black_pixel_threshold_hint = 每个像素通道的最大RGB值应被视为黑色（0-128）。 默认：20\nsubsettings_video_optimizer_black_bar_min_percentage = 黑色条最小百分比\nsubsettings_video_optimizer_black_bar_min_percentage_hint = 最小的行/列中黑色像素百分比，被认为是黑色条纹的阈值（50-100）。 默认：90\nsubsettings_video_optimizer_max_samples = 最大样本\nsubsettings_video_optimizer_max_samples_hint = 分析视频的最大帧数（5-1000）。默认：60\nsubsettings_video_optimizer_min_crop_size = 最小作物尺寸\nsubsettings_video_optimizer_min_crop_size_hint = 至少裁剪的像素数量（1-1000）。较小的裁剪将被忽略。默认：5\nsubsettings_video_optimizer_video_codec = 视频编码器\nsubsettings_video_optimizer_excluded_codecs = 排除编解码器\nsubsettings_video_optimizer_video_quality = 视频质量 (CRF)\nsubsettings_reset = 重置\nsubsettings_exif_ignored_tags_text = 忽略的标签：\nsubsettings_exif_ignored_tags_hint_text = 逗号分隔的排除扫描的标签列表（例如：GPS、缩图）。某些标签，如 TIFF 文件中的 ImageWidth，被隐藏以防止损坏图像。.\nclean_button_text = 清洁\nclean_text = 清理EXIF数据\nclean_confirmation_text = 您确定要从所选项目删除 EXIF 数据吗？\ncrop_videos_text = 裁剪视频\ncrop_video_confirmation_text = 您确定要裁剪所选视频吗？\ncrop_reencode_video_text = 重新编码视频\nreencode_videos_text = 重新编码视频\noptimize_button_text = 优化\noptimize_confirmation_text = 您确定要重新编码所选视频吗？\noptimize_fail_if_bigger_text = 如果优化文件更大则失败\noptimize_overwrite_files_text = 覆盖文件\noptimize_limit_video_size_text = 限制视频大小\noptimize_max_width_text = 最大宽度：\noptimize_max_height_text = 最大高度：\nhardlink_button_text = 硬链接\nhardlink_text = 创建硬链接\nhardlink_confirmation_text = 您确定要为所选项目创建硬链接吗？\nsoftlink_button_text = 软链接\nsoftlink_text = 创建软链接\nsoftlink_confirmation_text = 您确定要为所选项目创建软链接（符号链接）吗？\n"
  },
  {
    "path": "krokiet/i18n/zh-TW/krokiet.ftl",
    "content": "# In Rust translations\nrust_init_error_title = 應用程式啟動期間發生嚴重錯誤\nrust_init_error_message = \n        當啟動應用程式時發生了嚴重錯誤：\n \n        { $error_message }\n \n        這可能是因為缺少或損壞的 OpenGL/Vulkan 驅動程式、在虛擬機器中或在 Krokiet 或其其中一個函式庫中執行所致，或 Krokiet 或其函式庫中的錯誤。\n \n        您可以嘗試執行不同的建置（skia_opengl、skia_vulkan、femtovg_opengl - 預設）或使用軟體渲染器，看看是否能解決問題。.\nrust_loaded_preset = 已載入預設設定 { $preset_idx }\nrust_file_already_exists = 檔案 \"{ $file }\" 已存在，不會被覆蓋。\nrust_error_removing_file_after_copy = 移除檔案 \"{ $file }\" 發生錯誤 (將其複製到不同分割區後)，原因：{ $reason}\nrust_error_copying_file = 複製 \"{ $input }\" 到 \"{ $output }\" 發生錯誤，原因：{ $reason }\nrust_loading_tags_cache = 正在載入標籤快取\nrust_loading_fingerprints_cache = 正在載入指紋快取\nrust_saving_tags_cache = 正在儲存標籤快取\nrust_saving_fingerprints_cache = 正在儲存指紋快取\nrust_loading_prehash_cache = 正在載入 PreHash 快取\nrust_saving_prehash_cache = 正在儲存 PreHash 快取\nrust_loading_hash_cache = 正在載入雜湊快取\nrust_saving_hash_cache = 正在儲存雜湊快取\nrust_loading_exif_cache = 載入 EXIF 緩存\nrust_saving_exif_cache = 儲存 EXIF 緩存\nrust_scanning_name = 正在掃描 { $entries_checked } 個檔案名稱\nrust_scanning_size_name = 正在掃描 { $entries_checked } 檔案的大小與名稱\nrust_scanning_size = 正在掃描 { $entries_checked } 個檔案的大小\nrust_scanning_file = 正在掃描第 { $entries_checked } 個檔案\nrust_scanning_folder = 正在掃描第 { $entries_checked } 個資料夾\nrust_checked_tags = 已檢查 { $items_stats } 的標籤\nrust_checked_content = 已檢查 { $items_stats } 的內容（{ $size_stats }）\nrust_compared_tags = 已比對 { $items_stats } 的標籤\nrust_compared_content = 已比對 { $items_stats } 的內容\nrust_hashed_images = 已雜湊 { $items_stats } 張圖像（{ $size_stats }）\nrust_compared_image_hashes = 已比對 { $items_stats } 的圖像雜湊\nrust_hashed_videos = 已雜湊 { $items_stats } 個影片\nrust_created_thumbnails = 已為 { $items_stats } 個影片建立縮圖\nrust_checked_files = 已檢查 { $items_stats } 個檔案（{ $size_stats }）\nrust_checked_files_bad_extensions = 已檢查 { $items_stats } 個檔案\nrust_checked_files_bad_names = 已檢查 { $items_stats } 個檔案\nrust_checked_videos = 已檢閱 { $items_stats } 影片 ({ $size_stats })\nrust_analyzed_partial_hash = 已分析 { $items_stats } 個檔案的部分雜湊（{ $size_stats }）\nrust_analyzed_full_hash = 已分析 { $items_stats } 個檔案的完整雜湊（{ $size_stats }）\nrust_failed_to_rename_file = 無法將檔案 { $old_path } 重新命名為 { $new_path }，錯誤：{ $error }\nrust_no_included_paths = 無法在未設定任何包含路徑時開始掃描。.\nrust_all_paths_referenced = 無法在所有包含路徑都設定為參照路徑時開始掃描，您需要停選啟動參照檢查方塊旁邊的輸入路徑。.\nrust_found_empty_folders = 在 { $time } 內找到 { $items_found } 個空資料夾\nrust_found_empty_files = 在 { $time } 內找到 { $items_found } 個空檔案\nrust_found_similar_images = 在 { $time } 內找到 { $items_found } 個相似圖檔，共 { $groups } 組\nrust_found_similar_videos = 在 { $time } 內找到 { $items_found } 個相似影片檔案，共 { $groups } 組\nrust_found_similar_music_files = 在 { $time } 內找到 { $items_found } 個相似音樂檔案，共 { $groups } 組\nrust_found_invalid_symlinks = 在 { $time } 內找到 { $items_found } 個無效符號連結\nrust_found_temporary_files = 在 { $time } 內找到 { $items_found } 個暫存檔案\nrust_no_file_type_selected = 無法找到損壞檔案，請先選擇檔案類型。.\nrust_found_broken_files = 在 { $time } 內找到 { $items_found } 個損壞檔案，佔用 { $size }\nrust_found_bad_extensions = 在 { $time } 內找到 { $items_found } 個副檔名錯誤的檔案\nrust_found_bad_names = 找到 { $items_found } 個檔名有問題，在 { $time }\nrust_found_video_optimizer = 找到 { $items_found } 個檔案進行優化，耗時 { $time }\nrust_found_duplicate_files = 在 { $time } 內找到 { $items_found } 個重複檔案，共 { $groups } 組，佔用 { $size }\nrust_found_duplicate_files_no_lost_space = 在 { $time } 內找到 { $items_found } 個重複檔案，共 { $groups } 組\nrust_found_big_files = 在 { $time } 內找到 { $items_found } 個大檔案，總大小為 { $size }\nrust_found_exif_files = 找到 { $items_found } 個具有 EXIF 數據的文件於 { $time }\nrust_cannot_load_preset = 無法載入並切換至預設設定 { $preset_idx }，原因：{ $reason }，將改用預設設定\nrust_saved_preset = 已儲存預設設定 { $preset_idx }\nrust_cannot_save_preset = 無法儲存預設設定 { $preset_idx }，原因：{ $reason }\nrust_reset_preset = 已重設預設設定 { $preset_idx }\nrust_cannot_create_output_folder = 無法建立輸出資料夾 { $output_folder }，原因：{ $error }\nrust_delete_summary = 已刪除 { $deleted } 個項目，刪除失敗 { $failed } 個項目，共 { $total } 個項目\nrust_rename_summary = 已重新命名 { $renamed } 個項目，重新命名失敗 { $failed } 個項目，共 { $total } 個項目\nrust_move_summary = 已移動 { $moved } 個項目，移動失敗 { $failed } 個項目，共 { $total } 個項目\nrust_hardlink_summary = 硬連結 { $hardlinked } 項目，未能硬連結 { $failed } 項目，共 { $total } 項目\nrust_symlink_summary = 連結符 { $symlinked } 項目失敗，連結符 { $failed } 項目失敗，總共有 { $total } 項目\nrust_optimize_video_summary = 優化 { $optimized } 影片，未優化 { $failed } 影片，共 { $total } 影片\nrust_clean_exif_summary = 已清理 { $cleaned } 檔案，未能清理 { $failed } 檔案，總共 { $total } 檔案。\nrust_deleting_files = 正在刪除 { $items_stats } 個檔案（{ $size_stats }）\nrust_deleting_no_size_files = 正在刪除 { $items_stats } 個檔案\nrust_renaming_files = 正在重新命名 { $items_stats } 個檔案\nrust_moving_files = 正在移動 { $items_stats } 個檔案（{ $size_stats }）\nrust_moving_no_size_files = 正在移動 { $items_stats } 個檔案\nrust_hardlinking_files = 硬連結 { $items_stats } 檔案 ({ $size_stats })\nrust_hardlinking_no_size_files = 硬連結 { $items_stats } 檔案\nrust_symlinking_files = 建立對 { $items_stats } 檔案的符號連結 ({ $size_stats })\nrust_symlinking_no_size_files = 建立對 { $items_stats } 檔案的符號連結\nrust_optimizing_videos = 優化 { $items_stats } 影片 ({ $size_stats })\nrust_optimizing_no_size_videos = 優化 { $items_stats } 影片\nrust_cleaning_exif = 清除 { $items_stats } 檔案的 EXIF 資訊 ({ $size_stats })\nrust_cleaning_no_size_exif = 清除 { $items_stats } 檔案的 EXIF 資料\nrust_no_files_deleted = 沒有選取任何檔案或資料夾進行刪除\nrust_no_files_renamed = 沒有選取任何檔案或資料夾重命名\nrust_no_files_moved = 沒有選取任何檔案或資料夾進行移動\nrust_no_files_hardlinked = 沒有選擇任何檔案或資料夾進行硬連結\nrust_no_files_symlinked = 沒有選擇任何檔案或資料夾用於符號連結\nrust_no_videos_optimized = 未選擇任何影片進行優化\nrust_no_exif_cleaned = 沒有選擇任何檔案用於 EXIF 清理\nrust_extracted_exif_tags = 從 { $items_stats } 檔案中提取 EXIF 標籤 ({ $size_stats })\nrust_delete_confirmation = 您確定要刪除所選項目嗎？\nrust_delete_confirmation_number_simple = 選取了 { $items } 個項目。.\nrust_delete_confirmation_number_groups = 在 { $groups } 個群組中選取了 { $items } 個項目。.\nrust_delete_confirmation_selected_all_in_group = 已選取 { $groups } 個群組中的所有項目。.\nrust_move_confirmation = 確定您是否要移動所選的項目？\nrust_move_confirmation_number_simple = { $items } 項目已選取。.\nrust_clean_exif_confirmation = 您確定要從所選項目中移除 EXIF 數據嗎？\nrust_clean_exif_confirmation_number_simple = { $items } 項目已選取。.\nclean_exif_overwrite_files_text = 覆蓋檔案\nrust_optimize_video_confirmation = 您確定要優化所選取的影片嗎？\nrust_optimize_video_confirmation_number_simple = { $items } 項目已選取。.\nrust_hardlink_confirmation = 您確定要為所選項目建立硬連結嗎？\nrust_hardlink_confirmation_number_simple = { $items } 項目已選取。.\nrust_symlink_confirmation = 確定您是否想為所選項目建立對象鏈接？\nrust_symlink_confirmation_number_simple = { $items } 項目已選取。.\nrust_rename_confirmation = 確定您是否要更名所選取的項目？\nrust_rename_confirmation_number_simple = { $items } 項目已選取。.\nrust_cache_processed_files = 已處理 { $files } 緩存檔案\nrust_cache_entries_stats = 移除 { $removed } 條出於所有 { $all }，{ $left } 條剩餘\nrust_cache_size_reduced = 減少緩存檔案大小於 { $size }\nrust_cache_time_elapsed = 時間經過：{ $time }\nrust_symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason }\nrust_hardlink_failed = 無法硬連結 { $name } 至 { $target }，理由 { $reason }\n\n# Slint translations, but in arrays\n\ncolumn_selection = 選擇\ncolumn_size = 大小\ncolumn_file_name = 檔案名稱\ncolumn_path = 路徑\ncolumn_modification_date = 修改日期\ncolumn_similarity = 相似度\ncolumn_dimensions = 尺寸\ncolumn_new_dimensions = 新境界\ncolumn_title = 標題\ncolumn_artist = 藝人\ncolumn_year = 年份\ncolumn_bitrate = 位元率\ncolumn_length = 長度\ncolumn_genre = 類型\ncolumn_type_of_error = 錯誤類型\ncolumn_symlink_name = 符號連結名稱\ncolumn_symlink_folder = 符號連結資料夾\ncolumn_destination_path = 目的地路徑\ncolumn_current_extension = 目前副檔名\ncolumn_proper_extension = 正確副檔名\ncolumn_fps = 每秒幀數\ncolumn_codec = 編碼解碼器\ncolumn_duration = 持續時間\ncolumn_exif_tags = EXIF 標籤\ncolumn_new_name = 新名稱\n# Slint translations\nok_button = 確定\ncancel_button = 取消\ndo_you_want_to_continue = 您想繼續嗎？\nmain_window_title = 克羅基特 - 資料清潔器\nscan_button = 掃描\nstop_button = 停止\nstop_text = 停止\nselect_button = 選擇\nmove_button = 移動\ndelete_button = 刪除\nsave_button = 儲存\nsort_button = 排序\nrename_button = 重新命名\nmotto = 這個程式免費供使用，並且永遠會如此。\\n請參閱 MIT/GPL 授權以取得詳細資料。.\nunicorn = 你可能不會看獨角獸，但獨角獸總是看著你。.\nrepository = 儲存庫\ninstruction = 說明\ndonation = 贊助\ntranslation = 翻譯\nincluded_paths = 包含的路徑\nexcluded_paths = 排除路徑\nref = 參考\npath = 路徑\ntool_duplicate_files = 重複檔案\ntool_empty_folders = 空資料夾\ntool_big_files = 大檔案\ntool_empty_files = 空檔案\ntool_temporary_files = 臨時檔案\ntool_similar_images = 相似影像\ntool_similar_videos = 相似影片\ntool_music_duplicates = 音樂重複\ntool_invalid_symlinks = 無效的符號連結\ntool_broken_files = 損壞的檔案\ntool_bad_extensions = 錯誤的副檔名\ntool_bad_names = 糟糕的名字\ntool_video_optimizer = 影片優化器\ntool_exif_remover = Exif 移除\nsort_by_full_name = 依完整路徑排序\nsort_by_selection = 依選擇狀態排序\nsort_reverse = 反向排序\nselection_all = 選擇全部\nselection_deselect_all = 取消全部\nselection_invert_selection = 反向選擇\nselection_the_biggest_size = 選擇最大檔案\nselection_the_biggest_resolution = 選擇最大解析度\nselection_the_smallest_size = 選擇最小檔案\nselection_the_smallest_resolution = 選擇最小解析度\nselection_newest = 選擇最新的\nselection_oldest = 選擇最舊的\nselection_shortest_path = 選擇最短路徑\nselection_longest_path = 選擇最長路徑\nstage_current = 當前階段：\nstage_all = 所有階段:\nsubsettings = 子選項設定\nsubsettings_images_hash_size = 哈希大小\nsubsettings_images_resize_algorithm = 縮圖演算法\nsubsettings_images_ignore_same_size = 忽略相同大小的圖像\nsubsettings_images_max_difference = 最大差異值\nsubsettings_images_duplicates_hash_type = 哈希類型\nsubsettings_duplicates_check_method = 檢查方法\nsubsettings_duplicates_name_case_sensitive = 區分大小寫（僅適用於名稱模式）\nsubsettings_biggest_files_sub_method = 方法\nsubsettings_biggest_files_sub_number_of_files = 檔案數量\nsubsettings_videos_max_difference = 最大差異值\nsubsettings_videos_ignore_same_size = 忽略相同大小的視頻\nsubsettings_music_audio_check_type = 音訊檢查方式\nsubsettings_music_approximate_comparison = 近似標籤比對\nsubsettings_music_compared_tags = 比對標籤\nsubsettings_music_title = 標題\nsubsettings_music_artist = 藝人\nsubsettings_music_bitrate = 位元率\nsubsettings_music_genre = 類型\nsubsettings_music_year = 年份\nsubsettings_music_length = 長度\nsubsettings_music_max_difference = 最大差異值\nsubsettings_music_minimal_fragment_duration = 最小片段長度\nsubsettings_music_compare_fingerprints_only_with_similar_titles = 比較類似標題之群組內\nsubsettings_broken_files_type = 要檢查的檔案類型\nsubsettings_broken_files_audio = 音訊\nsubsettings_broken_files_pdf = PDF\nsubsettings_broken_files_archive = 歸檔\nsubsettings_broken_files_image = 影像\nsubsettings_broken_files_video = 影片\nsubsettings_broken_files_video_info = 使用 ffmpeg/ffprobe。 相當慢，且可能偵測到刻板錯誤，即使檔案播放正常。.\nsubsettings_bad_names_issues = 檔案檢查\nsubsettings_bad_names_uppercase_extension = 大寫延伸\nsubsettings_bad_names_uppercase_extension_hint = 尋找具有大寫字母的檔案名稱（例如：.JPG、.Mp3）並建議使用小寫字母版本\nsubsettings_bad_names_emoji_used = 表情符號在名字裡\nsubsettings_bad_names_emoji_used_hint = 尋找名稱包含表情符號 (😀、🎉 等等) 的檔案並建議移除它們\nsubsettings_bad_names_space_at_start_end = 首尾空白\nsubsettings_bad_names_space_at_start_end_hint = 找到名稱開頭或結尾有空格的檔案，並建議修剪它們\nsubsettings_bad_names_non_ascii = 非ASCII字元\nsubsettings_bad_names_non_ascii_hint = 找到非ASCII字符（ą、ć、ñ等）并建议用ASCII等效字符（a、c、n）替换，或如果不存在映射则删除\nsubsettings_bad_names_restricted_charset = 有限字集\nsubsettings_bad_names_restricted_charset_hint = 將非ASCII字元轉譯為ASCII，然後尋找包含不在0-9a-zA-Z及使用者定義允許字元內的檔案\nsubsettings_bad_names_allowed_chars = 允許的字元\nsubsettings_bad_names_remove_duplicated = 重複字\nsubsettings_bad_names_remove_duplicated_hint = 尋找連續重複的非字母數字字符（例如，“file---name..txt”）並建議移除重複項目\nsettings_global_settings = 全球設定\nsettings_dark_theme = 深色主題\nsettings_show_only_icons = 僅顯示圖示\nsettings_excluded_items = 排除項目：\nsettings_allowed_extensions = 允許的副檔名：\nsettings_excluded_extensions = 排除的副檔名：\nsettings_file_size = 檔案大小（KB）\nsettings_minimum_file_size = 最小:\nsettings_maximum_file_size = 最大:\nsettings_recursive_search = 遞迴搜尋\nsettings_use_cache = 使用快取\nsettings_save_as_json = 同時將快取儲存為 JSON 檔案\nsettings_move_to_trash = 移動已刪除的檔案到回收桶\nsettings_ignore_other_filesystems = 忽略其它檔案系統（僅限 Linux）\nsettings_delete_outdated_cache_entries = 刪除自動過時快取項目\nsettings_delete_outdated_cache_entries_hint = 當啟用時，應用程式會在緩存載入期間（每周最多一次）驗證緩存記錄是否仍然指向有效的且未修改的檔案/資料\nsettings_hide_hard_links = 隱藏硬連結\nsettings_hide_hard_links_hint = 隱藏相同檔案的硬連結於結果中\nsettings_thread_number = 執行緒數量\nsettings_restart_required = ---您需要重新啟動應用程式以套用執行緒數量的更改---\nsettings_duplicate_image_preview = 圖像預覽\nsettings_duplicate_minimal_hash_cache_size = 快取檔案的最小大小 - 雜湊（KB）\nsettings_duplicate_use_prehash = 使用預雜湊\nsettings_duplicate_minimal_prehash_cache_size = 快取檔案的最小大小 - 預雜湊（KB）\nsettings_similar_images_show_image_preview = 圖像預覽\nsettings_application_scale_text = 應用規模\nsettings_application_scale_hint_text = 當手動縮放已啟用時，這允許您選擇自訂縮放比例，但完全停用根據螢幕 DPI 進行的自動縮放。.\nsettings_restart_required_scale_text = 您需要重新啟動應用程式以應用比例變更。\nsettings_use_manual_application_scale_text = 使用手動應用量表\nsettings_video_thumbnails_preview = 圖像預覽\nsettings_open_config_folder = 開啟設定資料夾\nsettings_open_cache_folder = 開啟快取資料夾\nsettings_language = 語言\nsettings_current_preset = 目前預設：\nsettings_edit_name = 編輯名稱\nsettings_choose_name_for_prefix = 選擇名稱作為前綴\nsettings_save = 儲存\nsettings_load = 載入\nsettings_reset = 重設\nsettings_similar_videos_tool = 相似影片工具\nsettings_video_thumbnails_clear_unused_thumbnails = 刪除啟動時超過 7 天未使用的影片缩圖\nsettings_video_thumbnails_header = 影片縮圖\nsettings_video_thumbnails_generate = 產生縮圖\nsettings_video_thumbnails_position = 視頻中縮圖位置 (%)\nsettings_video_thumbnails_generate_grid = 生成縮圖網格而不是單張圖片\nsettings_video_thumbnails_generate_grid_hint = 生成多個圖片為網格比生成單個縮圖要慢很多\nsettings_video_thumbnails_grid_tiles_per_side = 縮圖網格每側瓷磚數量\nsettings_video_thumbnails_grid_tiles_per_side_hint = 網格每側縮圖瓦片的數量。例如，選擇 2 會產生一個 2 x 2 網格，結果會產生一個由 4 張圖片組成的單一縮圖。.\nsettings_similar_images_tool = 相似圖像工具\nsettings_general_settings = 一般設定\nsettings_cache_header_text = 快取設定\nsettings_clean_cache_button_text = 清除過時快取\nsettings_settings = 設定\nsettings_load_tabs_sizes_at_startup = 啟動時載入分頁大小\nsettings_load_windows_size_at_startup = 啟動時載入視窗大小\nsettings_limit_lines_of_messages = 限制訊息行數為 500 行（用以應對 TextEdit 元件載入緩慢的問題）\nsettings_play_audio_on_scan_completion_text = 當掃描成功時播放聲音\nsettings_audio_feature_hint_text = 僅在啟用音頻功能時可用\nsettings_audio_env_variable_hint_text = 可以改變聲音，通過將 KROKIET_AUDIO_STOP_FILE 環境變數設置為有效的音頻檔案路徑\npopup_save_title = 儲存結果\npopup_save_message = 這將把結果保存到3個不同的檔案中\npopup_rename_title = 重新命名檔案\npopup_new_paths_title = 請添加路徑一行一列\npopup_move_title = 移動檔案\npopup_move_copy_checkbox = 複製檔案而非移動\npopup_move_preserve_folder_checkbox = 保留資料夾結構\nmove_confirmation_text = 確定您是否要移動所選的項目？\nrename_confirmation_text = 確定您是否要更名所選取的項目？\ndelete = 刪除項目\nstopping_scan = 正在停止掃描，請稍候...\nsearching = 正在搜尋...\nsubsettings_videos_crop_detect = 裁剪偵測方法\nsubsettings_videos_skip_forward_amount = 跳過時長 [s]\nsubsettings_videos_vid_hash_duration = 影片雜湊持續時間\nsettings_cache_number_size_text = 快取檔案大小：{ $size }，檔案數量：{ $number }\nsettings_video_thumbnails_number_size_text = 影片縮圖大小：{ $size }，檔案數量：{ $number }\nsettings_log_number_size_text = 日誌檔案大小：{ $size }，檔案數量：{ $number }\npopup_clean_cache_title_text = 清除過時快取\npopup_clean_cache_confirmation_text = 您確定要清除過時快取項目嗎？這將會移除不再存在或已修改的檔案的快取項目。.\npopup_clean_cache_progress_text = 處理快取檔案：\npopup_clean_cache_current_file_text = 目前檔案：\npopup_clean_cache_file_progress_text = 目前檔案進度：\npopup_clean_cache_overall_progress_text = 總進度：\npopup_clean_cache_stopped_by_user_text = 使用者已停止清理緩存\npopup_clean_cache_finished_text = 快取清除完成成功！\npopup_clean_cache_error_details_text = 錯誤詳情：\npopup_clean_cache_files_with_errors = 包含錯誤的檔案：\nsubsettings_video_optimizer_mode = 模式\nsubsettings_video_optimizer_crop_type = 作物類型\nsubsettings_video_optimizer_black_pixel_threshold = 黑色像素閾值\nsubsettings_video_optimizer_black_pixel_threshold_hint = 每個像素通道的最大 RGB 值被視為黑色 (0-128)。預設值：20\nsubsettings_video_optimizer_black_bar_min_percentage = 黑色條紋最小百分比\nsubsettings_video_optimizer_black_bar_min_percentage_hint = 最小的行/列中黑色像素百分比被认为是黑色条带（50-100）。 默认：90\nsubsettings_video_optimizer_max_samples = 最大樣本\nsubsettings_video_optimizer_max_samples_hint = 分析視訊的最大畫面數量 (5-1000)。預設值：60\nsubsettings_video_optimizer_min_crop_size = 最小作物尺寸\nsubsettings_video_optimizer_min_crop_size_hint = 至少裁剪的像素點 (1-1000)。較小的裁剪會被忽略。預設值：5\nsubsettings_video_optimizer_video_codec = 視頻碼控\nsubsettings_video_optimizer_excluded_codecs = 排除碼型\nsubsettings_video_optimizer_video_quality = 影片品質 (CRF)\nsubsettings_reset = 重置\nsubsettings_exif_ignored_tags_text = 忽略的標籤：\nsubsettings_exif_ignored_tags_hint_text = 逗點分隔清單，列出不應從掃描中排除的標籤（例如：GPS、小圖樣）。某些標籤，例如 TIFF 檔案中的 ImageWidth，會被隱藏以防止破壞圖片。.\nclean_button_text = 清潔\nclean_text = 清除 EXIF 數據\nclean_confirmation_text = 您確定要從所選項目中移除 EXIF 數據嗎？\ncrop_videos_text = 裁剪影片\ncrop_video_confirmation_text = 您確定要裁剪所選取的影片嗎？\ncrop_reencode_video_text = 重新編碼影片\nreencode_videos_text = 重新編碼影片\noptimize_button_text = 優化\noptimize_confirmation_text = 您確定要重新編碼所選取的影片嗎？\noptimize_fail_if_bigger_text = 如果優化後的檔案更大\noptimize_overwrite_files_text = 覆蓋檔案\noptimize_limit_video_size_text = 限制影片大小\noptimize_max_width_text = 最大寬度：\noptimize_max_height_text = 最大高度：\nhardlink_button_text = 硬連結\nhardlink_text = 建立硬連結\nhardlink_confirmation_text = 您確定要為所選項目建立硬連結嗎？\nsoftlink_button_text = 軟連結\nsoftlink_text = 建立軟連結\nsoftlink_confirmation_text = 您確定要為所選項目建立軟連結 (symlinks) 嗎？\n"
  },
  {
    "path": "krokiet/i18n.toml",
    "content": "# (Required) The language identifier of the language used in the\n# source code for gettext system, and the primary fallback language\n# (for which all strings must be present) when using the fluent\n# system.\nfallback_language = \"en\"\n\n# Use the fluent localization system.\n[fluent]\n# (Required) The path to the assets directory.\n# The paths inside the assets directory should be structured like so:\n# `assets_dir/{language}/{domain}.ftl`\nassets_dir = \"i18n\"\n\n"
  },
  {
    "path": "krokiet/src/audio_player.rs",
    "content": "#![allow(unused_imports)]\n#![allow(dead_code)]\n#![allow(clippy::unused_self)]\nuse std::io::{BufReader, Cursor};\n\n#[cfg(feature = \"audio\")]\nuse rodio::{Decoder, DeviceSinkBuilder, Player};\n\nconst DEFAULT_STOP_AUDIO: &[u8] = include_bytes!(\"../audio/stop_bit.mp3\");\n\npub struct AudioPlayer {\n    #[cfg(feature = \"audio\")]\n    audio_data: Vec<u8>,\n}\n\nimpl AudioPlayer {\n    pub fn new() -> Self {\n        #[cfg(feature = \"audio\")]\n        {\n            let audio_data = Self::load_audio_data();\n            Self { audio_data }\n        }\n        #[cfg(not(feature = \"audio\"))]\n        {\n            Self {}\n        }\n    }\n\n    #[cfg(feature = \"audio\")]\n    fn load_audio_data() -> Vec<u8> {\n        if let Ok(custom_path) = std::env::var(\"KROKIET_AUDIO_STOP_FILE\") {\n            match std::fs::read(&custom_path) {\n                Ok(data) => {\n                    let cursor = Cursor::new(data.clone());\n                    let buf_reader = BufReader::new(cursor);\n                    match Decoder::new(buf_reader) {\n                        Ok(_) => {\n                            log::info!(\"Loaded custom audio file from: {custom_path}\");\n                            return data;\n                        }\n                        Err(e) => {\n                            log::error!(\"Failed to decode custom audio file from {custom_path}: {e}\");\n                        }\n                    }\n                }\n                Err(e) => {\n                    log::error!(\"Failed to read custom audio file from {custom_path}: {e}\");\n                }\n            }\n        }\n\n        DEFAULT_STOP_AUDIO.to_vec()\n    }\n\n    pub fn play_scan_completed(&self) {\n        #[cfg(feature = \"audio\")]\n        {\n            let audio_data = self.audio_data.clone();\n            std::thread::spawn(move || {\n                if let Err(e) = Self::play_audio_blocking(&audio_data) {\n                    log::error!(\"Failed to play scan completion audio: {e}\");\n                }\n            });\n        }\n        #[cfg(not(feature = \"audio\"))]\n        {\n            // No-op when audio feature is disabled\n        }\n    }\n\n    #[cfg(feature = \"audio\")]\n    fn play_audio_blocking(audio_data: &[u8]) -> Result<(), String> {\n        let stream_handle = DeviceSinkBuilder::open_default_sink().map_err(|e| format!(\"Failed to get audio output stream: {e}\"))?;\n\n        let sink = Player::connect_new(stream_handle.mixer());\n\n        let cursor = Cursor::new(audio_data.to_vec());\n        let buf_reader = BufReader::new(cursor);\n        let source = Decoder::new(buf_reader).map_err(|e| format!(\"Failed to decode audio: {e}\"))?;\n\n        sink.append(source);\n\n        sink.sleep_until_end();\n\n        drop(stream_handle);\n\n        Ok(())\n    }\n}\n\nimpl Default for AudioPlayer {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "krokiet/src/clear_outdated_video_thumbnails.rs",
    "content": "use std::{fs, thread};\n\nuse czkawka_core::common::config_cache_path::get_config_cache_path;\nuse czkawka_core::common::video_utils::VIDEO_THUMBNAILS_SUBFOLDER;\nuse humansize::{BINARY, format_size};\nuse log::{debug, error, info};\n\nuse crate::MainWindow;\nuse crate::settings::collect_settings;\n\nconst DURATION_BEFORE_CLEANING_SECS: u64 = 7 * 24 * 60 * 60; // 7 days\n\npub fn clear_outdated_video_thumbnails(app: &MainWindow) {\n    let settings_custom = collect_settings(app);\n    if settings_custom.video_thumbnails_unused_thumbnails {\n        thread::spawn(|| {\n            let Some(config_cache_path) = get_config_cache_path() else {\n                return;\n            };\n\n            let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER);\n\n            if !thumbnails_dir.exists() {\n                return;\n            }\n\n            let files = fs::read_dir(&thumbnails_dir).and_then(|e| e.collect::<Result<Vec<_>, std::io::Error>>()).map(|e| {\n                e.into_iter()\n                    .filter(|entry| entry.path().is_file())\n                    .map(|e| e.path().to_string_lossy().to_string())\n                    .collect::<Vec<_>>()\n            });\n            let files = match files {\n                Ok(files) => files,\n                Err(e) => {\n                    error!(\"Failed to read video thumbnails directory(\\\"{}\\\") - {}\", thumbnails_dir.to_string_lossy(), e);\n                    return;\n                }\n            };\n\n            let mut removed_files = 0;\n            let mut removed_size = 0u64;\n            for file in files {\n                let metadata = fs::metadata(&file);\n                let metadata = match metadata {\n                    Ok(metadata) => metadata,\n                    Err(e) => {\n                        error!(\"Failed to get metadata for file(\\\"{file}\\\") - {e}\");\n                        continue;\n                    }\n                };\n\n                let modified = metadata.modified();\n                let modified = match modified {\n                    Ok(modified) => modified,\n                    Err(e) => {\n                        error!(\"Failed to get modified time for file(\\\"{file}\\\") - {e}\");\n                        continue;\n                    }\n                };\n\n                let age = std::time::SystemTime::now().duration_since(modified);\n                let age = match age {\n                    Ok(age) => age,\n                    Err(e) => {\n                        error!(\"Failed to calculate age for file(\\\"{file}\\\") - {e}\");\n                        continue;\n                    }\n                };\n\n                if age.as_secs() > DURATION_BEFORE_CLEANING_SECS {\n                    let file_size = metadata.len();\n                    let result = fs::remove_file(&file);\n                    if let Err(e) = result {\n                        error!(\"Failed to remove outdated thumbnail file(\\\"{file}\\\") - {e}\");\n                        continue;\n                    }\n                    removed_files += 1;\n                    removed_size += file_size;\n                }\n            }\n\n            if removed_files > 0 {\n                info!(\n                    \"Cleared outdated video thumbnails: removed {} files, freed {}\",\n                    removed_files,\n                    format_size(removed_size, BINARY),\n                );\n            } else {\n                debug!(\"No outdated video thumbnails to clear.\");\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "krokiet/src/common.rs",
    "content": "#![allow(dead_code)]\n\nuse std::path::PathBuf;\n\nuse num_enum::TryFromPrimitive;\nuse slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};\n\nuse crate::{ActiveTab, ExcludedPathsModel, IncludedPathsModel, MainWindow, Settings, SingleMainListModel};\n// Int model is used to store data in unchanged(* except that we need to split u64 into two i32) form and is used to sort/select data\n// Str model is used to display data in gui\n\n// Duplicates\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataDuplicateFiles {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_DUPLICATE_FILES: usize = IntDataDuplicateFiles::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataDuplicateFiles {\n    Size,\n    Name,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_DUPLICATE_FILES: usize = StrDataDuplicateFiles::ModificationDate as usize + 1;\n\n// Empty Folders\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataEmptyFolders {\n    ModificationDatePart1,\n    ModificationDatePart2,\n}\npub const MAX_INT_DATA_EMPTY_FOLDERS: usize = IntDataEmptyFolders::ModificationDatePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataEmptyFolders {\n    Name,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_EMPTY_FOLDERS: usize = StrDataEmptyFolders::ModificationDate as usize + 1;\n// Big Files\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataBigFiles {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_BIG_FILES: usize = IntDataBigFiles::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataBigFiles {\n    Size,\n    Name,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_BIG_FILES: usize = StrDataBigFiles::ModificationDate as usize + 1;\n\n// Empty files\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataEmptyFiles {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_EMPTY_FILES: usize = IntDataEmptyFiles::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataEmptyFiles {\n    Name,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_EMPTY_FILES: usize = StrDataEmptyFiles::ModificationDate as usize + 1;\n// Temporary Files\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataTemporaryFiles {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_TEMPORARY_FILES: usize = IntDataTemporaryFiles::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataTemporaryFiles {\n    Name,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_TEMPORARY_FILES: usize = StrDataTemporaryFiles::ModificationDate as usize + 1;\n\n// Similar Images\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataSimilarImages {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n    Width,\n    Height,\n    PixelCount,\n    SimilarityValue,\n}\npub const MAX_INT_DATA_SIMILAR_IMAGES: usize = IntDataSimilarImages::SimilarityValue as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataSimilarImages {\n    Similarity,\n    Size,\n    Resolution,\n    Name,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_SIMILAR_IMAGES: usize = StrDataSimilarImages::ModificationDate as usize + 1;\n\n// Similar Videos\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataSimilarVideos {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n    BitratePart1,\n    BitratePart2,\n    Duration,\n    Fps,\n    Dimensions,\n}\npub const MAX_INT_DATA_SIMILAR_VIDEOS: usize = IntDataSimilarVideos::Dimensions as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataSimilarVideos {\n    Size,\n    Name,\n    Path,\n    Dimensions,\n    Duration,\n    Bitrate,\n    Fps,\n    Codec,\n    ModificationDate,\n    PreviewPath,\n}\npub const MAX_STR_DATA_SIMILAR_VIDEOS: usize = StrDataSimilarVideos::PreviewPath as usize + 1;\n\n// Similar Music\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataSimilarMusic {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n    Bitrate,\n    Length,\n}\npub const MAX_INT_DATA_SIMILAR_MUSIC: usize = IntDataSimilarMusic::Length as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataSimilarMusic {\n    Size,\n    Name,\n    Title,\n    Artist,\n    Year,\n    Bitrate,\n    Length,\n    Genre,\n    Path,\n    ModificationDate,\n}\npub const MAX_STR_DATA_SIMILAR_MUSIC: usize = StrDataSimilarMusic::ModificationDate as usize + 1;\n\n// Invalid Symlinks\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataInvalidSymlinks {\n    ModificationDatePart1,\n    ModificationDatePart2,\n}\npub const MAX_INT_DATA_INVALID_SYMLINKS: usize = IntDataInvalidSymlinks::ModificationDatePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataInvalidSymlinks {\n    SymlinkName,\n    SymlinkFolder,\n    DestinationPath,\n    TypeOfError,\n    ModificationDate,\n}\npub const MAX_STR_DATA_INVALID_SYMLINKS: usize = StrDataInvalidSymlinks::ModificationDate as usize + 1;\n\n// Broken Files\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataBrokenFiles {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_BROKEN_FILES: usize = IntDataBrokenFiles::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataBrokenFiles {\n    Name,\n    Path,\n    TypeOfError,\n    Size,\n    ModificationDate,\n}\npub const MAX_STR_DATA_BROKEN_FILES: usize = StrDataBrokenFiles::ModificationDate as usize + 1;\n// Bad Extensions\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataBadExtensions {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_BAD_EXTENSIONS: usize = IntDataBadExtensions::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataBadExtensions {\n    Name,\n    Path,\n    CurrentExtension,\n    ProperExtensionsGroup,\n    ProperExtension,\n}\npub const MAX_STR_DATA_BAD_EXTENSIONS: usize = StrDataBadExtensions::ProperExtension as usize + 1;\n\n// Bad Names\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataBadNames {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n}\npub const MAX_INT_DATA_BAD_NAMES: usize = IntDataBadNames::SizePart2 as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataBadNames {\n    Name,\n    NewName,\n    Path,\n}\npub const MAX_STR_DATA_BAD_NAMES: usize = StrDataBadNames::Path as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\n// Exif Remover\npub enum IntDataExifRemover {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n    ExifTagsCount,\n}\npub const MAX_INT_DATA_EXIF_REMOVER: usize = IntDataExifRemover::ExifTagsCount as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataExifRemover {\n    Size,\n    Name,\n    Path,\n    ExifTags,\n    ModificationDate,\n    ExifGroupsNames,\n    ExifTagsU16,\n}\npub const MAX_STR_DATA_EXIF_REMOVER: usize = StrDataExifRemover::ExifTagsU16 as usize + 1;\n\n// Video Optimizer\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum IntDataVideoOptimizer {\n    ModificationDatePart1,\n    ModificationDatePart2,\n    SizePart1,\n    SizePart2,\n    PixelCount,\n    DiffInPixels,\n    Width,\n    Height,\n    RectLeft,\n    RectTop,\n    RectRight,\n    RectBottom,\n}\npub const MAX_INT_DATA_VIDEO_OPTIMIZER: usize = IntDataVideoOptimizer::RectBottom as usize + 1;\n\n#[repr(u8)]\n#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]\npub enum StrDataVideoOptimizer {\n    Size,\n    Name,\n    Path,\n    Codec,\n    Dimensions,\n    NewDimensions,\n    ModificationDate,\n    PreviewPath,\n}\npub const MAX_STR_DATA_VIDEO_OPTIMIZER: usize = StrDataVideoOptimizer::PreviewPath as usize + 1;\n\npub(crate) enum SortIdx {\n    StrIdx(i32),\n    IntIdx(i32),\n    IntIdxPair(i32, i32),\n    Selection,\n}\n\nimpl ActiveTab {\n    pub(crate) fn get_str_int_sort_idx(self, str_idx: i32) -> SortIdx {\n        // This not exists in enums, because selection is stored in other field\n        if str_idx == 0 {\n            return SortIdx::Selection;\n        }\n        let str_idx = str_idx - 1; // Adjust for selection\n\n        match self {\n            Self::EmptyFolders => match StrDataEmptyFolders::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for EmptyFolders\")) {\n                StrDataEmptyFolders::Name | StrDataEmptyFolders::Path => SortIdx::StrIdx(str_idx),\n                StrDataEmptyFolders::ModificationDate => SortIdx::IntIdxPair(IntDataEmptyFolders::ModificationDatePart1 as i32, IntDataEmptyFolders::ModificationDatePart2 as i32),\n            },\n            Self::EmptyFiles => match StrDataEmptyFiles::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for EmptyFiles\")) {\n                StrDataEmptyFiles::Name | StrDataEmptyFiles::Path => SortIdx::StrIdx(str_idx),\n                StrDataEmptyFiles::ModificationDate => SortIdx::IntIdxPair(IntDataEmptyFiles::ModificationDatePart1 as i32, IntDataEmptyFiles::ModificationDatePart2 as i32),\n            },\n            Self::SimilarImages => match StrDataSimilarImages::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for SimilarImages\")) {\n                StrDataSimilarImages::Similarity | StrDataSimilarImages::Name | StrDataSimilarImages::Path => SortIdx::StrIdx(str_idx),\n                StrDataSimilarImages::ModificationDate => {\n                    SortIdx::IntIdxPair(IntDataSimilarImages::ModificationDatePart1 as i32, IntDataSimilarImages::ModificationDatePart2 as i32)\n                }\n                StrDataSimilarImages::Size => SortIdx::IntIdxPair(IntDataSimilarImages::SizePart1 as i32, IntDataSimilarImages::SizePart2 as i32),\n                StrDataSimilarImages::Resolution => SortIdx::IntIdx(IntDataSimilarImages::PixelCount as i32),\n            },\n            Self::DuplicateFiles => match StrDataDuplicateFiles::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for DuplicateFiles\")) {\n                StrDataDuplicateFiles::Name | StrDataDuplicateFiles::Path => SortIdx::StrIdx(str_idx),\n                StrDataDuplicateFiles::ModificationDate => {\n                    SortIdx::IntIdxPair(IntDataDuplicateFiles::ModificationDatePart1 as i32, IntDataDuplicateFiles::ModificationDatePart2 as i32)\n                }\n                StrDataDuplicateFiles::Size => SortIdx::IntIdxPair(IntDataDuplicateFiles::SizePart1 as i32, IntDataDuplicateFiles::SizePart2 as i32),\n            },\n            Self::BigFiles => match StrDataBigFiles::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for BigFiles\")) {\n                StrDataBigFiles::Name | StrDataBigFiles::Path => SortIdx::StrIdx(str_idx),\n                StrDataBigFiles::ModificationDate => SortIdx::IntIdxPair(IntDataBigFiles::ModificationDatePart1 as i32, IntDataBigFiles::ModificationDatePart2 as i32),\n                StrDataBigFiles::Size => SortIdx::IntIdxPair(IntDataBigFiles::SizePart1 as i32, IntDataBigFiles::SizePart2 as i32),\n            },\n            Self::TemporaryFiles => match StrDataTemporaryFiles::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for TemporaryFiles\")) {\n                StrDataTemporaryFiles::Name | StrDataTemporaryFiles::Path => SortIdx::StrIdx(str_idx),\n                StrDataTemporaryFiles::ModificationDate => {\n                    SortIdx::IntIdxPair(IntDataTemporaryFiles::ModificationDatePart1 as i32, IntDataTemporaryFiles::ModificationDatePart2 as i32)\n                }\n            },\n            Self::SimilarVideos => match StrDataSimilarVideos::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for SimilarVideos\")) {\n                StrDataSimilarVideos::Name | StrDataSimilarVideos::Path | StrDataSimilarVideos::Codec | StrDataSimilarVideos::PreviewPath => SortIdx::StrIdx(str_idx),\n                StrDataSimilarVideos::ModificationDate => {\n                    SortIdx::IntIdxPair(IntDataSimilarVideos::ModificationDatePart1 as i32, IntDataSimilarVideos::ModificationDatePart2 as i32)\n                }\n                StrDataSimilarVideos::Size => SortIdx::IntIdxPair(IntDataSimilarVideos::SizePart1 as i32, IntDataSimilarVideos::SizePart2 as i32),\n                StrDataSimilarVideos::Bitrate => SortIdx::IntIdxPair(IntDataSimilarVideos::BitratePart1 as i32, IntDataSimilarVideos::BitratePart2 as i32),\n                StrDataSimilarVideos::Duration => SortIdx::IntIdx(IntDataSimilarVideos::Duration as i32),\n                StrDataSimilarVideos::Fps => SortIdx::IntIdx(IntDataSimilarVideos::Fps as i32),\n                StrDataSimilarVideos::Dimensions => SortIdx::IntIdx(IntDataSimilarVideos::Dimensions as i32),\n            },\n            Self::SimilarMusic => match StrDataSimilarMusic::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for SimilarMusic\")) {\n                StrDataSimilarMusic::Name\n                | StrDataSimilarMusic::Path\n                | StrDataSimilarMusic::Title\n                | StrDataSimilarMusic::Artist\n                | StrDataSimilarMusic::Year\n                | StrDataSimilarMusic::Bitrate\n                | StrDataSimilarMusic::Length\n                | StrDataSimilarMusic::Genre => SortIdx::StrIdx(str_idx),\n                StrDataSimilarMusic::ModificationDate => SortIdx::IntIdxPair(IntDataSimilarMusic::ModificationDatePart1 as i32, IntDataSimilarMusic::ModificationDatePart2 as i32),\n                StrDataSimilarMusic::Size => SortIdx::IntIdxPair(IntDataSimilarMusic::SizePart1 as i32, IntDataSimilarMusic::SizePart2 as i32),\n            },\n            Self::InvalidSymlinks => match StrDataInvalidSymlinks::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for InvalidSymlinks\")) {\n                StrDataInvalidSymlinks::SymlinkName | StrDataInvalidSymlinks::SymlinkFolder | StrDataInvalidSymlinks::DestinationPath | StrDataInvalidSymlinks::TypeOfError => {\n                    SortIdx::StrIdx(str_idx)\n                }\n                StrDataInvalidSymlinks::ModificationDate => {\n                    SortIdx::IntIdxPair(IntDataInvalidSymlinks::ModificationDatePart1 as i32, IntDataInvalidSymlinks::ModificationDatePart2 as i32)\n                }\n            },\n            Self::BrokenFiles => match StrDataBrokenFiles::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for BrokenFiles\")) {\n                StrDataBrokenFiles::Name | StrDataBrokenFiles::Path | StrDataBrokenFiles::TypeOfError => SortIdx::StrIdx(str_idx),\n                StrDataBrokenFiles::ModificationDate => SortIdx::IntIdxPair(IntDataBrokenFiles::ModificationDatePart1 as i32, IntDataBrokenFiles::ModificationDatePart2 as i32),\n                StrDataBrokenFiles::Size => SortIdx::IntIdxPair(IntDataBrokenFiles::SizePart1 as i32, IntDataBrokenFiles::SizePart2 as i32),\n            },\n            Self::BadExtensions => match StrDataBadExtensions::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for BadExtensions\")) {\n                StrDataBadExtensions::Name\n                | StrDataBadExtensions::Path\n                | StrDataBadExtensions::CurrentExtension\n                | StrDataBadExtensions::ProperExtensionsGroup\n                | StrDataBadExtensions::ProperExtension => SortIdx::StrIdx(str_idx),\n            },\n            Self::BadNames => match StrDataBadNames::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for BadNames\")) {\n                StrDataBadNames::Name | StrDataBadNames::Path | StrDataBadNames::NewName => SortIdx::StrIdx(str_idx),\n            },\n            Self::ExifRemover => match StrDataExifRemover::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for ExifRemover\")) {\n                StrDataExifRemover::ExifTagsU16 | StrDataExifRemover::ExifGroupsNames | StrDataExifRemover::Name | StrDataExifRemover::Path => SortIdx::StrIdx(str_idx),\n                StrDataExifRemover::ModificationDate => SortIdx::IntIdxPair(IntDataExifRemover::ModificationDatePart1 as i32, IntDataExifRemover::ModificationDatePart2 as i32),\n                StrDataExifRemover::ExifTags => SortIdx::IntIdx(IntDataExifRemover::ExifTagsCount as i32),\n                StrDataExifRemover::Size => SortIdx::IntIdxPair(IntDataExifRemover::SizePart1 as i32, IntDataExifRemover::SizePart2 as i32),\n            },\n            Self::VideoOptimizer => match StrDataVideoOptimizer::try_from(str_idx as u8).unwrap_or_else(|_| panic!(\"Invalid str idx {str_idx} for VideoOptimizer\")) {\n                StrDataVideoOptimizer::Name | StrDataVideoOptimizer::Path | StrDataVideoOptimizer::Codec | StrDataVideoOptimizer::PreviewPath => SortIdx::StrIdx(str_idx),\n                StrDataVideoOptimizer::ModificationDate => {\n                    SortIdx::IntIdxPair(IntDataVideoOptimizer::ModificationDatePart1 as i32, IntDataVideoOptimizer::ModificationDatePart2 as i32)\n                }\n                StrDataVideoOptimizer::Size => SortIdx::IntIdxPair(IntDataVideoOptimizer::SizePart1 as i32, IntDataVideoOptimizer::SizePart2 as i32),\n                StrDataVideoOptimizer::Dimensions => SortIdx::IntIdx(IntDataVideoOptimizer::PixelCount as i32),\n                StrDataVideoOptimizer::NewDimensions => SortIdx::IntIdx(IntDataVideoOptimizer::DiffInPixels as i32),\n            },\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n\n    // Remember to match updated this according to ui/main_lists.slint and connect_scan.rs files\n    pub(crate) fn get_str_path_idx(self) -> usize {\n        match self {\n            Self::EmptyFolders => StrDataEmptyFolders::Path as usize,\n            Self::EmptyFiles => StrDataEmptyFiles::Path as usize,\n            Self::SimilarImages => StrDataSimilarImages::Path as usize,\n            Self::DuplicateFiles => StrDataDuplicateFiles::Path as usize,\n            Self::BigFiles => StrDataBigFiles::Path as usize,\n            Self::TemporaryFiles => StrDataTemporaryFiles::Path as usize,\n            Self::SimilarVideos => StrDataSimilarVideos::Path as usize,\n            Self::SimilarMusic => StrDataSimilarMusic::Path as usize,\n            Self::InvalidSymlinks => StrDataInvalidSymlinks::SymlinkFolder as usize,\n            Self::BrokenFiles => StrDataBrokenFiles::Path as usize,\n            Self::BadExtensions => StrDataBadExtensions::Path as usize,\n            Self::BadNames => StrDataBadNames::Path as usize,\n            Self::ExifRemover => StrDataExifRemover::Path as usize,\n            Self::VideoOptimizer => StrDataVideoOptimizer::Path as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n\n    pub(crate) fn get_str_name_idx(self) -> usize {\n        match self {\n            Self::EmptyFolders => StrDataEmptyFolders::Name as usize,\n            Self::EmptyFiles => StrDataEmptyFiles::Name as usize,\n            Self::SimilarImages => StrDataSimilarImages::Name as usize,\n            Self::DuplicateFiles => StrDataDuplicateFiles::Name as usize,\n            Self::BigFiles => StrDataBigFiles::Name as usize,\n            Self::TemporaryFiles => StrDataTemporaryFiles::Name as usize,\n            Self::SimilarVideos => StrDataSimilarVideos::Name as usize,\n            Self::SimilarMusic => StrDataSimilarMusic::Name as usize,\n            Self::InvalidSymlinks => StrDataInvalidSymlinks::SymlinkName as usize,\n            Self::BrokenFiles => StrDataBrokenFiles::Name as usize,\n            Self::BadExtensions => StrDataBadExtensions::Name as usize,\n            Self::BadNames => StrDataBadNames::Name as usize,\n            Self::ExifRemover => StrDataExifRemover::Name as usize,\n            Self::VideoOptimizer => StrDataVideoOptimizer::Name as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n\n    pub(crate) fn get_str_proper_extension(self) -> usize {\n        match self {\n            Self::BadExtensions => StrDataBadExtensions::ProperExtension as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get proper extension from this tab\"),\n        }\n    }\n    pub(crate) fn get_int_modification_date_idx(self) -> usize {\n        match self {\n            Self::EmptyFiles => IntDataEmptyFiles::ModificationDatePart1 as usize,\n            Self::EmptyFolders => IntDataEmptyFolders::ModificationDatePart1 as usize,\n            Self::SimilarImages => IntDataSimilarImages::ModificationDatePart1 as usize,\n            Self::DuplicateFiles => IntDataDuplicateFiles::ModificationDatePart1 as usize,\n            Self::BigFiles => IntDataBigFiles::ModificationDatePart1 as usize,\n            Self::TemporaryFiles => IntDataTemporaryFiles::ModificationDatePart1 as usize,\n            Self::SimilarVideos => IntDataSimilarVideos::ModificationDatePart1 as usize,\n            Self::SimilarMusic => IntDataSimilarMusic::ModificationDatePart1 as usize,\n            Self::InvalidSymlinks => IntDataInvalidSymlinks::ModificationDatePart1 as usize,\n            Self::BrokenFiles => IntDataBrokenFiles::ModificationDatePart1 as usize,\n            Self::BadExtensions => IntDataBadExtensions::ModificationDatePart1 as usize,\n            Self::BadNames => IntDataBadNames::ModificationDatePart1 as usize,\n            Self::ExifRemover => IntDataExifRemover::ModificationDatePart1 as usize,\n            Self::VideoOptimizer => IntDataVideoOptimizer::ModificationDatePart1 as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n    pub(crate) fn get_int_size_opt_idx(self) -> Option<usize> {\n        let res = match self {\n            Self::EmptyFiles => IntDataEmptyFiles::SizePart1 as usize,\n            Self::SimilarImages => IntDataSimilarImages::SizePart1 as usize,\n            Self::DuplicateFiles => IntDataDuplicateFiles::SizePart1 as usize,\n            Self::BigFiles => IntDataBigFiles::SizePart1 as usize,\n            Self::SimilarVideos => IntDataSimilarVideos::SizePart1 as usize,\n            Self::SimilarMusic => IntDataSimilarMusic::SizePart1 as usize,\n            Self::BrokenFiles => IntDataBrokenFiles::SizePart1 as usize,\n            Self::TemporaryFiles => IntDataTemporaryFiles::SizePart1 as usize,\n            Self::BadExtensions => IntDataBadExtensions::SizePart1 as usize,\n            Self::BadNames => IntDataBadNames::SizePart1 as usize,\n            Self::ExifRemover => IntDataExifRemover::SizePart1 as usize,\n            Self::VideoOptimizer => IntDataVideoOptimizer::SizePart1 as usize,\n            Self::Settings | Self::About | Self::EmptyFolders | Self::InvalidSymlinks => return None,\n        };\n        Some(res)\n    }\n    pub(crate) fn get_int_size_idx(self) -> usize {\n        self.get_int_size_opt_idx().unwrap_or_else(|| panic!(\"Unable to get size index for tab: {self:?}\"))\n    }\n    pub(crate) fn get_int_width_idx(self) -> usize {\n        match self {\n            Self::SimilarImages => IntDataSimilarImages::Width as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get height from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_int_height_idx(self) -> usize {\n        match self {\n            Self::SimilarImages => IntDataSimilarImages::Height as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get height from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_int_pixel_count_idx(self) -> usize {\n        match self {\n            Self::SimilarImages => IntDataSimilarImages::PixelCount as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get total pixel count from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_str_video_codec_idx(self) -> usize {\n        match self {\n            Self::SimilarVideos => StrDataSimilarVideos::Codec as usize,\n            Self::VideoOptimizer => StrDataVideoOptimizer::Codec as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get video codec from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_exif_tag_names_idx(self) -> usize {\n        match self {\n            Self::ExifRemover => StrDataExifRemover::ExifTags as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get exif tag names from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_exif_tag_groups_idx(self) -> usize {\n        match self {\n            Self::ExifRemover => StrDataExifRemover::ExifGroupsNames as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get exif tag groups from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_exif_tag_u16_idx(self) -> usize {\n        match self {\n            Self::ExifRemover => StrDataExifRemover::ExifTagsU16 as usize,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n            _ => panic!(\"Unable to get exif tag u16 from this tab\"),\n        }\n    }\n\n    pub(crate) fn get_is_header_mode(self) -> bool {\n        match self {\n            Self::EmptyFolders\n            | Self::EmptyFiles\n            | Self::BrokenFiles\n            | Self::BigFiles\n            | Self::TemporaryFiles\n            | Self::InvalidSymlinks\n            | Self::BadExtensions\n            | Self::BadNames\n            | Self::ExifRemover\n            | Self::VideoOptimizer => false,\n            Self::SimilarImages | Self::DuplicateFiles | Self::SimilarVideos | Self::SimilarMusic => true,\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n    pub(crate) fn get_tool_model(self, app: &MainWindow) -> ModelRc<SingleMainListModel> {\n        match self {\n            Self::EmptyFolders => app.get_empty_folder_model(),\n            Self::SimilarImages => app.get_similar_images_model(),\n            Self::EmptyFiles => app.get_empty_files_model(),\n            Self::DuplicateFiles => app.get_duplicate_files_model(),\n            Self::BigFiles => app.get_big_files_model(),\n            Self::TemporaryFiles => app.get_temporary_files_model(),\n            Self::SimilarVideos => app.get_similar_videos_model(),\n            Self::SimilarMusic => app.get_similar_music_model(),\n            Self::InvalidSymlinks => app.get_invalid_symlinks_model(),\n            Self::BrokenFiles => app.get_broken_files_model(),\n            Self::BadExtensions => app.get_bad_extensions_model(),\n            Self::BadNames => app.get_bad_names_model(),\n            Self::ExifRemover => app.get_exif_remover_model(),\n            Self::VideoOptimizer => app.get_video_optimizer_model(),\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n\n    pub(crate) fn set_tool_model(self, app: &MainWindow, model: ModelRc<SingleMainListModel>) {\n        match self {\n            Self::EmptyFolders => app.set_empty_folder_model(model),\n            Self::SimilarImages => app.set_similar_images_model(model),\n            Self::EmptyFiles => app.set_empty_files_model(model),\n            Self::DuplicateFiles => app.set_duplicate_files_model(model),\n            Self::BigFiles => app.set_big_files_model(model),\n            Self::TemporaryFiles => app.set_temporary_files_model(model),\n            Self::SimilarVideos => app.set_similar_videos_model(model),\n            Self::SimilarMusic => app.set_similar_music_model(model),\n            Self::InvalidSymlinks => app.set_invalid_symlinks_model(model),\n            Self::BrokenFiles => app.set_broken_files_model(model),\n            Self::BadExtensions => app.set_bad_extensions_model(model),\n            Self::BadNames => app.set_bad_names_model(model),\n            Self::ExifRemover => app.set_exif_remover_model(model),\n            Self::VideoOptimizer => app.set_video_optimizer_model(model),\n            Self::Settings | Self::About => panic!(\"Button should be disabled\"),\n        }\n    }\n}\n\npub(crate) fn create_included_paths_model_from_pathbuf(items: &[PathBuf], referenced: &[PathBuf]) -> ModelRc<IncludedPathsModel> {\n    let referenced_as_string = referenced.iter().map(|x| x.to_string_lossy().to_string()).collect::<Vec<_>>();\n    let converted = items\n        .iter()\n        .map(|x| {\n            let path_as_string = x.to_string_lossy().to_string();\n            IncludedPathsModel {\n                path: x.to_string_lossy().to_string().into(),\n                referenced_path: referenced_as_string.contains(&path_as_string),\n                selected_row: false,\n            }\n        })\n        .collect::<Vec<_>>();\n    ModelRc::new(VecModel::from(converted))\n}\n\npub(crate) fn create_excluded_paths_model_from_pathbuf(items: &[PathBuf]) -> ModelRc<ExcludedPathsModel> {\n    let converted = items\n        .iter()\n        .map(|x| ExcludedPathsModel {\n            path: x.to_string_lossy().to_string().into(),\n            selected_row: false,\n        })\n        .collect::<Vec<_>>();\n    ModelRc::new(VecModel::from(converted))\n}\n\npub(crate) fn check_if_there_are_any_included_folders(app: &MainWindow) -> bool {\n    let included = app.global::<Settings>().get_included_paths_model();\n    included.iter().count() > 0\n}\n\npub(crate) fn check_if_all_included_dirs_are_referenced(app: &MainWindow) -> bool {\n    let included = app.global::<Settings>().get_included_paths_model();\n    included.iter().all(|x| x.referenced_path)\n}\n\npub(crate) fn create_vec_model_from_vec_string(items: Vec<String>) -> VecModel<SharedString> {\n    VecModel::from(items.into_iter().map(SharedString::from).collect::<Vec<_>>())\n}\n\n// Workaround for https://github.com/slint-ui/slint/discussions/4596\n// Currently there is no way to save u64 in slint, so we need to split it into two i32\npub(crate) fn split_u64_into_i32s(value: u64) -> (i32, i32) {\n    let part1: i32 = (value >> 32) as i32;\n    let part2: i32 = value as i32;\n    (part1, part2)\n}\n\npub(crate) fn connect_i32_into_u64(part1: i32, part2: i32) -> u64 {\n    ((part1 as u64) << 32) | (part2 as u64 & 0xFFFF_FFFF)\n}\n\npub(crate) fn create_model_from_model_vec<T: Clone + 'static>(model_vec: &[T]) -> ModelRc<T> {\n    ModelRc::new(VecModel::from(model_vec.to_owned()))\n}\n\n#[cfg(test)]\nmod test {\n    use crate::common::split_u64_into_i32s;\n\n    #[test]\n    fn test_split_u64_into_i32s_small() {\n        let value = 1;\n        let (part1, part2) = split_u64_into_i32s(value);\n        assert_eq!(part1, 0);\n        assert_eq!(part2, 1);\n    }\n\n    #[test]\n    fn test_split_u64_into_i32s_big() {\n        let value = u64::MAX;\n        let (part1, part2) = split_u64_into_i32s(value);\n        assert_eq!(part1, -1);\n        assert_eq!(part2, -1);\n    }\n\n    #[test]\n    fn test_connect_i32_into_u64_small() {\n        let part1 = 0;\n        let part2 = 1;\n        let value = super::connect_i32_into_u64(part1, part2);\n        assert_eq!(value, 1);\n    }\n\n    #[test]\n    fn test_connect_i32_into_u64_big() {\n        let part1 = -1;\n        let part2 = -1;\n        let value = super::connect_i32_into_u64(part1, part2);\n        assert_eq!(value, u64::MAX);\n    }\n\n    #[test]\n    fn test_connect_split_zero() {\n        for start_value in [0, 1, 10, u32::MAX as u64, i32::MAX as u64, u64::MAX] {\n            let (part1, part2) = split_u64_into_i32s(start_value);\n            let end_value = super::connect_i32_into_u64(part1, part2);\n            assert_eq!(start_value, end_value);\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_clean_cache.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::cache::{CacheProgressCleaning, clean_all_cache_files};\nuse humansize::{BINARY, format_size};\nuse slint::ComponentHandle;\n\nuse crate::create_calculate_task_size::{SizeCountResult, update_cache_sizes};\nuse crate::{CacheCleaningProgress, CacheCleaningResult, Callabler, GuiState, MainWindow, flk};\n\npub(crate) fn connect_clean_cache(app: &MainWindow, cache_size_task_sender: std::sync::mpsc::Sender<std::sync::mpsc::Sender<SizeCountResult>>) {\n    let app_weak = app.as_weak();\n    let stop_flag: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));\n    let stop_flag_start: Arc<AtomicBool> = stop_flag.clone();\n\n    app.global::<Callabler>().on_start_cache_cleaning(move || {\n        let app_weak = app_weak.clone();\n        let stop_flag = stop_flag_start.clone();\n        let cache_size_task_sender = cache_size_task_sender.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n\n        thread::spawn(move || {\n            let start_time = std::time::Instant::now();\n\n            let (progress_sender, progress_receiver): (Sender<CacheProgressCleaning>, _) = crossbeam_channel::unbounded();\n\n            let app_weak_progress = app_weak.clone();\n            let stop_flag_progress = stop_flag.clone();\n            let progress_thread = thread::spawn(move || {\n                while !stop_flag_progress.load(Ordering::Relaxed) {\n                    if let Ok(progress) = progress_receiver.recv_timeout(std::time::Duration::from_millis(200)) {\n                        app_weak_progress\n                            .upgrade_in_event_loop(move |app| {\n                                let slint_progress = CacheCleaningProgress {\n                                    current_cache_file: progress.current_cache_file as i32,\n                                    total_cache_files: progress.total_cache_files as i32,\n                                    current_file_name: progress.current_file_name.into(),\n                                    checked_entries: progress.checked_entries as i32,\n                                    all_entries: progress.all_entries as i32,\n                                };\n                                app.global::<GuiState>().set_cache_cleaning_progress(slint_progress);\n                            })\n                            .expect(\"Failed to update progress in event loop\");\n                    }\n                }\n            });\n\n            let result = clean_all_cache_files(&stop_flag, Some(&progress_sender));\n\n            progress_thread.join().expect(\"Failed to join progress thread\");\n\n            let elapsed = format!(\"{:?}\", start_time.elapsed());\n\n            app_weak\n                .upgrade_in_event_loop(move |app| {\n                    let gui_state = app.global::<GuiState>();\n                    gui_state.set_cache_cleaning_is_cleaning(false);\n                    gui_state.set_cache_cleaning_finished(true);\n\n                    match result {\n                        Ok(stats) => {\n                            let processed_files_text = flk!(\"rust_cache_processed_files\", files = stats.total_files_found);\n                            let entries_stats_text = flk!(\n                                \"rust_cache_entries_stats\",\n                                removed = stats.total_entries_removed,\n                                all = stats.total_entries_before,\n                                left = stats.total_entries_left\n                            );\n                            let size_reduced = stats.total_size_before.saturating_sub(stats.total_size_after);\n                            let size_stats_text = flk!(\"rust_cache_size_reduced\", size = format_size(size_reduced, BINARY));\n                            let time_text = flk!(\"rust_cache_time_elapsed\", time = elapsed);\n\n                            let slint_result = CacheCleaningResult {\n                                processed_files_text: processed_files_text.into(),\n                                entries_stats_text: entries_stats_text.into(),\n                                size_stats_text: size_stats_text.into(),\n                                time_text: time_text.into(),\n                                errors_count: stats.files_with_errors as i32,\n                                errors: stats.errors.join(\"\\n\").into(),\n                            };\n                            gui_state.set_cache_cleaning_result(slint_result);\n                        }\n                        Err(e) => {\n                            let time_text = flk!(\"rust_cache_time_elapsed\", time = elapsed);\n                            let slint_result = CacheCleaningResult {\n                                processed_files_text: \"\".into(),\n                                entries_stats_text: \"\".into(),\n                                size_stats_text: \"\".into(),\n                                time_text: time_text.into(),\n                                errors_count: 0,\n                                errors: e.into(),\n                            };\n                            gui_state.set_cache_cleaning_result(slint_result);\n                        }\n                    }\n\n                    update_cache_sizes(&app, &cache_size_task_sender);\n                })\n                .expect(\"Failed to update final result in event loop\");\n        });\n    });\n\n    let stop_flag_stop = stop_flag;\n    app.global::<Callabler>().on_stop_cache_cleaning(move || {\n        stop_flag_stop.store(true, Ordering::Relaxed);\n    });\n}\n"
  },
  {
    "path": "krokiet/src/connect_directories_changes.rs",
    "content": "use rfd::FileDialog;\nuse slint::{ComponentHandle, Model, ModelRc, VecModel};\n\nuse crate::connect_rfd::{hide_file_dialog_overlay, show_file_dialog_overlay};\nuse crate::{Callabler, ExcludedPathsModel, IncludedPathsModel, MainWindow, Settings};\n\npub(crate) fn connect_add_remove_directories(app: &MainWindow) {\n    connect_add_directories(app);\n    connect_add_files(app);\n    connect_remove_directories(app);\n    connect_add_manual_directories(app);\n}\n\nfn connect_add_manual_directories(app: &MainWindow) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_added_manual_paths(move |included_paths, list_of_files_to_add| {\n        let folders = list_of_files_to_add.lines().filter(|x| !x.is_empty()).map(str::to_string).collect::<Vec<_>>();\n        if folders.is_empty() {\n            return;\n        }\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let settings = app.global::<Settings>();\n\n        if included_paths {\n            add_included_paths(&settings, &folders);\n        } else {\n            add_excluded_paths(&settings, &folders);\n        }\n    });\n}\n\nfn filter_model<T: Clone>(model: &ModelRc<T>, index_to_remove: i32) -> Vec<T> {\n    model.iter().enumerate().filter(|(idx, _)| *idx as i32 != index_to_remove).map(|(_, item)| item).collect()\n}\n\nfn connect_remove_directories(app: &MainWindow) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_remove_item_paths(move |included_paths, index_to_remove| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let settings = app.global::<Settings>();\n\n        if included_paths {\n            let included_model = settings.get_included_paths_model();\n            let new_model = filter_model(&included_model, index_to_remove);\n\n            assert_eq!(included_model.iter().count(), new_model.len() + 1, \"Removing item should reduce model size by 1\");\n            settings.set_included_paths_model(ModelRc::new(VecModel::from(new_model)));\n        } else {\n            let excluded_model = settings.get_excluded_paths_model();\n            let new_model = filter_model(&excluded_model, index_to_remove);\n\n            assert_eq!(excluded_model.iter().count(), new_model.len() + 1, \"Removing item should reduce model size by 1\");\n            settings.set_excluded_paths_model(ModelRc::new(VecModel::from(new_model)));\n        }\n    });\n}\n\nfn connect_add_directories(app: &MainWindow) {\n    let a = app.as_weak();\n    app.on_folder_choose_requested(move |included_paths| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n\n        show_file_dialog_overlay(&app);\n\n        let weak = a.clone();\n        std::thread::spawn(move || {\n            let directory = std::env::current_dir().unwrap_or(std::path::PathBuf::from(\"/\"));\n            let folders = FileDialog::new().set_directory(directory).pick_folders();\n\n            hide_file_dialog_overlay(&weak);\n\n            if let Some(folders) = folders {\n                let folders = folders.iter().map(|x| x.to_string_lossy().to_string()).collect::<Vec<_>>();\n                weak.upgrade_in_event_loop(move |app| {\n                    let settings = app.global::<Settings>();\n                    if included_paths {\n                        add_included_paths(&settings, &folders);\n                    } else {\n                        add_excluded_paths(&settings, &folders);\n                    }\n                })\n                .expect(\"Failed to update directories\");\n            }\n        });\n    });\n}\n\nfn connect_add_files(app: &MainWindow) {\n    let a = app.as_weak();\n    app.on_file_choose_requested(move |included_paths| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n\n        show_file_dialog_overlay(&app);\n\n        let weak = a.clone();\n        std::thread::spawn(move || {\n            let directory = std::env::current_dir().unwrap_or(std::path::PathBuf::from(\"/\"));\n            let files = FileDialog::new().set_directory(directory).pick_files();\n\n            hide_file_dialog_overlay(&weak);\n\n            if let Some(files) = files {\n                let files = files.iter().map(|x| x.to_string_lossy().to_string()).collect::<Vec<_>>();\n                weak.upgrade_in_event_loop(move |app| {\n                    let settings = app.global::<Settings>();\n                    if included_paths {\n                        add_included_paths(&settings, &files);\n                    } else {\n                        add_excluded_paths(&settings, &files);\n                    }\n                })\n                .expect(\"Failed to update files\");\n            }\n        });\n    });\n}\n\nfn add_included_paths(settings: &Settings, folders: &[String]) {\n    let old_folders = settings.get_included_paths_model();\n    let old_folders_path = old_folders.iter().map(|x| x.path.to_string()).collect::<Vec<_>>();\n    let mut new_folders = old_folders.iter().collect::<Vec<_>>();\n\n    let filtered_folders = folders.iter().filter(|x| !old_folders_path.contains(x)).collect::<Vec<_>>();\n\n    for x in &mut new_folders {\n        x.selected_row = false;\n    }\n\n    new_folders.extend(filtered_folders.iter().map(|path| IncludedPathsModel {\n        path: (*path).into(),\n        referenced_path: false,\n        selected_row: false,\n    }));\n\n    new_folders.sort_by_key(|x| x.path.clone());\n\n    let new_folders_model = ModelRc::new(VecModel::from(new_folders));\n    settings.set_included_paths_model(new_folders_model);\n}\n\nfn add_excluded_paths(settings: &Settings, folders: &[String]) {\n    let old_folders = settings.get_excluded_paths_model();\n    let old_folders_path = old_folders.iter().map(|x| x.path.to_string()).collect::<Vec<_>>();\n    let mut new_folders = old_folders.iter().collect::<Vec<_>>();\n\n    let filtered_folders = folders.iter().filter(|x| !old_folders_path.contains(x)).collect::<Vec<_>>();\n\n    for x in &mut new_folders {\n        x.selected_row = false;\n    }\n\n    new_folders.extend(filtered_folders.iter().map(|path| ExcludedPathsModel {\n        path: (*path).into(),\n        selected_row: false,\n    }));\n\n    new_folders.sort_by_key(|x| x.path.clone());\n\n    let new_folders_model = ModelRc::new(VecModel::from(new_folders));\n    settings.set_excluded_paths_model(new_folders_model);\n}\n"
  },
  {
    "path": "krokiet/src/connect_open.rs",
    "content": "use czkawka_core::common::config_cache_path::get_config_cache_path;\nuse log::error;\nuse slint::ComponentHandle;\n\nuse crate::{Callabler, MainWindow};\n\npub(crate) fn connect_open_items(app: &MainWindow) {\n    app.global::<Callabler>().on_open_config_folder(move || {\n        let Some(config_cache) = get_config_cache_path() else {\n            error!(\"Failed to open config folder\");\n            return;\n        };\n        if let Err(e) = open::that(&config_cache.config_folder) {\n            error!(\"Failed to open config folder \\\"{}\\\": {e}\", config_cache.config_folder.to_string_lossy());\n        }\n    });\n\n    app.global::<Callabler>().on_open_cache_folder(move || {\n        let Some(config_cache) = get_config_cache_path() else {\n            error!(\"Failed to open cache folder\");\n            return;\n        };\n        if let Err(e) = open::that(&config_cache.cache_folder) {\n            error!(\"Failed to open cache folder \\\"{}\\\": {e}\", config_cache.cache_folder.to_string_lossy());\n        }\n    });\n\n    app.global::<Callabler>().on_open_link(move |link| match open::that(link.as_str()) {\n        Ok(()) => {}\n        Err(e) => {\n            error!(\"Failed to open link: {e}\");\n        }\n    });\n}\n"
  },
  {
    "path": "krokiet/src/connect_progress_receiver.rs",
    "content": "use std::thread;\n\nuse crossbeam_channel::Receiver;\nuse czkawka_core::common::model::ToolType;\nuse czkawka_core::common::progress_data::{CurrentStage, ProgressData};\nuse humansize::{BINARY, format_size};\nuse slint::ComponentHandle;\n\nuse crate::{MainWindow, ProgressToSend, flk};\n\npub(crate) fn connect_progress_gathering(app: &MainWindow, progress_receiver: Receiver<ProgressData>) {\n    let a = app.as_weak();\n\n    thread::spawn(move || {\n        loop {\n            let Ok(progress_data) = progress_receiver.recv() else {\n                return; // Channel closed, so exit the thread since app closing\n            };\n\n            a.upgrade_in_event_loop(move |app| {\n                let removing_empty_folders = progress_data.tool_type == ToolType::EmptyFolders;\n\n                let to_send = if progress_data.current_stage_idx == 0 && !progress_data.sstage.is_special_non_tool_stage() {\n                    progress_collect_items(&progress_data, !removing_empty_folders)\n                } else if progress_data.sstage.check_if_loading_saving_cache() {\n                    progress_save_load_cache(&progress_data)\n                } else {\n                    progress_default(&progress_data)\n                };\n\n                app.set_progress_datas(to_send);\n            })\n            .expect(\"Failed to spawn thread for progress gathering\");\n        }\n    });\n}\n\nfn progress_save_load_cache(item: &ProgressData) -> ProgressToSend {\n    let step_name = match item.sstage {\n        CurrentStage::SameMusicCacheLoadingTags => flk!(\"rust_loading_tags_cache\"),\n        CurrentStage::SameMusicCacheLoadingFingerprints => flk!(\"rust_loading_fingerprints_cache\"),\n        CurrentStage::SameMusicCacheSavingTags => flk!(\"rust_saving_tags_cache\"),\n        CurrentStage::SameMusicCacheSavingFingerprints => flk!(\"rust_saving_fingerprints_cache\"),\n        CurrentStage::DuplicatePreHashCacheLoading => flk!(\"rust_loading_prehash_cache\"),\n        CurrentStage::DuplicatePreHashCacheSaving => flk!(\"rust_saving_prehash_cache\"),\n        CurrentStage::DuplicateCacheLoading => flk!(\"rust_loading_hash_cache\"),\n        CurrentStage::DuplicateCacheSaving => flk!(\"rust_saving_hash_cache\"),\n        CurrentStage::ExifRemoverCacheLoading => flk!(\"rust_loading_exif_cache\"),\n        CurrentStage::ExifRemoverCacheSaving => flk!(\"rust_saving_exif_cache\"),\n        CurrentStage::DeletingFiles\n        | CurrentStage::RenamingFiles\n        | CurrentStage::MovingFiles\n        | CurrentStage::HardlinkingFiles\n        | CurrentStage::SymlinkingFiles\n        | CurrentStage::OptimizingVideos\n        | CurrentStage::CleaningExif\n        | CurrentStage::CollectingFiles\n        | CurrentStage::DuplicateScanningName\n        | CurrentStage::DuplicateScanningSizeName\n        | CurrentStage::DuplicateScanningSize\n        | CurrentStage::DuplicatePreHashing\n        | CurrentStage::DuplicateFullHashing\n        | CurrentStage::SameMusicReadingTags\n        | CurrentStage::SameMusicCalculatingFingerprints\n        | CurrentStage::SameMusicComparingTags\n        | CurrentStage::SameMusicComparingFingerprints\n        | CurrentStage::SimilarImagesCalculatingHashes\n        | CurrentStage::SimilarImagesComparingHashes\n        | CurrentStage::SimilarVideosCalculatingHashes\n        | CurrentStage::SimilarVideosCreatingThumbnails\n        | CurrentStage::BrokenFilesChecking\n        | CurrentStage::BadExtensionsChecking\n        | CurrentStage::BadNamesChecking\n        | CurrentStage::ExifRemoverExtractingTags\n        | CurrentStage::VideoOptimizerCreatingThumbnails\n        | CurrentStage::VideoOptimizerProcessingVideos => unreachable!(),\n    };\n    let (all_progress, current_progress, current_progress_size) = common_get_data(item);\n    ProgressToSend {\n        all_progress,\n        current_progress,\n        current_progress_size,\n        step_name: step_name.into(),\n    }\n}\n\nfn progress_collect_items(item: &ProgressData, files: bool) -> ProgressToSend {\n    let step_name = match item.sstage {\n        CurrentStage::DuplicateScanningName => flk!(\"rust_scanning_name\", entries_checked = item.entries_checked),\n        CurrentStage::DuplicateScanningSizeName => flk!(\"rust_scanning_size_name\", entries_checked = item.entries_checked),\n        CurrentStage::DuplicateScanningSize => flk!(\"rust_scanning_size\", entries_checked = item.entries_checked),\n        _ => {\n            if files {\n                flk!(\"rust_scanning_file\", entries_checked = item.entries_checked)\n            } else {\n                flk!(\"rust_scanning_folder\", entries_checked = item.entries_checked)\n            }\n        }\n    };\n    let (all_progress, current_progress) = no_current_stage_get_data(item);\n    ProgressToSend {\n        all_progress,\n        current_progress,\n        current_progress_size: -1,\n        step_name: step_name.into(),\n    }\n}\n\nfn progress_default(item: &ProgressData) -> ProgressToSend {\n    let items_stats = format!(\"{}/{}\", item.entries_checked, item.entries_to_check);\n    let size_stats = format!(\"{}/{}\", format_size(item.bytes_checked, BINARY), format_size(item.bytes_to_check, BINARY));\n    let step_name = match item.sstage {\n        CurrentStage::SameMusicReadingTags => flk!(\"rust_checked_tags\", items_stats = items_stats),\n        CurrentStage::SameMusicCalculatingFingerprints => flk!(\"rust_checked_content\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::SameMusicComparingTags => flk!(\"rust_compared_tags\", items_stats = items_stats),\n        CurrentStage::SameMusicComparingFingerprints => flk!(\"rust_compared_content\", items_stats = items_stats),\n        CurrentStage::SimilarImagesCalculatingHashes => flk!(\"rust_hashed_images\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::SimilarImagesComparingHashes => flk!(\"rust_compared_image_hashes\", items_stats = items_stats),\n        CurrentStage::SimilarVideosCalculatingHashes => flk!(\"rust_hashed_videos\", items_stats = items_stats),\n        CurrentStage::BrokenFilesChecking => flk!(\"rust_checked_files\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::BadExtensionsChecking => flk!(\"rust_checked_files_bad_extensions\", items_stats = items_stats),\n        CurrentStage::BadNamesChecking => flk!(\"rust_checked_files_bad_names\", items_stats = items_stats),\n        CurrentStage::SimilarVideosCreatingThumbnails | CurrentStage::VideoOptimizerCreatingThumbnails => flk!(\"rust_created_thumbnails\", items_stats = items_stats),\n        CurrentStage::VideoOptimizerProcessingVideos => flk!(\"rust_checked_videos\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::DuplicatePreHashing => flk!(\"rust_analyzed_partial_hash\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::DuplicateFullHashing => flk!(\"rust_analyzed_full_hash\", items_stats = items_stats, size_stats = size_stats),\n\n        CurrentStage::DeletingFiles if item.bytes_to_check != 0 => flk!(\"rust_deleting_files\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::DeletingFiles => flk!(\"rust_deleting_no_size_files\", items_stats = items_stats),\n        CurrentStage::RenamingFiles => flk!(\"rust_renaming_files\", items_stats = items_stats),\n        CurrentStage::MovingFiles if item.bytes_to_check != 0 => flk!(\"rust_moving_files\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::MovingFiles => flk!(\"rust_moving_no_size_files\", items_stats = items_stats),\n        CurrentStage::HardlinkingFiles if item.bytes_to_check != 0 => flk!(\"rust_hardlinking_files\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::HardlinkingFiles => flk!(\"rust_hardlinking_no_size_files\", items_stats = items_stats),\n        CurrentStage::SymlinkingFiles if item.bytes_to_check != 0 => flk!(\"rust_symlinking_files\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::SymlinkingFiles => flk!(\"rust_symlinking_no_size_files\", items_stats = items_stats),\n        CurrentStage::OptimizingVideos if item.bytes_to_check != 0 => flk!(\"rust_optimizing_videos\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::OptimizingVideos => flk!(\"rust_optimizing_no_size_videos\", items_stats = items_stats),\n        CurrentStage::CleaningExif if item.bytes_to_check != 0 => flk!(\"rust_cleaning_exif\", items_stats = items_stats, size_stats = size_stats),\n        CurrentStage::CleaningExif => flk!(\"rust_cleaning_no_size_exif\", items_stats = items_stats),\n\n        CurrentStage::ExifRemoverExtractingTags => flk!(\"rust_extracted_exif_tags\", items_stats = items_stats, size_stats = size_stats),\n\n        CurrentStage::CollectingFiles\n        | CurrentStage::DuplicateCacheSaving\n        | CurrentStage::DuplicateCacheLoading\n        | CurrentStage::DuplicatePreHashCacheSaving\n        | CurrentStage::DuplicatePreHashCacheLoading\n        | CurrentStage::DuplicateScanningName\n        | CurrentStage::DuplicateScanningSizeName\n        | CurrentStage::DuplicateScanningSize\n        | CurrentStage::SameMusicCacheSavingTags\n        | CurrentStage::SameMusicCacheLoadingTags\n        | CurrentStage::SameMusicCacheSavingFingerprints\n        | CurrentStage::SameMusicCacheLoadingFingerprints\n        | CurrentStage::ExifRemoverCacheLoading\n        | CurrentStage::ExifRemoverCacheSaving => unreachable!(\"This stages(caches, initial files scanning) should be handled somewhere else\"),\n    };\n    let (all_progress, current_progress, current_progress_size) = common_get_data(item);\n\n    // Deleting is a single operation, so we don't need to show two same progress bars\n    let all_progress = if item.sstage.is_special_non_tool_stage() { -1 } else { all_progress };\n\n    ProgressToSend {\n        all_progress,\n        current_progress,\n        current_progress_size,\n        step_name: step_name.into(),\n    }\n}\n\n// Used when current stage not have enough data to show status, so we show only all_stages\n// Happens if we are searching files and we don't know how many files we need to check\nfn no_current_stage_get_data(item: &ProgressData) -> (i32, i32) {\n    let all_stages = (item.current_stage_idx as f64) / (item.max_stage_idx + 1) as f64;\n\n    ((all_stages * 100.0) as i32, -1)\n}\n\n// Used to calculate number of files to check and also to calculate current progress according to number of files to check and checked\nfn common_get_data(item: &ProgressData) -> (i32, i32, i32) {\n    let (current_items_checked, current_stage_items_to_check) = if item.bytes_to_check > 0 {\n        (item.bytes_checked, item.bytes_to_check)\n    } else {\n        (item.entries_checked as u64, item.entries_to_check as u64)\n    };\n\n    if item.entries_to_check != 0 {\n        let all_stages = (item.current_stage_idx as f64 + current_items_checked as f64 / current_stage_items_to_check as f64) / (item.max_stage_idx + 1) as f64;\n        let all_stages = all_stages.min(0.99);\n\n        let current_stage = current_items_checked as f64 / current_stage_items_to_check as f64;\n        let current_stage = current_stage.min(0.99);\n\n        let current_stage_size = if item.bytes_to_check != 0 {\n            ((item.bytes_checked as f64 / item.bytes_to_check as f64).min(0.99) * 100.0) as i32\n        } else {\n            -1\n        };\n\n        ((all_stages * 100.0) as i32, (current_stage * 100.0) as i32, current_stage_size)\n    } else {\n        let all_stages = (item.current_stage_idx as f64) / (item.max_stage_idx + 1) as f64;\n        let all_stages = all_stages.min(0.99);\n        ((all_stages * 100.0) as i32, 0, -1)\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_rfd.rs",
    "content": "use slint::{ComponentHandle, Weak};\n\nuse crate::{GuiState, MainWindow};\n\npub(crate) fn show_file_dialog_overlay(app: &MainWindow) {\n    app.global::<GuiState>().set_file_dialog_open(true);\n}\n\npub(crate) fn hide_file_dialog_overlay(app: &Weak<MainWindow>) {\n    let app = app.clone();\n    app.upgrade_in_event_loop(move |app| {\n        app.global::<GuiState>().set_file_dialog_open(false);\n    })\n    .expect(\"Failed to hide file dialog overlay\");\n}\n"
  },
  {
    "path": "krokiet/src/connect_row_selection.rs",
    "content": "use std::collections::HashMap;\nuse std::hash::{Hash, Hasher};\nuse std::mem;\nuse std::sync::{LazyLock, RwLock, RwLockWriteGuard};\n\nuse czkawka_core::TOOLS_NUMBER;\nuse log::{error, trace};\nuse slint::{ComponentHandle, Model, ModelRc, VecModel};\n\nuse crate::common::{connect_i32_into_u64, split_u64_into_i32s};\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow, SingleMainListModel};\n\nconst SELECTED_ROWS_LIMIT: usize = 1000;\n\n#[derive(Debug, Default, Clone)]\npub(crate) struct SelectionData {\n    // Should be always valid\n    number_of_selected_rows: usize,\n    // Needs to be empty, when exceeded limit\n    selected_rows: Vec<usize>,\n    // If exceeded limit, then we need to reload entire model, because it should be faster that changing each row\n    exceeded_limit: bool,\n}\n\npub(crate) static TOOLS_SELECTION: LazyLock<RwLock<HashMap<ActiveTab, SelectionData>>> = LazyLock::new(|| RwLock::new(HashMap::new()));\n\n// pub(crate) fn get_selection_data(active_tab: ActiveTab) -> SelectionData {\n//     let lock = TOOLS_SELECTION.read().expect(\"Selection data is not initialized or is poisoned\");\n//     let keys = lock.keys().cloned().collect::<Vec<_>>();\n//     lock.get(&active_tab)\n//         .unwrap_or_else(|| panic!(\"Failed to get selection data for tab {active_tab:?} - {keys:?}\"))\n//         .clone()\n// }\n\npub(crate) fn reset_selection(app: &MainWindow, active_tab: ActiveTab, reset_all_selection: bool) {\n    if reset_all_selection {\n        let mut lock = get_write_selection_lock();\n        let keys = lock.keys().cloned().collect::<Vec<_>>();\n        let selection = lock\n            .get_mut(&active_tab)\n            .unwrap_or_else(|| panic!(\"Failed to get selection data for tab {active_tab:?} - {keys:?}\"));\n        selection.selected_rows.clear();\n        selection.number_of_selected_rows = 0;\n        selection.exceeded_limit = false;\n    }\n\n    app.invoke_reset_selection(active_tab);\n}\n\n// E.g. when sorting things, selected rows in vector, may be invalid\n// So we need to recalculate them\npub(crate) fn recalculate_small_selection_if_needed(model: &ModelRc<SingleMainListModel>, active_tab: ActiveTab) {\n    let mut lock = get_write_selection_lock();\n    let keys = lock.keys().cloned().collect::<Vec<_>>();\n    let selection = lock\n        .get_mut(&active_tab)\n        .unwrap_or_else(|| panic!(\"Failed to get selection data for tab {active_tab:?} - {keys:?}\"));\n\n    if selection.exceeded_limit || selection.selected_rows.is_empty() {\n        return;\n    }\n\n    let selection_not_changed = selection.selected_rows.iter().all(|e| {\n        let model_data = model\n            .row_data(*e)\n            .unwrap_or_else(|| panic!(\"Failed to get row data with id {}, with model {} items\", e, model.row_count()));\n        model_data.selected_row\n    });\n\n    if selection_not_changed {\n        return;\n    }\n\n    selection.selected_rows = model.iter().enumerate().filter_map(|(idx, e)| if e.selected_row { Some(idx) } else { None }).collect();\n}\n\npub(crate) fn initialize_selection_struct() {\n    let tools: [ActiveTab; TOOLS_NUMBER] = [\n        ActiveTab::DuplicateFiles,\n        ActiveTab::EmptyFolders,\n        ActiveTab::BigFiles,\n        ActiveTab::EmptyFiles,\n        ActiveTab::TemporaryFiles,\n        ActiveTab::SimilarImages,\n        ActiveTab::SimilarVideos,\n        ActiveTab::SimilarMusic,\n        ActiveTab::InvalidSymlinks,\n        ActiveTab::BrokenFiles,\n        ActiveTab::BadExtensions,\n        ActiveTab::BadNames,\n        ActiveTab::ExifRemover,\n        ActiveTab::VideoOptimizer,\n    ];\n\n    let map: HashMap<_, _> = tools.into_iter().map(|tool| (tool, SelectionData::default())).collect();\n    let mut tool = TOOLS_SELECTION.write().expect(\"Failed to get write selection lock\");\n    if !cfg!(test) {\n        let data = mem::replace(&mut *tool, map);\n        assert!(data.is_empty(), \"Selection data is already initialized, but it should be empty\");\n    } else {\n        let _ = mem::replace(&mut *tool, map);\n    }\n}\n\n// fn get_read_selection_lock() -> RwLockReadGuard<'static, HashMap<ActiveTab, SelectionData>> {\n//     let selection = TOOLS_SELECTION.get().expect(\"Selection data is not initialized\");\n//     selection.read().expect(\"Failed to lock selection data\")\n// }\nfn get_write_selection_lock() -> RwLockWriteGuard<'static, HashMap<ActiveTab, SelectionData>> {\n    TOOLS_SELECTION.write().expect(\"Selection data is not initialized or is poisoned\")\n}\n\nimpl Hash for ActiveTab {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        (*self as u8).hash(state);\n    }\n}\nimpl Eq for ActiveTab {}\n\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n\npub(crate) fn connect_row_selections(app: &MainWindow) {\n    initialize_selection_struct();\n\n    selection::connect_select_all_rows(app); // CTRL + A\n    selection::reverse_single_unique_item(app); // LMB\n    selection::reverse_checked_on_selection(app); // Space\n    selection::reverse_selection_on_specific_item(app); // CTRL + LMB\n    selection::select_items_with_shift(app); // SHIFT + LMB\n    opener::open_provided_item(app);\n    opener::open_provided_parent_item(app);\n    opener::connect_on_open_item(app);\n    checker::change_number_of_checked_items(app);\n}\n\nmod opener {\n    use super::{Callabler, ComponentHandle, GuiState, MainWindow, Model, error};\n\n    pub(crate) fn connect_on_open_item(app: &MainWindow) {\n        app.global::<Callabler>().on_open_item(move |path| {\n            open_item_simple(path.as_str());\n        });\n        app.global::<Callabler>().on_open_parent(move |path| {\n            let Some(parent_path) = std::path::Path::new(&path).parent() else {\n                return error!(\"Failed to get parent path for \\\"{path}\\\"\");\n            };\n            open_item_simple(&parent_path.to_string_lossy());\n        });\n    }\n\n    fn open_item_simple(path_to_open: &str) {\n        if let Err(e) = open::that(path_to_open) {\n            error!(\"Failed to open file: {e}\");\n        }\n    }\n\n    fn open_item(app: &MainWindow, items_path_str: &[usize], id: usize) {\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        let model = active_tab.get_tool_model(app);\n        let model_data = model\n            .row_data(id)\n            .unwrap_or_else(|| panic!(\"Failed to get row data with id {id}, with model {} items\", model.row_count()));\n\n        let get_debug_crash_data = || {\n            format!(\n                \"Model data str - {} - cannot find path/name at index/es - {:?}, active tab - {active_tab:?}\",\n                model_data.val_str.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(\", \"),\n                items_path_str\n            )\n        };\n\n        let path_to_open = if items_path_str.len() == 1 {\n            format!(\n                \"{}\",\n                model_data.val_str.iter().nth(items_path_str[0]).unwrap_or_else(|| panic!(\"{}\", get_debug_crash_data()))\n            )\n        } else {\n            format!(\n                \"{}/{}\",\n                model_data.val_str.iter().nth(items_path_str[0]).unwrap_or_else(|| panic!(\"{}\", get_debug_crash_data())),\n                model_data.val_str.iter().nth(items_path_str[1]).unwrap_or_else(|| panic!(\"{}\", get_debug_crash_data()))\n            )\n        };\n        open_item_simple(&path_to_open);\n    }\n\n    pub(crate) fn open_provided_item(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_open_item_with_index(move |idx| {\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n\n            open_item(&app, &[active_tab.get_str_path_idx(), active_tab.get_str_name_idx()], idx as usize);\n        });\n    }\n\n    pub(crate) fn open_provided_parent_item(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_open_parent_item_with_index(move |idx| {\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n\n            open_item(&app, &[active_tab.get_str_path_idx()], idx as usize);\n        });\n    }\n}\nmod selection {\n    use slint::ModelRc;\n\n    use super::{\n        Callabler, ComponentHandle, GuiState, MainWindow, Model, get_write_selection_lock, reverse_selection_of_item_with_id, row_select_items_with_shift,\n        rows_deselect_all_by_mode, rows_reverse_checked_selection, rows_select_all_by_mode, trace,\n    };\n    use crate::SingleMainListModel;\n    use crate::connect_row_selection::SelectionData;\n    use crate::connect_row_selection::checker::change_number_of_enabled_items;\n\n    fn validate_selection_and_model(selection: &SelectionData, model: &ModelRc<SingleMainListModel>) {\n        assert!(\n            selection.number_of_selected_rows == selection.selected_rows.len() || selection.exceeded_limit,\n            \"Number of selected rows {} should be equal to length of selected rows vector {} if not exceeded limit\",\n            selection.number_of_selected_rows,\n            selection.selected_rows.len()\n        );\n        assert!(\n            model.row_count() >= selection.number_of_selected_rows,\n            \"Number of model items {} should be bigger than number of selected items {}\",\n            model.row_count(),\n            selection.number_of_selected_rows\n        );\n    }\n\n    pub(crate) fn connect_select_all_rows(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_select_all(move || {\n            trace!(\"Clicked select all\");\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n\n            let mut lock = get_write_selection_lock();\n            let selection = lock.get_mut(&active_tab).expect(\"Failed to get selection data\");\n            let model = active_tab.get_tool_model(&app);\n\n            validate_selection_and_model(selection, &model);\n            if let Some(new_model) = rows_select_all_by_mode(selection, &model) {\n                validate_selection_and_model(selection, &new_model);\n                active_tab.set_tool_model(&app, new_model);\n            }\n        });\n    }\n\n    pub(crate) fn reverse_single_unique_item(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_reverse_single_unique_item(move |id| {\n            trace!(\"Clicked reverse single unique item\");\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n            let mut lock = get_write_selection_lock();\n            let selection = lock.get_mut(&active_tab).expect(\"Failed to get selection data\");\n\n            {\n                let model = active_tab.get_tool_model(&app);\n                validate_selection_and_model(selection, &model);\n\n                if let Some(new_model) = rows_deselect_all_by_mode(selection, &model) {\n                    active_tab.set_tool_model(&app, new_model);\n                }\n            }\n\n            // needs to get model again, because it could be replaced\n            let model = active_tab.get_tool_model(&app);\n            reverse_selection_of_item_with_id(selection, &model, id as usize);\n            validate_selection_and_model(selection, &model);\n        });\n    }\n\n    pub(crate) fn reverse_checked_on_selection(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_reverse_checked_selection(move || {\n            trace!(\"Clicked reverse checked on selection\");\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n            let mut lock = get_write_selection_lock();\n            let selection = lock.get_mut(&active_tab).expect(\"Failed to get selection data\");\n            let model = active_tab.get_tool_model(&app);\n\n            validate_selection_and_model(selection, &model);\n\n            let (checked_items, unchecked_items, new_model) = rows_reverse_checked_selection(selection, &model);\n            if let Some(new_model) = new_model {\n                active_tab.set_tool_model(&app, new_model);\n            }\n\n            change_number_of_enabled_items(&app, active_tab, checked_items as i64 - unchecked_items as i64);\n            validate_selection_and_model(selection, &active_tab.get_tool_model(&app));\n        });\n    }\n    pub(crate) fn reverse_selection_on_specific_item(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_reverse_item_selection(move |id| {\n            trace!(\"Clicked reverse selection on specific item\");\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n            let mut lock = get_write_selection_lock();\n            let selection = lock.get_mut(&active_tab).expect(\"Failed to get selection data\");\n            let model = active_tab.get_tool_model(&app);\n            validate_selection_and_model(selection, &model);\n            reverse_selection_of_item_with_id(selection, &model, id as usize);\n            validate_selection_and_model(selection, &model);\n        });\n    }\n\n    pub(crate) fn select_items_with_shift(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_row_select_items_with_shift(move |first_idx, second_idx| {\n            trace!(\"Clicked select items with shift\");\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n            let mut lock = get_write_selection_lock();\n            let selection = lock.get_mut(&active_tab).expect(\"Failed to get selection data\");\n            let model = active_tab.get_tool_model(&app);\n\n            assert!(first_idx >= 0);\n            assert!(second_idx >= 0);\n            assert!((first_idx as usize) < model.row_count());\n            assert!((second_idx as usize) < model.row_count());\n\n            validate_selection_and_model(selection, &model);\n            if let Some(new_model) = row_select_items_with_shift(selection, &model, (first_idx as usize, second_idx as usize)) {\n                validate_selection_and_model(selection, &new_model);\n                active_tab.set_tool_model(&app, new_model);\n            }\n        });\n    }\n}\n\npub(crate) mod checker {\n    use super::{ActiveTab, Callabler, ComponentHandle, GuiState, MainWindow, connect_i32_into_u64, split_u64_into_i32s, trace};\n\n    pub(crate) fn change_number_of_checked_items(app: &MainWindow) {\n        let a = app.as_weak();\n        app.global::<Callabler>().on_change_number_of_checked_items(move |number_of_changed_items| {\n            trace!(\"Changing number of checked items with {number_of_changed_items}\");\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n            change_number_of_enabled_items(&app, active_tab, number_of_changed_items as i64);\n        });\n    }\n\n    // TODO - sad day for code readability, because slint not supports i64 - https://github.com/slint-ui/slint/issues/6589\n    pub(crate) fn set_number_of_enabled_items(app: &MainWindow, active_tab: ActiveTab, items_number: u64) {\n        let (it1, it2) = split_u64_into_i32s(items_number);\n        match active_tab {\n            ActiveTab::DuplicateFiles => {\n                app.global::<GuiState>().set_selected_results_duplicates(it1);\n                app.global::<GuiState>().set_selected_results_duplicates2(it2);\n            }\n            ActiveTab::EmptyFolders => {\n                app.global::<GuiState>().set_selected_results_empty_folders(it1);\n                app.global::<GuiState>().set_selected_results_empty_folders2(it2);\n            }\n            ActiveTab::BigFiles => {\n                app.global::<GuiState>().set_selected_results_big_files(it1);\n                app.global::<GuiState>().set_selected_results_big_files2(it2);\n            }\n            ActiveTab::EmptyFiles => {\n                app.global::<GuiState>().set_selected_results_empty_files(it1);\n                app.global::<GuiState>().set_selected_results_empty_files2(it2);\n            }\n            ActiveTab::TemporaryFiles => {\n                app.global::<GuiState>().set_selected_results_temporary_files(it1);\n                app.global::<GuiState>().set_selected_results_temporary_files2(it2);\n            }\n            ActiveTab::SimilarImages => {\n                app.global::<GuiState>().set_selected_results_similar_images(it1);\n                app.global::<GuiState>().set_selected_results_similar_images2(it2);\n            }\n            ActiveTab::SimilarVideos => {\n                app.global::<GuiState>().set_selected_results_similar_videos(it1);\n                app.global::<GuiState>().set_selected_results_similar_videos2(it2);\n            }\n            ActiveTab::SimilarMusic => {\n                app.global::<GuiState>().set_selected_results_similar_music(it1);\n                app.global::<GuiState>().set_selected_results_similar_music2(it2);\n            }\n            ActiveTab::InvalidSymlinks => {\n                app.global::<GuiState>().set_selected_results_invalid_symlinks(it1);\n                app.global::<GuiState>().set_selected_results_invalid_symlinks2(it2);\n            }\n            ActiveTab::BrokenFiles => {\n                app.global::<GuiState>().set_selected_results_broken_files(it1);\n                app.global::<GuiState>().set_selected_results_broken_files2(it2);\n            }\n            ActiveTab::BadExtensions => {\n                app.global::<GuiState>().set_selected_results_bad_extensions(it1);\n                app.global::<GuiState>().set_selected_results_bad_extensions2(it2);\n            }\n            ActiveTab::BadNames => {\n                app.global::<GuiState>().set_selected_results_bad_names(it1);\n                app.global::<GuiState>().set_selected_results_bad_names2(it2);\n            }\n            ActiveTab::ExifRemover => {\n                app.global::<GuiState>().set_selected_results_exif_remover(it1);\n                app.global::<GuiState>().set_selected_results_exif_remover2(it2);\n            }\n            ActiveTab::VideoOptimizer => {\n                app.global::<GuiState>().set_selected_results_video_optimizer(it1);\n                app.global::<GuiState>().set_selected_results_video_optimizer2(it2);\n            }\n            _ => unreachable!(\"Current tab is not a tool that has enabled items\"),\n        }\n    }\n\n    pub(crate) fn change_number_of_enabled_items(app: &MainWindow, active_tab: ActiveTab, additions: i64) {\n        let before_number_of_items = get_number_of_enabled_items(app, active_tab);\n        let after_number_of_items = before_number_of_items\n            .checked_add_signed(additions)\n            .unwrap_or_else(|| panic!(\"Overflow when adding signed number to items: before_number_of_items = {before_number_of_items}, additions = {additions}\"));\n        set_number_of_enabled_items(app, active_tab, after_number_of_items);\n    }\n\n    pub(crate) fn get_number_of_enabled_items(app: &MainWindow, active_tab: ActiveTab) -> u64 {\n        let (it1, it2) = match active_tab {\n            ActiveTab::DuplicateFiles => (\n                app.global::<GuiState>().get_selected_results_duplicates(),\n                app.global::<GuiState>().get_selected_results_duplicates2(),\n            ),\n            ActiveTab::EmptyFolders => (\n                app.global::<GuiState>().get_selected_results_empty_folders(),\n                app.global::<GuiState>().get_selected_results_empty_folders2(),\n            ),\n            ActiveTab::BigFiles => (\n                app.global::<GuiState>().get_selected_results_big_files(),\n                app.global::<GuiState>().get_selected_results_big_files2(),\n            ),\n            ActiveTab::EmptyFiles => (\n                app.global::<GuiState>().get_selected_results_empty_files(),\n                app.global::<GuiState>().get_selected_results_empty_files2(),\n            ),\n            ActiveTab::TemporaryFiles => (\n                app.global::<GuiState>().get_selected_results_temporary_files(),\n                app.global::<GuiState>().get_selected_results_temporary_files2(),\n            ),\n            ActiveTab::SimilarImages => (\n                app.global::<GuiState>().get_selected_results_similar_images(),\n                app.global::<GuiState>().get_selected_results_similar_images2(),\n            ),\n            ActiveTab::SimilarVideos => (\n                app.global::<GuiState>().get_selected_results_similar_videos(),\n                app.global::<GuiState>().get_selected_results_similar_videos2(),\n            ),\n            ActiveTab::SimilarMusic => (\n                app.global::<GuiState>().get_selected_results_similar_music(),\n                app.global::<GuiState>().get_selected_results_similar_music2(),\n            ),\n            ActiveTab::InvalidSymlinks => (\n                app.global::<GuiState>().get_selected_results_invalid_symlinks(),\n                app.global::<GuiState>().get_selected_results_invalid_symlinks2(),\n            ),\n            ActiveTab::BrokenFiles => (\n                app.global::<GuiState>().get_selected_results_broken_files(),\n                app.global::<GuiState>().get_selected_results_broken_files2(),\n            ),\n            ActiveTab::BadExtensions => (\n                app.global::<GuiState>().get_selected_results_bad_extensions(),\n                app.global::<GuiState>().get_selected_results_bad_extensions2(),\n            ),\n            ActiveTab::BadNames => (\n                app.global::<GuiState>().get_selected_results_bad_names(),\n                app.global::<GuiState>().get_selected_results_bad_names2(),\n            ),\n            ActiveTab::ExifRemover => (\n                app.global::<GuiState>().get_selected_results_exif_remover(),\n                app.global::<GuiState>().get_selected_results_exif_remover2(),\n            ),\n            ActiveTab::VideoOptimizer => (\n                app.global::<GuiState>().get_selected_results_video_optimizer(),\n                app.global::<GuiState>().get_selected_results_video_optimizer2(),\n            ),\n            _ => unreachable!(\"Current tab is not a tool that has enabled items\"),\n        };\n        connect_i32_into_u64(it1, it2)\n    }\n}\n\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n////////////////////\n\n//\n// Deselect\n//\n\nfn rows_deselect_all_by_mode(selection: &mut SelectionData, model: &ModelRc<SingleMainListModel>) -> Option<ModelRc<SingleMainListModel>> {\n    let new_model = if selection.exceeded_limit {\n        Some(rows_deselect_all_selected_by_replacing_models(model))\n    } else if !selection.selected_rows.is_empty() {\n        rows_deselect_all_selected_one_by_one(model, selection);\n        None\n    } else {\n        assert_ne!(model.row_count(), 0);\n        None\n    };\n\n    selection.selected_rows.clear();\n    selection.exceeded_limit = false;\n    selection.number_of_selected_rows = 0;\n\n    new_model\n}\n\nfn rows_deselect_all_selected_one_by_one(model: &ModelRc<SingleMainListModel>, selection: &SelectionData) {\n    for id in &selection.selected_rows {\n        let mut model_data = model\n            .row_data(*id)\n            .unwrap_or_else(|| panic!(\"Failed to get row data with id {id}, with model {} items\", model.row_count()));\n        assert!(model_data.selected_row);\n        model_data.selected_row = false;\n        model.set_row_data(*id, model_data);\n    }\n}\n\nfn rows_deselect_all_selected_by_replacing_models(model: &ModelRc<SingleMainListModel>) -> ModelRc<SingleMainListModel> {\n    let new_model = model\n        .iter()\n        .map(|mut row| {\n            row.selected_row = false;\n            row\n        })\n        .collect::<Vec<_>>();\n    ModelRc::new(VecModel::from(new_model))\n}\n\n//\n// Select All\n//\nfn rows_select_all_by_mode(selection: &mut SelectionData, model: &ModelRc<SingleMainListModel>) -> Option<ModelRc<SingleMainListModel>> {\n    assert!(\n        model.row_count() >= selection.number_of_selected_rows,\n        \"Number of model items {} should be bigger than number of selected items {}\",\n        model.row_count(),\n        selection.number_of_selected_rows\n    );\n    let new_model = if model.row_count() - selection.number_of_selected_rows > 100 {\n        rows_select_all_by_replacing_models(selection, model)\n    } else {\n        rows_select_all_one_by_one(model);\n        None\n    };\n\n    if model.row_count() > SELECTED_ROWS_LIMIT || selection.exceeded_limit {\n        selection.exceeded_limit = true;\n        selection.selected_rows.clear();\n        selection.number_of_selected_rows = new_model.as_ref().unwrap_or(model).iter().filter(|e| e.selected_row).count();\n    } else {\n        selection.selected_rows = new_model\n            .as_ref()\n            .unwrap_or(model)\n            .iter()\n            .enumerate()\n            .filter_map(|(idx, item)| if item.selected_row { Some(idx) } else { None })\n            .collect();\n        selection.number_of_selected_rows = selection.selected_rows.len();\n    }\n\n    new_model\n}\n\nfn rows_select_all_one_by_one(model: &ModelRc<SingleMainListModel>) {\n    let items_to_update = model.iter().filter(|e| !e.selected_row && !e.header_row).count();\n    trace!(\"[FAST][ONE_BY_ONE] select all {}/{} items\", items_to_update, model.row_count());\n    for id in 0..model.row_count() {\n        let mut model_data = model\n            .row_data(id)\n            .unwrap_or_else(|| panic!(\"Failed to get row data with id {id}, with model {} items\", model.row_count()));\n\n        if model_data.header_row {\n            continue;\n        }\n\n        if model_data.selected_row {\n            continue;\n        }\n\n        model_data.selected_row = true;\n        model.set_row_data(id, model_data);\n    }\n}\n\nfn rows_select_all_by_replacing_models(selection: &SelectionData, model: &ModelRc<SingleMainListModel>) -> Option<ModelRc<SingleMainListModel>> {\n    // May happen with simple models, but for more advanced with header rows, we need something like \"selection.all_items_selected\"\n    if selection.number_of_selected_rows == model.row_count() {\n        trace!(\n            \"[SLOW][REPLACE_MODEL], but no need to replace it - {} items both exists and selected\",\n            selection.number_of_selected_rows\n        );\n        return None;\n    }\n    trace!(\"[SLOW][REPLACE_MODEL] select all {} items\", model.row_count());\n\n    let new_model = model\n        .iter()\n        .map(|mut row| {\n            row.selected_row = !row.header_row;\n            row\n        })\n        .collect::<Vec<_>>();\n    Some(ModelRc::new(VecModel::from(new_model)))\n}\n\n//\n// Reverse selection and selecting\n//\nfn reverse_selection_of_item_with_id(selection: &mut SelectionData, model: &ModelRc<SingleMainListModel>, id: usize) {\n    let mut model_data = model\n        .row_data(id)\n        .unwrap_or_else(|| panic!(\"Failed to get row data with id {id}, with model {} items\", model.row_count()));\n\n    if model_data.header_row {\n        assert!(!model_data.selected_row);\n        return;\n    }\n\n    let was_selected = model_data.selected_row;\n    model_data.selected_row = !model_data.selected_row;\n    model.set_row_data(id, model_data);\n\n    if was_selected {\n        assert!(selection.number_of_selected_rows > 0);\n        if !selection.exceeded_limit {\n            selection.selected_rows.retain(|&x| x != id);\n        }\n        selection.number_of_selected_rows -= 1;\n    } else {\n        if !selection.exceeded_limit {\n            selection.selected_rows.push(id);\n            selection.selected_rows.sort_unstable();\n        }\n        selection.number_of_selected_rows += 1;\n    }\n}\n\nfn row_select_items_with_shift(selection: &mut SelectionData, model: &ModelRc<SingleMainListModel>, indexes: (usize, usize)) -> Option<ModelRc<SingleMainListModel>> {\n    let (smaller_idx, bigger_idx) = if indexes.0 < indexes.1 { (indexes.0, indexes.1) } else { (indexes.1, indexes.0) };\n\n    if bigger_idx - smaller_idx > SELECTED_ROWS_LIMIT || selection.exceeded_limit {\n        trace!(\"[SLOW][REPLACE_MODEL] selecting from {} items\", model.row_count());\n        // To not iterate twice over the same model, which may be slow, we check if we exceeded limit\n        // This may not be 100% correct, because we may select only 501 items and 500 headers\n        // But gains are bigger than selecting\n        selection.exceeded_limit = bigger_idx - smaller_idx > SELECTED_ROWS_LIMIT;\n        selection.selected_rows.clear();\n        selection.number_of_selected_rows = 0;\n\n        let new_model: Vec<_> = model\n            .iter()\n            .enumerate()\n            .map(|(idx, mut row)| {\n                row.selected_row = !row.header_row && (smaller_idx..=bigger_idx).contains(&idx);\n                if row.selected_row {\n                    selection.number_of_selected_rows += 1;\n                    if !selection.exceeded_limit {\n                        selection.selected_rows.push(idx);\n                    }\n                }\n                row\n            })\n            .collect();\n\n        Some(ModelRc::new(VecModel::from(new_model)))\n    } else {\n        trace!(\n            \"[FAST][ONE_BY_ONE] deselecting {} items, and later selecting, maybe {}/{} items\",\n            selection.selected_rows.len(),\n            bigger_idx - smaller_idx,\n            model.row_count()\n        );\n        // Deselect all previously selected rows, that are not in the range\n        for idx in &selection.selected_rows {\n            if !(smaller_idx..=bigger_idx).contains(idx) {\n                let mut model_data = model\n                    .row_data(*idx)\n                    .unwrap_or_else(|| panic!(\"Failed to get row data with id {idx}, with model {} items\", model.row_count()));\n                assert!(model_data.selected_row); // Probably can be removed in future\n                model_data.selected_row = false;\n                model.set_row_data(*idx, model_data);\n            }\n        }\n\n        // select new rows\n        selection.number_of_selected_rows = 0;\n        selection.selected_rows.clear();\n        selection.exceeded_limit = false;\n\n        for idx in smaller_idx..=bigger_idx {\n            let mut model_data = model\n                .row_data(idx)\n                .unwrap_or_else(|| panic!(\"Failed to get row data with id {idx}, with model {} items\", model.row_count()));\n\n            // Every item in range is selected\n            // We don't set this in if below, because this doesn't take in to account,\n            // already selected items, that we don't deselect in above for loop\n            if !model_data.header_row {\n                selection.selected_rows.push(idx);\n                selection.number_of_selected_rows += 1;\n            }\n\n            if !model_data.selected_row && !model_data.header_row {\n                model_data.selected_row = true;\n                model.set_row_data(idx, model_data);\n            }\n        }\n\n        None\n    }\n}\n\nfn rows_reverse_checked_selection(selection: &SelectionData, model: &ModelRc<SingleMainListModel>) -> (u64, u64, Option<ModelRc<SingleMainListModel>>) {\n    let (mut checked_items, mut unchecked_items) = (0, 0);\n\n    if selection.exceeded_limit {\n        trace!(\"[SLOW][REPLACE_MODEL] reverse checked selection(SPACE)\");\n        let new_model = model\n            .iter()\n            .map(|mut row| {\n                if row.selected_row {\n                    assert!(!row.header_row); // Header row should not be selected\n                    row.checked = !row.checked;\n                    if row.checked {\n                        checked_items += 1;\n                    } else {\n                        unchecked_items += 1;\n                    }\n                }\n                row\n            })\n            .collect::<Vec<_>>();\n        return (checked_items, unchecked_items, Some(ModelRc::new(VecModel::from(new_model))));\n    } else if !selection.selected_rows.is_empty() {\n        trace!(\"[FAST][ONE_BY_ONE] reverse selection(SPACE)\");\n        for id in &selection.selected_rows {\n            let mut model_data = model\n                .row_data(*id)\n                .unwrap_or_else(|| panic!(\"Failed to get row data with id {id}, with model {} items\", model.row_count()));\n            assert!(model_data.selected_row);\n            assert!(!model_data.header_row);\n            model_data.checked = !model_data.checked;\n            if model_data.checked {\n                checked_items += 1;\n            } else {\n                unchecked_items += 1;\n            }\n            model.set_row_data(*id, model_data);\n        }\n    }\n    (checked_items, unchecked_items, None)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::common::create_model_from_model_vec;\n    use crate::test_common::get_model_vec;\n\n    #[test]\n    fn rows_deselect_all_by_mode_with_exceeded_limit() {\n        let mut model = get_model_vec(3);\n        model[0].selected_row = true;\n        model[1].selected_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 2,\n            selected_rows: vec![0, 1],\n            exceeded_limit: true,\n        };\n\n        let new_model = rows_deselect_all_by_mode(&mut selection, &model);\n\n        assert!(new_model.is_some());\n        let new_model = new_model.unwrap();\n        assert!(!new_model.row_data(0).unwrap().selected_row);\n        assert!(!new_model.row_data(1).unwrap().selected_row);\n        assert!(!new_model.row_data(2).unwrap().selected_row);\n        assert!(selection.selected_rows.is_empty());\n        assert!(!selection.exceeded_limit);\n        assert_eq!(selection.number_of_selected_rows, 0);\n    }\n\n    #[test]\n    fn rows_deselect_all_by_mode_with_selected_rows() {\n        let mut model = get_model_vec(3);\n        model[0].selected_row = true;\n        model[1].selected_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 2,\n            selected_rows: vec![0, 1],\n            exceeded_limit: false,\n        };\n\n        let new_model = rows_deselect_all_by_mode(&mut selection, &model);\n\n        assert!(new_model.is_none());\n        assert!(!model.row_data(0).unwrap().selected_row);\n        assert!(!model.row_data(1).unwrap().selected_row);\n        assert!(!model.row_data(2).unwrap().selected_row);\n        assert!(selection.selected_rows.is_empty());\n        assert!(!selection.exceeded_limit);\n        assert_eq!(selection.number_of_selected_rows, 0);\n    }\n\n    #[test]\n    fn rows_deselect_all_by_mode_with_no_selected_rows() {\n        let model = get_model_vec(3);\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 0,\n            selected_rows: Vec::new(),\n            exceeded_limit: false,\n        };\n\n        let new_model = rows_deselect_all_by_mode(&mut selection, &model);\n\n        assert!(new_model.is_none());\n        assert!(!model.row_data(0).unwrap().selected_row);\n        assert!(!model.row_data(1).unwrap().selected_row);\n        assert!(!model.row_data(2).unwrap().selected_row);\n        assert!(selection.selected_rows.is_empty());\n        assert!(!selection.exceeded_limit);\n        assert_eq!(selection.number_of_selected_rows, 0);\n    }\n\n    #[test]\n    fn rows_select_all_by_mode_with_few_selected_rows() {\n        let mut model = get_model_vec(3);\n        model[0].selected_row = true;\n\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 1,\n            selected_rows: vec![0],\n            exceeded_limit: false,\n        };\n\n        let new_model = rows_select_all_by_mode(&mut selection, &model);\n\n        assert!(new_model.is_none());\n        assert!(model.row_data(0).unwrap().selected_row);\n        assert!(model.row_data(1).unwrap().selected_row);\n        assert!(model.row_data(2).unwrap().selected_row);\n        assert_eq!(selection.selected_rows, vec![0, 1, 2]);\n        assert!(!selection.exceeded_limit);\n        assert_eq!(selection.number_of_selected_rows, 3);\n    }\n\n    #[test]\n    fn rows_select_all_by_mode_with_header_rows() {\n        let mut model = get_model_vec(5);\n        model[0].header_row = true;\n        model[3].header_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 0,\n            selected_rows: Vec::new(),\n            exceeded_limit: false,\n        };\n\n        let new_model = rows_select_all_by_mode(&mut selection, &model);\n\n        assert!(new_model.is_none());\n        assert!(!model.row_data(0).unwrap().selected_row); // header row\n        assert!(model.row_data(1).unwrap().selected_row);\n        assert!(model.row_data(2).unwrap().selected_row);\n        assert!(!model.row_data(3).unwrap().selected_row); // header row\n        assert!(model.row_data(4).unwrap().selected_row);\n        assert_eq!(selection.selected_rows, vec![1, 2, 4]);\n        assert!(!selection.exceeded_limit);\n        assert_eq!(selection.number_of_selected_rows, 3);\n    }\n\n    #[test]\n    fn rows_select_all_by_mode_with_exceeded_limit() {\n        let mut model = get_model_vec(500);\n        model[11].header_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 0,\n            selected_rows: Vec::new(),\n            exceeded_limit: true,\n        };\n\n        let new_model = rows_select_all_by_mode(&mut selection, &model);\n\n        assert!(new_model.is_some());\n        let new_model = new_model.unwrap();\n        for idx in 0..new_model.row_count() {\n            if idx == 11 {\n                assert!(!new_model.row_data(idx).unwrap().selected_row, \"idx: {idx}\");\n            } else {\n                assert!(new_model.row_data(idx).unwrap().selected_row, \"idx: {idx}\");\n            }\n        }\n\n        assert!(selection.selected_rows.is_empty());\n        assert!(selection.exceeded_limit);\n        assert_eq!(selection.number_of_selected_rows, 499);\n    }\n\n    #[test]\n    fn reverse_selection_of_item_with_id_select_item() {\n        let model = get_model_vec(3);\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 0,\n            selected_rows: Vec::new(),\n            exceeded_limit: false,\n        };\n\n        reverse_selection_of_item_with_id(&mut selection, &model, 1);\n\n        assert!(!model.row_data(0).unwrap().selected_row);\n        assert!(model.row_data(1).unwrap().selected_row);\n        assert!(!model.row_data(2).unwrap().selected_row);\n        assert_eq!(selection.selected_rows, vec![1]);\n        assert_eq!(selection.number_of_selected_rows, 1);\n    }\n\n    #[test]\n    fn reverse_selection_of_item_with_id_deselect_item() {\n        let mut model = get_model_vec(3);\n        model[1].header_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 1,\n            selected_rows: vec![2],\n            exceeded_limit: false,\n        };\n\n        reverse_selection_of_item_with_id(&mut selection, &model, 1);\n\n        assert!(!model.row_data(1).unwrap().selected_row);\n        assert_eq!(selection.selected_rows, vec![2]);\n        assert_eq!(selection.number_of_selected_rows, 1);\n    }\n    #[test]\n    fn row_select_items_with_shift_simple() {\n        let model = get_model_vec(5);\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 0,\n            selected_rows: Vec::new(),\n            exceeded_limit: false,\n        };\n\n        let new_model = row_select_items_with_shift(&mut selection, &model, (1, 3));\n\n        assert!(new_model.is_none());\n        assert!(!model.row_data(0).unwrap().selected_row);\n        assert!(model.row_data(1).unwrap().selected_row);\n        assert!(model.row_data(2).unwrap().selected_row);\n        assert!(model.row_data(3).unwrap().selected_row);\n        assert!(!model.row_data(4).unwrap().selected_row);\n        assert_eq!(selection.selected_rows, vec![1, 2, 3]);\n        assert_eq!(selection.number_of_selected_rows, 3);\n    }\n\n    #[test]\n    fn row_select_items_with_shift_with_header_rows() {\n        let mut model = get_model_vec(5);\n        model[1].header_row = true;\n        model[3].header_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let mut selection = SelectionData {\n            number_of_selected_rows: 0,\n            selected_rows: Vec::new(),\n            exceeded_limit: false,\n        };\n\n        let new_model = row_select_items_with_shift(&mut selection, &model, (0, 4));\n\n        assert!(new_model.is_none());\n        assert!(model.row_data(0).unwrap().selected_row);\n        assert!(!model.row_data(1).unwrap().selected_row); // header row\n        assert!(model.row_data(2).unwrap().selected_row);\n        assert!(!model.row_data(3).unwrap().selected_row); // header row\n        assert!(model.row_data(4).unwrap().selected_row);\n        assert_eq!(selection.selected_rows, vec![0, 2, 4]);\n        assert_eq!(selection.number_of_selected_rows, 3);\n    }\n\n    #[test]\n    fn rows_reverse_checked_selection_with_selected_rows() {\n        let mut model = get_model_vec(4);\n        model[0].selected_row = true;\n        model[1].selected_row = true;\n        model[2].selected_row = true;\n        model[2].checked = true;\n        let model = create_model_from_model_vec(&model);\n\n        let selection = SelectionData {\n            number_of_selected_rows: 3,\n            selected_rows: vec![0, 1, 2],\n            exceeded_limit: false,\n        };\n\n        let (checked_items, unchecked_items, new_model) = rows_reverse_checked_selection(&selection, &model);\n\n        assert!(new_model.is_none());\n        assert!(model.row_data(0).unwrap().checked);\n        assert!(model.row_data(1).unwrap().checked);\n        assert!(!model.row_data(2).unwrap().checked);\n        assert_eq!(checked_items, 2);\n        assert_eq!(unchecked_items, 1);\n    }\n\n    #[test]\n    fn rows_reverse_checked_selection_with_exceeded_limit() {\n        let mut model = get_model_vec(3);\n        model[0].selected_row = true;\n        model[1].selected_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let selection = SelectionData {\n            number_of_selected_rows: 2,\n            selected_rows: Vec::new(),\n            exceeded_limit: true,\n        };\n\n        let (checked_items, unchecked_items, new_model) = rows_reverse_checked_selection(&selection, &model);\n\n        assert!(new_model.is_some());\n        let new_model = new_model.unwrap();\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(new_model.row_data(1).unwrap().checked);\n        assert!(!new_model.row_data(2).unwrap().checked);\n        assert_eq!(checked_items, 2);\n        assert_eq!(unchecked_items, 0);\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_save.rs",
    "content": "use std::sync::{Arc, Mutex};\n\nuse rfd::FileDialog;\nuse slint::ComponentHandle;\n\nuse crate::connect_rfd::{hide_file_dialog_overlay, show_file_dialog_overlay};\nuse crate::shared_models::SharedModels;\nuse crate::{Callabler, GuiState, MainWindow};\n\npub(crate) fn connect_save(app: &MainWindow, shared_models: Arc<Mutex<SharedModels>>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_save_results(move || {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        show_file_dialog_overlay(&app);\n\n        let weak = a.clone();\n        let shared_models = Arc::clone(&shared_models);\n        std::thread::spawn(move || {\n            let folder = FileDialog::new().pick_folder();\n\n            hide_file_dialog_overlay(&weak);\n\n            if let Some(folder) = folder {\n                let folder_str = folder.to_string_lossy().to_string();\n                weak.upgrade_in_event_loop(move |app| {\n                    if let Err(e) = shared_models.lock().unwrap().save_results(active_tab, &folder_str) {\n                        app.global::<GuiState>().set_info_text(e.into());\n                    }\n                })\n                .expect(\"Failed to save results\");\n            }\n        });\n    });\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/bad_extensions.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::bad_extensions;\nuse czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsParameters, BadFileEntry};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_BAD_EXTENSIONS, MAX_STR_DATA_BAD_EXTENSIONS, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_bad_extensions(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = BadExtensionsParameters::new();\n            let mut tool = BadExtensions::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_bad_extensions_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_bad_extensions_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_bad_extensions_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\n\nfn write_bad_extensions_results(app: &MainWindow, vector: Vec<BadFileEntry>, messages_data: MessagesData, info: bad_extensions::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_files_with_bad_extension;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_bad_extensions(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_bad_extensions_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_bad_extensions\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::BadExtensions);\n}\n\nfn prepare_data_model_bad_extensions(fe: BadFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_BAD_EXTENSIONS] = [\n        file.into(),\n        directory.into(),\n        fe.current_extension.into(),\n        fe.proper_extensions_group.into(),\n        fe.proper_extension.into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let data_model_int_arr: [i32; MAX_INT_DATA_BAD_EXTENSIONS] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/bad_names.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::bad_names;\nuse czkawka_core::tools::bad_names::{BadNameEntry, BadNames, BadNamesParameters};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_BAD_NAMES, MAX_STR_DATA_BAD_NAMES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_bad_names(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let checked_issues = czkawka_core::tools::bad_names::NameIssues {\n                uppercase_extension: sd.custom_settings.bad_names_sub_uppercase_extension,\n                emoji_used: sd.custom_settings.bad_names_sub_emoji_used,\n                space_at_start_or_end: sd.custom_settings.bad_names_sub_space_at_start_end,\n                non_ascii_graphical: sd.custom_settings.bad_names_sub_non_ascii,\n                restricted_charset_allowed: if sd.custom_settings.bad_names_sub_restricted_charset_enabled {\n                    Some(sd.custom_settings.bad_names_sub_restricted_charset.clone())\n                } else {\n                    None\n                },\n                remove_duplicated_non_alphanumeric: sd.custom_settings.bad_names_sub_remove_duplicated,\n            };\n            let params = BadNamesParameters::new(checked_issues);\n            let mut tool = BadNames::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_bad_names_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_bad_names_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_bad_names_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_bad_names_results(app: &MainWindow, vector: Vec<BadNameEntry>, messages_data: MessagesData, info: bad_names::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_files_with_bad_names;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_bad_names(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_bad_names_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_bad_names\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::BadNames);\n}\n\nfn prepare_data_model_bad_names(fe: BadNameEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_BAD_NAMES] = [file.into(), fe.new_name.into(), directory.into()];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_BAD_NAMES] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/big_files.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::model::FileEntry;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path};\nuse czkawka_core::tools::big_file;\nuse czkawka_core::tools::big_file::{BigFile, BigFileParameters, SearchMode};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_BIG_FILES, MAX_STR_DATA_BIG_FILES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_big_files(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let big_files_mode = sd.combo_box_items.biggest_files_method.value;\n            let params = BigFileParameters::new(sd.custom_settings.biggest_files_sub_number_of_files as usize, big_files_mode);\n            let mut tool = BigFile::new(params);\n\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_big_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            if big_files_mode == SearchMode::BiggestFiles {\n                vector.par_sort_unstable_by_key(|fe| u64::MAX - fe.size);\n            } else {\n                vector.par_sort_unstable_by_key(|fe| fe.size);\n            }\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            let files_size = tool.get_big_files().iter().map(|f| f.size).sum::<u64>();\n            sd.shared_models.lock().unwrap().shared_big_files_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_big_files_results(&app, vector, messages_data, info, sd, stopped_search, files_size);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_big_files_results(app: &MainWindow, vector: Vec<FileEntry>, messages_data: MessagesData, info: big_file::Info, sd: ScanData, stopped_search: bool, files_size: u64) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_real_files;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_big_files(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_big_files_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(\n            flk!(\n                \"rust_found_big_files\",\n                items_found = items_found,\n                time = scanning_time_str,\n                size = format_size(files_size, BINARY)\n            )\n            .into(),\n        );\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::BigFiles);\n}\n\nfn prepare_data_model_big_files(fe: FileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_BIG_FILES] = [\n        format_size(fe.size, BINARY).into(),\n        file.into(),\n        directory.into(),\n        get_dt_timestamp_string(fe.modified_date).into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_BIG_FILES] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/broken_files.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::broken_files;\nuse czkawka_core::tools::broken_files::{BrokenEntry, BrokenFiles, BrokenFilesParameters, CheckedTypes};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_BROKEN_FILES, MAX_STR_DATA_BROKEN_FILES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_broken_files(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut checked_types: CheckedTypes = CheckedTypes::NONE;\n            if sd.custom_settings.broken_files_sub_audio {\n                checked_types |= CheckedTypes::AUDIO;\n            }\n            if sd.custom_settings.broken_files_sub_pdf {\n                checked_types |= CheckedTypes::PDF;\n            }\n            if sd.custom_settings.broken_files_sub_image {\n                checked_types |= CheckedTypes::IMAGE;\n            }\n            if sd.custom_settings.broken_files_sub_archive {\n                checked_types |= CheckedTypes::ARCHIVE;\n            }\n            if sd.custom_settings.broken_files_sub_video {\n                checked_types |= CheckedTypes::VIDEO;\n            }\n\n            if checked_types == CheckedTypes::NONE {\n                a.upgrade_in_event_loop(move |app| {\n                    app.invoke_scan_ended(flk!(\"rust_no_file_type_selected\").into());\n                })\n                .expect(\"Cannot upgrade in event loop :(\");\n                return Ok(());\n            }\n\n            let params = BrokenFilesParameters::new(checked_types);\n            let mut tool = BrokenFiles::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_broken_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            let size = tool.get_broken_files().iter().map(|e| e.size).sum::<u64>();\n            sd.shared_models.lock().unwrap().shared_broken_files_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_broken_files_results(&app, vector, messages_data, info, sd, stopped_search, size);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_broken_files_results(app: &MainWindow, vector: Vec<BrokenEntry>, messages_data: MessagesData, info: broken_files::Info, sd: ScanData, stopped_search: bool, size: u64) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_broken_files;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_broken_files(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_broken_files_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(\n            flk!(\n                \"rust_found_broken_files\",\n                items_found = items_found,\n                time = scanning_time_str,\n                size = format_size(size, BINARY)\n            )\n            .into(),\n        );\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::BrokenFiles);\n}\n\nfn prepare_data_model_broken_files(fe: BrokenEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_BROKEN_FILES] = [\n        file.into(),\n        directory.into(),\n        fe.error_string.into(),\n        format_size(fe.size, BINARY).into(),\n        get_dt_timestamp_string(fe.modified_date).into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_BROKEN_FILES] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/duplicate.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::model::CheckingMethod;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::duplicate;\nuse czkawka_core::tools::duplicate::{DuplicateEntry, DuplicateFinder, DuplicateFinderParameters};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_DUPLICATE_FILES, MAX_STR_DATA_DUPLICATE_FILES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_duplicates(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let hash_type = sd.combo_box_items.duplicates_hash_type.value;\n            let check_method = sd.combo_box_items.duplicates_check_method.value;\n\n            let params = DuplicateFinderParameters::new(\n                check_method,\n                hash_type,\n                sd.custom_settings.duplicate_use_prehash,\n                sd.custom_settings.duplicate_minimal_hash_cache_size as u64,\n                sd.custom_settings.duplicate_minimal_prehash_cache_size as u64,\n                sd.custom_settings.duplicates_sub_name_case_sensitive,\n            );\n            let mut tool = DuplicateFinder::new(params);\n\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            let mut vector;\n            if tool.get_use_reference() {\n                match tool.get_params().check_method {\n                    CheckingMethod::Hash => {\n                        vector = tool\n                            .get_files_with_identical_hashes_referenced()\n                            .values()\n                            .flatten()\n                            .cloned()\n                            .map(|(original, other)| (Some(original), other))\n                            .collect::<Vec<_>>();\n                    }\n                    CheckingMethod::Name | CheckingMethod::Size | CheckingMethod::SizeName => {\n                        let values: Vec<_> = match tool.get_params().check_method {\n                            CheckingMethod::Name => tool.get_files_with_identical_name_referenced().values().cloned().collect(),\n                            CheckingMethod::Size => tool.get_files_with_identical_size_referenced().values().cloned().collect(),\n                            CheckingMethod::SizeName => tool.get_files_with_identical_size_names_referenced().values().cloned().collect(),\n                            _ => unreachable!(\"Invalid check method.\"),\n                        };\n                        vector = values.into_iter().map(|(original, other)| (Some(original), other)).collect::<Vec<_>>();\n                    }\n                    _ => unreachable!(\"Invalid check method.\"),\n                }\n            } else {\n                match tool.get_params().check_method {\n                    CheckingMethod::Hash => {\n                        vector = tool.get_files_sorted_by_hash().values().flatten().cloned().map(|items| (None, items)).collect::<Vec<_>>();\n                    }\n                    CheckingMethod::Name | CheckingMethod::Size | CheckingMethod::SizeName => {\n                        let values: Vec<_> = match tool.get_params().check_method {\n                            CheckingMethod::Name => tool.get_files_sorted_by_names().values().cloned().collect(),\n                            CheckingMethod::Size => tool.get_files_sorted_by_size().values().cloned().collect(),\n                            CheckingMethod::SizeName => tool.get_files_sorted_by_size_name().values().cloned().collect(),\n                            _ => unreachable!(\"Invalid check method.\"),\n                        };\n                        vector = values.into_iter().map(|items| (None, items)).collect::<Vec<_>>();\n                    }\n                    _ => unreachable!(\"Invalid check method.\"),\n                }\n            }\n\n            for (_first, vec) in &mut vector {\n                vec.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n            }\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            let (duplicates_number, groups_number, lost_space) = match tool.get_check_method() {\n                CheckingMethod::Hash => (info.number_of_duplicated_files_by_hash, info.number_of_groups_by_hash, info.lost_space_by_hash),\n                CheckingMethod::Name => (info.number_of_duplicated_files_by_name, info.number_of_groups_by_name, 0),\n                CheckingMethod::Size => (info.number_of_duplicated_files_by_size, info.number_of_groups_by_size, info.lost_space_by_size),\n                CheckingMethod::SizeName => (info.number_of_duplicated_files_by_size_name, info.number_of_groups_by_size_name, info.lost_space_by_size),\n                _ => unreachable!(\"invalid check method {:?}\", tool.get_check_method()),\n            };\n            sd.shared_models.lock().unwrap().shared_duplication_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_duplicate_results(&app, vector, messages_data, info, sd, stopped_search, duplicates_number, groups_number, lost_space);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_duplicate_results(\n    app: &MainWindow,\n    vector: Vec<(Option<DuplicateEntry>, Vec<DuplicateEntry>)>,\n    messages_data: MessagesData,\n    info: duplicate::Info,\n    sd: ScanData,\n    stopped_search: bool,\n    items_found: usize,\n    groups: usize,\n    lost_space: u64,\n) {\n    let scanning_time_str = format_time(info.scanning_time);\n\n    let items = Rc::new(VecModel::default());\n    for (ref_fe, vec_fe) in vector.into_iter().rev() {\n        if let Some(ref_fe) = ref_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_duplicates(ref_fe);\n            insert_data_to_model(&items, data_model_str, data_model_int, Some(true));\n        } else {\n            insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false));\n        }\n\n        for fe in vec_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_duplicates(fe);\n            insert_data_to_model(&items, data_model_str, data_model_int, None);\n        }\n    }\n    app.set_duplicate_files_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        if lost_space > 0 {\n            app.invoke_scan_ended(\n                flk!(\n                    \"rust_found_duplicate_files\",\n                    items_found = items_found,\n                    groups = groups,\n                    size = format_size(lost_space, BINARY),\n                    time = scanning_time_str\n                )\n                .into(),\n            );\n        } else {\n            app.invoke_scan_ended(\n                flk!(\n                    \"rust_found_duplicate_files_no_lost_space\",\n                    items_found = items_found,\n                    groups = groups,\n                    time = scanning_time_str\n                )\n                .into(),\n            );\n        }\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::DuplicateFiles);\n}\nfn prepare_data_model_duplicates(fe: DuplicateEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(fe.get_path());\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_DUPLICATE_FILES] = [\n        format_size(fe.size, BINARY).into(),\n        file.into(),\n        directory.into(),\n        get_dt_timestamp_string(fe.get_modified_date()).into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_DUPLICATE_FILES] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/empty_files.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::model::FileEntry;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::empty_files;\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_EMPTY_FILES, MAX_STR_DATA_EMPTY_FILES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_empty_files(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = EmptyFiles::new();\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_empty_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_empty_files_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_empty_files_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_empty_files_results(app: &MainWindow, vector: Vec<FileEntry>, messages_data: MessagesData, info: empty_files::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_empty_files;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_empty_files(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_empty_files_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_empty_files\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::EmptyFiles);\n}\n\nfn prepare_data_model_empty_files(fe: FileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(fe.get_path());\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_EMPTY_FILES] = [file.into(), directory.into(), get_dt_timestamp_string(fe.get_modified_date()).into()];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_EMPTY_FILES] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/empty_folders.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::empty_folder;\nuse czkawka_core::tools::empty_folder::{EmptyFolder, FolderEntry};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_EMPTY_FOLDERS, MAX_STR_DATA_EMPTY_FOLDERS, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_empty_folders(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = EmptyFolder::new();\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_empty_folder_list().values().cloned().collect::<Vec<_>>();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_empty_folders_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_empty_folders_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_empty_folders_results(app: &MainWindow, vector: Vec<FolderEntry>, messages_data: MessagesData, info: empty_folder::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_empty_folders;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_empty_folders(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_empty_folder_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_empty_folders\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::EmptyFolders);\n}\n\nfn prepare_data_model_empty_folders(fe: FolderEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_EMPTY_FOLDERS] = [file.into(), directory.into(), get_dt_timestamp_string(fe.modified_date).into()];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let data_model_int_arr: [i32; MAX_INT_DATA_EMPTY_FOLDERS] = [modification_split.0, modification_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/exif_remover.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path};\nuse czkawka_core::tools::exif_remover;\nuse czkawka_core::tools::exif_remover::{ExifEntry, ExifRemover, ExifRemoverParameters};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_EXIF_REMOVER, MAX_STR_DATA_EXIF_REMOVER, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_exif_remover(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            // Parse ignored tags from comma-separated string, trimming whitespace\n            let ignored_tags: Vec<String> = sd\n                .custom_settings\n                .ignored_exif_tags\n                .split(',')\n                .map(|s| s.trim().to_string())\n                .filter(|s| !s.is_empty())\n                .collect();\n\n            let params = ExifRemoverParameters::new(ignored_tags);\n            let mut tool = ExifRemover::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_exif_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| b.exif_tags.len().cmp(&a.exif_tags.len()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_exif_remover_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_exif_remover_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_exif_remover_results(app: &MainWindow, vector: Vec<ExifEntry>, messages_data: MessagesData, info: exif_remover::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_files_with_exif;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_exif_remover(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_exif_remover_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_exif_files\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::ExifRemover);\n}\n\nfn prepare_data_model_exif_remover(fe: ExifEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(&fe.path);\n    let size_str = format_size(fe.size, BINARY);\n    let exif_tags = format!(\n        \"{} ({})\",\n        fe.exif_tags.len(),\n        fe.exif_tags.iter().map(|item_tag| item_tag.name.clone()).collect::<Vec<String>>().join(\", \")\n    );\n    let exif_groups_name = fe.exif_tags.iter().map(|item_tag| item_tag.group.clone()).collect::<Vec<String>>().join(\",\");\n    let exif_tag_u16 = fe.exif_tags.iter().map(|item_tag| item_tag.code.to_string()).collect::<Vec<String>>().join(\",\");\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_EXIF_REMOVER] = [\n        size_str.into(),\n        file.into(),\n        directory.into(),\n        exif_tags.into(),\n        get_dt_timestamp_string(fe.get_modified_date()).into(),\n        exif_groups_name.into(),\n        exif_tag_u16.into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_EXIF_REMOVER] = [modification_split.0, modification_split.1, size_split.0, size_split.1, fe.exif_tags.len() as i32];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/invalid_symlinks.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::invalid_symlinks;\nuse czkawka_core::tools::invalid_symlinks::{InvalidSymlinks, SymlinksFileEntry};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_INVALID_SYMLINKS, MAX_STR_DATA_INVALID_SYMLINKS, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_invalid_symlinks(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = InvalidSymlinks::new();\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_invalid_symlinks().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_same_invalid_symlinks = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_invalid_symlinks_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_invalid_symlinks_results(app: &MainWindow, vector: Vec<SymlinksFileEntry>, messages_data: MessagesData, info: invalid_symlinks::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_invalid_symlinks;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_invalid_symlinks(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_invalid_symlinks_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_invalid_symlinks\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::InvalidSymlinks);\n}\n\nfn prepare_data_model_invalid_symlinks(fe: SymlinksFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(fe.get_path());\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_INVALID_SYMLINKS] = [\n        file.into(),\n        directory.into(),\n        fe.symlink_info.destination_path.to_string_lossy().to_string().into(),\n        fe.symlink_info.type_of_error.to_string().into(),\n        get_dt_timestamp_string(fe.get_modified_date()).into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let data_model_int_arr: [i32; MAX_INT_DATA_INVALID_SYMLINKS] = [modification_split.0, modification_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/same_music.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path};\nuse czkawka_core::tools::same_music;\nuse czkawka_core::tools::same_music::core::format_audio_duration;\nuse czkawka_core::tools::same_music::{MusicEntry, MusicSimilarity, SameMusic, SameMusicParameters};\nuse humansize::{BINARY, format_size};\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_SIMILAR_MUSIC, MAX_STR_DATA_SIMILAR_MUSIC, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_similar_music(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut music_similarity: MusicSimilarity = MusicSimilarity::NONE;\n            if sd.custom_settings.similar_music_sub_title {\n                music_similarity |= MusicSimilarity::TRACK_TITLE;\n            }\n            if sd.custom_settings.similar_music_sub_artist {\n                music_similarity |= MusicSimilarity::TRACK_ARTIST;\n            }\n            if sd.custom_settings.similar_music_sub_bitrate {\n                music_similarity |= MusicSimilarity::BITRATE;\n            }\n            if sd.custom_settings.similar_music_sub_length {\n                music_similarity |= MusicSimilarity::LENGTH;\n            }\n            if sd.custom_settings.similar_music_sub_year {\n                music_similarity |= MusicSimilarity::YEAR;\n            }\n            if sd.custom_settings.similar_music_sub_genre {\n                music_similarity |= MusicSimilarity::GENRE;\n            }\n\n            let params = SameMusicParameters::new(\n                music_similarity,\n                sd.custom_settings.similar_music_sub_approximate_comparison,\n                sd.combo_box_items.audio_check_type.value,\n                sd.custom_settings.similar_music_sub_minimal_fragment_duration_value,\n                sd.custom_settings.similar_music_sub_maximum_difference_value as f64,\n                sd.custom_settings.similar_music_compare_fingerprints_only_with_similar_titles,\n            );\n            let mut tool = SameMusic::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            let mut vector: Vec<_> = if tool.get_use_reference() {\n                tool.get_similar_music_referenced()\n                    .iter()\n                    .cloned()\n                    .map(|(original, others)| (Some(original), others))\n                    .collect()\n            } else {\n                tool.get_duplicated_music_entries().iter().cloned().map(|items| (None, items)).collect()\n            };\n\n            vector.sort_by_cached_key(|(_, a)| u64::MAX - a.iter().map(|e| e.size).sum::<u64>());\n            for (_first_entry, vec_fe) in &mut vector {\n                vec_fe.sort_unstable_by_key(|a| u64::MAX - a.size);\n            }\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            let items_found = info.number_of_duplicates;\n            let groups = info.number_of_groups;\n            sd.shared_models.lock().unwrap().shared_same_music_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_similar_music_results(&app, vector, messages_data, info, sd, stopped_search, items_found, groups);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_similar_music_results(\n    app: &MainWindow,\n    vector: Vec<(Option<MusicEntry>, Vec<MusicEntry>)>,\n    messages_data: MessagesData,\n    info: same_music::Info,\n    sd: ScanData,\n    stopped_search: bool,\n    items_found: usize,\n    groups: usize,\n) {\n    let scanning_time_str = format_time(info.scanning_time);\n\n    let items = Rc::new(VecModel::default());\n    for (ref_fe, vec_fe) in vector {\n        if let Some(ref_fe) = ref_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_similar_music(ref_fe);\n            insert_data_to_model(&items, data_model_str, data_model_int, Some(true));\n        } else {\n            insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false));\n        }\n\n        for fe in vec_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_similar_music(fe);\n            insert_data_to_model(&items, data_model_str, data_model_int, None);\n        }\n    }\n    app.set_similar_music_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_similar_music_files\", items_found = items_found, groups = groups, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::SimilarMusic);\n}\nfn prepare_data_model_similar_music(fe: MusicEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(fe.get_path());\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_SIMILAR_MUSIC] = [\n        format_size(fe.size, BINARY).into(),\n        file.into(),\n        fe.track_title.clone().into(),\n        fe.track_artist.clone().into(),\n        fe.year.clone().into(),\n        fe.bitrate.to_string().into(),\n        format_audio_duration(fe.length).into(),\n        fe.genre.clone().into(),\n        directory.into(),\n        get_dt_timestamp_string(fe.get_modified_date()).into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_SIMILAR_MUSIC] = [modification_split.0, modification_split.1, size_split.0, size_split.1, fe.bitrate as i32, fe.length as i32];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/similar_images.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path};\nuse czkawka_core::tools::similar_images;\nuse czkawka_core::tools::similar_images::core::get_string_from_similarity;\nuse czkawka_core::tools::similar_images::{ImagesEntry, SimilarImages, SimilarImagesParameters};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_SIMILAR_IMAGES, MAX_STR_DATA_SIMILAR_IMAGES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_similar_images(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let hash_alg = sd.combo_box_items.image_hash_alg.value;\n            let resize_algorithm = sd.combo_box_items.resize_algorithm.value;\n            let hash_size = sd\n                .custom_settings\n                .similar_images_sub_hash_size\n                .parse()\n                .unwrap_or_else(|_| panic!(\"Cannot parse hash size {}\", sd.custom_settings.similar_images_sub_hash_size));\n\n            let params = SimilarImagesParameters::new(\n                sd.custom_settings.similar_images_sub_similarity as u32,\n                hash_size,\n                hash_alg,\n                resize_algorithm,\n                sd.custom_settings.similar_images_sub_ignore_same_size,\n            );\n            let mut tool = SimilarImages::new(params);\n\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            let mut vector: Vec<_> = if tool.get_use_reference() {\n                tool.get_similar_images_referenced()\n                    .iter()\n                    .cloned()\n                    .map(|(original, others)| (Some(original), others))\n                    .collect()\n            } else {\n                tool.get_similar_images().iter().cloned().map(|items| (None, items)).collect()\n            };\n\n            for (_first_entry, vec_fe) in &mut vector {\n                vec_fe.par_sort_unstable_by_key(|e| (e.difference, u64::MAX - e.size));\n            }\n            vector.sort_by_key(|(_header, vc)| u64::MAX - vc.iter().map(|e| e.size).sum::<u64>()); // Also sorts by size, to show the biggest groups first\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            let items_found = info.number_of_duplicates;\n            let groups = info.number_of_groups;\n            sd.shared_models.lock().unwrap().shared_similar_images_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_similar_images_results(&app, vector, messages_data, info, sd, stopped_search, hash_size, items_found, groups);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_similar_images_results(\n    app: &MainWindow,\n    vector: Vec<(Option<ImagesEntry>, Vec<ImagesEntry>)>,\n    messages_data: MessagesData,\n    info: similar_images::Info,\n    sd: ScanData,\n    stopped_search: bool,\n    hash_size: u8,\n    items_found: usize,\n    groups: usize,\n) {\n    let scanning_time_str = format_time(info.scanning_time);\n\n    let items = Rc::new(VecModel::default());\n    for (ref_fe, vec_fe) in vector {\n        if let Some(ref_fe) = ref_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_similar_images(ref_fe, hash_size);\n            insert_data_to_model(&items, data_model_str, data_model_int, Some(true));\n        } else {\n            insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false));\n        }\n\n        for fe in vec_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_similar_images(fe, hash_size);\n            insert_data_to_model(&items, data_model_str, data_model_int, None);\n        }\n    }\n    app.set_similar_images_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_similar_images\", items_found = items_found, groups = groups, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::SimilarImages);\n}\nfn prepare_data_model_similar_images(fe: ImagesEntry, hash_size: u8) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(fe.get_path());\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_SIMILAR_IMAGES] = [\n        get_string_from_similarity(fe.difference, hash_size).into(),\n        format_size(fe.size, BINARY).into(),\n        format!(\"{}x{}\", fe.width, fe.height).into(),\n        file.into(),\n        directory.into(),\n        get_dt_timestamp_string(fe.get_modified_date()).into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_SIMILAR_IMAGES] = [\n        modification_split.0,\n        modification_split.1,\n        size_split.0,\n        size_split.1,\n        fe.width as i32,\n        fe.height as i32,\n        (fe.width as u64 * fe.height as u64) as i32, // Limited to 2000MP, but using u64, because in cache it can exceed i32\n        fe.difference as i32,\n    ];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/similar_videos.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::similar_videos;\nuse czkawka_core::tools::similar_videos::core::{format_bitrate_opt, format_duration_opt};\nuse czkawka_core::tools::similar_videos::{SimilarVideos, SimilarVideosParameters, VideosEntry};\nuse humansize::{BINARY, format_size};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_SIMILAR_VIDEOS, MAX_STR_DATA_SIMILAR_VIDEOS, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_similar_videos(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let params = SimilarVideosParameters::new(\n                sd.custom_settings.similar_videos_sub_similarity,\n                sd.custom_settings.similar_videos_sub_ignore_same_size,\n                sd.custom_settings.similar_videos_skip_forward_amount,\n                sd.custom_settings.similar_videos_vid_hash_duration,\n                sd.combo_box_items.videos_crop_detect.value,\n                sd.custom_settings.video_thumbnails_generate,\n                sd.custom_settings.video_thumbnails_percentage,\n                sd.custom_settings.video_thumbnails_generate_grid,\n                sd.custom_settings.video_thumbnails_grid_tiles_per_side,\n            );\n            let mut tool = SimilarVideos::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            let mut vector: Vec<_> = if tool.get_use_reference() {\n                tool.get_similar_videos_referenced()\n                    .iter()\n                    .cloned()\n                    .map(|(original, others)| (Some(original), others))\n                    .collect()\n            } else {\n                tool.get_similar_videos().iter().cloned().map(|items| (None, items)).collect()\n            };\n            for (_first_entry, vec_fe) in &mut vector {\n                vec_fe.par_sort_unstable_by(|a, b| match a.size.cmp(&b.size) {\n                    std::cmp::Ordering::Equal => split_path_compare(a.path.as_path(), b.path.as_path()),\n                    std::cmp::Ordering::Less => std::cmp::Ordering::Greater,\n                    std::cmp::Ordering::Greater => std::cmp::Ordering::Less,\n                });\n            }\n            vector.sort_by_key(|(_header, vc)| u64::MAX - vc.iter().map(|e| e.size).sum::<u64>()); // Also sorts by size, to show the biggest groups first\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            let items_found = info.number_of_duplicates;\n            let groups = info.number_of_groups;\n            sd.shared_models.lock().unwrap().shared_similar_videos_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_similar_videos_results(&app, vector, messages_data, info, sd, stopped_search, items_found, groups);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_similar_videos_results(\n    app: &MainWindow,\n    vector: Vec<(Option<VideosEntry>, Vec<VideosEntry>)>,\n    messages_data: MessagesData,\n    info: similar_videos::Info,\n    sd: ScanData,\n    stopped_search: bool,\n    items_found: usize,\n    groups: usize,\n) {\n    let scanning_time_str = format_time(info.scanning_time);\n\n    let items = Rc::new(VecModel::default());\n    for (ref_fe, vec_fe) in vector {\n        if let Some(ref_fe) = ref_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_similar_videos(ref_fe);\n            insert_data_to_model(&items, data_model_str, data_model_int, Some(true));\n        } else {\n            insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false));\n        }\n\n        for fe in vec_fe {\n            let (data_model_str, data_model_int) = prepare_data_model_similar_videos(fe);\n            insert_data_to_model(&items, data_model_str, data_model_int, None);\n        }\n    }\n    app.set_similar_videos_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_similar_videos\", items_found = items_found, groups = groups, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::SimilarVideos);\n}\nfn prepare_data_model_similar_videos(fe: VideosEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(fe.get_path());\n    let bitrate = format_bitrate_opt(fe.bitrate);\n    let fps = fe.fps.map(|e| format!(\"{e:.2}\")).unwrap_or_default();\n    let codec = fe.codec.clone().unwrap_or_default();\n    let dimensions = if let (Some(w), Some(h)) = (fe.width, fe.height) {\n        format!(\"{w}x{h}\")\n    } else {\n        \"\".to_string()\n    };\n    let preview_path = fe.thumbnail_path.as_ref().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();\n    let duration = format_duration_opt(fe.duration);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_SIMILAR_VIDEOS] = [\n        format_size(fe.size, BINARY).into(),\n        file.into(),\n        directory.into(),\n        dimensions.into(),\n        duration.into(),\n        bitrate.into(),\n        fps.into(),\n        codec.into(),\n        get_dt_timestamp_string(fe.get_modified_date()).into(),\n        preview_path.into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let bitrate_split = split_u64_into_i32s(fe.bitrate.unwrap_or(0));\n    let duration_i32 = fe.duration.map_or(0, |d| (d * 100.0) as i32);\n    let fps_i32 = fe.fps.map_or(0, |f| (f * 100.0) as i32);\n    let dimension = fe.width.and_then(|w| fe.height.map(|h| w as i32 * h as i32)).unwrap_or_default();\n    let data_model_int_arr: [i32; MAX_INT_DATA_SIMILAR_VIDEOS] = [\n        modification_split.0,\n        modification_split.1,\n        size_split.0,\n        size_split.1,\n        bitrate_split.0,\n        bitrate_split.1,\n        duration_i32,\n        fps_i32,\n        dimension,\n    ];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/temporary_files.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::{ResultEntry, Search};\nuse czkawka_core::common::{format_time, split_path, split_path_compare};\nuse czkawka_core::tools::temporary;\nuse czkawka_core::tools::temporary::{Temporary, TemporaryFileEntry};\nuse rayon::prelude::*;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_TEMPORARY_FILES, MAX_STR_DATA_TEMPORARY_FILES, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_temporary_files(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let mut tool = Temporary::new();\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let mut vector = tool.get_temporary_files().clone();\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path()));\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n            sd.shared_models.lock().unwrap().shared_temporary_files_state = Some(tool);\n\n            let messages_data = MessagesData { critical, messages };\n\n            a.upgrade_in_event_loop(move |app| {\n                write_temporary_files_results(&app, vector, messages_data, info, sd, stopped_search);\n            })\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\nfn write_temporary_files_results(app: &MainWindow, vector: Vec<TemporaryFileEntry>, messages_data: MessagesData, info: temporary::Info, sd: ScanData, stopped_search: bool) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_temporary_files;\n\n    let items = Rc::new(VecModel::default());\n    for fe in vector {\n        let (data_model_str, data_model_int) = prepare_data_model_temporary_files(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n    app.set_temporary_files_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_temporary_files\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::TemporaryFiles);\n}\n\nfn prepare_data_model_temporary_files(fe: TemporaryFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_TEMPORARY_FILES] = [file.into(), directory.into(), get_dt_timestamp_string(fe.modified_date).into()];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.get_modified_date());\n    let size_split = split_u64_into_i32s(fe.size);\n    let data_model_int_arr: [i32; MAX_INT_DATA_TEMPORARY_FILES] = [modification_split.0, modification_split.1, size_split.0, size_split.1];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan/video_optimizer.rs",
    "content": "use std::rc::Rc;\nuse std::thread;\n\nuse czkawka_core::common::consts::DEFAULT_THREAD_SIZE;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::Search;\nuse czkawka_core::common::{format_time, split_path};\nuse czkawka_core::tools::video_optimizer;\nuse czkawka_core::tools::video_optimizer::{\n    VideoCropEntry, VideoCropParams, VideoOptimizer, VideoOptimizerMode, VideoOptimizerParameters, VideoTranscodeEntry, VideoTranscodeParams,\n};\nuse humansize::{BINARY, format_size};\nuse log::error;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};\n\nuse crate::common::{MAX_INT_DATA_VIDEO_OPTIMIZER, MAX_STR_DATA_VIDEO_OPTIMIZER, split_u64_into_i32s};\nuse crate::connect_scan::{MessagesData, ScanData, get_dt_timestamp_string, get_text_messages, insert_data_to_model, reset_selection_at_end, set_common_settings};\nuse crate::{ActiveTab, GuiState, MainWindow, flk};\n\npub(crate) fn scan_video_optimizer(a: Weak<MainWindow>, sd: ScanData) {\n    thread::Builder::new()\n        .stack_size(DEFAULT_THREAD_SIZE)\n        .spawn(move || {\n            let video_optimizer_mode = sd.combo_box_items.video_optimizer_mode.value;\n            let params = if video_optimizer_mode == VideoOptimizerMode::VideoCrop {\n                let crop_detect = sd.combo_box_items.video_optimizer_crop_type.value;\n                let params = VideoCropParams::with_custom_params(\n                    crop_detect,\n                    sd.custom_settings.video_optimizer_black_pixel_threshold,\n                    sd.custom_settings.video_optimizer_black_bar_min_percentage,\n                    sd.custom_settings.video_optimizer_max_samples,\n                    sd.custom_settings.video_optimizer_min_crop_size,\n                    sd.custom_settings.video_thumbnails_generate,\n                    sd.custom_settings.video_thumbnails_percentage,\n                    sd.custom_settings.video_thumbnails_generate_grid,\n                    sd.custom_settings.video_thumbnails_grid_tiles_per_side,\n                );\n                VideoOptimizerParameters::VideoCrop(params)\n            } else {\n                let excluded_codecs: Vec<String> = sd\n                    .custom_settings\n                    .video_optimizer_excluded_codecs\n                    .split(',')\n                    .map(|s| s.trim().to_lowercase())\n                    .filter(|s| !s.is_empty())\n                    .collect();\n                let params = VideoTranscodeParams::new(\n                    excluded_codecs,\n                    sd.custom_settings.video_thumbnails_generate,\n                    sd.custom_settings.video_thumbnails_percentage,\n                    sd.custom_settings.video_thumbnails_generate_grid,\n                    sd.custom_settings.video_thumbnails_grid_tiles_per_side,\n                );\n                VideoOptimizerParameters::VideoTranscode(params)\n            };\n\n            let is_crop_mode = matches!(params, VideoOptimizerParameters::VideoCrop(_));\n\n            let mut tool = VideoOptimizer::new(params);\n            set_common_settings(&mut tool, &sd.custom_settings, &sd.stop_flag);\n\n            tool.search(&sd.stop_flag, Some(&sd.progress_sender));\n\n            let (critical, messages) = get_text_messages(&tool, &sd.basic_settings);\n\n            let info = tool.get_information();\n            let stopped_search = tool.get_stopped_search();\n\n            if is_crop_mode {\n                let video_crop_entries = tool.get_video_crop_entries().clone();\n                sd.shared_models.lock().unwrap().shared_video_optimizer_state = Some(tool);\n\n                let messages_data = MessagesData { critical, messages };\n\n                a.upgrade_in_event_loop(move |app| {\n                    write_video_optimizer_crop_results(&app, video_crop_entries, messages_data, info, sd, stopped_search);\n                })\n            } else {\n                let video_transcode_entries = tool.get_video_transcode_entries().clone();\n                sd.shared_models.lock().unwrap().shared_video_optimizer_state = Some(tool);\n\n                let messages_data = MessagesData { critical, messages };\n\n                a.upgrade_in_event_loop(move |app| {\n                    write_video_optimizer_transcode_results(&app, video_transcode_entries, messages_data, info, sd, stopped_search);\n                })\n            }\n        })\n        .expect(\"Cannot start thread - not much we can do here\");\n}\n\nfn write_video_optimizer_transcode_results(\n    app: &MainWindow,\n    video_transcode_entries: Vec<VideoTranscodeEntry>,\n    messages_data: MessagesData,\n    info: video_optimizer::Info,\n    sd: ScanData,\n    stopped_search: bool,\n) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_videos_to_transcode;\n\n    let items = Rc::new(VecModel::default());\n\n    for fe in video_transcode_entries {\n        let (data_model_str, data_model_int) = prepare_data_model_video_optimizer_transcode(fe);\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n\n    app.set_video_optimizer_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_video_optimizer\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::VideoOptimizer);\n}\n\nfn write_video_optimizer_crop_results(\n    app: &MainWindow,\n    video_crop_entries: Vec<VideoCropEntry>,\n    messages_data: MessagesData,\n    info: video_optimizer::Info,\n    sd: ScanData,\n    stopped_search: bool,\n) {\n    let scanning_time_str = format_time(info.scanning_time);\n    let items_found = info.number_of_videos_to_crop;\n\n    let items = Rc::new(VecModel::default());\n\n    for fe in video_crop_entries {\n        let Some((data_model_str, data_model_int)) = prepare_data_model_video_optimizer_crop(fe) else {\n            continue;\n        };\n        insert_data_to_model(&items, data_model_str, data_model_int, None);\n    }\n\n    app.set_video_optimizer_model(items.into());\n    if let Some(critical) = messages_data.critical {\n        app.invoke_scan_ended(critical.into());\n    } else {\n        if !stopped_search && sd.basic_settings.play_audio_on_scan_completion {\n            sd.audio_player.play_scan_completed();\n        }\n        app.invoke_scan_ended(flk!(\"rust_found_video_optimizer\", items_found = items_found, time = scanning_time_str).into());\n    }\n    app.global::<GuiState>().set_info_text(messages_data.messages.into());\n    reset_selection_at_end(app, ActiveTab::VideoOptimizer);\n}\n\nfn prepare_data_model_video_optimizer_transcode(fe: VideoTranscodeEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {\n    let (directory, file) = split_path(&fe.path);\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_VIDEO_OPTIMIZER] = [\n        format_size(fe.size, BINARY).into(),\n        file.into(),\n        directory.into(),\n        fe.codec.into(),\n        format!(\"{}x{}\", fe.width, fe.height).into(),\n        \"-\".into(),\n        get_dt_timestamp_string(fe.modified_date).into(),\n        fe.thumbnail_path.as_ref().map(|e| e.to_string_lossy().to_string()).unwrap_or_default().into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.modified_date);\n    let size_split = split_u64_into_i32s(fe.size);\n    let dimension = fe.width as i32 * fe.height as i32; // Video dimension, limited to 16K vs 16K, so no overflow\n    let data_model_int_arr: [i32; MAX_INT_DATA_VIDEO_OPTIMIZER] = [\n        modification_split.0,\n        modification_split.1,\n        size_split.0,\n        size_split.1,\n        dimension,\n        0,\n        fe.width as i32,\n        fe.height as i32,\n        0,\n        0,\n        0,\n        0,\n    ];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    (data_model_str, data_model_int)\n}\n\nfn prepare_data_model_video_optimizer_crop(fe: VideoCropEntry) -> Option<(ModelRc<SharedString>, ModelRc<i32>)> {\n    let (directory, file) = split_path(&fe.path);\n    let (left, top, right, bottom) = fe.new_image_dimensions;\n\n    let (_width, _height, pixels_diff, dim_string) = if left > right || top > bottom {\n        error!(\n            \"ERROR: Invalid rectangle coordinates in cache for file \\\"{}\\\": left={}, top={}, right={}, bottom={}. Skipping dimensions display.\",\n            fe.path.to_string_lossy(),\n            left,\n            top,\n            right,\n            bottom\n        );\n        return None;\n    } else {\n        let new_width = (right - left) as i32;\n        let new_height = (bottom - top) as i32;\n        let pixels_diff = fe.width * fe.height - new_width as u32 * new_height as u32;\n        (\n            new_width,\n            new_height,\n            pixels_diff,\n            format!(\"{}x{} ({}x{})\", new_width, new_height, fe.width as i32 - new_width, fe.height as i32 - new_height),\n        )\n    };\n\n    let data_model_str_arr: [SharedString; MAX_STR_DATA_VIDEO_OPTIMIZER] = [\n        format_size(fe.size, BINARY).into(),\n        file.into(),\n        directory.into(),\n        fe.codec.into(),\n        format!(\"{}x{}\", fe.width, fe.height).into(),\n        dim_string.into(),\n        get_dt_timestamp_string(fe.modified_date).into(),\n        fe.thumbnail_path.as_ref().map(|e| e.to_string_lossy().to_string()).unwrap_or_default().into(),\n    ];\n    let data_model_str = VecModel::from_slice(&data_model_str_arr);\n    let modification_split = split_u64_into_i32s(fe.modified_date);\n    let size_split = split_u64_into_i32s(fe.size);\n    let dimension = fe.width as i32 * fe.height as i32;\n    let data_model_int_arr: [i32; MAX_INT_DATA_VIDEO_OPTIMIZER] = [\n        modification_split.0,\n        modification_split.1,\n        size_split.0,\n        size_split.1,\n        dimension,\n        pixels_diff as i32,\n        fe.width as i32,\n        fe.height as i32,\n        left as i32,\n        top as i32,\n        right as i32,\n        bottom as i32,\n    ];\n    let data_model_int = VecModel::from_slice(&data_model_int_arr);\n    Some((data_model_str, data_model_int))\n}\n"
  },
  {
    "path": "krokiet/src/connect_scan.rs",
    "content": "#![allow(clippy::needless_pass_by_value)]\n\nmod bad_extensions;\nmod bad_names;\nmod big_files;\nmod broken_files;\nmod duplicate;\nmod empty_files;\nmod empty_folders;\nmod exif_remover;\nmod invalid_symlinks;\nmod same_music;\nmod similar_images;\nmod similar_videos;\nmod temporary_files;\nmod video_optimizer;\n\nuse std::rc::Rc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\n\nuse chrono::{Local, TimeZone, Utc};\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::helpers::messages::MessageLimit;\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel};\n\nuse crate::audio_player::AudioPlayer;\nuse crate::common::{check_if_all_included_dirs_are_referenced, check_if_there_are_any_included_folders};\nuse crate::connect_row_selection::checker::set_number_of_enabled_items;\nuse crate::connect_row_selection::reset_selection;\nuse crate::connect_scan::bad_extensions::scan_bad_extensions;\nuse crate::connect_scan::bad_names::scan_bad_names;\nuse crate::connect_scan::big_files::scan_big_files;\nuse crate::connect_scan::broken_files::scan_broken_files;\nuse crate::connect_scan::duplicate::scan_duplicates;\nuse crate::connect_scan::empty_files::scan_empty_files;\nuse crate::connect_scan::empty_folders::scan_empty_folders;\nuse crate::connect_scan::exif_remover::scan_exif_remover;\nuse crate::connect_scan::invalid_symlinks::scan_invalid_symlinks;\nuse crate::connect_scan::same_music::scan_similar_music;\nuse crate::connect_scan::similar_images::scan_similar_images;\nuse crate::connect_scan::similar_videos::scan_similar_videos;\nuse crate::connect_scan::temporary_files::scan_temporary_files;\nuse crate::connect_scan::video_optimizer::scan_video_optimizer;\nuse crate::settings::model::{BasicSettings, ComboBoxItems, SettingsCustom};\nuse crate::settings::{collect_base_settings, collect_combo_box_settings, collect_settings};\nuse crate::shared_models::SharedModels;\nuse crate::{ActiveTab, GuiState, MainWindow, ProgressToSend, SingleMainListModel, flk};\n\npub struct ScanData {\n    pub progress_sender: Sender<ProgressData>,\n    pub stop_flag: Arc<AtomicBool>,\n    pub custom_settings: SettingsCustom,\n    pub basic_settings: BasicSettings,\n    pub combo_box_items: ComboBoxItems,\n    pub shared_models: Arc<Mutex<SharedModels>>,\n    pub audio_player: Arc<AudioPlayer>,\n}\n\npub struct MessagesData {\n    pub critical: Option<String>,\n    pub messages: String,\n}\n\npub(crate) fn connect_scan_button(\n    app: &MainWindow,\n    progress_sender: Sender<ProgressData>,\n    stop_flag: Arc<AtomicBool>,\n    shared_models: Arc<Mutex<SharedModels>>,\n    audio_player: Arc<AudioPlayer>,\n) {\n    let a = app.as_weak();\n    app.on_scan_starting(move |active_tab| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n\n        if !check_if_there_are_any_included_folders(&app) {\n            app.invoke_scan_ended(flk!(\"rust_no_included_paths\").into());\n            return;\n        }\n\n        if check_if_all_included_dirs_are_referenced(&app) {\n            app.invoke_scan_ended(flk!(\"rust_all_paths_referenced\").into());\n            return;\n        }\n\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n\n        app.set_progress_datas(ProgressToSend {\n            all_progress: 0,\n            current_progress: -1,\n            current_progress_size: -1,\n            step_name: \"\".into(),\n        });\n\n        let custom_settings = collect_settings(&app);\n        let basic_settings = collect_base_settings(&app);\n        let combo_box_items = collect_combo_box_settings(&app);\n\n        let cloned_model = Arc::clone(&shared_models);\n\n        app.global::<GuiState>().set_info_text(\"\".into());\n\n        let a = app.as_weak();\n        let audio_player_clone = Arc::clone(&audio_player);\n\n        let scan_data = ScanData {\n            progress_sender,\n            stop_flag,\n            custom_settings,\n            basic_settings,\n            combo_box_items,\n            shared_models: cloned_model,\n            audio_player: audio_player_clone,\n        };\n\n        match active_tab {\n            ActiveTab::DuplicateFiles => scan_duplicates(a, scan_data),\n            ActiveTab::EmptyFolders => scan_empty_folders(a, scan_data),\n            ActiveTab::BigFiles => scan_big_files(a, scan_data),\n            ActiveTab::EmptyFiles => scan_empty_files(a, scan_data),\n            ActiveTab::SimilarImages => scan_similar_images(a, scan_data),\n            ActiveTab::SimilarVideos => scan_similar_videos(a, scan_data),\n            ActiveTab::SimilarMusic => scan_similar_music(a, scan_data),\n            ActiveTab::InvalidSymlinks => scan_invalid_symlinks(a, scan_data),\n            ActiveTab::BadExtensions => scan_bad_extensions(a, scan_data),\n            ActiveTab::BadNames => scan_bad_names(a, scan_data),\n            ActiveTab::BrokenFiles => scan_broken_files(a, scan_data),\n            ActiveTab::TemporaryFiles => scan_temporary_files(a, scan_data),\n            ActiveTab::ExifRemover => scan_exif_remover(a, scan_data),\n            ActiveTab::VideoOptimizer => scan_video_optimizer(a, scan_data),\n            ActiveTab::Settings | ActiveTab::About => panic!(\"Button should be disabled\"),\n        }\n    });\n}\n\nfn get_dt_timestamp_string(timestamp: u64) -> String {\n    let dt_local = Utc.timestamp_opt(timestamp as i64, 0).single().unwrap_or_default().with_timezone(&Local);\n    dt_local.format(\"%Y-%m-%d %H:%M:%S\").to_string()\n}\n\n////////////////////////////////////////// Common\n\nfn reset_selection_at_end(app: &MainWindow, active_tab: ActiveTab) {\n    reset_selection(app, active_tab, true);\n    set_number_of_enabled_items(app, active_tab, 0);\n}\n\nfn insert_data_to_model(items: &Rc<VecModel<SingleMainListModel>>, data_model_str: ModelRc<SharedString>, data_model_int: ModelRc<i32>, filled_header_row: Option<bool>) {\n    let main = SingleMainListModel {\n        checked: false,\n        header_row: filled_header_row.is_some(),\n        filled_header_row: filled_header_row.unwrap_or(false),\n        selected_row: false,\n        val_str: ModelRc::new(data_model_str),\n        val_int: ModelRc::new(data_model_int),\n    };\n    items.push(main);\n}\n\nfn get_text_messages<T>(component: &T, basic_settings: &BasicSettings) -> (Option<String>, String)\nwhere\n    T: CommonData,\n{\n    let limit = if basic_settings.settings_limit_lines_of_messages {\n        MessageLimit::Lines(500)\n    } else {\n        MessageLimit::NoLimit\n    };\n\n    let text_messages = component.get_text_messages();\n    (text_messages.critical.clone(), text_messages.create_messages_text(limit))\n}\n\nfn set_common_settings<T>(component: &mut T, custom_settings: &SettingsCustom, stop_flag: &Arc<AtomicBool>)\nwhere\n    T: CommonData,\n{\n    stop_flag.store(false, Ordering::Relaxed);\n\n    component.set_included_paths(custom_settings.included_paths.clone());\n    component.set_reference_paths(custom_settings.included_paths_referenced.clone());\n    component.set_excluded_paths(custom_settings.excluded_paths.clone());\n    component.set_recursive_search(custom_settings.recursive_search);\n    component.set_minimal_file_size(custom_settings.minimum_file_size as u64 * 1024);\n    component.set_maximal_file_size(custom_settings.maximum_file_size as u64 * 1024);\n    component.set_allowed_extensions(custom_settings.allowed_extensions.split(',').map(str::to_string).collect());\n    component.set_excluded_extensions(custom_settings.excluded_extensions.split(',').map(str::to_string).collect());\n    component.set_excluded_items(custom_settings.excluded_items.split(',').map(str::to_string).collect());\n    component.set_exclude_other_filesystems(custom_settings.ignore_other_file_systems);\n    component.set_use_cache(custom_settings.use_cache);\n    component.set_save_also_as_json(custom_settings.save_also_as_json);\n    component.set_delete_outdated_cache(custom_settings.delete_outdated_cache_entries);\n    component.set_hide_hard_links(custom_settings.hide_hard_links);\n}\n"
  },
  {
    "path": "krokiet/src/connect_select/custom_select.rs",
    "content": "use chrono::{NaiveDate, Utc};\nuse slint::{Model, ModelRc, SharedString, VecModel};\n\nuse crate::common::{\n    IntDataBigFiles, IntDataBrokenFiles, IntDataDuplicateFiles, IntDataEmptyFiles, IntDataEmptyFolders, IntDataExifRemover, IntDataInvalidSymlinks, IntDataSimilarImages,\n    IntDataSimilarMusic, IntDataSimilarVideos, IntDataTemporaryFiles, IntDataVideoOptimizer, StrDataBadExtensions, StrDataBadNames, StrDataBigFiles, StrDataBrokenFiles,\n    StrDataDuplicateFiles, StrDataEmptyFiles, StrDataEmptyFolders, StrDataExifRemover, StrDataInvalidSymlinks, StrDataSimilarImages, StrDataSimilarMusic, StrDataSimilarVideos,\n    StrDataTemporaryFiles, StrDataVideoOptimizer, connect_i32_into_u64,\n};\nuse crate::{ActiveTab, ColumnType, CustomSelectColumnModel, SingleMainListModel, flk};\npub(super) type SelectionResult = (u64, u64, ModelRc<SingleMainListModel>);\nmacro_rules! col_str {\n    ($name:expr, $idx:expr) => {\n        CustomSelectColumnModel {\n            column_name: SharedString::from($name),\n            column_idx: $idx as i32,\n            column_type: ColumnType::Str,\n            is_pair: false,\n            enabled: false,\n            filter_value: SharedString::default(),\n        }\n    };\n}\n\nmacro_rules! col_int {\n    ($name:expr, $idx:expr) => {\n        CustomSelectColumnModel {\n            column_name: SharedString::from($name),\n            column_idx: $idx as i32,\n            column_type: ColumnType::Int,\n            is_pair: false,\n            enabled: false,\n            filter_value: SharedString::default(),\n        }\n    };\n}\n\nmacro_rules! col_int_pair {\n    ($name:expr, $idx:expr) => {\n        CustomSelectColumnModel {\n            column_name: SharedString::from($name),\n            column_idx: $idx as i32,\n            column_type: ColumnType::Int,\n            is_pair: true,\n            enabled: false,\n            filter_value: SharedString::default(),\n        }\n    };\n}\n\nmacro_rules! col_date {\n    ($name:expr, $idx:expr) => {\n        CustomSelectColumnModel {\n            column_name: SharedString::from($name),\n            column_idx: $idx as i32,\n            column_type: ColumnType::Date,\n            is_pair: false,\n            enabled: false,\n            filter_value: SharedString::default(),\n        }\n    };\n}\n\nmacro_rules! col_full_path {\n    ($name:expr) => {\n        CustomSelectColumnModel {\n            column_name: SharedString::from($name),\n            column_idx: 0,\n            column_type: ColumnType::FullPath,\n            is_pair: false,\n            enabled: false,\n            filter_value: SharedString::default(),\n        }\n    };\n}\npub(super) fn build_custom_select_columns(active_tab: ActiveTab) -> Vec<CustomSelectColumnModel> {\n    let size = flk!(\"column_size\");\n    let file_name = flk!(\"column_file_name\");\n    let path = flk!(\"column_path\");\n    let mod_date = flk!(\"column_modification_date\");\n    let similarity = flk!(\"column_similarity\");\n    let dimensions = flk!(\"column_dimensions\");\n    let title = flk!(\"column_title\");\n    let artist = flk!(\"column_artist\");\n    let year = flk!(\"column_year\");\n    let bitrate = flk!(\"column_bitrate\");\n    let length = flk!(\"column_length\");\n    let genre = flk!(\"column_genre\");\n    let fps = flk!(\"column_fps\");\n    let codec = flk!(\"column_codec\");\n    let duration = flk!(\"column_duration\");\n    let type_of_error = flk!(\"column_type_of_error\");\n    let symlink_name = flk!(\"column_symlink_name\");\n    let symlink_folder = flk!(\"column_symlink_folder\");\n    let destination_path = flk!(\"column_destination_path\");\n    let current_extension = flk!(\"column_current_extension\");\n    let proper_extension = flk!(\"column_proper_extension\");\n    let exif_tags = flk!(\"column_exif_tags\");\n    let new_name = flk!(\"column_new_name\");\n    let full_path = flk!(\"column_full_path\");\n\n    match active_tab {\n        ActiveTab::DuplicateFiles => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataDuplicateFiles::Name),\n            col_str!(&path, StrDataDuplicateFiles::Path),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataDuplicateFiles::SizePart1),\n            col_date!(&mod_date, IntDataDuplicateFiles::ModificationDatePart1),\n        ],\n        ActiveTab::EmptyFolders => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataEmptyFolders::Name),\n            col_str!(&path, StrDataEmptyFolders::Path),\n            col_date!(&mod_date, IntDataEmptyFolders::ModificationDatePart1),\n        ],\n        ActiveTab::BigFiles => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataBigFiles::Name),\n            col_str!(&path, StrDataBigFiles::Path),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataBigFiles::SizePart1),\n            col_date!(&mod_date, IntDataBigFiles::ModificationDatePart1),\n        ],\n        ActiveTab::EmptyFiles => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataEmptyFiles::Name),\n            col_str!(&path, StrDataEmptyFiles::Path),\n            col_date!(&mod_date, IntDataEmptyFiles::ModificationDatePart1),\n        ],\n        ActiveTab::TemporaryFiles => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataTemporaryFiles::Name),\n            col_str!(&path, StrDataTemporaryFiles::Path),\n            col_date!(&mod_date, IntDataTemporaryFiles::ModificationDatePart1),\n        ],\n        ActiveTab::SimilarImages => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataSimilarImages::Name),\n            col_str!(&path, StrDataSimilarImages::Path),\n            col_int!(&similarity, IntDataSimilarImages::SimilarityValue),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataSimilarImages::SizePart1),\n            col_int!(format!(\"{} [px]\", dimensions), IntDataSimilarImages::PixelCount),\n            col_date!(&mod_date, IntDataSimilarImages::ModificationDatePart1),\n        ],\n        ActiveTab::SimilarVideos => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataSimilarVideos::Name),\n            col_str!(&path, StrDataSimilarVideos::Path),\n            col_str!(&codec, StrDataSimilarVideos::Codec),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataSimilarVideos::SizePart1),\n            col_int!(format!(\"{} [s]\", duration), IntDataSimilarVideos::Duration),\n            col_int_pair!(format!(\"{} [kbps]\", bitrate), IntDataSimilarVideos::BitratePart1),\n            col_int!(&fps, IntDataSimilarVideos::Fps),\n            col_date!(&mod_date, IntDataSimilarVideos::ModificationDatePart1),\n        ],\n        ActiveTab::SimilarMusic => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataSimilarMusic::Name),\n            col_str!(&path, StrDataSimilarMusic::Path),\n            col_str!(&title, StrDataSimilarMusic::Title),\n            col_str!(&artist, StrDataSimilarMusic::Artist),\n            col_str!(&year, StrDataSimilarMusic::Year),\n            col_str!(&genre, StrDataSimilarMusic::Genre),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataSimilarMusic::SizePart1),\n            col_int!(format!(\"{} [kbps]\", bitrate), IntDataSimilarMusic::Bitrate),\n            col_int!(format!(\"{} [s]\", length), IntDataSimilarMusic::Length),\n            col_date!(&mod_date, IntDataSimilarMusic::ModificationDatePart1),\n        ],\n        ActiveTab::InvalidSymlinks => vec![\n            col_str!(&symlink_name, StrDataInvalidSymlinks::SymlinkName),\n            col_str!(&symlink_folder, StrDataInvalidSymlinks::SymlinkFolder),\n            col_str!(&destination_path, StrDataInvalidSymlinks::DestinationPath),\n            col_str!(&type_of_error, StrDataInvalidSymlinks::TypeOfError),\n            col_date!(&mod_date, IntDataInvalidSymlinks::ModificationDatePart1),\n        ],\n        ActiveTab::BrokenFiles => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataBrokenFiles::Name),\n            col_str!(&path, StrDataBrokenFiles::Path),\n            col_str!(&type_of_error, StrDataBrokenFiles::TypeOfError),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataBrokenFiles::SizePart1),\n            col_date!(&mod_date, IntDataBrokenFiles::ModificationDatePart1),\n        ],\n        ActiveTab::BadExtensions => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataBadExtensions::Name),\n            col_str!(&path, StrDataBadExtensions::Path),\n            col_str!(&current_extension, StrDataBadExtensions::CurrentExtension),\n            col_str!(&proper_extension, StrDataBadExtensions::ProperExtensionsGroup),\n            col_str!(&proper_extension, StrDataBadExtensions::ProperExtension),\n        ],\n        ActiveTab::BadNames => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataBadNames::Name),\n            col_str!(&new_name, StrDataBadNames::NewName),\n            col_str!(&path, StrDataBadNames::Path),\n        ],\n        ActiveTab::ExifRemover => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataExifRemover::Name),\n            col_str!(&path, StrDataExifRemover::Path),\n            col_str!(&exif_tags, StrDataExifRemover::ExifTags),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataExifRemover::SizePart1),\n            col_int!(&exif_tags, IntDataExifRemover::ExifTagsCount),\n            col_date!(&mod_date, IntDataExifRemover::ModificationDatePart1),\n        ],\n        ActiveTab::VideoOptimizer => vec![\n            col_full_path!(&full_path),\n            col_str!(&file_name, StrDataVideoOptimizer::Name),\n            col_str!(&path, StrDataVideoOptimizer::Path),\n            col_str!(&codec, StrDataVideoOptimizer::Codec),\n            col_int_pair!(format!(\"{} [KB]\", size), IntDataVideoOptimizer::SizePart1),\n            col_date!(&mod_date, IntDataVideoOptimizer::ModificationDatePart1),\n        ],\n        ActiveTab::Settings | ActiveTab::About => vec![],\n    }\n}\n\n#[derive(Clone, Copy, PartialEq, Debug, Eq)]\nenum CmpOp {\n    Gte,\n    Lte,\n    Gt,\n    Lt,\n    Eq,\n}\nfn parse_op_and_value(filter: &str) -> Option<(CmpOp, u64)> {\n    let filter = filter.trim();\n    let (op, rest) = if let Some(r) = filter.strip_prefix(\">=\") {\n        (CmpOp::Gte, r)\n    } else if let Some(r) = filter.strip_prefix(\"<=\") {\n        (CmpOp::Lte, r)\n    } else if let Some(r) = filter.strip_prefix('>') {\n        (CmpOp::Gt, r)\n    } else if let Some(r) = filter.strip_prefix('<') {\n        (CmpOp::Lt, r)\n    } else if let Some(r) = filter.strip_prefix('=') {\n        (CmpOp::Eq, r)\n    } else {\n        (CmpOp::Eq, filter)\n    };\n    let val: u64 = rest.trim().parse().ok()?;\n    Some((op, val))\n}\nfn eval_op(actual: u64, op: CmpOp, threshold: u64) -> bool {\n    match op {\n        CmpOp::Gte => actual >= threshold,\n        CmpOp::Lte => actual <= threshold,\n        CmpOp::Gt => actual > threshold,\n        CmpOp::Lt => actual < threshold,\n        CmpOp::Eq => actual == threshold,\n    }\n}\n\nfn read_u64_pair(item: &SingleMainListModel, base_idx: usize) -> u64 {\n    let mut iter = item.val_int.iter();\n    let hi = iter.nth(base_idx).expect(\"base_idx out of bounds for int pair column\");\n    let lo = iter.next().expect(\"base_idx+1 out of bounds for int pair column\");\n    connect_i32_into_u64(hi, lo)\n}\n\nfn read_int_single(item: &SingleMainListModel, idx: usize) -> u64 {\n    item.val_int.iter().nth(idx).expect(\"idx out of bounds for int single column\") as u64\n}\nfn matches_int_filter(item: &SingleMainListModel, col: &CustomSelectColumnModel) -> bool {\n    let base_idx = col.column_idx as usize;\n    let filter = col.filter_value.as_str().trim();\n    let Some((op, threshold)) = parse_op_and_value(filter) else { return false };\n    let actual: u64 = if col.is_pair {\n        let bytes = read_u64_pair(item, base_idx);\n        bytes / 1024\n    } else {\n        read_int_single(item, base_idx)\n    };\n    eval_op(actual, op, threshold)\n}\nfn parse_date(s: &str) -> Option<u64> {\n    let s = s.trim();\n\n    let (date_part, time_part) = match s.split_once(' ') {\n        Some((a, b)) => (a, Some(b.trim())),\n        None => (s, None),\n    };\n\n    let d = NaiveDate::parse_from_str(date_part, \"%d-%m-%Y\")\n        .or_else(|_| NaiveDate::parse_from_str(date_part, \"%Y-%m-%d\"))\n        .ok()?;\n\n    let (h, m, sec) = if let Some(t) = time_part {\n        let parts: Vec<&str> = t.splitn(3, ':').collect();\n        let h: u32 = parts.first().and_then(|v| v.parse().ok()).unwrap_or(0);\n        let m: u32 = parts.get(1).and_then(|v| v.parse().ok()).unwrap_or(0);\n        let sec: u32 = parts.get(2).and_then(|v| v.parse().ok()).unwrap_or(0);\n        (h, m, sec)\n    } else {\n        (0, 0, 0)\n    };\n\n    let naive_dt = d.and_hms_opt(h, m, sec)?;\n    let dt = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc);\n\n    let ts = dt.timestamp();\n    if ts >= 86400 { Some((ts - 86400) as u64) } else { Some(ts as u64) }\n}\nfn matches_date_filter(item: &SingleMainListModel, col: &CustomSelectColumnModel) -> bool {\n    let base_idx = col.column_idx as usize;\n    let filter = col.filter_value.as_str().trim();\n\n    let (op, date_str) = if let Some(r) = filter.strip_prefix(\">=\") {\n        (CmpOp::Gte, r.trim())\n    } else if let Some(r) = filter.strip_prefix(\"<=\") {\n        (CmpOp::Lte, r.trim())\n    } else if let Some(r) = filter.strip_prefix('>') {\n        (CmpOp::Gt, r.trim())\n    } else if let Some(r) = filter.strip_prefix('<') {\n        (CmpOp::Lt, r.trim())\n    } else if let Some(r) = filter.strip_prefix('=') {\n        (CmpOp::Eq, r.trim())\n    } else {\n        (CmpOp::Eq, filter)\n    };\n    let Some(threshold) = parse_date(date_str) else { return false };\n    let actual = read_u64_pair(item, base_idx);\n    eval_op(actual, op, threshold)\n}\nfn matches_str_filter(raw_value: &str, filter: &str, case_sensitive: bool) -> bool {\n    use czkawka_core::common::items::new_excluded_item;\n    use czkawka_core::common::regex_check;\n    let (value_cmp, filter_cmp) = if case_sensitive {\n        (raw_value.to_owned(), filter.to_owned())\n    } else {\n        (raw_value.to_lowercase(), filter.to_lowercase())\n    };\n    let excluded = new_excluded_item(&filter_cmp);\n    regex_check(&excluded, &value_cmp)\n}\n\nfn matches_full_path_filter(col: &CustomSelectColumnModel, path_idx: usize, name_idx: usize, case_sensitive: bool, val_strs: &[SharedString]) -> bool {\n    let path = val_strs.get(path_idx).map_or(\"\", |s| s.as_str());\n    let name = val_strs.get(name_idx).map_or(\"\", |s| s.as_str());\n    let full = format!(\"{path}/{name}\");\n    matches_str_filter(&full, col.filter_value.as_str(), case_sensitive)\n}\n\npub(super) fn select_custom_columns(\n    model: &ModelRc<SingleMainListModel>,\n    active_tab: ActiveTab,\n    select_mode: bool,\n    columns: &[CustomSelectColumnModel],\n    case_sensitive: bool,\n    leave_one_in_group: bool,\n) -> SelectionResult {\n    let mut checked_items = 0u64;\n    let mut unchecked_items = 0u64;\n    let mut old_data = model.iter().collect::<Vec<_>>();\n    let active_columns: Vec<&CustomSelectColumnModel> = columns.iter().filter(|c| c.enabled && !c.filter_value.is_empty()).collect();\n    if active_columns.is_empty() {\n        return (0, 0, ModelRc::new(VecModel::from(old_data)));\n    }\n    let is_header_mode = active_tab.get_is_header_mode();\n    let path_idx = active_tab.get_str_path_idx();\n    let name_idx = active_tab.get_str_name_idx();\n    let item_matches = |item: &SingleMainListModel| -> bool {\n        let val_strs: Vec<SharedString> = item.val_str.iter().collect();\n        for col in &active_columns {\n            let matches = match col.column_type {\n                ColumnType::Str => {\n                    let idx = col.column_idx as usize;\n                    let raw = val_strs.get(idx).map_or(\"\", |s| s.as_str());\n                    matches_str_filter(raw, col.filter_value.as_str(), case_sensitive)\n                }\n                ColumnType::Int => matches_int_filter(item, col),\n                ColumnType::Date => matches_date_filter(item, col),\n                ColumnType::FullPath => matches_full_path_filter(col, path_idx, name_idx, case_sensitive, &val_strs),\n            };\n            if !matches {\n                return false;\n            }\n        }\n        true\n    };\n    if !is_header_mode {\n        for item in &mut old_data {\n            if item.header_row {\n                continue;\n            }\n            let matches = item_matches(item);\n            if select_mode {\n                if matches && !item.checked {\n                    item.checked = true;\n                    checked_items += 1;\n                }\n            } else if matches && item.checked {\n                item.checked = false;\n                unchecked_items += 1;\n            }\n        }\n    } else {\n        let headers_idx: Vec<usize> = old_data.iter().enumerate().filter_map(|(idx, m)| if m.header_row { Some(idx) } else { None }).collect();\n        for i in 0..headers_idx.len() {\n            let start_idx = headers_idx[i] + 1;\n            let end_idx = if i + 1 < headers_idx.len() { headers_idx[i + 1] } else { old_data.len() };\n            if start_idx >= end_idx {\n                continue;\n            }\n            let mut items_to_change: Vec<usize> = Vec::new();\n            let mut already_selected = 0usize;\n            let total_in_group = end_idx - start_idx;\n            for (j, item) in old_data.iter().enumerate().skip(start_idx).take(end_idx - start_idx) {\n                if item.header_row {\n                    continue;\n                }\n                if select_mode {\n                    if item.checked {\n                        already_selected += 1;\n                    } else if item_matches(item) {\n                        items_to_change.push(j);\n                    }\n                } else if item.checked && item_matches(item) {\n                    items_to_change.push(j);\n                }\n            }\n            if select_mode {\n                if leave_one_in_group && (total_in_group - already_selected == items_to_change.len()) && !items_to_change.is_empty() {\n                    items_to_change.pop();\n                }\n                for &idx in &items_to_change {\n                    old_data[idx].checked = true;\n                    checked_items += 1;\n                }\n            } else {\n                for &idx in &items_to_change {\n                    old_data[idx].checked = false;\n                    unchecked_items += 1;\n                }\n            }\n        }\n    }\n    (checked_items, unchecked_items, ModelRc::new(VecModel::from(old_data)))\n}\n\n#[cfg(test)]\nmod tests {\n    use slint::VecModel;\n\n    use super::*;\n    use crate::common::{IntDataDuplicateFiles, MAX_INT_DATA_DUPLICATE_FILES, MAX_STR_DATA_DUPLICATE_FILES, create_model_from_model_vec, split_u64_into_i32s};\n    use crate::test_common::get_model_vec;\n\n    fn make_item(val_str: &[&str], val_int: &[i32]) -> SingleMainListModel {\n        SingleMainListModel {\n            checked: false,\n            filled_header_row: false,\n            header_row: false,\n            selected_row: false,\n            val_str: ModelRc::new(VecModel::from(val_str.iter().map(|s| SharedString::from(*s)).collect::<Vec<_>>())),\n            val_int: ModelRc::new(VecModel::from(val_int.to_vec())),\n        }\n    }\n\n    fn make_header() -> SingleMainListModel {\n        SingleMainListModel {\n            header_row: true,\n            filled_header_row: false,\n            ..make_item(&[], &[])\n        }\n    }\n\n    fn dup_item(size_bytes: u64, name: &str, path: &str, mod_ts: u64) -> SingleMainListModel {\n        let size_str = format!(\"{size_bytes} B\");\n        let mod_str = \"2020-01-01 00:00:00\".to_string();\n        let val_str: [SharedString; MAX_STR_DATA_DUPLICATE_FILES] = [\n            SharedString::from(size_str.as_str()),\n            SharedString::from(name),\n            SharedString::from(path),\n            SharedString::from(mod_str.as_str()),\n        ];\n        let (sz1, sz2) = split_u64_into_i32s(size_bytes);\n        let (md1, md2) = split_u64_into_i32s(mod_ts);\n        let val_int: [i32; MAX_INT_DATA_DUPLICATE_FILES] = [md1, md2, sz1, sz2];\n        SingleMainListModel {\n            checked: false,\n            filled_header_row: false,\n            header_row: false,\n            selected_row: false,\n            val_str: ModelRc::new(VecModel::from(val_str.to_vec())),\n            val_int: ModelRc::new(VecModel::from(val_int.to_vec())),\n        }\n    }\n\n    fn enabled_col(mut col: CustomSelectColumnModel, filter: &str) -> CustomSelectColumnModel {\n        col.enabled = true;\n        col.filter_value = SharedString::from(filter);\n        col\n    }\n\n    #[test]\n    fn parse_op_and_val() {\n        let (op, val) = parse_op_and_value(\"42\").unwrap();\n        assert_eq!(op, CmpOp::Eq);\n        assert_eq!(val, 42);\n\n        let (op, val) = parse_op_and_value(\">= 100\").unwrap();\n        assert_eq!(op, CmpOp::Gte);\n        assert_eq!(val, 100);\n\n        let (op, val) = parse_op_and_value(\"<= 200\").unwrap();\n        assert_eq!(op, CmpOp::Lte);\n        assert_eq!(val, 200);\n\n        let (op, val) = parse_op_and_value(\"> 0\").unwrap();\n        assert_eq!(op, CmpOp::Gt);\n        assert_eq!(val, 0);\n\n        let (op, val) = parse_op_and_value(\"< 512\").unwrap();\n        assert_eq!(op, CmpOp::Lt);\n        assert_eq!(val, 512);\n\n        let (op, val) = parse_op_and_value(\"= 7\").unwrap();\n        assert_eq!(op, CmpOp::Eq);\n        assert_eq!(val, 7);\n\n        assert!(parse_op_and_value(\"\").is_none());\n\n        assert!(parse_op_and_value(\">= abc\").is_none());\n    }\n\n    #[test]\n    fn parse_dat() {\n        let ts = parse_date(\"15-01-2020\").unwrap();\n        assert_eq!(ts, 1578960000);\n\n        let ts = parse_date(\"2020-01-15\").unwrap();\n        assert_eq!(ts, 1578960000);\n\n        let ts1 = parse_date(\"15-01-2020\").unwrap();\n        let ts2 = parse_date(\"2020-01-15\").unwrap();\n        assert_eq!(ts1, ts2);\n\n        let base = parse_date(\"2020-01-15\").unwrap();\n        let with_time = parse_date(\"2020-01-15 12:30:45\").unwrap();\n        assert_eq!(with_time, base + 12 * 3600 + 30 * 60 + 45);\n\n        let base = parse_date(\"2020-01-15\").unwrap();\n        let with_time = parse_date(\"2020-01-15 08:00\").unwrap();\n        assert_eq!(with_time, base + 8 * 3600);\n\n        let ts1 = parse_date(\"15-01-2020 06:00:00\").unwrap();\n        let ts2 = parse_date(\"2020-01-15 06:00:00\").unwrap();\n        assert_eq!(ts1, ts2);\n\n        assert!(parse_date(\"not-a-date\").is_none());\n        assert!(parse_date(\"32-01-2020\").is_none());\n        assert!(parse_date(\"\").is_none());\n    }\n\n    #[test]\n    fn matches_str_filter_exact_case_sensitive() {\n        assert!(matches_str_filter(\"hello.rs\", \"hello.rs\", true));\n        assert!(!matches_str_filter(\"Hello.rs\", \"hello.rs\", true));\n        assert!(matches_str_filter(\"Hello.RS\", \"hello.rs\", false));\n        assert!(matches_str_filter(\"photo_2024.jpg\", \"*.jpg\", false));\n        assert!(!matches_str_filter(\"photo_2024.png\", \"*.jpg\", false));\n        assert!(matches_str_filter(\"my_backup_file.tar\", \"*backup*\", false));\n        assert!(!matches_str_filter(\"my_document.pdf\", \"*backup*\", false));\n        assert!(matches_str_filter(\"anything\", \"\", false));\n    }\n\n    fn str_col(idx: usize, filter: &str) -> CustomSelectColumnModel {\n        CustomSelectColumnModel {\n            column_name: SharedString::from(flk!(\"column_file_name\")),\n            column_idx: idx as i32,\n            column_type: ColumnType::Str,\n            is_pair: false,\n            enabled: true,\n            filter_value: SharedString::from(filter),\n        }\n    }\n\n    #[test]\n    fn select_custom_columns_no_active_columns_returns_unchanged() {\n        let mut rows = get_model_vec(3);\n        rows[0].checked = true;\n        let model = create_model_from_model_vec(&rows);\n        let cols: Vec<CustomSelectColumnModel> = vec![];\n\n        let (checked, unchecked, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 0);\n        assert_eq!(unchecked, 0);\n\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_flat_selects_matching_rows() {\n        let rows: Vec<SingleMainListModel> = vec![\n            make_item(&[\"photo.jpg\", \"/home\"], &[]),\n            make_item(&[\"backup.tar\", \"/home\"], &[]),\n            make_item(&[\"photo_old.jpg\", \"/tmp\"], &[]),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, unchecked, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 2);\n        assert_eq!(unchecked, 0);\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(new_model.row_data(2).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_flat_unselects_matching_rows() {\n        let rows: Vec<SingleMainListModel> = vec![\n            {\n                let mut r = make_item(&[\"photo.jpg\"], &[]);\n                r.checked = true;\n                r\n            },\n            {\n                let mut r = make_item(&[\"backup.tar\"], &[]);\n                r.checked = true;\n                r\n            },\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, unchecked, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, false, &cols, false, false);\n\n        assert_eq!(checked, 0);\n        assert_eq!(unchecked, 1);\n        assert!(!new_model.row_data(0).unwrap().checked);\n        assert!(new_model.row_data(1).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_flat_skips_header_rows() {\n        let rows: Vec<SingleMainListModel> = vec![\n            {\n                let mut r = make_item(&[\"photo.jpg\"], &[]);\n                r.header_row = true;\n                r\n            },\n            make_item(&[\"photo.jpg\"], &[]),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, _unchecked, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 1);\n        assert!(!new_model.row_data(0).unwrap().checked);\n        assert!(new_model.row_data(1).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_flat_case_sensitive_no_match() {\n        let rows = vec![make_item(&[\"Photo.JPG\"], &[])];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, _, _) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, true, false);\n        assert_eq!(checked, 0);\n    }\n\n    #[test]\n    fn select_custom_columns_flat_case_insensitive_matches() {\n        let rows = vec![make_item(&[\"Photo.JPG\"], &[])];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, _, _) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n        assert_eq!(checked, 1);\n    }\n\n    fn grouped_model(names: &[(&str, bool)]) -> (Vec<SingleMainListModel>, ModelRc<SingleMainListModel>) {\n        let rows: Vec<SingleMainListModel> = names.iter().map(|(n, is_hdr)| if *is_hdr { make_header() } else { make_item(&[n], &[]) }).collect();\n        let model = create_model_from_model_vec(&rows);\n        (rows, model)\n    }\n\n    #[test]\n    fn select_custom_columns_header_mode_selects_matching_in_group() {\n        let (_rows, model) = grouped_model(&[(\"\", true), (\"a.jpg\", false), (\"b.tar\", false), (\"\", true), (\"c.jpg\", false)]);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, unchecked, new_model) = select_custom_columns(&model, ActiveTab::DuplicateFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 2);\n        assert_eq!(unchecked, 0);\n        assert!(!new_model.row_data(0).unwrap().checked);\n        assert!(new_model.row_data(1).unwrap().checked);\n        assert!(!new_model.row_data(2).unwrap().checked);\n        assert!(!new_model.row_data(3).unwrap().checked);\n        assert!(new_model.row_data(4).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_header_mode_leave_one_in_group_keeps_one_unselected() {\n        let (_rows, model) = grouped_model(&[(\"\", true), (\"a.jpg\", false), (\"b.jpg\", false)]);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, _, new_model) = select_custom_columns(&model, ActiveTab::DuplicateFiles, true, &cols, false, true);\n\n        assert_eq!(checked, 1);\n        assert!(new_model.row_data(1).unwrap().checked);\n        assert!(!new_model.row_data(2).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_header_mode_leave_one_in_group_no_effect_when_already_partially_selected() {\n        let rows: Vec<SingleMainListModel> = vec![\n            make_header(),\n            {\n                let mut r = make_item(&[\"a.jpg\"], &[]);\n                r.checked = true;\n                r\n            },\n            make_item(&[\"b.jpg\"], &[]),\n            make_item(&[\"c.jpg\"], &[]),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, _, new_model) = select_custom_columns(&model, ActiveTab::DuplicateFiles, true, &cols, false, true);\n\n        assert_eq!(checked, 1);\n        assert!(new_model.row_data(1).unwrap().checked);\n\n        let b = new_model.row_data(2).unwrap().checked;\n        let c = new_model.row_data(3).unwrap().checked;\n        assert!(b ^ c, \"exactly one of b/c should be selected by leave_one_in_group\");\n    }\n\n    #[test]\n    fn select_custom_columns_header_mode_unselect() {\n        let rows: Vec<SingleMainListModel> = vec![\n            make_header(),\n            {\n                let mut r = make_item(&[\"a.jpg\"], &[]);\n                r.checked = true;\n                r\n            },\n            {\n                let mut r = make_item(&[\"b.jpg\"], &[]);\n                r.checked = true;\n                r\n            },\n            make_item(&[\"c.tar\"], &[]),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\")];\n\n        let (checked, unchecked, new_model) = select_custom_columns(&model, ActiveTab::DuplicateFiles, false, &cols, false, false);\n\n        assert_eq!(checked, 0);\n        assert_eq!(unchecked, 2);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(!new_model.row_data(2).unwrap().checked);\n        assert!(!new_model.row_data(3).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_int_pair_size_filter() {\n        let small = dup_item(512 * 1024, \"small.bin\", \"/tmp\", 0);\n        let large = dup_item(3 * 1024 * 1024, \"large.bin\", \"/tmp\", 0);\n\n        let rows = vec![make_header(), small, large];\n        let model = create_model_from_model_vec(&rows);\n\n        let size_col = enabled_col(\n            CustomSelectColumnModel {\n                column_name: SharedString::from(format!(\"{} [KB]\", flk!(\"column_size\"))),\n                column_idx: IntDataDuplicateFiles::SizePart1 as i32,\n                column_type: ColumnType::Int,\n                is_pair: true,\n                enabled: false,\n                filter_value: SharedString::default(),\n            },\n            \">= 1024\",\n        );\n\n        let (checked, _, new_model) = select_custom_columns(&model, ActiveTab::DuplicateFiles, true, &[size_col], false, false);\n\n        assert_eq!(checked, 1);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(new_model.row_data(2).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_multiple_columns_requires_all_to_match() {\n        let rows: Vec<SingleMainListModel> = vec![\n            make_item(&[\"photo.jpg\", \"/home/user\"], &[]),\n            make_item(&[\"photo.jpg\", \"/tmp\"], &[]),\n            make_item(&[\"backup.tar\", \"/home/user\"], &[]),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![str_col(0, \"*.jpg\"), str_col(1, \"/home/*\")];\n\n        let (checked, _, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 1);\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(!new_model.row_data(2).unwrap().checked);\n    }\n\n    fn make_full_path_col(filter: &str) -> CustomSelectColumnModel {\n        CustomSelectColumnModel {\n            column_name: SharedString::from(flk!(\"column_full_path\")),\n            column_idx: 0,\n            column_type: ColumnType::FullPath,\n            is_pair: false,\n            enabled: true,\n            filter_value: SharedString::from(filter),\n        }\n    }\n\n    // BigFiles: StrData layout: Size=0, Name=1, Path=2, ModificationDate=3\n    // get_str_path_idx() = 2, get_str_name_idx() = 1\n    // full_path = \"{val_str[2]}/{val_str[1]}\"\n    fn bigfiles_item_with_path(name: &str, path: &str) -> SingleMainListModel {\n        make_item(&[\"0 B\", name, path, \"2020-01-01\"], &[0, 0, 0, 0])\n    }\n\n    #[test]\n    fn select_custom_columns_full_path_matches_path_slash_name() {\n        let rows = vec![\n            bigfiles_item_with_path(\"photo.jpg\", \"/home/user\"),\n            bigfiles_item_with_path(\"backup.tar\", \"/tmp\"),\n            bigfiles_item_with_path(\"notes.jpg\", \"/home/user/docs\"),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![make_full_path_col(\"*.jpg\")];\n\n        let (checked, _, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 2);\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(new_model.row_data(2).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_full_path_filters_by_directory() {\n        let rows = vec![\n            bigfiles_item_with_path(\"file.rs\", \"/home/user/project\"),\n            bigfiles_item_with_path(\"file.rs\", \"/tmp\"),\n            bigfiles_item_with_path(\"other.rs\", \"/home/user/project\"),\n        ];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![make_full_path_col(\"/home/user/*\")];\n\n        let (checked, _, new_model) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n\n        assert_eq!(checked, 2);\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(new_model.row_data(2).unwrap().checked);\n    }\n\n    #[test]\n    fn select_custom_columns_full_path_case_insensitive() {\n        let rows = vec![bigfiles_item_with_path(\"Photo.JPG\", \"/HOME/User\")];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![make_full_path_col(\"*.jpg\")];\n\n        let (checked, _, _) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, false, false);\n        assert_eq!(checked, 1);\n    }\n\n    #[test]\n    fn select_custom_columns_full_path_case_sensitive_no_match() {\n        let rows = vec![bigfiles_item_with_path(\"Photo.JPG\", \"/HOME/User\")];\n        let model = create_model_from_model_vec(&rows);\n        let cols = vec![make_full_path_col(\"*.jpg\")];\n\n        let (checked, _, _) = select_custom_columns(&model, ActiveTab::BigFiles, true, &cols, true, false);\n        assert_eq!(checked, 0);\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_select/mod.rs",
    "content": "pub(crate) mod custom_select;\n\nuse std::sync::{Arc, Mutex};\n\nuse regex::Regex;\nuse slint::{ComponentHandle, Model, ModelRc, VecModel};\n\nuse crate::common::{connect_i32_into_u64, create_model_from_model_vec};\nuse crate::connect_row_selection::checker::change_number_of_enabled_items;\nuse crate::connect_translation::translate_select_mode;\nuse crate::shared_models::SharedModels;\nuse crate::{ActiveTab, Callabler, CustomSelectColumnModel, GuiState, MainWindow, SelectMode, SelectModel, SingleMainListModel};\n\ntype SelectionResult = (u64, u64, ModelRc<SingleMainListModel>);\n\n// TODO optimize this, not sure if it is possible to not copy entire model to just select item\n// https://github.com/slint-ui/slint/discussions/4595\npub(crate) fn connect_select(app: &MainWindow, shared_models: &Arc<Mutex<SharedModels>>) {\n    set_select_buttons(app);\n\n    let shared_models = shared_models.clone();\n    let a = app.as_weak();\n    app.global::<Callabler>().on_select_items(move |select_mode| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        let current_model = active_tab.get_tool_model(&app);\n\n        let (checked_items, unchecked_items, new_model) = match select_mode {\n            SelectMode::SelectAll => select_all(&current_model),\n            SelectMode::UnselectAll => deselect_all(&current_model),\n            SelectMode::InvertSelection => invert_selection(&current_model),\n            SelectMode::SelectTheBiggestSize => select_by_property(&current_model, active_tab, Property::Size, true),\n            SelectMode::SelectTheSmallestSize => select_by_property(&current_model, active_tab, Property::Size, false),\n            SelectMode::SelectTheBiggestResolution => select_by_property(&current_model, active_tab, Property::Resolution, false),\n            SelectMode::SelectTheSmallestResolution => select_by_property(&current_model, active_tab, Property::Resolution, true),\n            SelectMode::SelectNewest => select_by_property(&current_model, active_tab, Property::Date, true),\n            SelectMode::SelectOldest => select_by_property(&current_model, active_tab, Property::Date, false),\n            SelectMode::SelectShortestPath => select_by_property(&current_model, active_tab, Property::PathLength, false),\n            SelectMode::SelectLongestPath => select_by_property(&current_model, active_tab, Property::PathLength, true),\n\n            SelectMode::SelectCustom => return,\n        };\n        active_tab.set_tool_model(&app, new_model);\n        change_number_of_enabled_items(&app, active_tab, checked_items as i64 - unchecked_items as i64);\n    });\n\n    app.global::<Callabler>().on_validate_regex(|regex_str| {\n        if regex_str.is_empty() {\n            return true;\n        }\n        Regex::new(regex_str.as_str()).is_ok()\n    });\n\n    let a = app.as_weak();\n    app.global::<Callabler>().on_populate_custom_select_columns(move || {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        let columns = custom_select::build_custom_select_columns(active_tab);\n        app.global::<GuiState>().set_custom_select_columns(create_model_from_model_vec(&columns));\n    });\n\n    let a = app.as_weak();\n    app.global::<Callabler>().on_update_custom_select_column(move |idx, enabled, filter_value| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let model = app.global::<GuiState>().get_custom_select_columns();\n        let idx = idx as usize;\n        if let Some(mut col) = model.row_data(idx) {\n            col.enabled = enabled;\n            col.filter_value = filter_value;\n            model.set_row_data(idx, col);\n        }\n    });\n\n    let a = app.as_weak();\n    app.global::<Callabler>()\n        .on_select_items_custom_columns(move |select_mode, case_sensitive, leave_one_in_group| {\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n            let active_tab = app.global::<GuiState>().get_active_tab();\n            let current_model = active_tab.get_tool_model(&app);\n            let columns: Vec<CustomSelectColumnModel> = app.global::<GuiState>().get_custom_select_columns().iter().collect();\n\n            let leave_one_in_group = leave_one_in_group && (active_tab.get_is_header_mode() && !shared_models.lock().expect(\"Lock poisoned\").get_use_reference_folders(active_tab));\n\n            let (checked_items, unchecked_items, new_model) =\n                custom_select::select_custom_columns(&current_model, active_tab, select_mode, &columns, case_sensitive, leave_one_in_group);\n            active_tab.set_tool_model(&app, new_model);\n            change_number_of_enabled_items(&app, active_tab, checked_items as i64 - unchecked_items as i64);\n        });\n}\n\n#[derive(Clone, Copy)]\nenum Property {\n    Size,\n    Date,\n    PathLength,\n    Resolution,\n}\n\npub(crate) fn set_select_buttons(app: &MainWindow) {\n    let active_tab = app.global::<GuiState>().get_active_tab();\n    let mut base_buttons = vec![SelectMode::SelectCustom, SelectMode::SelectAll, SelectMode::UnselectAll, SelectMode::InvertSelection];\n\n    let additional_buttons = match active_tab {\n        ActiveTab::DuplicateFiles | ActiveTab::SimilarVideos | ActiveTab::SimilarMusic => vec![\n            SelectMode::SelectOldest,\n            SelectMode::SelectNewest,\n            SelectMode::SelectTheSmallestSize,\n            SelectMode::SelectTheBiggestSize,\n            SelectMode::SelectShortestPath,\n            SelectMode::SelectLongestPath,\n        ],\n        ActiveTab::SimilarImages => vec![\n            SelectMode::SelectOldest,\n            SelectMode::SelectNewest,\n            SelectMode::SelectTheSmallestSize,\n            SelectMode::SelectTheBiggestSize,\n            SelectMode::SelectTheSmallestResolution,\n            SelectMode::SelectTheBiggestResolution,\n            SelectMode::SelectShortestPath,\n            SelectMode::SelectLongestPath,\n        ],\n        ActiveTab::EmptyFolders\n        | ActiveTab::BigFiles\n        | ActiveTab::EmptyFiles\n        | ActiveTab::TemporaryFiles\n        | ActiveTab::InvalidSymlinks\n        | ActiveTab::BrokenFiles\n        | ActiveTab::BadExtensions\n        | ActiveTab::BadNames\n        | ActiveTab::ExifRemover\n        | ActiveTab::VideoOptimizer\n        | ActiveTab::Settings\n        | ActiveTab::About => Vec::new(), // Not available in settings and about, so may be set any value here\n    };\n\n    base_buttons.extend(additional_buttons);\n    base_buttons.reverse();\n\n    let new_select_model = base_buttons\n        .into_iter()\n        .map(|e| SelectModel {\n            name: translate_select_mode(e),\n            data: e,\n        })\n        .collect::<Vec<_>>();\n\n    app.global::<GuiState>().set_select_results_list(ModelRc::new(VecModel::from(new_select_model)));\n}\n\nfn extract_comparable_field(model: &SingleMainListModel, property: Property, active_tab: ActiveTab) -> u64 {\n    let mut val_ints = model.val_int.iter();\n    let mut val_strs = model.val_str.iter();\n    match property {\n        Property::Size => {\n            let high = val_ints.nth(active_tab.get_int_size_idx()).expect(\"can find file size property\");\n            let low = val_ints.next().expect(\"can find file size property\");\n            connect_i32_into_u64(high, low)\n        }\n        Property::Date => {\n            let high = val_ints.nth(active_tab.get_int_modification_date_idx()).expect(\"can find file last modified property\");\n            let low = val_ints.next().expect(\"can find file last modified property\");\n            connect_i32_into_u64(high, low)\n        }\n        Property::PathLength => val_strs.nth(active_tab.get_str_path_idx()).expect(\"can find file path property\").len() as u64,\n        Property::Resolution => val_ints.nth(active_tab.get_int_pixel_count_idx()).expect(\"can find pixel count proerty\") as u64,\n    }\n}\n\nfn select_by_property(model: &ModelRc<SingleMainListModel>, active_tab: ActiveTab, property: Property, increasing_order: bool) -> SelectionResult {\n    let mut checked_items = 0;\n\n    let is_header_mode = active_tab.get_is_header_mode();\n    assert!(is_header_mode); // non header modes not really have reason to use this function\n\n    let mut old_data = model.iter().collect::<Vec<_>>();\n    let headers_idx = find_header_idx_and_deselect_all(&mut old_data);\n    if increasing_order {\n        for i in 0..(headers_idx.len() - 1) {\n            let mut max_item = 0;\n            let mut max_item_idx = 1;\n            #[expect(clippy::needless_range_loop)]\n            for j in (headers_idx[i] + 1)..headers_idx[i + 1] {\n                let item = extract_comparable_field(&old_data[j], property, active_tab);\n                if item > max_item {\n                    max_item = item;\n                    max_item_idx = j;\n                }\n            }\n            if !old_data[max_item_idx].checked {\n                checked_items += 1;\n            }\n            old_data[max_item_idx].checked = true;\n        }\n    } else {\n        for i in 0..(headers_idx.len() - 1) {\n            let mut min_item = u64::MAX;\n            let mut min_item_idx = 1;\n            #[expect(clippy::needless_range_loop)]\n            for j in (headers_idx[i] + 1)..headers_idx[i + 1] {\n                let item = extract_comparable_field(&old_data[j], property, active_tab);\n                if item < min_item {\n                    min_item = item;\n                    min_item_idx = j;\n                }\n            }\n            if !old_data[min_item_idx].checked {\n                checked_items += 1;\n            }\n            old_data[min_item_idx].checked = true;\n        }\n    }\n\n    (checked_items, 0, ModelRc::new(VecModel::from(old_data)))\n}\n\nfn select_all(model: &ModelRc<SingleMainListModel>) -> SelectionResult {\n    let mut checked_items = 0;\n    let mut old_data = model.iter().collect::<Vec<_>>();\n    for x in &mut old_data {\n        if !x.header_row {\n            if !x.checked {\n                checked_items += 1;\n            }\n            x.checked = true;\n        }\n    }\n    (checked_items, 0, ModelRc::new(VecModel::from(old_data)))\n}\n\nfn deselect_all(model: &ModelRc<SingleMainListModel>) -> SelectionResult {\n    let mut unchecked_items = 0;\n    let mut old_data = model.iter().collect::<Vec<_>>();\n    for x in &mut old_data {\n        if x.checked {\n            unchecked_items += 1;\n        }\n        x.checked = false;\n    }\n    (0, unchecked_items, ModelRc::new(VecModel::from(old_data)))\n}\n\nfn invert_selection(model: &ModelRc<SingleMainListModel>) -> SelectionResult {\n    let mut checked_items = 0;\n    let mut unchecked_items = 0;\n    let mut old_data = model.iter().collect::<Vec<_>>();\n    for x in &mut old_data {\n        if !x.header_row {\n            if x.checked {\n                unchecked_items += 1;\n            } else {\n                checked_items += 1;\n            }\n\n            x.checked = !x.checked;\n        }\n    }\n    (checked_items, unchecked_items, ModelRc::new(VecModel::from(old_data)))\n}\n\nfn find_header_idx_and_deselect_all(old_data: &mut [SingleMainListModel]) -> Vec<usize> {\n    let mut header_idx = old_data\n        .iter()\n        .enumerate()\n        .filter_map(|(idx, m)| if m.header_row { Some(idx) } else { None })\n        .collect::<Vec<_>>();\n    header_idx.push(old_data.len());\n\n    for x in old_data.iter_mut() {\n        if !x.header_row {\n            x.checked = false;\n        }\n    }\n    header_idx\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::common::create_model_from_model_vec;\n    use crate::test_common::get_model_vec;\n\n    #[test]\n    fn find_header_idx_returns_correct_indices_for_headers() {\n        let mut model = get_model_vec(5);\n        model[1].header_row = true;\n        model[3].header_row = true;\n\n        let header_indices = find_header_idx_and_deselect_all(&mut model);\n\n        assert_eq!(header_indices, vec![1, 3, 5]);\n    }\n\n    #[test]\n    fn find_header_idx_marks_all_non_header_rows_as_unchecked() {\n        let mut model = get_model_vec(5);\n        for row in &mut model {\n            row.checked = true;\n        }\n        model[1].header_row = true;\n\n        find_header_idx_and_deselect_all(&mut model);\n\n        assert!(!model[0].checked);\n        assert!(model[1].checked);\n        assert!(!model[2].checked);\n        assert!(!model[3].checked);\n        assert!(!model[4].checked);\n    }\n\n    #[test]\n    fn select_all_marks_all_non_header_rows_as_checked() {\n        let mut model = get_model_vec(5);\n        model[1].header_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        let (checked_items, unchecked_items, new_model) = select_all(&model);\n\n        assert_eq!(checked_items, 4);\n        assert_eq!(unchecked_items, 0);\n        assert!(new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(new_model.row_data(2).unwrap().checked);\n        assert!(new_model.row_data(3).unwrap().checked);\n        assert!(new_model.row_data(4).unwrap().checked);\n    }\n\n    #[test]\n    fn deselect_all_unmarks_all_rows_as_checked() {\n        let mut model = get_model_vec(5);\n        for row in &mut model {\n            row.checked = true;\n        }\n        let model = create_model_from_model_vec(&model);\n\n        let (checked_items, unchecked_items, new_model) = deselect_all(&model);\n\n        assert_eq!(checked_items, 0);\n        assert_eq!(unchecked_items, 5);\n        assert!(!new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(!new_model.row_data(2).unwrap().checked);\n        assert!(!new_model.row_data(3).unwrap().checked);\n        assert!(!new_model.row_data(4).unwrap().checked);\n    }\n\n    #[test]\n    fn invert_selection_toggles_checked_state_for_non_header_rows() {\n        let mut model = get_model_vec(5);\n        model[0].checked = true;\n        model[1].header_row = true;\n        model[2].checked = false;\n        let model = create_model_from_model_vec(&model);\n\n        let (checked_items, unchecked_items, new_model) = invert_selection(&model);\n\n        assert_eq!(checked_items, 3);\n        assert_eq!(unchecked_items, 1);\n        assert!(!new_model.row_data(0).unwrap().checked);\n        assert!(!new_model.row_data(1).unwrap().checked);\n        assert!(new_model.row_data(2).unwrap().checked);\n        assert!(new_model.row_data(3).unwrap().checked);\n        assert!(new_model.row_data(4).unwrap().checked);\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_show_confirmation.rs",
    "content": "use std::sync::{Arc, Mutex};\n\nuse czkawka_core::tools::video_optimizer::VideoOptimizerParameters;\nuse rfd::FileDialog;\nuse slint::ComponentHandle;\n\nuse crate::connect_rfd::{hide_file_dialog_overlay, show_file_dialog_overlay};\nuse crate::model_operations::get_checked_info_from_app;\nuse crate::shared_models::SharedModels;\nuse crate::{MainWindow, PopupRequest, Translations, flk};\n\npub(crate) fn connect_show_confirmation(app: &MainWindow, shared_models: Arc<Mutex<SharedModels>>) {\n    let a = app.as_weak();\n    app.on_request_setup_action_popup(move |popup_request: PopupRequest| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let translation = app.global::<Translations>();\n        let res = get_checked_info_from_app(&app);\n        let mut data = \"\".to_string();\n\n        match popup_request {\n            PopupRequest::Delete => {\n                let mut base = flk!(\"rust_delete_confirmation\");\n                if let Some(group_res) = res.groups_with_checked_items {\n                    base.push_str(\n                        format!(\n                            \"\\n{}\",\n                            flk!(\n                                \"rust_delete_confirmation_number_groups\",\n                                items = res.checked_items_number,\n                                groups = group_res.groups_with_checked_items\n                            )\n                        )\n                        .as_str(),\n                    );\n                    if group_res.number_of_groups_with_all_items_checked > 0 {\n                        base.push_str(\n                            format!(\n                                \"\\n{}\",\n                                flk!(\"rust_delete_confirmation_selected_all_in_group\", groups = group_res.number_of_groups_with_all_items_checked)\n                            )\n                            .as_str(),\n                        );\n                    }\n                } else {\n                    base.push_str(format!(\"\\n{}\", flk!(\"rust_delete_confirmation_number_simple\", items = res.checked_items_number)).as_str());\n                }\n                translation.set_delete_confirmation_text(base.into());\n            }\n            PopupRequest::Move => {\n                let res = get_checked_info_from_app(&app);\n                let checked_items_number = res.checked_items_number;\n\n                show_file_dialog_overlay(&app);\n\n                let weak = a.clone();\n                std::thread::spawn(move || {\n                    let folder = FileDialog::new().pick_folder();\n\n                    hide_file_dialog_overlay(&weak);\n\n                    if let Some(folder) = folder {\n                        let data = folder.to_string_lossy().to_string();\n                        weak.upgrade_in_event_loop(move |app| {\n                            let mut base = flk!(\"rust_move_confirmation\");\n                            base.push_str(format!(\"\\n{}\", flk!(\"rust_move_confirmation_number_simple\", items = checked_items_number)).as_str());\n                            app.global::<Translations>().set_move_confirmation_text(base.into());\n                            app.invoke_show_action_popup(PopupRequest::Move, data.into());\n                        })\n                        .expect(\"Failed to show move popup\");\n                    }\n                });\n                return;\n            }\n            PopupRequest::OptimizeVideo => {\n                let mut base = flk!(\"rust_optimize_video_confirmation\");\n                base.push_str(format!(\"\\n{}\", flk!(\"rust_optimize_video_confirmation_number_simple\", items = res.checked_items_number)).as_str());\n                translation.set_optimize_confirmation_text(base.into());\n\n                let shared_model = shared_models.lock();\n                let shared_model = shared_model.as_ref().expect(\"Failed to lock shared models\");\n                let shared_model = shared_model.shared_video_optimizer_state.as_ref().expect(\"Item should be present for video optimizer\");\n                data = if matches!(shared_model.get_params(), VideoOptimizerParameters::VideoCrop(_)) {\n                    \"crop\".to_string()\n                } else {\n                    \"transcode\".to_string()\n                }\n            }\n            PopupRequest::CleanExif => {\n                let mut base = flk!(\"rust_clean_exif_confirmation\");\n                base.push_str(format!(\"\\n{}\", flk!(\"rust_clean_exif_confirmation_number_simple\", items = res.checked_items_number)).as_str());\n                translation.set_clean_confirmation_text(base.into());\n            }\n            PopupRequest::Symlink => {\n                let mut base = flk!(\"rust_symlink_confirmation\");\n                base.push_str(format!(\"\\n{}\", flk!(\"rust_symlink_confirmation_number_simple\", items = res.checked_items_number)).as_str());\n                translation.set_softlink_confirmation_text(base.into());\n            }\n            PopupRequest::Hardlink => {\n                let mut base = flk!(\"rust_hardlink_confirmation\");\n                base.push_str(format!(\"\\n{}\", flk!(\"rust_hardlink_confirmation_number_simple\", items = res.checked_items_number)).as_str());\n                translation.set_hardlink_confirmation_text(base.into());\n            }\n            PopupRequest::RenameBadExtension | PopupRequest::RenameBadFileName => {\n                let mut base = flk!(\"rust_rename_confirmation\");\n                base.push_str(format!(\"\\n{}\", flk!(\"rust_rename_confirmation_number_simple\", items = res.checked_items_number)).as_str());\n                translation.set_rename_confirmation_text(base.into());\n            }\n            PopupRequest::Save => {\n                // There is no confirmation saving\n            }\n        }\n\n        app.invoke_show_action_popup(popup_request, data.into());\n    });\n}\n"
  },
  {
    "path": "krokiet/src/connect_show_preview.rs",
    "content": "use std::fs::metadata;\nuse std::path::Path;\nuse std::sync::{Arc, Mutex};\n\nuse czkawka_core::common::image::{ImgResizeOptions, check_if_can_display_image, get_dynamic_image_from_path};\nuse czkawka_core::helpers::debug_timer::Timer;\nuse czkawka_core::re_exported::FirFilterType;\nuse image::{DynamicImage, Rgba, RgbaImage};\nuse log::{debug, error};\nuse slint::ComponentHandle;\n\nuse crate::shared_models::SharedModels;\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow, Settings};\n\npub type ImageBufferRgba = image::ImageBuffer<image::Rgba<u8>, Vec<u8>>;\n\npub(crate) fn connect_show_preview(app: &MainWindow, shared_models: Arc<Mutex<SharedModels>>) {\n    let a = app.as_weak();\n    app.global::<Callabler>()\n        .on_load_image_preview(move |image_path, crop_left, crop_top, crop_right, crop_bottom, orig_width, orig_height| {\n            let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n\n            let settings = app.global::<Settings>();\n            let gui_state = app.global::<GuiState>();\n\n            let active_tab = gui_state.get_active_tab();\n\n            if !((active_tab == ActiveTab::SimilarImages && settings.get_similar_images_show_image_preview())\n                || (active_tab == ActiveTab::DuplicateFiles && settings.get_duplicate_image_preview())\n                || ((active_tab == ActiveTab::SimilarVideos || active_tab == ActiveTab::VideoOptimizer) && settings.get_video_thumbnails_preview()))\n            {\n                set_preview_visible(&gui_state, None);\n                return;\n            }\n\n            if !check_if_can_display_image(&image_path) {\n                set_preview_visible(&gui_state, None);\n                return;\n            }\n\n            // Video Thumbnails files can be empty if generation failed or thumbnails are disabled\n            if metadata(&image_path).is_ok_and(|m| m.len() == 0) {\n                set_preview_visible(&gui_state, None);\n                return;\n            }\n\n            // Do not load the same image again\n            if image_path == gui_state.get_preview_image_path() {\n                return;\n            }\n\n            let path = Path::new(image_path.as_str());\n\n            let images_in_thumbnails_line = if active_tab == ActiveTab::VideoOptimizer {\n                shared_models\n                    .lock()\n                    .expect(\"Failed to lock model mutex\")\n                    .shared_video_optimizer_state\n                    .as_ref()\n                    .map_or(1, |state| state.get_params().get_generate_number_of_items_in_thumbnail_grid())\n            } else {\n                1\n            };\n\n            if let Some((mut timer, img)) = load_image(path) {\n                let mut img_to_use = img.into_rgba8();\n\n                if crop_left != -1 && crop_top != -1 && crop_right != -1 && crop_bottom != -1 && orig_width > 0 && orig_height > 0 {\n                    img_to_use = draw_crop_rectangle_on_image(\n                        img_to_use,\n                        crop_left,\n                        crop_top,\n                        crop_right,\n                        crop_bottom,\n                        orig_width as u32,\n                        orig_height as u32,\n                        images_in_thumbnails_line as u32,\n                    );\n                    timer.checkpoint(\"cropping image\");\n                }\n\n                let slint_image = convert_into_slint_image(&img_to_use);\n                timer.checkpoint(\"converting image to Slint image\");\n\n                gui_state.set_preview_image(slint_image);\n                timer.checkpoint(\"setting image in GUI\");\n\n                debug!(\"{}\", timer.report(\"total\", true));\n                set_preview_visible(&gui_state, Some(image_path.as_str()));\n            } else {\n                set_preview_visible(&gui_state, None);\n            }\n        });\n}\n\nfn set_preview_visible(gui_state: &GuiState, preview: Option<&str>) {\n    if let Some(preview) = preview {\n        gui_state.set_preview_image_path(preview.into());\n        gui_state.set_preview_visible(true);\n    } else {\n        gui_state.set_preview_image_path(\"\".into());\n        gui_state.set_preview_visible(false);\n    }\n}\n\nfn convert_into_slint_image(img: &RgbaImage) -> slint::Image {\n    let buffer = slint::SharedPixelBuffer::<slint::Rgba8Pixel>::clone_from_slice(img.as_raw(), img.width(), img.height());\n    slint::Image::from_rgba8(buffer)\n}\n\nfn load_image(image_path: &Path) -> Option<(Timer, DynamicImage)> {\n    if !image_path.is_file() {\n        return None;\n    }\n\n    let mut debug_timer = Timer::new(\"Loading and converting image in slint\");\n\n    let img = match get_dynamic_image_from_path(\n        &image_path.to_string_lossy(),\n        Some(ImgResizeOptions {\n            max_width: 1024,\n            max_height: 1024,\n            filter: FirFilterType::Bilinear,\n        }),\n    ) {\n        Ok(img) => img.image,\n        Err(e) => {\n            error!(\"Failed to load image \\\"{}\\\": {e}\", image_path.to_string_lossy());\n            return None;\n        }\n    };\n\n    debug_timer.checkpoint(\"loading image\");\n\n    Some((debug_timer, img))\n}\n\nfn draw_crop_rectangle_on_image(\n    mut buf: ImageBufferRgba,\n    crop_left: i32,\n    crop_top: i32,\n    crop_right: i32,\n    crop_bottom: i32,\n    original_width: u32,\n    _original_height: u32,\n    images_in_thumbnails_line: u32,\n) -> ImageBufferRgba {\n    let width = buf.width() / images_in_thumbnails_line;\n    let height = buf.height() / images_in_thumbnails_line;\n\n    let scale_factor = original_width as f32 / width as f32;\n\n    let crop_left = (crop_left as f32 / scale_factor).round() as i32;\n    let crop_top = (crop_top as f32 / scale_factor).round() as i32;\n    let crop_right = (crop_right as f32 / scale_factor).round() as i32;\n    let crop_bottom = (crop_bottom as f32 / scale_factor).round() as i32;\n\n    let l = (crop_left.max(0) as u32).min(width.saturating_sub(1));\n    let t = (crop_top.max(0) as u32).min(height.saturating_sub(1));\n    let r = (crop_right.max(0) as u32).min(width.saturating_sub(1));\n    let b = (crop_bottom.max(0) as u32).min(height.saturating_sub(1));\n\n    if l > r || t > b {\n        return buf;\n    }\n\n    let thickness = (width.max(height) / 100 * images_in_thumbnails_line).max(2);\n\n    for x_im in 0..images_in_thumbnails_line {\n        for y_im in 0..images_in_thumbnails_line {\n            for side in [-1, 1] {\n                for th in 0..(thickness as i32 / 2) {\n                    let th_val = side * th;\n\n                    let top_y = (t as i32 + th_val) as u32;\n                    let bottom_y = (b as i32 - th_val) as u32;\n                    let left_x = (l as i32) as u32;\n                    let right_x = (r as i32) as u32;\n\n                    for x in left_x..=right_x {\n                        for y in [top_y, bottom_y] {\n                            if (0..height).contains(&y) && (0..width).contains(&x) {\n                                buf.put_pixel(x + x_im * width, y + y_im * height, get_pixel_color(x, y));\n                            }\n                        }\n                    }\n\n                    let top_y = (t as i32) as u32;\n                    let bottom_y = (b as i32) as u32;\n                    let left_x = (l as i32 + th_val) as u32;\n                    let right_x = (r as i32 - th_val) as u32;\n\n                    for y in top_y..=bottom_y {\n                        for x in [left_x, right_x] {\n                            if (0..height).contains(&y) && (0..width).contains(&x) {\n                                buf.put_pixel(x + x_im * width, y + y_im * height, get_pixel_color(x, y));\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    buf\n}\n\n#[inline]\nfn get_pixel_color(x: u32, y: u32) -> Rgba<u8> {\n    match (x + y) % 9 {\n        0 => Rgba([127u8, 0u8, 0u8, 255u8]),\n        1 => Rgba([0u8, 127u8, 0u8, 255u8]),\n        2 => Rgba([0u8, 0u8, 127u8, 255u8]),\n        3 => Rgba([255u8, 255u8, 0u8, 255u8]),\n        4 => Rgba([0u8, 255u8, 255u8, 255u8]),\n        5 => Rgba([255u8, 0u8, 255u8, 255u8]),\n        6 => Rgba([255u8, 255u8, 255u8, 255u8]),\n        7 => Rgba([128u8, 0u8, 128u8, 255u8]),\n        8 => Rgba([0u8, 0u8, 0u8, 255u8]),\n        _ => unreachable!(\"Modulo 9 should always be in 0..8\"),\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_sort.rs",
    "content": "use std::mem;\n\nuse slint::{ComponentHandle, Model, ModelRc, VecModel};\n\nuse crate::common::{SortIdx, connect_i32_into_u64};\nuse crate::connect_row_selection::recalculate_small_selection_if_needed;\nuse crate::connect_translation::translate_sort_mode;\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow, SingleMainListModel, SortColumnMode, SortMode, SortModel};\n\npub(crate) fn connect_sort_column(app: &MainWindow) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_change_sort_column_mode(move |sort_column_mode, column_idx| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        let model = active_tab.get_tool_model(&app);\n\n        let idx = active_tab.get_str_int_sort_idx(column_idx);\n        let new_model = match idx {\n            SortIdx::StrIdx(str_idx) => {\n                let sort_function = |e: &SingleMainListModel| {\n                    e.val_str\n                        .iter()\n                        .nth(str_idx as usize)\n                        .unwrap_or_else(|| panic!(\"Failed to get str index - {str_idx} on {} items\", e.val_str.iter().count()))\n                };\n\n                common_sort_function(&model, active_tab, sort_function, sort_column_mode == SortColumnMode::Descending)\n            }\n            SortIdx::IntIdx(int_idx) => {\n                let sort_function = |e: &SingleMainListModel| {\n                    e.val_int\n                        .iter()\n                        .nth(int_idx as usize)\n                        .unwrap_or_else(|| panic!(\"Failed to get int index - {int_idx} on {} items\", e.val_int.iter().count()))\n                };\n\n                common_sort_function(&model, active_tab, sort_function, sort_column_mode == SortColumnMode::Descending)\n            }\n            SortIdx::IntIdxPair(int_idx1, int_idx2) => {\n                let sort_function = |e: &SingleMainListModel| {\n                    let items = e.val_int.iter().collect::<Vec<_>>();\n                    connect_i32_into_u64(items[int_idx1 as usize], items[int_idx2 as usize])\n                };\n\n                common_sort_function(&model, active_tab, sort_function, sort_column_mode == SortColumnMode::Descending)\n            }\n            SortIdx::Selection => {\n                if sort_column_mode == SortColumnMode::Ascending {\n                    let sort_function = |e: &SingleMainListModel| e.checked;\n                    common_sort_function(&model, active_tab, sort_function, false)\n                } else {\n                    let sort_function = |e: &SingleMainListModel| !e.checked;\n                    common_sort_function(&model, active_tab, sort_function, false)\n                }\n            }\n        };\n\n        active_tab.set_tool_model(&app, new_model);\n    });\n}\n\npub(crate) fn connect_sort(app: &MainWindow) {\n    set_sort_buttons(app);\n\n    let a = app.as_weak();\n    app.global::<Callabler>().on_sort_items(move |sort_mode| {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        let current_model = active_tab.get_tool_model(&app);\n\n        let new_model = match sort_mode {\n            SortMode::FullName => sorts::sort_by_full_name(&current_model, active_tab),\n            SortMode::Selection => sorts::sort_selection(&current_model, active_tab),\n            SortMode::Reverse => sorts::reverse_sort(&current_model, active_tab),\n        };\n\n        active_tab.set_tool_model(&app, new_model);\n    });\n}\n\npub(crate) fn set_sort_buttons(app: &MainWindow) {\n    let mut base_buttons = vec![SortMode::FullName, SortMode::Reverse, SortMode::Selection];\n    base_buttons.reverse();\n\n    let new_sort_model = base_buttons\n        .into_iter()\n        .map(|e| SortModel {\n            name: translate_sort_mode(e),\n            data: e,\n        })\n        .collect::<Vec<_>>();\n\n    app.global::<GuiState>().set_sort_results_list(ModelRc::new(VecModel::from(new_sort_model)));\n}\n\nmod sorts {\n    use super::{\n        ActiveTab, Model, ModelRc, SingleMainListModel, VecModel, common_sort_function, convert_group_header_into_rc_model, group_by_header, recalculate_small_selection_if_needed,\n    };\n\n    pub(super) fn reverse_sort(model: &ModelRc<SingleMainListModel>, active_tab: ActiveTab) -> ModelRc<SingleMainListModel> {\n        if !active_tab.get_is_header_mode() {\n            let mut items = model.iter().collect::<Vec<_>>();\n            items.reverse();\n            let new_model = ModelRc::new(VecModel::from(items));\n            recalculate_small_selection_if_needed(&new_model, active_tab);\n            return new_model;\n        }\n\n        let mut grouped_items = group_by_header(model);\n        for (_, items) in &mut grouped_items {\n            items.reverse();\n        }\n\n        let new_model = convert_group_header_into_rc_model(grouped_items, model.row_count());\n        recalculate_small_selection_if_needed(&new_model, active_tab);\n        new_model\n    }\n\n    pub(super) fn sort_selection(model: &ModelRc<SingleMainListModel>, active_tab: ActiveTab) -> ModelRc<SingleMainListModel> {\n        let sort_function = |e: &SingleMainListModel| !e.selected_row;\n\n        common_sort_function(model, active_tab, sort_function, false)\n    }\n\n    pub(super) fn sort_by_full_name(model: &ModelRc<SingleMainListModel>, active_tab: ActiveTab) -> ModelRc<SingleMainListModel> {\n        let sort_function = |e: &SingleMainListModel| {\n            let name_idx = active_tab.get_str_name_idx();\n            let path_idx = active_tab.get_str_path_idx();\n            let items = e.val_str.iter().collect::<Vec<_>>();\n            format!(\"{}/{}\", items[path_idx], items[name_idx])\n        };\n\n        common_sort_function(model, active_tab, sort_function, false)\n    }\n}\n\nfn common_sort_function<T: Ord>(\n    model: &ModelRc<SingleMainListModel>,\n    active_tab: ActiveTab,\n    sort_function: impl Fn(&SingleMainListModel) -> T,\n    reverse: bool,\n) -> ModelRc<SingleMainListModel> {\n    if !active_tab.get_is_header_mode() {\n        let mut items = model.iter().collect::<Vec<_>>();\n        items.sort_by_cached_key(&sort_function);\n        if reverse {\n            items.reverse();\n        }\n        let new_model = ModelRc::new(VecModel::from(items));\n        recalculate_small_selection_if_needed(&new_model, active_tab);\n        return new_model;\n    }\n\n    let mut grouped_items = group_by_header(model);\n    for (_, items) in &mut grouped_items {\n        items.sort_by_cached_key(&sort_function);\n        if reverse {\n            items.reverse();\n        }\n    }\n\n    let new_model = convert_group_header_into_rc_model(grouped_items, model.row_count());\n    recalculate_small_selection_if_needed(&new_model, active_tab);\n    new_model\n}\n\nfn convert_group_header_into_rc_model(grouped: Vec<(SingleMainListModel, Vec<SingleMainListModel>)>, model_size: usize) -> ModelRc<SingleMainListModel> {\n    let mut items = Vec::with_capacity(model_size);\n    for (header, group) in grouped {\n        items.push(header);\n        items.extend(group);\n    }\n    ModelRc::new(VecModel::from(items))\n}\n\nfn group_by_header(model: &ModelRc<SingleMainListModel>) -> Vec<(SingleMainListModel, Vec<SingleMainListModel>)> {\n    let mut grouped_items: Vec<(SingleMainListModel, Vec<SingleMainListModel>)> = Vec::new();\n\n    let mut current_header: Option<SingleMainListModel> = None;\n    let mut current_group: Vec<SingleMainListModel> = Vec::new();\n    for item in model.iter() {\n        if item.header_row {\n            if let Some(header) = current_header.take() {\n                assert!(!current_group.is_empty());\n                grouped_items.push((header, mem::take(&mut current_group)));\n            } else {\n                assert!(current_group.is_empty());\n            }\n            current_header = Some(item.clone());\n        } else {\n            assert!(current_header.is_some());\n            current_group.push(item.clone());\n        }\n    }\n\n    if let Some(header) = current_header {\n        assert!(!current_group.is_empty());\n        grouped_items.push((header, current_group));\n    } else {\n        assert!(current_group.is_empty());\n    }\n\n    grouped_items\n}\n\n#[cfg(test)]\nmod tests {\n    use slint::Model;\n\n    use crate::common::create_model_from_model_vec;\n    use crate::connect_row_selection::initialize_selection_struct;\n    use crate::connect_sort::sorts::{reverse_sort, sort_by_full_name, sort_selection};\n    use crate::connect_sort::{convert_group_header_into_rc_model, group_by_header};\n    use crate::test_common::get_model_vec;\n    use crate::{ActiveTab, SingleMainListModel};\n\n    #[test]\n    fn group_by_header_splits_items_into_groups_correctly() {\n        initialize_selection_struct();\n        let mut model = get_model_vec(6);\n        model[0].header_row = true;\n        model[1].header_row = false;\n        model[2].header_row = false;\n        model[3].header_row = true;\n        model[4].header_row = false;\n        model[5].header_row = false;\n        let model = create_model_from_model_vec(&model);\n\n        let grouped = group_by_header(&model);\n\n        assert_eq!(grouped.len(), 2);\n        assert_eq!(grouped[0].0, model.row_data(0).unwrap());\n        assert_eq!(grouped[0].1.len(), 2);\n        assert_eq!(grouped[0].1[0], model.row_data(1).unwrap());\n        assert_eq!(grouped[0].1[1], model.row_data(2).unwrap());\n        assert_eq!(grouped[1].0, model.row_data(3).unwrap());\n        assert_eq!(grouped[1].1.len(), 2);\n        assert_eq!(grouped[1].1[0], model.row_data(4).unwrap());\n        assert_eq!(grouped[1].1[1], model.row_data(5).unwrap());\n    }\n\n    #[test]\n    fn group_by_header_handles_empty_model() {\n        initialize_selection_struct();\n        let model = create_model_from_model_vec(&[]);\n\n        let grouped = group_by_header(&model);\n\n        assert!(grouped.is_empty());\n    }\n\n    #[test]\n    #[should_panic]\n    fn group_by_header_panics_when_no_header_before_items() {\n        initialize_selection_struct();\n        let mut model = get_model_vec(3);\n        model[0].header_row = false;\n        model[1].header_row = false;\n        model[2].header_row = false;\n        let model = create_model_from_model_vec(&model);\n\n        group_by_header(&model);\n    }\n\n    #[test]\n    #[should_panic]\n    fn group_by_header_panics_when_group_is_empty() {\n        initialize_selection_struct();\n        let mut model = get_model_vec(3);\n        model[0].header_row = true;\n        model[1].header_row = true;\n        model[2].header_row = true;\n        let model = create_model_from_model_vec(&model);\n\n        group_by_header(&model);\n    }\n\n    #[test]\n    fn convert_group_header_into_rc_model_combines_groups_correctly() {\n        initialize_selection_struct();\n        let mut model = get_model_vec(6);\n        model[0].header_row = true;\n        model[1].header_row = false;\n        model[2].header_row = false;\n        model[3].header_row = true;\n        model[4].header_row = false;\n        model[5].header_row = false;\n\n        let grouped = vec![\n            (model[0].clone(), vec![model[1].clone(), model[2].clone()]),\n            (model[3].clone(), vec![model[4].clone(), model[5].clone()]),\n        ];\n\n        let combined_model = convert_group_header_into_rc_model(grouped, model.len());\n\n        assert_eq!(combined_model.row_count(), 6);\n        assert_eq!(combined_model.row_data(0).unwrap(), model[0]);\n        assert_eq!(combined_model.row_data(1).unwrap(), model[1]);\n        assert_eq!(combined_model.row_data(2).unwrap(), model[2]);\n        assert_eq!(combined_model.row_data(3).unwrap(), model[3]);\n        assert_eq!(combined_model.row_data(4).unwrap(), model[4]);\n        assert_eq!(combined_model.row_data(5).unwrap(), model[5]);\n    }\n\n    #[test]\n    fn convert_group_header_into_rc_model_handles_empty_groups() {\n        initialize_selection_struct();\n        let grouped: Vec<(SingleMainListModel, Vec<SingleMainListModel>)> = Vec::new();\n\n        let combined_model = convert_group_header_into_rc_model(grouped, 0);\n\n        assert_eq!(combined_model.row_count(), 0);\n    }\n\n    #[test]\n    fn sort_by_full_name_sorts_flat_model_correctly() {\n        initialize_selection_struct();\n        let active_tab = ActiveTab::BigFiles;\n        // To be sure that we set correct values in val_int, which must be equal to index\n        assert_eq!(active_tab.get_str_name_idx(), 1);\n        assert_eq!(active_tab.get_str_path_idx(), 2);\n        assert!(!active_tab.get_is_header_mode());\n\n        let mut model = get_model_vec(5);\n        model[0].val_str = create_model_from_model_vec(&[\"\".into(), \"E\".into(), \"A\".into()]);\n        model[1].val_str = create_model_from_model_vec(&[\"\".into(), \"D\".into(), \"B\".into()]);\n        model[2].val_str = create_model_from_model_vec(&[\"\".into(), \"A\".into(), \"C\".into()]);\n        model[3].val_str = create_model_from_model_vec(&[\"\".into(), \"A\".into(), \"D\".into()]);\n        model[4].val_str = create_model_from_model_vec(&[\"\".into(), \"F\".into(), \"B\".into()]);\n        let model = create_model_from_model_vec(&model);\n\n        let sorted_model = sort_by_full_name(&model, active_tab);\n\n        assert_eq!(sorted_model.row_data(0).unwrap().val_str.iter().skip(1).collect::<Vec<_>>(), vec![\"E\", \"A\"]);\n        assert_eq!(sorted_model.row_data(1).unwrap().val_str.iter().skip(1).collect::<Vec<_>>(), vec![\"D\", \"B\"]);\n        assert_eq!(sorted_model.row_data(2).unwrap().val_str.iter().skip(1).collect::<Vec<_>>(), vec![\"F\", \"B\"]);\n        assert_eq!(sorted_model.row_data(3).unwrap().val_str.iter().skip(1).collect::<Vec<_>>(), vec![\"A\", \"C\"]);\n        assert_eq!(sorted_model.row_data(4).unwrap().val_str.iter().skip(1).collect::<Vec<_>>(), vec![\"A\", \"D\"]);\n    }\n\n    #[test]\n    fn sort_by_selection_sorts_flat_model_correctly() {\n        initialize_selection_struct();\n        let active_tab = ActiveTab::BigFiles;\n        // To be sure that we set correct values in val_int, which must be equal to index\n        assert!(!active_tab.get_is_header_mode());\n\n        let mut model = get_model_vec(4);\n        model[0].selected_row = true;\n        model[0].val_int = create_model_from_model_vec(&[15]);\n        model[1].selected_row = false;\n        model[1].val_int = create_model_from_model_vec(&[14]);\n        model[2].selected_row = true;\n        model[2].val_int = create_model_from_model_vec(&[9]);\n        model[3].selected_row = false;\n        model[3].val_int = create_model_from_model_vec(&[29]);\n        let model = create_model_from_model_vec(&model);\n\n        let sorted_model = sort_selection(&model, active_tab);\n\n        assert!(sorted_model.row_data(0).unwrap().selected_row);\n        assert_eq!(sorted_model.row_data(0).unwrap().val_int.iter().collect::<Vec<_>>(), vec![15]);\n        assert!(sorted_model.row_data(1).unwrap().selected_row);\n        assert_eq!(sorted_model.row_data(1).unwrap().val_int.iter().collect::<Vec<_>>(), vec![9]);\n        assert!(!sorted_model.row_data(2).unwrap().selected_row);\n        assert_eq!(sorted_model.row_data(2).unwrap().val_int.iter().collect::<Vec<_>>(), vec![14]);\n        assert!(!sorted_model.row_data(3).unwrap().selected_row);\n        assert_eq!(sorted_model.row_data(3).unwrap().val_int.iter().collect::<Vec<_>>(), vec![29]);\n    }\n\n    #[test]\n    fn sort_reverse_sorts_flat_model_correctly() {\n        initialize_selection_struct();\n        let active_tab = ActiveTab::BigFiles;\n        // To be sure that we set correct values in val_int, which must be equal to index\n        assert_eq!(active_tab.get_int_modification_date_idx(), 0);\n        assert!(!active_tab.get_is_header_mode());\n\n        let mut model = get_model_vec(5);\n        model[0].val_int = create_model_from_model_vec(&[9, 9]);\n        model[1].val_int = create_model_from_model_vec(&[9, 10]);\n        model[2].val_int = create_model_from_model_vec(&[14, 50]);\n        model[3].val_int = create_model_from_model_vec(&[15, 17]);\n        model[4].val_int = create_model_from_model_vec(&[29, 0]);\n        let model = create_model_from_model_vec(&model);\n\n        let sorted_model = reverse_sort(&model, active_tab);\n\n        assert_eq!(sorted_model.row_data(0).unwrap().val_int.iter().collect::<Vec<_>>(), vec![29, 0]);\n        assert_eq!(sorted_model.row_data(1).unwrap().val_int.iter().collect::<Vec<_>>(), vec![15, 17]);\n        assert_eq!(sorted_model.row_data(2).unwrap().val_int.iter().collect::<Vec<_>>(), vec![14, 50]);\n        assert_eq!(sorted_model.row_data(3).unwrap().val_int.iter().collect::<Vec<_>>(), vec![9, 10]);\n        assert_eq!(sorted_model.row_data(4).unwrap().val_int.iter().collect::<Vec<_>>(), vec![9, 9]);\n    }\n}\n"
  },
  {
    "path": "krokiet/src/connect_stop.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crate::MainWindow;\n\npub(crate) fn connect_stop_button(app: &MainWindow, stop_sender: Arc<AtomicBool>) {\n    app.on_scan_stopping(move || {\n        stop_sender.store(true, std::sync::atomic::Ordering::Relaxed);\n    });\n}\n"
  },
  {
    "path": "krokiet/src/connect_tab_changed.rs",
    "content": "use std::sync::mpsc;\n\nuse slint::ComponentHandle;\n\nuse crate::create_calculate_task_size::SizeCountResult;\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow};\npub(crate) fn connect_tab_changed(app: &MainWindow, cache_task_sender: mpsc::Sender<std::sync::mpsc::Sender<SizeCountResult>>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_tab_changed(move || {\n        let cache_task_sender = cache_task_sender.clone();\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        crate::connect_select::set_select_buttons(&app);\n        crate::connect_sort::set_sort_buttons(&app);\n\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        if active_tab != ActiveTab::Settings {\n            return;\n        }\n\n        crate::create_calculate_task_size::request_and_update_cache_sizes(a.clone(), cache_task_sender);\n    });\n}\n"
  },
  {
    "path": "krokiet/src/connect_translation.rs",
    "content": "use czkawka_core::TOOLS_NUMBER;\nuse i18n_embed::DesktopLanguageRequester;\nuse i18n_embed::unic_langid::LanguageIdentifier;\nuse log::{error, info};\nuse slint::{ComponentHandle, ModelRc, SharedString, VecModel};\n\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow, SelectMode, Settings, SortMode, SortModel, Translations, flk, localizer_krokiet};\n\npub struct Language {\n    pub long_name: &'static str,\n    pub short_name: &'static str,\n}\n\npub const LANGUAGE_LIST: &[Language] = &[\n    Language {\n        long_name: \"English\",\n        short_name: \"en\",\n    },\n    Language {\n        long_name: \"Polski (Polish)\",\n        short_name: \"pl\",\n    },\n    Language {\n        long_name: \"Français (French)\",\n        short_name: \"fr\",\n    },\n    Language {\n        long_name: \"Italiano (Italian)\",\n        short_name: \"it\",\n    },\n    Language {\n        long_name: \"Русский (Russian)\",\n        short_name: \"ru\",\n    },\n    Language {\n        long_name: \"український (Ukrainian)\",\n        short_name: \"uk\",\n    },\n    Language {\n        long_name: \"한국어 (Korean)\",\n        short_name: \"ko\",\n    },\n    Language {\n        long_name: \"Česky (Czech)\",\n        short_name: \"cs\",\n    },\n    Language {\n        long_name: \"Deutsch (German)\",\n        short_name: \"de\",\n    },\n    Language {\n        long_name: \"日本語 (Japanese)\",\n        short_name: \"ja\",\n    },\n    Language {\n        long_name: \"Português (Portuguese)\",\n        short_name: \"pt-PT\",\n    },\n    Language {\n        long_name: \"Português Brasileiro (Brazilian Portuguese)\",\n        short_name: \"pt-BR\",\n    },\n    Language {\n        long_name: \"简体中文 (Simplified Chinese)\",\n        short_name: \"zh-CN\",\n    },\n    Language {\n        long_name: \"繁體中文 (Traditional Chinese)\",\n        short_name: \"zh-TW\",\n    },\n    Language {\n        long_name: \"Español (Spanish)\",\n        short_name: \"es-ES\",\n    },\n    Language {\n        long_name: \"Norsk (Norwegian)\",\n        short_name: \"no\",\n    },\n    Language {\n        long_name: \"Svenska (Swedish)\",\n        short_name: \"sv-SE\",\n    },\n    Language {\n        long_name: \"العربية (Arabic)\",\n        short_name: \"ar\",\n    },\n    Language {\n        long_name: \"Български (Bulgarian)\",\n        short_name: \"bg\",\n    },\n    Language {\n        long_name: \"Ελληνικά (Greek)\",\n        short_name: \"el\",\n    },\n    Language {\n        long_name: \"Nederlands (Dutch)\",\n        short_name: \"nl\",\n    },\n    Language {\n        long_name: \"Română (Romanian)\",\n        short_name: \"ro\",\n    },\n    Language {\n        long_name: \"Türkçe (Turkish)\",\n        short_name: \"tr\",\n    },\n];\n\npub(crate) fn connect_translations(app: &MainWindow) {\n    change_language(app);\n\n    let a = app.as_weak();\n    app.global::<Callabler>().on_changed_language(move || {\n        let app = a.upgrade().unwrap();\n        change_language(&app);\n    });\n}\n\npub fn find_the_closest_language_idx_to_system() -> usize {\n    let requested_languages = DesktopLanguageRequester::requested_languages();\n\n    if let Some(language) = requested_languages.first() {\n        let strip_function = |s: &str| s.chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>();\n\n        let system_language = strip_function(&language.to_string());\n        info!(\"System language: {system_language}\");\n        for (idx, lang) in LANGUAGE_LIST.iter().enumerate() {\n            let lang_short = strip_function(lang.short_name);\n            info!(\"Language: {}\", lang.short_name);\n            if system_language == lang_short {\n                return idx;\n            }\n        }\n    }\n    0\n}\n\npub(crate) fn change_language(app: &MainWindow) {\n    let localizers = vec![\n        (\"czkawka_core\", czkawka_core::localizer_core::localizer_core()),\n        (\"krokiet\", localizer_krokiet::localizer_krokiet()),\n    ];\n\n    let lang = app.global::<Settings>().get_language_index();\n    let lang_items = &LANGUAGE_LIST[lang as usize];\n\n    let lang_identifier = vec![LanguageIdentifier::from_bytes(lang_items.short_name.as_bytes()).expect(\"Failed to create LanguageIdentifier\")];\n    for (lib, localizer) in localizers {\n        if let Err(error) = localizer.select(&lang_identifier) {\n            error!(\"Error while loading languages for {lib} {error:?}\");\n        }\n    }\n\n    translate_items(app);\n}\n\n// To generate this, check misc folder\n// This is ugly workaround for missing fluent language support in slint\nfn translate_items(app: &MainWindow) {\n    let translation = app.global::<Translations>();\n    let settings = app.global::<Settings>();\n\n    translation.set_ok_button_text(flk!(\"ok_button\").into());\n    translation.set_cancel_button_text(flk!(\"cancel_button\").into());\n    translation.set_do_you_want_to_continue_text(flk!(\"do_you_want_to_continue\").into());\n    translation.set_main_window_title_text(flk!(\"main_window_title\").into());\n    translation.set_file_dialog_open_text(flk!(\"file_dialog_open\").into());\n    translation.set_scan_button_text(flk!(\"scan_button\").into());\n    translation.set_stop_button_text(flk!(\"stop_button\").into());\n    translation.set_select_button_text(flk!(\"select_button\").into());\n    translation.set_move_button_text(flk!(\"move_button\").into());\n    translation.set_delete_button_text(flk!(\"delete_button\").into());\n    translation.set_save_button_text(flk!(\"save_button\").into());\n    translation.set_sort_button_text(flk!(\"sort_button\").into());\n    translation.set_rename_button_text(flk!(\"rename_button\").into());\n    translation.set_motto_text(flk!(\"motto\").into());\n    translation.set_unicorn_text(flk!(\"unicorn\").into());\n    translation.set_repository_text(flk!(\"repository\").into());\n    translation.set_instruction_text(flk!(\"instruction\").into());\n    translation.set_donation_text(flk!(\"donation\").into());\n    translation.set_translation_text(flk!(\"translation\").into());\n    translation.set_included_paths_text(flk!(\"included_paths\").into());\n    translation.set_excluded_paths_text(flk!(\"excluded_paths\").into());\n    translation.set_ref_text(flk!(\"ref\").into());\n    translation.set_path_text(flk!(\"path\").into());\n    translation.set_tool_duplicate_files_text(flk!(\"tool_duplicate_files\").into());\n    translation.set_tool_empty_folders_text(flk!(\"tool_empty_folders\").into());\n    translation.set_tool_big_files_text(flk!(\"tool_big_files\").into());\n    translation.set_tool_empty_files_text(flk!(\"tool_empty_files\").into());\n    translation.set_tool_temporary_files_text(flk!(\"tool_temporary_files\").into());\n    translation.set_tool_similar_images_text(flk!(\"tool_similar_images\").into());\n    translation.set_tool_similar_videos_text(flk!(\"tool_similar_videos\").into());\n    translation.set_tool_music_duplicates_text(flk!(\"tool_music_duplicates\").into());\n    translation.set_tool_invalid_symlinks_text(flk!(\"tool_invalid_symlinks\").into());\n    translation.set_tool_broken_files_text(flk!(\"tool_broken_files\").into());\n    translation.set_tool_bad_extensions_text(flk!(\"tool_bad_extensions\").into());\n    translation.set_tool_exif_remover_text(flk!(\"tool_exif_remover\").into());\n    translation.set_tool_video_optimizer_text(flk!(\"tool_video_optimizer\").into());\n    translation.set_tool_bad_names_text(flk!(\"tool_bad_names\").into());\n    translation.set_sort_by_full_name_text(flk!(\"sort_by_full_name\").into());\n    translation.set_sort_by_selection_text(flk!(\"sort_by_selection\").into());\n    translation.set_sort_reverse_text(flk!(\"sort_reverse\").into());\n    translation.set_settings_dark_theme_text(flk!(\"settings_dark_theme\").into());\n    translation.set_settings_show_only_icons_text(flk!(\"settings_show_only_icons\").into());\n    translation.set_settings_global_settings_text(flk!(\"settings_global_settings\").into());\n    translation.set_selection_all_text(flk!(\"selection_all\").into());\n    translation.set_selection_deselect_all_text(flk!(\"selection_deselect_all\").into());\n    translation.set_stage_current_text(flk!(\"stage_current\").into());\n    translation.set_stage_all_text(flk!(\"stage_all\").into());\n    translation.set_subsettings_text(flk!(\"subsettings\").into());\n    translation.set_subsettings_images_hash_size_text(flk!(\"subsettings_images_hash_size\").into());\n    translation.set_subsettings_images_resize_algorithm_text(flk!(\"subsettings_images_resize_algorithm\").into());\n    translation.set_subsettings_images_ignore_same_size_text(flk!(\"subsettings_images_ignore_same_size\").into());\n    translation.set_subsettings_images_max_difference_text(flk!(\"subsettings_images_max_difference\").into());\n    translation.set_subsettings_images_duplicates_hash_type_text(flk!(\"subsettings_images_duplicates_hash_type\").into());\n    translation.set_subsettings_duplicates_check_method_text(flk!(\"subsettings_duplicates_check_method\").into());\n    translation.set_subsettings_duplicates_name_case_sensitive_text(flk!(\"subsettings_duplicates_name_case_sensitive\").into());\n    translation.set_subsettings_biggest_files_sub_method_text(flk!(\"subsettings_biggest_files_sub_method\").into());\n    translation.set_subsettings_biggest_files_sub_number_of_files_text(flk!(\"subsettings_biggest_files_sub_number_of_files\").into());\n    translation.set_subsettings_videos_max_difference_text(flk!(\"subsettings_videos_max_difference\").into());\n    translation.set_subsettings_videos_ignore_same_size_text(flk!(\"subsettings_videos_ignore_same_size\").into());\n    translation.set_subsettings_music_audio_check_type_text(flk!(\"subsettings_music_audio_check_type\").into());\n    translation.set_subsettings_music_approximate_comparison_text(flk!(\"subsettings_music_approximate_comparison\").into());\n    translation.set_subsettings_music_compared_tags_text(flk!(\"subsettings_music_compared_tags\").into());\n    translation.set_subsettings_music_title_text(flk!(\"subsettings_music_title\").into());\n    translation.set_subsettings_music_artist_text(flk!(\"subsettings_music_artist\").into());\n    translation.set_subsettings_music_bitrate_text(flk!(\"subsettings_music_bitrate\").into());\n    translation.set_subsettings_music_genre_text(flk!(\"subsettings_music_genre\").into());\n    translation.set_subsettings_music_year_text(flk!(\"subsettings_music_year\").into());\n    translation.set_subsettings_music_length_text(flk!(\"subsettings_music_length\").into());\n    translation.set_subsettings_music_max_difference_text(flk!(\"subsettings_music_max_difference\").into());\n    translation.set_subsettings_music_minimal_fragment_duration_text(flk!(\"subsettings_music_minimal_fragment_duration\").into());\n    translation.set_subsettings_music_compare_fingerprints_only_with_similar_titles_text(flk!(\"subsettings_music_compare_fingerprints_only_with_similar_titles\").into());\n    translation.set_subsettings_broken_files_type_text(flk!(\"subsettings_broken_files_type\").into());\n    translation.set_subsettings_broken_files_audio_text(flk!(\"subsettings_broken_files_audio\").into());\n    translation.set_subsettings_broken_files_video_text(flk!(\"subsettings_broken_files_video\").into());\n    translation.set_subsettings_broken_files_pdf_text(flk!(\"subsettings_broken_files_pdf\").into());\n    translation.set_subsettings_broken_files_archive_text(flk!(\"subsettings_broken_files_archive\").into());\n    translation.set_subsettings_broken_files_image_text(flk!(\"subsettings_broken_files_image\").into());\n    translation.set_subsettings_broken_files_video_info_text(flk!(\"subsettings_broken_files_video_info\").into());\n    translation.set_subsettings_bad_names_issues_text(flk!(\"subsettings_bad_names_issues\").into());\n    translation.set_subsettings_bad_names_uppercase_extension_text(flk!(\"subsettings_bad_names_uppercase_extension\").into());\n    translation.set_subsettings_bad_names_uppercase_extension_hint_text(flk!(\"subsettings_bad_names_uppercase_extension_hint\").into());\n    translation.set_subsettings_bad_names_emoji_used_text(flk!(\"subsettings_bad_names_emoji_used\").into());\n    translation.set_subsettings_bad_names_emoji_used_hint_text(flk!(\"subsettings_bad_names_emoji_used_hint\").into());\n    translation.set_subsettings_bad_names_space_at_start_end_text(flk!(\"subsettings_bad_names_space_at_start_end\").into());\n    translation.set_subsettings_bad_names_space_at_start_end_hint_text(flk!(\"subsettings_bad_names_space_at_start_end_hint\").into());\n    translation.set_subsettings_bad_names_non_ascii_text(flk!(\"subsettings_bad_names_non_ascii\").into());\n    translation.set_subsettings_bad_names_non_ascii_hint_text(flk!(\"subsettings_bad_names_non_ascii_hint\").into());\n    translation.set_subsettings_bad_names_restricted_charset_text(flk!(\"subsettings_bad_names_restricted_charset\").into());\n    translation.set_subsettings_bad_names_restricted_charset_hint_text(flk!(\"subsettings_bad_names_restricted_charset_hint\").into());\n    translation.set_subsettings_bad_names_allowed_chars_text(flk!(\"subsettings_bad_names_allowed_chars\").into());\n    translation.set_subsettings_bad_names_remove_duplicated_text(flk!(\"subsettings_bad_names_remove_duplicated\").into());\n    translation.set_subsettings_bad_names_remove_duplicated_hint_text(flk!(\"subsettings_bad_names_remove_duplicated_hint\").into());\n    translation.set_subsettings_video_optimizer_mode_text(flk!(\"subsettings_video_optimizer_mode\").into());\n    translation.set_subsettings_video_optimizer_crop_type_text(flk!(\"subsettings_video_optimizer_crop_type\").into());\n    translation.set_subsettings_video_optimizer_black_pixel_threshold_text(flk!(\"subsettings_video_optimizer_black_pixel_threshold\").into());\n    translation.set_subsettings_video_optimizer_black_pixel_threshold_hint_text(flk!(\"subsettings_video_optimizer_black_pixel_threshold_hint\").into());\n    translation.set_subsettings_video_optimizer_black_bar_min_percentage_text(flk!(\"subsettings_video_optimizer_black_bar_min_percentage\").into());\n    translation.set_subsettings_video_optimizer_black_bar_min_percentage_hint_text(flk!(\"subsettings_video_optimizer_black_bar_min_percentage_hint\").into());\n    translation.set_subsettings_video_optimizer_max_samples_text(flk!(\"subsettings_video_optimizer_max_samples\").into());\n    translation.set_subsettings_video_optimizer_max_samples_hint_text(flk!(\"subsettings_video_optimizer_max_samples_hint\").into());\n    translation.set_subsettings_video_optimizer_min_crop_size_text(flk!(\"subsettings_video_optimizer_min_crop_size\").into());\n    translation.set_subsettings_video_optimizer_min_crop_size_hint_text(flk!(\"subsettings_video_optimizer_min_crop_size_hint\").into());\n    translation.set_subsettings_video_optimizer_video_codec_text(flk!(\"subsettings_video_optimizer_video_codec\").into());\n    translation.set_subsettings_video_optimizer_excluded_codecs_text(flk!(\"subsettings_video_optimizer_excluded_codecs\").into());\n    translation.set_subsettings_video_optimizer_video_quality_text(flk!(\"subsettings_video_optimizer_video_quality\").into());\n    translation.set_subsettings_reset_text(flk!(\"subsettings_reset\").into());\n    translation.set_subsettings_exif_ignored_tags_text(flk!(\"subsettings_exif_ignored_tags_text\").into());\n    translation.set_subsettings_exif_ignored_tags_hint_text(flk!(\"subsettings_exif_ignored_tags_hint_text\").into());\n    translation.set_clean_button_text(flk!(\"clean_button_text\").into());\n    translation.set_clean_text(flk!(\"clean_text\").into());\n    translation.set_clean_confirmation_text(flk!(\"clean_confirmation_text\").into());\n    translation.set_crop_videos_text(flk!(\"crop_videos_text\").into());\n    translation.set_crop_video_confirmation_text(flk!(\"crop_video_confirmation_text\").into());\n    translation.set_crop_reencode_video_text(flk!(\"crop_reencode_video_text\").into());\n    translation.set_reencode_videos_text(flk!(\"reencode_videos_text\").into());\n    translation.set_optimize_button_text(flk!(\"optimize_button_text\").into());\n    translation.set_optimize_confirmation_text(flk!(\"optimize_confirmation_text\").into());\n    translation.set_optimize_fail_if_bigger_text(flk!(\"optimize_fail_if_bigger_text\").into());\n    translation.set_optimize_overwrite_files_text(flk!(\"optimize_overwrite_files_text\").into());\n    translation.set_optimize_limit_video_size_text(flk!(\"optimize_limit_video_size_text\").into());\n    translation.set_optimize_max_width_text(flk!(\"optimize_max_width_text\").into());\n    translation.set_optimize_max_height_text(flk!(\"optimize_max_height_text\").into());\n    translation.set_hardlink_button_text(flk!(\"hardlink_button_text\").into());\n    translation.set_hardlink_text(flk!(\"hardlink_text\").into());\n    translation.set_hardlink_confirmation_text(flk!(\"hardlink_confirmation_text\").into());\n    translation.set_softlink_button_text(flk!(\"softlink_button_text\").into());\n    translation.set_softlink_text(flk!(\"softlink_text\").into());\n    translation.set_softlink_confirmation_text(flk!(\"softlink_confirmation_text\").into());\n    translation.set_move_confirmation_text(flk!(\"move_confirmation_text\").into());\n    translation.set_rename_confirmation_text(flk!(\"rename_confirmation_text\").into());\n    translation.set_settings_excluded_items_text(flk!(\"settings_excluded_items\").into());\n    translation.set_settings_allowed_extensions_text(flk!(\"settings_allowed_extensions\").into());\n    translation.set_settings_excluded_extensions_text(flk!(\"settings_excluded_extensions\").into());\n    translation.set_settings_file_size_text(flk!(\"settings_file_size\").into());\n    translation.set_settings_minimum_file_size_text(flk!(\"settings_minimum_file_size\").into());\n    translation.set_settings_maximum_file_size_text(flk!(\"settings_maximum_file_size\").into());\n    translation.set_settings_recursive_search_text(flk!(\"settings_recursive_search\").into());\n    translation.set_settings_use_cache_text(flk!(\"settings_use_cache\").into());\n    translation.set_settings_save_as_json_text(flk!(\"settings_save_as_json\").into());\n    translation.set_settings_move_to_trash_text(flk!(\"settings_move_to_trash\").into());\n    translation.set_settings_ignore_other_filesystems_text(flk!(\"settings_ignore_other_filesystems\").into());\n    translation.set_settings_thread_number_text(flk!(\"settings_thread_number\").into());\n    translation.set_settings_restart_required_text(flk!(\"settings_restart_required\").into());\n    translation.set_settings_duplicate_image_preview_text(flk!(\"settings_duplicate_image_preview\").into());\n    translation.set_settings_similar_videos_preview_text(flk!(\"settings_video_thumbnails_preview\").into());\n    translation.set_settings_similar_videos_preview_hint_text(flk!(\"settings_similar_videos_preview_hint\").into());\n    translation.set_settings_application_scale_text(flk!(\"settings_application_scale_text\").into());\n    translation.set_settings_application_scale_hint_text(flk!(\"settings_application_scale_hint_text\").into());\n    translation.set_settings_restart_required_scale_text(flk!(\"settings_restart_required_scale_text\").into());\n    translation.set_settings_use_manual_application_scale_text(flk!(\"settings_use_manual_application_scale_text\").into());\n    translation.set_settings_duplicate_minimal_hash_cache_size_text(flk!(\"settings_duplicate_minimal_hash_cache_size\").into());\n    translation.set_settings_duplicate_use_prehash_text(flk!(\"settings_duplicate_use_prehash\").into());\n    translation.set_settings_duplicate_minimal_prehash_cache_size_text(flk!(\"settings_duplicate_minimal_prehash_cache_size\").into());\n    translation.set_settings_delete_outdated_cache_entries_text(flk!(\"settings_delete_outdated_cache_entries\").into());\n    translation.set_settings_delete_outdated_cache_entries_hint_text(flk!(\"settings_delete_outdated_cache_entries_hint\").into());\n    translation.set_settings_hide_hard_links_text(flk!(\"settings_hide_hard_links\").into());\n    translation.set_settings_hide_hard_links_hint_text(flk!(\"settings_hide_hard_links_hint\").into());\n    translation.set_settings_similar_images_show_image_preview_text(flk!(\"settings_similar_images_show_image_preview\").into());\n    translation.set_settings_open_config_folder_text(flk!(\"settings_open_config_folder\").into());\n    translation.set_settings_open_cache_folder_text(flk!(\"settings_open_cache_folder\").into());\n    translation.set_settings_language_text(flk!(\"settings_language\").into());\n    translation.set_settings_current_preset_text(flk!(\"settings_current_preset\").into());\n    translation.set_settings_edit_name_text(flk!(\"settings_edit_name\").into());\n    translation.set_settings_choose_name_for_prefix_text(flk!(\"settings_choose_name_for_prefix\").into());\n    translation.set_settings_save_text(flk!(\"settings_save\").into());\n    translation.set_settings_load_text(flk!(\"settings_load\").into());\n    translation.set_settings_reset_text(flk!(\"settings_reset\").into());\n    translation.set_settings_similar_videos_tool_text(flk!(\"settings_similar_videos_tool\").into());\n    translation.set_settings_video_thumbnails_clear_unused_thumbnails_text(flk!(\"settings_video_thumbnails_clear_unused_thumbnails\").into());\n    translation.set_settings_video_thumbnails_header_text(flk!(\"settings_video_thumbnails_header\").into());\n    translation.set_settings_video_thumbnails_generate_text(flk!(\"settings_video_thumbnails_generate\").into());\n    translation.set_settings_video_thumbnails_position_text(flk!(\"settings_video_thumbnails_position\").into());\n    translation.set_settings_video_thumbnails_generate_grid_text(flk!(\"settings_video_thumbnails_generate_grid\").into());\n    translation.set_settings_video_thumbnails_generate_grid_hint_text(flk!(\"settings_video_thumbnails_generate_grid_hint\").into());\n    translation.set_settings_video_thumbnails_grid_tiles_per_side_text(flk!(\"settings_video_thumbnails_grid_tiles_per_side\").into());\n    translation.set_settings_video_thumbnails_grid_tiles_per_side_hint_text(flk!(\"settings_video_thumbnails_grid_tiles_per_side_hint\").into());\n    translation.set_settings_similar_images_tool_text(flk!(\"settings_similar_images_tool\").into());\n    translation.set_settings_general_settings_text(flk!(\"settings_general_settings\").into());\n    translation.set_settings_settings_text(flk!(\"settings_settings\").into());\n    translation.set_popup_save_title_text(flk!(\"popup_save_title\").into());\n    translation.set_popup_save_message_text(flk!(\"popup_save_message\").into());\n    translation.set_popup_rename_title_text(flk!(\"popup_rename_title\").into());\n    translation.set_popup_new_directories_title_text(flk!(\"popup_new_paths_title\").into());\n    translation.set_popup_move_title_text(flk!(\"popup_move_title\").into());\n    translation.set_popup_move_copy_checkbox_text(flk!(\"popup_move_copy_checkbox\").into());\n    translation.set_popup_move_preserve_folder_checkbox_text(flk!(\"popup_move_preserve_folder_checkbox\").into());\n    translation.set_delete_text(flk!(\"delete\").into());\n    translation.set_delete_confirmation_text(flk!(\"rust_delete_confirmation\").into());\n    translation.set_stopping_scan_text(flk!(\"stopping_scan\").into());\n    translation.set_searching_text(flk!(\"searching\").into());\n    translation.set_subsettings_videos_crop_detect_text(flk!(\"subsettings_videos_crop_detect\").into());\n    translation.set_subsettings_videos_skip_forward_amount_text(flk!(\"subsettings_videos_skip_forward_amount\").into());\n    translation.set_subsettings_videos_vid_hash_duration_text(flk!(\"subsettings_videos_vid_hash_duration\").into());\n    translation.set_settings_load_tabs_sizes_at_startup_text(flk!(\"settings_load_tabs_sizes_at_startup\").into());\n    translation.set_settings_load_windows_size_at_startup_text(flk!(\"settings_load_windows_size_at_startup\").into());\n    translation.set_settings_limit_lines_of_messages_text(flk!(\"settings_limit_lines_of_messages\").into());\n    translation.set_settings_play_audio_on_scan_completion_text(flk!(\"settings_play_audio_on_scan_completion_text\").into());\n    translation.set_settings_audio_feature_hint_text(flk!(\"settings_audio_feature_hint_text\").into());\n    translation.set_settings_audio_env_variable_hint_text(flk!(\"settings_audio_env_variable_hint_text\").into());\n    translation.set_settings_cache_number_size_text(\"\".into());\n    translation.set_settings_video_thumbnails_number_size_text(\"\".into());\n    translation.set_settings_log_number_size_text(\"\".into());\n    translation.set_settings_video_thumbnails_clear_unused_thumbnails_text(flk!(\"settings_video_thumbnails_clear_unused_thumbnails\").into());\n    translation.set_clean_exif_overwrite_files_text(flk!(\"clean_exif_overwrite_files_text\").into());\n    translation.set_subsettings_broken_files_video_info_text(flk!(\"subsettings_broken_files_video_info\").into());\n    translation.set_stop_text(flk!(\"stop_text\").into());\n    translation.set_settings_cache_header_text(flk!(\"settings_cache_header_text\").into());\n    translation.set_settings_clean_cache_button_text(flk!(\"settings_clean_cache_button_text\").into());\n    translation.set_popup_clean_cache_title_text(flk!(\"popup_clean_cache_title_text\").into());\n    translation.set_popup_clean_cache_confirmation_text(flk!(\"popup_clean_cache_confirmation_text\").into());\n    translation.set_popup_clean_cache_progress_text(flk!(\"popup_clean_cache_progress_text\").into());\n    translation.set_popup_clean_cache_current_file_text(flk!(\"popup_clean_cache_current_file_text\").into());\n    translation.set_popup_clean_cache_file_progress_text(flk!(\"popup_clean_cache_file_progress_text\").into());\n    translation.set_popup_clean_cache_overall_progress_text(flk!(\"popup_clean_cache_overall_progress_text\").into());\n    translation.set_popup_clean_cache_stopped_by_user_text(flk!(\"popup_clean_cache_stopped_by_user_text\").into());\n    translation.set_popup_clean_cache_finished_text(flk!(\"popup_clean_cache_finished_text\").into());\n    translation.set_popup_clean_cache_error_details_text(flk!(\"popup_clean_cache_error_details_text\").into());\n    translation.set_popup_clean_cache_files_with_errors(flk!(\"popup_clean_cache_files_with_errors\").into());\n    translation.set_popup_custom_select_title_text(flk!(\"popup_custom_select_title_text\").into());\n    translation.set_popup_custom_select_button_text(flk!(\"popup_custom_select_button_text\").into());\n    translation.set_popup_custom_unselect_button_text(flk!(\"popup_custom_unselect_button_text\").into());\n    translation.set_popup_custom_column_name_header_text(flk!(\"popup_custom_column_name_header_text\").into());\n    translation.set_popup_custom_filter_value_header_text(flk!(\"popup_custom_filter_value_header_text\").into());\n    translation.set_popup_custom_hint_str_text(flk!(\"popup_custom_hint_str_text\").into());\n    translation.set_popup_custom_hint_int_text(flk!(\"popup_custom_hint_int_text\").into());\n    translation.set_popup_custom_hint_date_text(flk!(\"popup_custom_hint_date_text\").into());\n    translation.set_popup_custom_case_sensitive_text(flk!(\"popup_custom_case_sensitive_text\").into());\n    translation.set_popup_custom_leave_one_in_group_text(flk!(\"popup_custom_leave_one_in_group_text\").into());\n\n    let tools_model: [(SharedString, ActiveTab); TOOLS_NUMBER] = [\n        (flk!(\"tool_duplicate_files\").into(), ActiveTab::DuplicateFiles),\n        (flk!(\"tool_empty_folders\").into(), ActiveTab::EmptyFolders),\n        (flk!(\"tool_big_files\").into(), ActiveTab::BigFiles),\n        (flk!(\"tool_empty_files\").into(), ActiveTab::EmptyFiles),\n        (flk!(\"tool_temporary_files\").into(), ActiveTab::TemporaryFiles),\n        (flk!(\"tool_similar_images\").into(), ActiveTab::SimilarImages),\n        (flk!(\"tool_similar_videos\").into(), ActiveTab::SimilarVideos),\n        (flk!(\"tool_music_duplicates\").into(), ActiveTab::SimilarMusic),\n        (flk!(\"tool_invalid_symlinks\").into(), ActiveTab::InvalidSymlinks),\n        (flk!(\"tool_broken_files\").into(), ActiveTab::BrokenFiles),\n        (flk!(\"tool_bad_extensions\").into(), ActiveTab::BadExtensions),\n        (flk!(\"tool_bad_names\").into(), ActiveTab::BadNames),\n        (flk!(\"tool_exif_remover\").into(), ActiveTab::ExifRemover),\n        (flk!(\"tool_video_optimizer\").into(), ActiveTab::VideoOptimizer),\n    ];\n    let gui_state = app.global::<GuiState>();\n    gui_state.set_tools_model(ModelRc::new(VecModel::from(tools_model.to_vec())));\n\n    let sort_model: [SortModel; 3] = [\n        SortModel {\n            data: SortMode::FullName,\n            name: flk!(\"sort_by_full_name\").into(),\n        },\n        SortModel {\n            data: SortMode::Selection,\n            name: flk!(\"sort_by_selection\").into(),\n        },\n        SortModel {\n            data: SortMode::Reverse,\n            name: flk!(\"sort_reverse\").into(),\n        },\n    ];\n\n    gui_state.set_sort_results_list(ModelRc::new(VecModel::from(sort_model.to_vec())));\n\n    let selection = flk!(\"column_selection\");\n    let size = flk!(\"column_size\");\n    let file_name = flk!(\"column_file_name\");\n    let path = flk!(\"column_path\");\n    let mod_date = flk!(\"column_modification_date\");\n    let similarity = flk!(\"column_similarity\");\n    let dimensions = flk!(\"column_dimensions\");\n    let title = flk!(\"column_title\");\n    let artist = flk!(\"column_artist\");\n    let year = flk!(\"column_year\");\n    let bitrate = flk!(\"column_bitrate\");\n    let length = flk!(\"column_length\");\n    let genre = flk!(\"column_genre\");\n    let fps = flk!(\"column_fps\");\n    let codec = flk!(\"column_codec\");\n    let duration = flk!(\"column_duration\");\n    let type_of_error = flk!(\"column_type_of_error\");\n    let symlink_name = flk!(\"column_symlink_name\");\n    let symlink_folder = flk!(\"column_symlink_folder\");\n    let destination_path = flk!(\"column_destination_path\");\n    let current_extension = flk!(\"column_current_extension\");\n    let proper_extension = flk!(\"column_proper_extension\");\n    let exif_tags = flk!(\"column_exif_tags\");\n    let new_dimensions = flk!(\"column_new_dimensions\");\n    let new_name = flk!(\"column_new_name\");\n\n    let fnm = |model: &[&str]| {\n        let shared_string = model.iter().map(|s| (*s).into()).collect::<Vec<SharedString>>();\n        ModelRc::new(VecModel::from(shared_string))\n    };\n\n    settings.set_duplicates_column_name(fnm(&[&selection, &size, &file_name, &path, &mod_date]));\n    settings.set_empty_folders_column_name(fnm(&[&selection, &file_name, &path, &mod_date]));\n    settings.set_empty_files_column_name(fnm(&[&selection, &file_name, &path, &mod_date]));\n    settings.set_temporary_files_column_name(fnm(&[&selection, &file_name, &path, &mod_date]));\n    settings.set_big_files_column_name(fnm(&[&selection, &size, &file_name, &path, &mod_date]));\n    settings.set_similar_images_column_name(fnm(&[&selection, &similarity, &size, &dimensions, &file_name, &path, &mod_date]));\n    settings.set_similar_videos_column_name(fnm(&[&selection, &size, &file_name, &path, &dimensions, &duration, &bitrate, &fps, &codec, &mod_date]));\n    settings.set_similar_music_column_name(fnm(&[&selection, &size, &file_name, &title, &artist, &year, &bitrate, &length, &genre, &path, &mod_date]));\n    settings.set_invalid_symlink_column_name(fnm(&[&selection, &symlink_name, &symlink_folder, &destination_path, &mod_date]));\n    settings.set_broken_files_column_name(fnm(&[&selection, &file_name, &path, &type_of_error, &size, &mod_date]));\n    settings.set_bad_extensions_column_name(fnm(&[&selection, &file_name, &path, &current_extension, &proper_extension]));\n    settings.set_exif_remover_column_name(fnm(&[&selection, &size, &file_name, &path, &exif_tags, &mod_date]));\n    settings.set_video_optimizer_column_name(fnm(&[&selection, &size, &file_name, &path, &codec, &dimensions, &new_dimensions, &mod_date]));\n    settings.set_bad_names_column_name(fnm(&[&selection, &file_name, &new_name, &path]));\n}\n\npub(crate) fn translate_select_mode(select_mode: SelectMode) -> SharedString {\n    match select_mode {\n        SelectMode::SelectAll => flk!(\"selection_all\").into(),\n        SelectMode::UnselectAll => flk!(\"selection_deselect_all\").into(),\n        SelectMode::InvertSelection => flk!(\"selection_invert_selection\").into(),\n        SelectMode::SelectTheBiggestSize => flk!(\"selection_the_biggest_size\").into(),\n        SelectMode::SelectTheBiggestResolution => flk!(\"selection_the_biggest_resolution\").into(),\n        SelectMode::SelectTheSmallestSize => flk!(\"selection_the_smallest_size\").into(),\n        SelectMode::SelectTheSmallestResolution => flk!(\"selection_the_smallest_resolution\").into(),\n        SelectMode::SelectNewest => flk!(\"selection_newest\").into(),\n        SelectMode::SelectOldest => flk!(\"selection_oldest\").into(),\n        SelectMode::SelectShortestPath => flk!(\"selection_shortest_path\").into(),\n        SelectMode::SelectLongestPath => flk!(\"selection_longest_path\").into(),\n        SelectMode::SelectCustom => flk!(\"selection_custom_select_unselect\").into(),\n    }\n}\n\npub(crate) fn translate_sort_mode(sort_mode: SortMode) -> SharedString {\n    match sort_mode {\n        SortMode::FullName => flk!(\"sort_by_full_name\").into(),\n        SortMode::Selection => flk!(\"sort_by_selection\").into(),\n        SortMode::Reverse => flk!(\"sort_reverse\").into(),\n    }\n}\n"
  },
  {
    "path": "krokiet/src/create_calculate_task_size.rs",
    "content": "use std::sync::mpsc::{Receiver, Sender};\nuse std::{fs, thread};\n\nuse czkawka_core::common::config_cache_path::get_config_cache_path;\nuse czkawka_core::common::video_utils::VIDEO_THUMBNAILS_SUBFOLDER;\nuse humansize::{BINARY, format_size};\nuse log::{error, info};\nuse slint::ComponentHandle;\n\nuse crate::{MainWindow, Translations, flk};\n#[derive(Debug, Copy, Clone)]\npub struct SizeCountResult {\n    pub video_thumbnails_size_bytes: u64,\n    pub video_thumbnails_count: u64,\n    pub cache_files_size_bytes: u64,\n    pub cache_files_count: u64,\n    pub log_file_size_bytes: u64,\n    pub log_file_count: u64,\n}\n\nfn collect_file_size_and_count(path: &std::path::Path, extensions: Option<&[&str]>) -> (u64, u64) {\n    let mut total_size: u64 = 0;\n    let mut total_count: u64 = 0;\n\n    let Ok(dir_entry) = fs::read_dir(path) else {\n        return (0, 0);\n    };\n\n    for entry in dir_entry.flatten() {\n        let entry_path = entry.path();\n        if !entry_path.is_file() {\n            continue;\n        }\n        let Ok(metadata) = entry.metadata() else {\n            continue;\n        };\n\n        if let Some(extensions) = &extensions {\n            let Some(extension) = entry_path.extension().map(|e| e.to_string_lossy().to_lowercase()) else {\n                continue;\n            };\n\n            if extensions.iter().any(|&ext| ext == extension) {\n                total_size += metadata.len();\n                total_count += 1;\n            }\n        } else {\n            total_size += metadata.len();\n            total_count += 1;\n        }\n    }\n\n    (total_size, total_count)\n}\n\npub fn cache_size_count_task(task_receiver: &std::sync::mpsc::Receiver<std::sync::mpsc::Sender<SizeCountResult>>) {\n    let Some(cache_dir) = get_config_cache_path().map(|p| p.cache_folder) else {\n        info!(\"Cannot get config cache path, skipping size of config cache calculation.\");\n        return;\n    };\n    let thumbnails_dir = cache_dir.join(VIDEO_THUMBNAILS_SUBFOLDER);\n    while let Ok(sender) = task_receiver.recv() {\n        let (video_thumbnails_size_bytes, video_thumbnails_count) = collect_file_size_and_count(&thumbnails_dir, Some(&[\"jpg\"]));\n        let (cache_files_size_bytes, cache_files_count) = collect_file_size_and_count(&cache_dir, Some(&[\"bin\", \"json\"]));\n        let (log_file_size_bytes, log_file_count) = collect_file_size_and_count(&cache_dir, Some(&[\"log\"]));\n\n        let result = SizeCountResult {\n            video_thumbnails_size_bytes,\n            video_thumbnails_count,\n            cache_files_size_bytes,\n            cache_files_count,\n            log_file_size_bytes,\n            log_file_count,\n        };\n\n        let _ = sender.send(result).inspect_err(|e| {\n            error!(\"Failed to send size count result: {e}\");\n        });\n    }\n}\n\nfn update_translations_with_sizes(app: &MainWindow, res: SizeCountResult) {\n    let translations = app.global::<Translations>();\n    translations.set_settings_cache_number_size_text(\n        flk!(\n            \"settings_cache_number_size_text\",\n            size = format_size(res.cache_files_size_bytes, BINARY)\n            number = res.cache_files_count\n        )\n        .into(),\n    );\n    translations.set_settings_video_thumbnails_number_size_text(\n        flk!(\n            \"settings_video_thumbnails_number_size_text\",\n            size = format_size(res.video_thumbnails_size_bytes, BINARY)\n            number = res.video_thumbnails_count\n        )\n        .into(),\n    );\n    translations.set_settings_log_number_size_text(\n        flk!(\n            \"settings_log_number_size_text\",\n            size = format_size(res.log_file_size_bytes, BINARY)\n            number = res.log_file_count\n        )\n        .into(),\n    );\n}\n\npub(crate) fn request_and_update_cache_sizes(app_weak: slint::Weak<MainWindow>, task_sender: std::sync::mpsc::Sender<std::sync::mpsc::Sender<SizeCountResult>>) {\n    thread::spawn(move || {\n        let (result_sender, result_receiver) = std::sync::mpsc::channel();\n\n        let _ = task_sender.send(result_sender).inspect_err(|e| {\n            error!(\"Failed to send size count task: {e}\");\n        });\n\n        let Ok(res) = result_receiver.recv().inspect_err(|e| {\n            error!(\"Failed to receive size count task: {e}\");\n        }) else {\n            return;\n        };\n\n        app_weak\n            .upgrade_in_event_loop(move |app| {\n                update_translations_with_sizes(&app, res);\n            })\n            .expect(\"Failed to update app info text\");\n    });\n}\n\npub fn update_cache_sizes(app: &MainWindow, task_sender: &std::sync::mpsc::Sender<std::sync::mpsc::Sender<SizeCountResult>>) {\n    request_and_update_cache_sizes(app.as_weak(), task_sender.clone());\n}\n\npub(crate) fn create_calculate_task_size(task_receiver: Receiver<Sender<SizeCountResult>>) {\n    let _join_handler = std::thread::spawn(move || {\n        cache_size_count_task(&task_receiver);\n    });\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_clean_exif.rs",
    "content": "use std::path::MAIN_SEPARATOR;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse slint::{ComponentHandle, Weak};\n\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{Callabler, GuiState, MainWindow, Settings};\n\npub(crate) fn connect_clean(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_clean_exif_items(move || {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let override_file = app.global::<Settings>().get_popup_clean_exif_overwrite_files();\n\n        let processor = ModelProcessor::new(active_tab);\n        processor.clean_exif_selected_files(progress_sender, weak_app, stop_flag, override_file);\n    });\n}\n\nimpl ModelProcessor {\n    fn clean_exif_selected_files(self, progress_sender: Sender<ProgressData>, weak_app: Weak<MainWindow>, stop_flag: Arc<AtomicBool>, override_file: bool) {\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n            let tag_groups_idx = self.active_tab.get_exif_tag_groups_idx();\n            let tag_u16_idx = self.active_tab.get_exif_tag_u16_idx();\n\n            let clean_fnc = move |data: &SimplerSingleMainListModel| {\n                clean_exif_single_file(\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", data.val_str[path_idx], data.val_str[name_idx]),\n                    &data.val_str[tag_groups_idx].split(',').map(|s| s.to_string()).collect::<Vec<_>>(),\n                    &data.val_str[tag_u16_idx].split(',').map(|s| s.to_string()).collect::<Vec<_>>(),\n                    override_file,\n                )\n            };\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(clean_fnc)),\n                MessageType::CleanExif,\n                false,\n            );\n        });\n    }\n}\n\n#[cfg(not(test))]\nfn clean_exif_single_file(file_path: &str, tag_groups: &[String], tags_u16: &[String], override_file: bool) -> Result<(), String> {\n    // Such data are split into multiple vectors, but in Krokiet they are not changed\n    assert_eq!(tags_u16.len(), tag_groups.len());\n    let connected_tags = tag_groups\n        .iter()\n        .zip(tags_u16.iter())\n        .map(|(group, code)| {\n            (\n                code.parse::<u16>()\n                    .inspect_err(|e| log::error!(\"Failed to parse EXIF tag code {code} for file {file_path:?}, reason: {e}\"))\n                    .unwrap_or_default(),\n                group.clone(),\n            )\n        })\n        .collect::<Vec<(u16, String)>>();\n    let _ = czkawka_core::tools::exif_remover::core::clean_exif_tags(file_path, &connected_tags, override_file)\n        .map_err(|e| format!(\"Failed to clean EXIF for file {file_path:?}, reason: {e}\"))?;\n    Ok(())\n}\n\n#[cfg(test)]\nfn clean_exif_single_file(full_path: &str, _tag_groups: &[String], _tags_u16: &[String], _override_file: bool) -> Result<(), String> {\n    if full_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {full_path}\"));\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_delete.rs",
    "content": "use std::path::MAIN_SEPARATOR;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse slint::{ComponentHandle, Weak};\n\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow, Settings};\n\npub(crate) fn connect_delete_button(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_delete_selected_items(move || {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n        let settings = app.global::<Settings>();\n\n        let processor = ModelProcessor::new(active_tab);\n        processor.delete_selected_items(settings.get_move_to_trash(), progress_sender, weak_app, stop_flag);\n    });\n}\n\nimpl ModelProcessor {\n    fn delete_selected_items(self, remove_to_trash: bool, progress_sender: Sender<ProgressData>, weak_app: Weak<MainWindow>, stop_flag: Arc<AtomicBool>) {\n        let is_empty_folder_tab = self.active_tab == ActiveTab::EmptyFolders;\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n\n            let dlt_fnc = move |data: &SimplerSingleMainListModel| {\n                remove_single_item(\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", data.val_str[path_idx], data.val_str[name_idx]),\n                    is_empty_folder_tab,\n                    remove_to_trash,\n                )\n            };\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(dlt_fnc)),\n                MessageType::Delete,\n                false,\n            );\n        });\n    }\n}\n\n#[cfg(not(test))]\nfn remove_single_item(full_path: &str, is_folder_tab: bool, remove_to_trash: bool) -> Result<(), String> {\n    if is_folder_tab {\n        return czkawka_core::common::remove_folder_if_contains_only_empty_folders(full_path, remove_to_trash);\n    }\n    czkawka_core::common::remove_single_file(full_path, remove_to_trash)\n}\n\n#[cfg(test)]\nfn remove_single_item(full_path: &str, _is_folder_tab: bool, _remove_to_trash: bool) -> Result<(), String> {\n    if full_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {full_path}\"));\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use crossbeam_channel::{Receiver, unbounded};\n    use slint::{Model, ModelRc, VecModel};\n\n    use super::*;\n    use crate::SingleMainListModel;\n    use crate::common::create_model_from_model_vec;\n    use crate::simpler_model::ToSlintModel;\n    use crate::test_common::get_model_vec;\n\n    impl ModelProcessor {\n        pub(crate) fn process_deletion_test(\n            &self,\n            remove_to_trash: bool,\n            progress_sender: Sender<ProgressData>,\n            model: ModelRc<SingleMainListModel>,\n        ) -> Option<(Vec<SingleMainListModel>, Vec<String>, usize, usize)> {\n            let is_empty_folder_tab = self.active_tab == ActiveTab::EmptyFolders;\n\n            let items_queued_to_delete = model.iter().filter(|e| e.checked).count();\n            if items_queued_to_delete == 0 {\n                return None; // No items to delete\n            }\n            let simplified_model = model.to_simpler_enumerated_vec();\n\n            let path_idx = 0;\n            let name_idx = 0;\n            let dlt_fnc = move |data: &SimplerSingleMainListModel| {\n                remove_single_item(\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", data.val_str[path_idx], data.val_str[name_idx]),\n                    is_empty_folder_tab,\n                    remove_to_trash,\n                )\n            };\n\n            let output = Self::process_items(\n                simplified_model,\n                items_queued_to_delete,\n                progress_sender,\n                &Arc::default(),\n                &ProcessFunction::Simple(Box::new(dlt_fnc)),\n                MessageType::Delete,\n                self.active_tab.get_int_size_opt_idx(),\n                false,\n            );\n\n            let (new_simple_model, errors, items_deleted) = Self::remove_processed_items_from_model(output);\n\n            Some((new_simple_model.to_vec_model(), errors, items_queued_to_delete, items_deleted))\n        }\n    }\n\n    #[test]\n    fn test_no_delete_items() {\n        let (progress, _receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n        let model = get_model_vec(10);\n        let model = create_model_from_model_vec(&model);\n        let processor = ModelProcessor::new(ActiveTab::EmptyFolders);\n        assert!(processor.process_deletion_test(false, progress, model).is_none());\n    }\n\n    #[test]\n    fn test_delete_selected_items() {\n        let (progress, _receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n        let mut model = get_model_vec(10);\n        model[0].checked = true;\n        model[0].val_str = ModelRc::new(VecModel::from(vec![\"normal1\".to_string().into(); 10]));\n        model[1].checked = true;\n        model[1].val_str = ModelRc::new(VecModel::from(vec![\"normal2\".to_string().into(); 10]));\n        model[3].checked = true;\n        model[3].val_str = ModelRc::new(VecModel::from(vec![\"test_error\".to_string().into(); 10]));\n        let model = create_model_from_model_vec(&model);\n        let processor = ModelProcessor::new(ActiveTab::EmptyFolders);\n        let (new_model, errors, items_queued_to_delete, items_deleted) = processor.process_deletion_test(false, progress, model).unwrap();\n\n        assert_eq!(new_model.len(), 8);\n        assert_eq!(errors.len(), 1);\n        assert_eq!(items_queued_to_delete, 3);\n        assert_eq!(items_deleted, 2);\n\n        assert!(new_model[1].checked);\n        assert!(new_model[1].val_str.iter().all(|s| s == \"test_error\"));\n        assert!(!new_model[0].checked);\n        assert!(new_model.iter().skip(2).all(|model| !model.checked));\n    }\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_hardlink.rs",
    "content": "use std::path::MAIN_SEPARATOR;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse slint::{ComponentHandle, Weak};\n\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{Callabler, GuiState, MainWindow};\n\npub(crate) fn connect_hardlink(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_hardlink_items(move || {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let processor = ModelProcessor::new(active_tab);\n        processor.hardlink_selected_items(progress_sender, weak_app, stop_flag);\n    });\n}\n\nimpl ModelProcessor {\n    fn hardlink_selected_items(self, progress_sender: Sender<ProgressData>, weak_app: Weak<MainWindow>, stop_flag: Arc<AtomicBool>) {\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n\n            let hardlink_fnc = move |original: &SimplerSingleMainListModel, derived: &SimplerSingleMainListModel| {\n                hardlink_single_item(\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", original.val_str[path_idx], original.val_str[name_idx]),\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", derived.val_str[path_idx], derived.val_str[name_idx]),\n                )\n            };\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Related(Box::new(hardlink_fnc)),\n                MessageType::Hardlink,\n                false,\n            );\n        });\n    }\n}\n\n#[cfg(not(test))]\nfn hardlink_single_item(original_path: &str, derived_path: &str) -> Result<(), String> {\n    czkawka_core::common::make_hard_link(original_path, derived_path)\n        .map_err(|e| crate::flk!(\"rust_hardlink_failed\", name = original_path, target = derived_path, reason = e.to_string()))\n}\n\n#[cfg(test)]\nfn hardlink_single_item(original_path: &str, _derived_path: &str) -> Result<(), String> {\n    if original_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {original_path}\"));\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_move.rs",
    "content": "use std::path::{Path, PathBuf};\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::{fs, path, thread};\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse slint::{ComponentHandle, Weak};\n\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{Callabler, GuiState, MainWindow, Settings, flk};\n\npub(crate) fn connect_move(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_move_items(move |output_folder| {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let preserve_structure = app.global::<Settings>().get_popup_move_preserve_folder_structure();\n        let copy_mode = app.global::<Settings>().get_popup_move_copy_mode();\n\n        let processor = ModelProcessor::new(active_tab);\n        processor.move_selected_items(progress_sender, weak_app, stop_flag, preserve_structure, copy_mode, &output_folder);\n    });\n}\n\nimpl ModelProcessor {\n    fn move_selected_items(\n        self,\n        progress_sender: Sender<ProgressData>,\n        weak_app: Weak<MainWindow>,\n        stop_flag: Arc<AtomicBool>,\n        preserve_structure: bool,\n        copy_mode: bool,\n        output_folder: &str,\n    ) {\n        if let Err(err) = fs::create_dir_all(output_folder) {\n            let app = weak_app.upgrade().expect(\"Failed to upgrade app :(\");\n            app.global::<GuiState>()\n                .set_info_text(flk!(\"rust_cannot_create_output_folder\", output_folder = output_folder, error = err.to_string()).into());\n            return;\n        }\n\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        let output_folder = output_folder.to_string();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n\n            let mlt_fnc = move |data: &SimplerSingleMainListModel| move_single_item(data, path_idx, name_idx, &output_folder, preserve_structure, copy_mode);\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(mlt_fnc)),\n                MessageType::Move,\n                false,\n            );\n        });\n    }\n}\n\nfn move_single_item(data: &SimplerSingleMainListModel, path_idx: usize, name_idx: usize, output_folder: &str, preserve_structure: bool, copy_mode: bool) -> Result<(), String> {\n    let path = &data.val_str[path_idx];\n    let name = &data.val_str[name_idx];\n\n    let (input_file, output_file) = collect_path_and_create_folders(path, name, output_folder, preserve_structure);\n    if output_file.exists() {\n        return Err(flk!(\"rust_file_already_exists\", file = output_file.to_string_lossy().to_string()));\n    }\n\n    if copy_mode {\n        try_to_copy_item(&input_file, &output_file)\n    } else {\n        // Try to rename file, may fail due various reasons\n        // It is the easiest way to move file, but only on same partition\n        if fs::rename(&input_file, &output_file).is_ok() {\n            return Ok(());\n        }\n\n        // It is possible that this failed, because file is on different partition, so\n        // we need to copy file and then remove old\n        try_to_copy_item(&input_file, &output_file)?;\n\n        if let Err(e) = fs::remove_file(&input_file) {\n            return Err(flk!(\n                \"rust_error_removing_file_after_copy\",\n                file = input_file.to_string_lossy().to_string(),\n                reason = e.to_string()\n            ));\n        }\n        Ok(())\n    }\n}\n\n// Tries to copy file/folder, and returns error if it fails\nfn try_to_copy_item(input_file: &Path, output_file: &Path) -> Result<(), String> {\n    let res = if input_file.is_dir() {\n        let options = fs_extra::dir::CopyOptions::new();\n        fs_extra::dir::copy(input_file, output_file, &options) // TODO consider to use less buggy library\n    } else {\n        let options = fs_extra::file::CopyOptions::new();\n        fs_extra::file::copy(input_file, output_file, &options)\n    };\n    if let Err(e) = res {\n        return Err(flk!(\n            \"rust_error_copying_file\",\n            input = input_file.to_string_lossy().to_string(),\n            output = output_file.to_string_lossy().to_string(),\n            reason = e.to_string()\n        ));\n    }\n    Ok(())\n}\n\n// Create input/output paths, and create output folder\nfn collect_path_and_create_folders(input_path: &str, input_file: &str, output_path: &str, preserve_structure: bool) -> (PathBuf, PathBuf) {\n    let input_full_path = PathBuf::from(input_path).join(input_file);\n\n    let mut output_full_path = PathBuf::from(output_path);\n    if preserve_structure {\n        output_full_path.extend(Path::new(input_path).components().filter(|c| matches!(c, path::Component::Normal(_))));\n    }\n    let _ = fs::create_dir_all(&output_full_path);\n    output_full_path.push(input_file);\n\n    (input_full_path, output_full_path)\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_optimize_video.rs",
    "content": "use std::path::MAIN_SEPARATOR;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse czkawka_core::tools::video_optimizer::{VideoCodec, VideoCropSingleFixParams, VideoCroppingMechanism, VideoTranscodeFixParams};\nuse slint::{ComponentHandle, Weak};\n\nuse crate::common::IntDataVideoOptimizer;\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::settings::collect_combo_box_settings;\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{Callabler, GuiState, MainWindow, Settings};\n\npub(crate) fn connect_optimize_video(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n\n    let progress_sender_crop = progress_sender.clone();\n    let stop_flag_crop = stop_flag.clone();\n    app.global::<Callabler>().on_crop_video_items(move || {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender_crop.clone();\n        let stop_flag = stop_flag_crop.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let settings = app.global::<Settings>();\n        let reencode = settings.get_popup_crop_video_reencode();\n        let video_quality = settings.get_video_optimizer_sub_video_quality();\n        let overwrite_files = settings.get_popup_crop_video_overwrite_files();\n\n        let crop_mechanism = collect_combo_box_settings(&app).video_optimizer_crop_type.value;\n\n        let processor = ModelProcessor::new(active_tab);\n\n        let requested_codec = if reencode {\n            Some(collect_combo_box_settings(&app).video_optimizer_video_codec.value)\n        } else {\n            None\n        };\n\n        processor.crop_selected_videos(progress_sender, weak_app, stop_flag, requested_codec, overwrite_files, video_quality, crop_mechanism);\n    });\n\n    let a2 = app.as_weak();\n    app.global::<Callabler>().on_reencode_video_items(move || {\n        let weak_app = a2.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a2.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let settings = app.global::<Settings>();\n        let codec = collect_combo_box_settings(&app).video_optimizer_video_codec.value;\n        let fail_if_bigger = settings.get_popup_reencode_video_fail_if_bigger();\n        let overwrite_files = settings.get_popup_reencode_video_overwrite_files();\n        let video_quality = settings.get_popup_reencode_video_quality();\n        let limit_video_size = settings.get_popup_reencode_video_limit_video_size();\n\n        let max_width_str = settings.get_popup_reencode_video_max_width();\n        let max_height_str = settings.get_popup_reencode_video_max_height();\n\n        let max_width = max_width_str.parse::<i32>().unwrap_or(0).max(0) as u32;\n        let max_height = max_height_str.parse::<i32>().unwrap_or(0).max(0) as u32;\n\n        let max_width = if max_width > 0 { max_width } else { 1920 };\n        let max_height = if max_height > 0 { max_height } else { 1920 };\n\n        let processor = ModelProcessor::new(active_tab);\n\n        processor.optimize_selected_videos(\n            progress_sender,\n            weak_app,\n            stop_flag,\n            codec,\n            fail_if_bigger,\n            overwrite_files,\n            video_quality,\n            limit_video_size,\n            max_width,\n            max_height,\n        );\n    });\n}\n\nimpl ModelProcessor {\n    fn optimize_selected_videos(\n        self,\n        progress_sender: Sender<ProgressData>,\n        weak_app: Weak<MainWindow>,\n        stop_flag: Arc<AtomicBool>,\n        requested_video_codec: VideoCodec,\n        fail_if_bigger: bool,\n        overwrite_files: bool,\n        video_quality: f32,\n        limit_video_size: bool,\n        max_width: u32,\n        max_height: u32,\n    ) {\n        let codec_str = requested_video_codec.as_ffprobe_codec_name().to_string();\n\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n            let size_idx = self.active_tab.get_int_size_idx();\n            let codec_idx = self.active_tab.get_str_video_codec_idx();\n\n            let stop_flag_clone = stop_flag.clone();\n            let optimize_fnc = move |data: &SimplerSingleMainListModel| {\n                let file_codec = &data.val_str[codec_idx];\n                if codec_str == *file_codec {\n                    return Ok(()); // No need to transcode if codec is the same\n                }\n\n                let full_path = format!(\"{}{MAIN_SEPARATOR}{}\", data.val_str[path_idx], data.val_str[name_idx]);\n                let original_size = data.get_size(size_idx);\n                let target_quality = video_quality as u32;\n\n                optimize_single_video(\n                    &stop_flag_clone,\n                    &full_path,\n                    original_size,\n                    VideoTranscodeFixParams {\n                        codec: requested_video_codec,\n                        quality: target_quality,\n                        fail_if_not_smaller: fail_if_bigger,\n                        overwrite_original: overwrite_files,\n                        limit_video_size,\n                        max_width,\n                        max_height,\n                    },\n                )\n            };\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(optimize_fnc)),\n                MessageType::OptimizeVideo,\n                true,\n            );\n        });\n    }\n\n    fn crop_selected_videos(\n        self,\n        progress_sender: Sender<ProgressData>,\n        weak_app: Weak<MainWindow>,\n        stop_flag: Arc<AtomicBool>,\n        requested_codec: Option<VideoCodec>,\n        overwrite_files: bool,\n        video_quality: f32,\n        video_crop_mechanism: VideoCroppingMechanism,\n    ) {\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n            let size_idx = self.active_tab.get_int_size_idx();\n            let codec_idx = self.active_tab.get_str_video_codec_idx();\n\n            let rect_left_idx = IntDataVideoOptimizer::RectLeft as usize;\n            let rect_top_idx = IntDataVideoOptimizer::RectTop as usize;\n            let rect_right_idx = IntDataVideoOptimizer::RectRight as usize;\n            let rect_bottom_idx = IntDataVideoOptimizer::RectBottom as usize;\n\n            let quality = if requested_codec.is_some() { Some(video_quality as u32) } else { None };\n\n            let stop_flag_clone = stop_flag.clone();\n            let crop_fnc = move |data: &SimplerSingleMainListModel| {\n                let full_path = format!(\"{}{MAIN_SEPARATOR}{}\", data.val_str[path_idx], data.val_str[name_idx]);\n                let original_size = data.get_size(size_idx);\n                let codec = &data.val_str[codec_idx];\n\n                let left = data.val_int[rect_left_idx] as u32;\n                let top = data.val_int[rect_top_idx] as u32;\n                let right = data.val_int[rect_right_idx] as u32;\n                let bottom = data.val_int[rect_bottom_idx] as u32;\n\n                crop_single_video(\n                    &stop_flag_clone,\n                    &full_path,\n                    original_size,\n                    VideoCropSingleFixParams {\n                        overwrite_original: overwrite_files,\n                        target_codec: requested_codec,\n                        quality,\n                        crop_rectangle: (left, top, right, bottom),\n                        crop_mechanism: video_crop_mechanism,\n                    },\n                    codec,\n                )\n            };\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(crop_fnc)),\n                MessageType::OptimizeVideo,\n                true,\n            );\n        });\n    }\n}\n\n#[cfg(not(test))]\nfn optimize_single_video(stop_flag: &Arc<AtomicBool>, video_path: &str, original_size: u64, transcode_params: VideoTranscodeFixParams) -> Result<(), String> {\n    czkawka_core::tools::video_optimizer::core::process_video(stop_flag, video_path, original_size, transcode_params)\n}\n\n#[cfg(test)]\nfn optimize_single_video(_stop_flag: &Arc<AtomicBool>, video_path: &str, _original_size: u64, _transcode_params: VideoTranscodeFixParams) -> Result<(), String> {\n    if video_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {video_path}\"));\n    }\n    Ok(())\n}\n\n#[cfg(not(test))]\nfn crop_single_video(stop_flag: &Arc<AtomicBool>, full_path: &str, _original_size: u64, params: VideoCropSingleFixParams, codec: &str) -> Result<(), String> {\n    czkawka_core::tools::video_optimizer::core::fix_video_crop(std::path::Path::new(full_path), &params, stop_flag, codec)\n}\n\n#[cfg(test)]\nfn crop_single_video(_stop_flag: &Arc<AtomicBool>, video_path: &str, _original_size: u64, _params: VideoCropSingleFixParams, _codec: &str) -> Result<(), String> {\n    if video_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {video_path}\"));\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_rename.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse slint::{ComponentHandle, Weak};\n\nuse crate::common::StrDataBadNames;\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{ActiveTab, Callabler, GuiState, MainWindow};\n\npub(crate) fn connect_rename(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_rename_files(move || {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let processor = ModelProcessor::new(active_tab);\n        match active_tab {\n            ActiveTab::BadExtensions => {\n                processor.rename_bad_extensions(progress_sender, weak_app, stop_flag);\n            }\n            ActiveTab::BadNames => {\n                processor.rename_bad_file_names(progress_sender, weak_app, stop_flag);\n            }\n            _ => panic!(\"{active_tab:?} is not supported for renaming bad extensions/bad file names\"),\n        }\n    });\n}\n\nimpl ModelProcessor {\n    fn rename_bad_extensions(self, progress_sender: Sender<ProgressData>, weak_app: Weak<MainWindow>, stop_flag: Arc<AtomicBool>) {\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n            let ext_idx = self.active_tab.get_str_proper_extension();\n\n            let rm_fnc = move |data: &SimplerSingleMainListModel| rename_single_extension_item(data, path_idx, name_idx, ext_idx);\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(rm_fnc)),\n                MessageType::Rename,\n                false,\n            );\n        });\n    }\n    fn rename_bad_file_names(self, progress_sender: Sender<ProgressData>, weak_app: Weak<MainWindow>, stop_flag: Arc<AtomicBool>) {\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n            let new_name_idx = StrDataBadNames::NewName as usize;\n\n            let rm_fnc = move |data: &SimplerSingleMainListModel| rename_single_file_name_item(data, path_idx, name_idx, new_name_idx);\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Simple(Box::new(rm_fnc)),\n                MessageType::Rename,\n                false,\n            );\n        });\n    }\n}\n\n#[cfg(not(test))]\nfn rename_single_file_name_item(data: &SimplerSingleMainListModel, path_idx: usize, name_idx: usize, new_file_name_idx: usize) -> Result<(), String> {\n    use std::path::MAIN_SEPARATOR;\n    let folder = &data.val_str[path_idx];\n    let file_name = &data.val_str[name_idx];\n    let new_file_name = &data.val_str[new_file_name_idx];\n\n    let new_full_path = format!(\"{folder}{MAIN_SEPARATOR}{new_file_name}\");\n    let old_full_path = format!(\"{folder}{MAIN_SEPARATOR}{file_name}\");\n\n    if let Err(e) = std::fs::rename(&old_full_path, &new_full_path) {\n        Err(crate::flk!(\n            \"rust_failed_to_rename_file\",\n            old_path = old_full_path,\n            new_path = new_full_path,\n            error = e.to_string()\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n#[cfg(not(test))]\nfn rename_single_extension_item(data: &SimplerSingleMainListModel, path_idx: usize, name_idx: usize, ext_idx: usize) -> Result<(), String> {\n    use std::path::MAIN_SEPARATOR;\n    let folder = &data.val_str[path_idx];\n    let file_name = &data.val_str[name_idx];\n    let new_extension = &data.val_str[ext_idx];\n\n    let file_stem = std::path::Path::new(&file_name).file_stem().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();\n    let new_full_path = format!(\"{folder}{MAIN_SEPARATOR}{file_stem}.{new_extension}\");\n    let old_full_path = format!(\"{folder}{MAIN_SEPARATOR}{file_name}\");\n\n    if let Err(e) = std::fs::rename(&old_full_path, &new_full_path) {\n        Err(crate::flk!(\n            \"rust_failed_to_rename_file\",\n            old_path = old_full_path,\n            new_path = new_full_path,\n            error = e.to_string()\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nfn rename_single_extension_item(data: &SimplerSingleMainListModel, path_idx: usize, _name_idx: usize, _ext_idx: usize) -> Result<(), String> {\n    let full_path = &data.val_str[path_idx];\n    if full_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {full_path}\"));\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nfn rename_single_file_name_item(data: &SimplerSingleMainListModel, path_idx: usize, _name_idx: usize, _file_name: usize) -> Result<(), String> {\n    let full_path = &data.val_str[path_idx];\n    if full_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {full_path}\"));\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use crossbeam_channel::{Receiver, unbounded};\n    use slint::{Model, ModelRc, VecModel};\n\n    use super::*;\n    use crate::common::create_model_from_model_vec;\n    use crate::simpler_model::ToSlintModel;\n    use crate::test_common::get_model_vec;\n    use crate::{ActiveTab, SingleMainListModel};\n\n    impl ModelProcessor {\n        pub(crate) fn process_rename_test(\n            &self,\n            progress_sender: Sender<ProgressData>,\n            model: ModelRc<SingleMainListModel>,\n        ) -> Option<(Vec<SingleMainListModel>, Vec<String>, usize, usize)> {\n            let items_queued_to_delete = model.iter().filter(|e| e.checked).count();\n            if items_queued_to_delete == 0 {\n                return None; // No items to delete\n            }\n            let simplified_model = model.to_simpler_enumerated_vec();\n\n            let path_idx = 0;\n            let name_idx = 0;\n            let ext_idx = 0;\n\n            let rm_fnc = move |data: &SimplerSingleMainListModel| rename_single_extension_item(data, path_idx, name_idx, ext_idx);\n\n            let output = Self::process_items(\n                simplified_model,\n                items_queued_to_delete,\n                progress_sender,\n                &Arc::default(),\n                &ProcessFunction::Simple(Box::new(rm_fnc)),\n                MessageType::Rename,\n                self.active_tab.get_int_size_opt_idx(),\n                false,\n            );\n\n            let (new_simple_model, errors, items_deleted) = Self::remove_processed_items_from_model(output);\n\n            Some((new_simple_model.to_vec_model(), errors, items_queued_to_delete, items_deleted))\n        }\n    }\n\n    #[test]\n    fn test_no_rename_items() {\n        let (progress, _receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n        let model = get_model_vec(10);\n        let model = create_model_from_model_vec(&model);\n        let processor = ModelProcessor::new(ActiveTab::EmptyFolders);\n        assert!(processor.process_rename_test(progress, model).is_none());\n    }\n\n    #[test]\n    fn test_rename_bad_extensions() {\n        let (progress, _receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n        let mut model = get_model_vec(10);\n        model[0].checked = true;\n        model[0].val_str = ModelRc::new(VecModel::from(vec![\"normal1\".to_string().into(); 10]));\n        model[1].checked = true;\n        model[1].val_str = ModelRc::new(VecModel::from(vec![\"normal2\".to_string().into(); 10]));\n        model[3].checked = true;\n        model[3].val_str = ModelRc::new(VecModel::from(vec![\"test_error\".to_string().into(); 10]));\n        let model = create_model_from_model_vec(&model);\n        let processor = ModelProcessor::new(ActiveTab::EmptyFolders);\n        let (new_model, errors, items_queued_to_delete, items_deleted) = processor.process_rename_test(progress, model).unwrap();\n\n        assert_eq!(new_model.len(), 8);\n        assert_eq!(errors.len(), 1);\n        assert_eq!(items_queued_to_delete, 3);\n        assert_eq!(items_deleted, 2);\n\n        assert!(new_model[1].checked);\n        assert!(new_model[1].val_str.iter().all(|s| s == \"test_error\"));\n        assert!(!new_model[0].checked);\n        assert!(new_model.iter().skip(2).all(|model| !model.checked));\n    }\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/connect_symlink.rs",
    "content": "use std::path::MAIN_SEPARATOR;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::ProgressData;\nuse slint::{ComponentHandle, Weak};\n\nuse crate::model_operations::model_processor::{MessageType, ModelProcessor, ProcessFunction};\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSimplerVec};\nuse crate::{Callabler, GuiState, MainWindow};\n\npub(crate) fn connect_symlink(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_flag: Arc<AtomicBool>) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_softlink_items(move || {\n        let weak_app = a.clone();\n        let progress_sender = progress_sender.clone();\n        let stop_flag = stop_flag.clone();\n        stop_flag.store(false, Ordering::Relaxed);\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let active_tab = app.global::<GuiState>().get_active_tab();\n\n        let processor = ModelProcessor::new(active_tab);\n        processor.symlink_selected_items(progress_sender, weak_app, stop_flag);\n    });\n}\n\nimpl ModelProcessor {\n    fn symlink_selected_items(self, progress_sender: Sender<ProgressData>, weak_app: Weak<MainWindow>, stop_flag: Arc<AtomicBool>) {\n        let model = self.active_tab.get_tool_model(&weak_app.upgrade().expect(\"Failed to upgrade app :(\"));\n        let simpler_model = model.to_simpler_enumerated_vec();\n        thread::spawn(move || {\n            let path_idx = self.active_tab.get_str_path_idx();\n            let name_idx = self.active_tab.get_str_name_idx();\n\n            let symlink_fnc = move |original: &SimplerSingleMainListModel, derived: &SimplerSingleMainListModel| {\n                symlink_single_item(\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", original.val_str[path_idx], original.val_str[name_idx]),\n                    &format!(\"{}{MAIN_SEPARATOR}{}\", derived.val_str[path_idx], derived.val_str[name_idx]),\n                )\n            };\n\n            self.process_and_update_gui_state(\n                &weak_app,\n                stop_flag,\n                &progress_sender,\n                simpler_model,\n                &ProcessFunction::Related(Box::new(symlink_fnc)),\n                MessageType::Symlink,\n                false,\n            );\n        });\n    }\n}\n\n#[cfg(not(test))]\nfn symlink_single_item(original_path: &str, derived_path: &str) -> Result<(), String> {\n    czkawka_core::common::make_file_symlink(original_path, derived_path)\n        .map_err(|e| crate::flk!(\"rust_symlink_failed\", name = original_path, target = derived_path, reason = e.to_string()))\n}\n\n#[cfg(test)]\nfn symlink_single_item(original_path: &str, _derived_path: &str) -> Result<(), String> {\n    if original_path.contains(\"test_error\") {\n        return Err(format!(\"Test error for item: {original_path}\"));\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "krokiet/src/file_actions/mod.rs",
    "content": "pub mod connect_clean_exif;\npub mod connect_delete;\npub mod connect_hardlink;\npub mod connect_move;\npub mod connect_optimize_video;\npub mod connect_rename;\npub mod connect_symlink;\n"
  },
  {
    "path": "krokiet/src/localizer_krokiet.rs",
    "content": "use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};\nuse i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};\nuse rust_embed::RustEmbed;\n\n#[derive(RustEmbed)]\n#[folder = \"i18n/\"]\nstruct Localizations;\n\npub static LANGUAGE_LOADER_KROKIET: std::sync::LazyLock<FluentLanguageLoader> = std::sync::LazyLock::new(|| {\n    let loader: FluentLanguageLoader = fluent_language_loader!();\n\n    loader.load_fallback_language(&Localizations).expect(\"Error while loading fallback language\");\n\n    loader\n});\n\n#[macro_export]\nmacro_rules! flk {\n    ( $($tt:tt)* ) => {{\n        i18n_embed_fl::fl!($crate::localizer_krokiet::LANGUAGE_LOADER_KROKIET, $($tt)*)\n    }};\n}\n\n// Get the `Localizer` to be used for localizing this library.\npub(crate) fn localizer_krokiet() -> Box<dyn Localizer> {\n    Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_KROKIET, &Localizations))\n}\n"
  },
  {
    "path": "krokiet/src/main.rs",
    "content": "// Remove console window in Windows OS\n#![windows_subsystem = \"windows\"]\n#![allow(clippy::unwrap_used)] // Cannot use due unwrap used in a lot of places in generated code\n#![allow(clippy::indexing_slicing)] // Cannot use due unwrap used in a lot of places in generated code\n#![allow(clippy::todo)] // Cannot use due unwrap used in a lot of places in generated code\n\nuse std::rc::Rc;\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicBool;\n\nuse crossbeam_channel::{Receiver, Sender, unbounded};\nuse czkawka_core::common::basic_gui_cli::process_cli_args;\nuse czkawka_core::common::config_cache_path::{print_infos_and_warnings, set_config_cache_path};\nuse czkawka_core::common::image::register_image_decoding_hooks;\nuse czkawka_core::common::logger::{filtering_messages, print_version_mode, setup_logger};\nuse czkawka_core::common::progress_data::ProgressData;\nuse file_actions::connect_clean_exif::connect_clean;\nuse file_actions::connect_delete::connect_delete_button;\nuse file_actions::connect_hardlink::connect_hardlink;\nuse file_actions::connect_move::connect_move;\nuse file_actions::connect_optimize_video::connect_optimize_video;\nuse file_actions::connect_rename::connect_rename;\nuse file_actions::connect_symlink::connect_symlink;\nuse log::{error, info};\nuse slint::VecModel;\n\nuse crate::clear_outdated_video_thumbnails::clear_outdated_video_thumbnails;\nuse crate::connect_clean_cache::connect_clean_cache;\nuse crate::connect_directories_changes::connect_add_remove_directories;\nuse crate::connect_open::connect_open_items;\nuse crate::connect_progress_receiver::connect_progress_gathering;\nuse crate::connect_row_selection::connect_row_selections;\nuse crate::connect_save::connect_save;\nuse crate::connect_scan::connect_scan_button;\nuse crate::connect_select::connect_select;\nuse crate::connect_show_confirmation::connect_show_confirmation;\nuse crate::connect_show_preview::connect_show_preview;\nuse crate::connect_sort::{connect_sort, connect_sort_column};\nuse crate::connect_stop::connect_stop_button;\nuse crate::connect_tab_changed::connect_tab_changed;\nuse crate::connect_translation::connect_translations;\nuse crate::create_calculate_task_size::create_calculate_task_size;\nuse crate::set_initial_gui_info::set_initial_gui_infos;\nuse crate::set_initial_scroll_list_data_indexes::set_initial_scroll_list_data_indexes;\n// TODO - at start this should be used, to be sure that rust models are in sync with slint models\n// currently I need to do this manually - https://github.com/slint-ui/slint/issues/7632\n// use crate::set_initial_gui_info::set_initial_gui_infos;\nuse crate::settings::{connect_changing_settings_preset, create_default_settings_files, load_initial_settings_from_file, save_all_settings_to_file, set_initial_settings_to_gui};\nuse crate::shared_models::SharedModels;\n\nmod audio_player;\nmod clear_outdated_video_thumbnails;\nmod common;\nmod connect_clean_cache;\nmod connect_directories_changes;\nmod connect_open;\nmod connect_progress_receiver;\nmod connect_rfd;\nmod connect_row_selection;\nmod connect_save;\nmod connect_scan;\nmod connect_select;\nmod connect_show_confirmation;\nmod connect_show_preview;\nmod connect_sort;\nmod connect_stop;\nmod connect_tab_changed;\nmod connect_translation;\nmod create_calculate_task_size;\nmod file_actions;\nmod localizer_krokiet;\nmod model_operations;\nmod set_initial_gui_info;\nmod set_initial_scroll_list_data_indexes;\nmod settings;\nmod shared_models;\nmod simpler_model;\n#[cfg(test)]\nmod test_common;\n\nslint::include_modules!();\n\nfn main() {\n    register_image_decoding_hooks();\n    let config_cache_path_set_result = set_config_cache_path(\"Czkawka\", \"Krokiet\");\n    let cli_args = process_cli_args(\"Krokiet\", \"krokiet_gui\", std::env::args().skip(1).collect());\n\n    let (base_settings, custom_settings, preset_to_load) = load_initial_settings_from_file(cli_args.as_ref());\n    if base_settings.use_manual_application_scale {\n        // SAFETY:\n        // set_var is safe when using on single threaded context\n        unsafe {\n            std::env::set_var(\"SLINT_SCALE_FACTOR\", format!(\"{:.2}\", base_settings.manual_application_scale));\n        }\n    }\n\n    setup_logger(false, \"krokiet\", filtering_messages);\n    print_version_mode(\"Krokiet\");\n    print_infos_and_warnings(config_cache_path_set_result.infos, config_cache_path_set_result.warnings);\n    print_krokiet_features();\n\n    create_default_settings_files();\n\n    let app = match MainWindow::new() {\n        Ok(app) => app,\n        Err(e) => {\n            error!(\"Error during creating main window: {e}\");\n            show_critical_error(e.to_string());\n            return;\n        }\n    };\n\n    #[cfg(feature = \"audio\")]\n    app.global::<GuiState>().set_audio_feature_enabled(true);\n\n    let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();\n    let stop_flag: Arc<AtomicBool> = Arc::default();\n\n    zeroing_all_models(&app);\n\n    let shared_models = SharedModels::new_shared();\n\n    // Create audio player for scan completion notifications\n    let audio_player = Arc::new(crate::audio_player::AudioPlayer::new());\n\n    // Disabled for now, due invalid settings model at start\n    set_initial_gui_infos(&app);\n\n    set_initial_scroll_list_data_indexes(&app);\n\n    let original_preset_idx = base_settings.default_preset;\n    set_initial_settings_to_gui(&app, &base_settings, &custom_settings, cli_args, preset_to_load);\n\n    connect_delete_button(&app, progress_sender.clone(), stop_flag.clone());\n    connect_scan_button(&app, progress_sender.clone(), stop_flag.clone(), Arc::clone(&shared_models), Arc::clone(&audio_player));\n    connect_stop_button(&app, stop_flag.clone());\n    connect_open_items(&app);\n    connect_progress_gathering(&app, progress_receiver);\n    connect_add_remove_directories(&app);\n    connect_show_preview(&app, Arc::clone(&shared_models));\n    connect_translations(&app);\n    connect_changing_settings_preset(&app);\n    connect_select(&app, &shared_models);\n    connect_move(&app, progress_sender.clone(), stop_flag.clone());\n    connect_rename(&app, progress_sender.clone(), stop_flag.clone());\n    connect_optimize_video(&app, progress_sender.clone(), stop_flag.clone());\n    connect_clean(&app, progress_sender.clone(), stop_flag.clone());\n    connect_hardlink(&app, progress_sender.clone(), stop_flag.clone());\n    connect_symlink(&app, progress_sender, stop_flag);\n    connect_save(&app, Arc::clone(&shared_models));\n    connect_row_selections(&app);\n    connect_sort(&app);\n    connect_sort_column(&app);\n    let (task_sender, task_receiver) = std::sync::mpsc::channel();\n    connect_tab_changed(&app, task_sender.clone());\n    create_calculate_task_size(task_receiver);\n    connect_clean_cache(&app, task_sender);\n    connect_show_confirmation(&app, Arc::clone(&shared_models));\n\n    clear_outdated_video_thumbnails(&app);\n\n    // Popups gather their size, after starting/closing popup at least once\n    // This is simpler solution, than setting sizes of popups manually for each language\n    app.invoke_initialize_popup_sizes();\n\n    match app.run() {\n        Ok(()) => {\n            save_all_settings_to_file(&app, original_preset_idx);\n        }\n        Err(e) => {\n            error!(\"Error during running the application: {e}\");\n            show_critical_error(e.to_string());\n        }\n    }\n}\n\npub fn show_critical_error(error: String) {\n    rfd::MessageDialog::new()\n        .set_title(flk!(\"rust_init_error_title\"))\n        .set_description(&flk!(\"rust_init_error_message\", error_message = error))\n        .show();\n}\n\npub(crate) fn zeroing_all_models(app: &MainWindow) {\n    app.set_empty_folder_model(Rc::new(VecModel::default()).into());\n    app.set_empty_files_model(Rc::new(VecModel::default()).into());\n    app.set_similar_images_model(Rc::new(VecModel::default()).into());\n    app.set_duplicate_files_model(Rc::new(VecModel::default()).into());\n    app.set_similar_music_model(Rc::new(VecModel::default()).into());\n    app.set_big_files_model(Rc::new(VecModel::default()).into());\n    app.set_bad_extensions_model(Rc::new(VecModel::default()).into());\n    app.set_bad_names_model(Rc::new(VecModel::default()).into());\n    app.set_broken_files_model(Rc::new(VecModel::default()).into());\n    app.set_similar_videos_model(Rc::new(VecModel::default()).into());\n    app.set_invalid_symlinks_model(Rc::new(VecModel::default()).into());\n    app.set_temporary_files_model(Rc::new(VecModel::default()).into());\n    app.set_video_optimizer_model(Rc::new(VecModel::default()).into());\n}\n\n#[allow(clippy::allow_attributes)]\n#[allow(unfulfilled_lint_expectations)] // Happens only on release build\n#[expect(clippy::vec_init_then_push)]\n#[expect(unused_mut)]\npub(crate) fn print_krokiet_features() {\n    let mut features: Vec<&str> = Vec::new();\n\n    #[cfg(feature = \"audio\")]\n    features.push(\"audio\");\n    #[cfg(feature = \"skia_opengl\")]\n    features.push(\"skia_opengl\");\n    #[cfg(feature = \"skia_vulkan\")]\n    features.push(\"skia_vulkan\");\n    #[cfg(feature = \"software\")]\n    features.push(\"software\");\n    #[cfg(feature = \"femtovg\")]\n    features.push(\"femtovg\");\n    #[cfg(feature = \"winit_femtovg\")]\n    features.push(\"winit_femtovg\");\n    #[cfg(feature = \"winit_skia_opengl\")]\n    features.push(\"winit_skia_opengl\");\n    #[cfg(feature = \"winit_skia_vulkan\")]\n    features.push(\"winit_skia_vulkan\");\n    #[cfg(feature = \"winit_software\")]\n    features.push(\"winit_software\");\n    #[cfg(feature = \"femtovg_wgpu\")]\n    features.push(\"femtovg_wgpu\");\n\n    info!(\"Krokiet features({}): [{}]\", features.len(), features.join(\", \"));\n}\n"
  },
  {
    "path": "krokiet/src/model_operations/mod.rs",
    "content": "pub mod model_processor;\n\nuse slint::{ComponentHandle, Model, ModelRc};\n\nuse crate::connect_row_selection::checker::get_number_of_enabled_items;\nuse crate::simpler_model::SimplerSingleMainListModel;\nuse crate::{GuiState, MainWindow, SingleMainListModel};\n\npub type ProcessingResult = Vec<(usize, SimplerSingleMainListModel, Option<Result<(), String>>)>;\n\nimpl SingleMainListModel {\n    #[allow(clippy::allow_attributes)]\n    #[expect(clippy::print_stdout)]\n    #[allow(dead_code)] // TODO - rust with some version shows this\n    pub(crate) fn debug_print(&self) {\n        let val_int: Vec<i32> = self.val_int.iter().collect();\n        let val_str: Vec<String> = self.val_str.iter().map(|e| e.to_string()).collect();\n        println!(\n            \"SingleMainListModel: checked: {}, filled_header_row: {}, header_row: {}, selected_row: {}, val_int: {:?}, val_str: {:?}\",\n            self.checked, self.filled_header_row, self.header_row, self.selected_row, val_int, val_str\n        );\n    }\n}\n\npub trait DebugPrintModelRc {\n    #[expect(dead_code)]\n    fn debug_print_model_rc(&self);\n}\nimpl DebugPrintModelRc for ModelRc<SingleMainListModel> {\n    #[expect(clippy::print_stdout)]\n    fn debug_print_model_rc(&self) {\n        println!(\"=====================START DEBUG PRINT RC MODELS=====================\");\n        println!(\"Model with {} items\", self.iter().count());\n        for item in self.iter() {\n            item.debug_print();\n        }\n        println!(\"=====================END DEBUG PRINT RC MODELS=====================\");\n    }\n}\n\n// TODO - tests\n// Removes orphan items in groups\npub(crate) fn remove_single_items_in_groups(mut items: Vec<SingleMainListModel>, have_header: bool) -> Vec<SingleMainListModel> {\n    // When have header, we must also throw out orphaned items\n    if have_header && !items.is_empty() {\n        // First row must be header\n        // If assert fails, that means, that we checked that for mode that not have headers\n        // or that we somehow removed header row, which cannot happen without serious bug\n        assert!(items[0].header_row);\n        assert!(!items[0].checked);\n        assert!(!items[0].selected_row);\n        let is_filled_header = items[0].filled_header_row;\n\n        if is_filled_header && items.len() <= 2 {\n            if items.len() == 2 {\n                if items[1].header_row {\n                    items.clear();\n                }\n            } else {\n                items.clear();\n            }\n        } else if !is_filled_header && items.len() <= 3 {\n            if items.len() == 3 {\n                if items[1].header_row || items[2].header_row {\n                    items.clear();\n                }\n            } else {\n                items.clear();\n            }\n        } else {\n            let header_step = if is_filled_header { 1 } else { 2 };\n\n            let mut last_header = 0;\n            let mut new_items: Vec<SingleMainListModel> = Vec::new();\n            for i in 1..items.len() {\n                if items[i].header_row {\n                    if i - last_header > header_step {\n                        new_items.extend(items[last_header..i].iter().cloned());\n                    }\n                    last_header = i;\n                }\n            }\n            if items.len() - last_header > header_step {\n                new_items.extend(items[last_header..].iter().cloned());\n            }\n\n            items = new_items;\n        }\n    }\n\n    items\n}\n\npub struct CheckedItemsInfo {\n    pub checked_items_number: u64,\n    pub groups_with_checked_items: Option<CheckedGroupItemsInfo>,\n}\npub struct CheckedGroupItemsInfo {\n    pub groups_with_checked_items: u64,\n    pub number_of_groups_with_all_items_checked: u64,\n}\n\nfn get_checked_group_info_from_model(model: &ModelRc<SingleMainListModel>) -> CheckedItemsInfo {\n    if model.iter().next().is_none() {\n        // Here I could panic, but i think that it is still possible to go here, without doing anything wrong\n        return CheckedItemsInfo {\n            checked_items_number: 0,\n            groups_with_checked_items: None,\n        };\n    }\n\n    let mut checked_items_number = 0;\n    let mut groups_with_checked_items = 0;\n    let mut number_of_groups_with_all_items_checked = 0;\n\n    let mut current_group_all_checked = true;\n    let mut group_with_selected_item = false;\n\n    // TODO Maybe collecting is a little useless, check if really needed\n    let model_collected = model.iter().collect::<Vec<_>>();\n    assert!(model_collected[0].header_row);\n    assert!(!model_collected.last().expect(\"Is not empty\").header_row);\n\n    let is_reference_folder = model_collected[0].filled_header_row;\n\n    for item in model_collected.iter().skip(1) {\n        if item.header_row {\n            if current_group_all_checked {\n                number_of_groups_with_all_items_checked += 1;\n            }\n            if group_with_selected_item {\n                groups_with_checked_items += 1;\n            }\n            current_group_all_checked = true;\n            group_with_selected_item = false;\n        } else if item.checked {\n            checked_items_number += 1;\n            group_with_selected_item = true;\n        } else {\n            current_group_all_checked = false;\n        }\n    }\n    if model_collected.len() > 1 {\n        if current_group_all_checked {\n            number_of_groups_with_all_items_checked += 1;\n        }\n        if group_with_selected_item {\n            groups_with_checked_items += 1;\n        }\n    }\n    if is_reference_folder {\n        // In reference folders, this warning is not needed, because it only would make\n        // sense, when also header would be available to be checked, which is not possible\n        number_of_groups_with_all_items_checked = 0;\n    }\n\n    CheckedItemsInfo {\n        checked_items_number,\n        groups_with_checked_items: Some(CheckedGroupItemsInfo {\n            groups_with_checked_items,\n            number_of_groups_with_all_items_checked,\n        }),\n    }\n}\n\npub(crate) fn get_checked_info_from_app(app: &MainWindow) -> CheckedItemsInfo {\n    let active_tab = app.global::<GuiState>().get_active_tab();\n    let model = active_tab.get_tool_model(app);\n    if active_tab.get_is_header_mode() {\n        get_checked_group_info_from_model(&model)\n    } else {\n        let checked_items_number = get_number_of_enabled_items(app, active_tab);\n        // Alternatively, this can be manually calculated here\n        // let checked_items_number = model.iter().filter(|item| item.checked).count();\n        CheckedItemsInfo {\n            checked_items_number,\n            groups_with_checked_items: None,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use slint::VecModel;\n\n    use super::*;\n    use crate::test_common::get_model_vec;\n\n    #[test]\n    fn remove_single_items_elements() {\n        let mut items = get_model_vec(3);\n        items[0].header_row = true;\n        items[1].header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert!(result.is_empty());\n\n        let mut items = get_model_vec(3);\n        items[0].header_row = true;\n        items[2].header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert!(result.is_empty());\n\n        let mut items = get_model_vec(3);\n        items[0].header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert_eq!(result.len(), 3);\n\n        let mut items = get_model_vec(3);\n        items[0].header_row = true;\n        items[0].filled_header_row = true;\n        items[2].header_row = true;\n        items[2].filled_header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert_eq!(result.len(), 2);\n        assert!(result[0].header_row);\n        assert!(!result[1].header_row);\n\n        let mut items = get_model_vec(2);\n        items[0].header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert_eq!(result.len(), 0);\n\n        let mut items = get_model_vec(10);\n        items[0].header_row = true;\n        items[9].header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert_eq!(result.len(), 9);\n\n        let mut items = get_model_vec(2);\n        items[0].header_row = true;\n        items[0].filled_header_row = true;\n        items[1].header_row = false;\n        let result = remove_single_items_in_groups(items, true);\n        assert_eq!(result.len(), 2);\n\n        let mut items = get_model_vec(2);\n        items[0].header_row = true;\n        items[0].filled_header_row = true;\n        items[1].header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert!(result.is_empty());\n\n        let mut items = get_model_vec(1);\n        items[0].header_row = true;\n        items[0].filled_header_row = true;\n        let result = remove_single_items_in_groups(items, true);\n        assert!(result.is_empty());\n\n        let items = Vec::new();\n        let result = remove_single_items_in_groups(items, true);\n        assert!(result.is_empty());\n\n        let mut items = get_model_vec(4);\n        items[0].header_row = true;\n        items[0].filled_header_row = true;\n        let result = remove_single_items_in_groups(items.clone(), true);\n        assert_eq!(result.len(), 4);\n    }\n\n    #[test]\n    #[should_panic]\n    fn panics_when_first_row_is_not_header_but_have_header() {\n        let mut items = get_model_vec(2);\n        items[0].header_row = false;\n        remove_single_items_in_groups(items, true);\n    }\n\n    #[test]\n    #[should_panic]\n    fn panics_when_first_row_is_checked_but_have_header() {\n        let mut items = get_model_vec(2);\n        items[0].header_row = true;\n        items[0].checked = true;\n        remove_single_items_in_groups(items, true);\n    }\n\n    #[test]\n    #[should_panic]\n    fn panics_when_first_row_is_selected_but_have_header() {\n        let mut items = get_model_vec(2);\n        items[0].header_row = true;\n        items[0].selected_row = true;\n        remove_single_items_in_groups(items, true);\n    }\n\n    #[test]\n    fn check_checked_function() {\n        let mut items = get_model_vec(4);\n        items[0].header_row = true;\n        items[1].checked = true;\n        items[2].checked = true;\n        items[3].checked = true;\n\n        let model = ModelRc::new(VecModel::from(items));\n        let result = get_checked_group_info_from_model(&model);\n        let groups_info = result.groups_with_checked_items.unwrap();\n        assert_eq!(result.checked_items_number, 3);\n        assert_eq!(groups_info.groups_with_checked_items, 1);\n        assert_eq!(groups_info.number_of_groups_with_all_items_checked, 1);\n\n        let mut items = get_model_vec(8);\n        items[0].header_row = true;\n        items[1].checked = true;\n        items[2].checked = true;\n        items[3].checked = false;\n        items[4].header_row = true;\n        items[5].checked = true;\n        items[6].header_row = true;\n        items[7].checked = false;\n\n        let model = ModelRc::new(VecModel::from(items));\n        let result = get_checked_group_info_from_model(&model);\n        let groups_info = result.groups_with_checked_items.unwrap();\n        assert_eq!(result.checked_items_number, 3);\n        assert_eq!(groups_info.groups_with_checked_items, 2);\n        assert_eq!(groups_info.number_of_groups_with_all_items_checked, 1);\n\n        let mut items = get_model_vec(8);\n        items[0].header_row = true;\n        items[0].filled_header_row = true;\n        items[1].checked = true;\n        items[2].checked = true;\n        items[3].checked = false;\n        items[4].header_row = true;\n        items[4].filled_header_row = true;\n        items[5].checked = true;\n        items[6].header_row = true;\n        items[6].filled_header_row = true;\n        items[7].checked = false;\n\n        let model = ModelRc::new(VecModel::from(items));\n        let result = get_checked_group_info_from_model(&model);\n        let groups_info = result.groups_with_checked_items.unwrap();\n        assert_eq!(result.checked_items_number, 3);\n        assert_eq!(groups_info.groups_with_checked_items, 2);\n        assert_eq!(groups_info.number_of_groups_with_all_items_checked, 0);\n    }\n}\n"
  },
  {
    "path": "krokiet/src/model_operations/model_processor.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};\nuse std::time::Duration;\n\nuse crossbeam_channel::Sender;\nuse czkawka_core::common::progress_data::{CurrentStage, ProgressData};\nuse czkawka_core::helpers::delayed_sender::DelayedSender;\nuse czkawka_core::helpers::messages::{MessageLimit, Messages};\nuse log::{debug, error};\nuse rayon::iter::{IntoParallelIterator, ParallelIterator};\nuse slint::{ComponentHandle, ModelRc, VecModel, Weak};\n\nuse crate::connect_row_selection::checker::set_number_of_enabled_items;\nuse crate::connect_row_selection::reset_selection;\nuse crate::model_operations::ProcessingResult;\nuse crate::simpler_model::{SimplerSingleMainListModel, ToSlintModel};\nuse crate::{ActiveTab, GuiState, MainWindow, SingleMainListModel, flk, model_operations};\n// This is quite ugly workaround for Slint strange limitation, where model cannot be passed to another thread\n// This was needed by me, because I wanted to process deletion without blocking main gui thread, with additional sending progress about entire operation.\n// After trying different solutions, looks that the simplest and quite not really efficient solution is to convert slint model, to simpler model, which can be passed to another thread.\n// Models are converted multiple times, so this have some big overhead\n// ModelRc<SingleMainListModel> --cloning when iterating + converting--> SimplerSingleMainListModel --conversion before setting to model--> ModelRc<SingleMainListModel> --cloning when iterating to remove useless items--> ModelRc<SingleMainListModel>\n\npub struct ModelProcessor {\n    pub active_tab: ActiveTab,\n}\n\n#[derive(Clone, Copy)]\npub enum MessageType {\n    Delete,\n    Rename,\n    Move,\n    Hardlink,\n    Symlink,\n    OptimizeVideo,\n    CleanExif,\n}\n\nimpl MessageType {\n    fn get_empty_message(self) -> String {\n        match self {\n            Self::Delete => flk!(\"rust_no_files_deleted\"),\n            Self::Rename => flk!(\"rust_no_files_renamed\"),\n            Self::Move => flk!(\"rust_no_files_moved\"),\n            Self::Hardlink => flk!(\"rust_no_files_hardlinked\"),\n            Self::Symlink => flk!(\"rust_no_files_symlinked\"),\n            Self::OptimizeVideo => flk!(\"rust_no_videos_optimized\"),\n            Self::CleanExif => flk!(\"rust_no_exif_cleaned\"),\n        }\n    }\n    fn get_summary_message(self, processed: usize, failed: usize, total: usize) -> String {\n        match self {\n            Self::Delete => flk!(\"rust_delete_summary\", deleted = processed, failed = failed, total = total),\n            Self::Rename => flk!(\"rust_rename_summary\", renamed = processed, failed = failed, total = total),\n            Self::Move => flk!(\"rust_move_summary\", moved = processed, failed = failed, total = total),\n            Self::Hardlink => flk!(\"rust_hardlink_summary\", hardlinked = processed, failed = failed, total = total),\n            Self::Symlink => flk!(\"rust_symlink_summary\", symlinked = processed, failed = failed, total = total),\n            Self::OptimizeVideo => flk!(\"rust_optimize_video_summary\", optimized = processed, failed = failed, total = total),\n            Self::CleanExif => flk!(\"rust_clean_exif_summary\", cleaned = processed, failed = failed, total = total),\n        }\n    }\n    fn get_base_progress(self) -> ProgressData {\n        match self {\n            Self::Delete => ProgressData::get_empty_state(CurrentStage::DeletingFiles),\n            Self::Rename => ProgressData::get_empty_state(CurrentStage::RenamingFiles),\n            Self::Move => ProgressData::get_empty_state(CurrentStage::MovingFiles),\n            Self::Hardlink => ProgressData::get_empty_state(CurrentStage::HardlinkingFiles),\n            Self::Symlink => ProgressData::get_empty_state(CurrentStage::SymlinkingFiles),\n            Self::OptimizeVideo => ProgressData::get_empty_state(CurrentStage::OptimizingVideos),\n            Self::CleanExif => ProgressData::get_empty_state(CurrentStage::CleaningExif),\n        }\n    }\n    fn msg_type(self) -> &'static str {\n        match self {\n            Self::Delete => \"delete\",\n            Self::Rename => \"rename\",\n            Self::Move => \"move\",\n            Self::Hardlink => \"hardlink\",\n            Self::Symlink => \"symlink\",\n            Self::OptimizeVideo => \"optimize_video\",\n            Self::CleanExif => \"clean_exif\",\n        }\n    }\n}\n\npub enum ProcessFunction {\n    // Takes as argument reference to one item on list, it is used by simple processing functions\n    // that operates on single item only like deleting file, renaming file, etc\n    Simple(Box<dyn Fn(&SimplerSingleMainListModel) -> Result<(), String> + Send + Sync + 'static>),\n    // // Takes as argument function that is responsible for processing two related items on list\n    // // It is used to e.g. hardlink 2 files together\n    Related(Box<dyn Fn(&SimplerSingleMainListModel, &SimplerSingleMainListModel) -> Result<(), String> + Send + Sync + 'static>),\n}\n\nimpl ModelProcessor {\n    pub fn new(active_tab: ActiveTab) -> Self {\n        Self { active_tab }\n    }\n\n    pub(crate) fn remove_single_items_in_groups(&self, items: Vec<SingleMainListModel>) -> Vec<SingleMainListModel> {\n        let have_header = self.active_tab.get_is_header_mode();\n        model_operations::remove_single_items_in_groups(items, have_header)\n    }\n\n    pub(crate) fn remove_processed_items_from_model(results: ProcessingResult) -> (Vec<SimplerSingleMainListModel>, Vec<String>, usize) {\n        let mut errors = Vec::new();\n        let mut items_processed = 0;\n\n        let new_model: Vec<SimplerSingleMainListModel> = results\n            .into_iter()\n            .filter_map(|(_idx, item, process_res)| match process_res {\n                Some(Ok(())) => {\n                    items_processed += 1;\n                    None\n                }\n                Some(Err(err)) => {\n                    errors.push(err);\n                    Some(item)\n                }\n                None => Some(item),\n            })\n            .collect();\n\n        (new_model, errors, items_processed)\n    }\n\n    pub(crate) fn process_items(\n        items_simplified: Vec<(usize, SimplerSingleMainListModel)>,\n        items_queued_to_process: usize,\n        sender: Sender<ProgressData>,\n        stop_flag: &Arc<AtomicBool>,\n        process_function: &ProcessFunction,\n        message_type: MessageType,\n        size_idx: Option<usize>,\n        force_single_threaded: bool,\n    ) -> ProcessingResult {\n        let rm_idx = Arc::new(AtomicUsize::new(0));\n        let size = Arc::new(AtomicU64::new(0));\n        let delayed_sender = DelayedSender::new(sender, Duration::from_millis(100));\n\n        let mut output: Vec<_> = match process_function {\n            ProcessFunction::Simple(process_simple) => {\n                let fnc = |(idx, data): (usize, SimplerSingleMainListModel)| -> (usize, SimplerSingleMainListModel, Option<Result<(), String>>) {\n                    if !data.checked {\n                        return (idx, data, None);\n                    }\n\n                    // Stop requested, so just return items\n                    if stop_flag.load(Ordering::Relaxed) {\n                        return (idx, data, None);\n                    }\n\n                    let rm_idx = rm_idx.fetch_add(1, Ordering::Relaxed);\n                    let size = size.fetch_add(size_idx.map(|size_idx| data.get_size(size_idx)).unwrap_or_default(), Ordering::Relaxed);\n                    let mut progress = message_type.get_base_progress();\n                    progress.entries_to_check = items_queued_to_process;\n                    progress.entries_checked = rm_idx;\n                    progress.bytes_checked = size;\n                    delayed_sender.send(progress);\n\n                    let res = process_simple(&data);\n\n                    (idx, data, Some(res))\n                };\n\n                if force_single_threaded {\n                    items_simplified.into_iter().map(fnc).collect()\n                } else {\n                    items_simplified.into_par_iter().map(fnc).collect()\n                }\n            }\n            ProcessFunction::Related(process_rel) => {\n                // Grouping items by headers\n\n                if let Some((_first_idx, first_item)) = items_simplified.first() {\n                    assert!(first_item.header_row, \"In related processing function, first item must be header row\");\n                }\n\n                let mut grouped_results: Vec<Vec<_>> = Vec::new();\n                for (idx, item) in items_simplified {\n                    if item.header_row {\n                        grouped_results.push(vec![(idx, item)]);\n                    } else {\n                        if let Some(last) = grouped_results.last_mut() {\n                            last.push((idx, item));\n                        }\n                    }\n                }\n\n                let fnc = |grouped_items: Vec<(usize, SimplerSingleMainListModel)>| -> Vec<(usize, SimplerSingleMainListModel, Option<Result<(), String>>)> {\n                    // In this mode,header may be used if contains filled data or first selected item in group if this is not reference mode\n                    let Some((main_idx, main_item)) = grouped_items\n                        .iter()\n                        .find(|(_idx, data)| data.checked || (data.header_row && data.filled_header_row))\n                        .cloned()\n                    else {\n                        // No selected items in group, so return all items as is\n                        return grouped_items.into_iter().map(|(idx, data)| (idx, data, None)).collect();\n                    };\n                    // Other selected items will be changed, items immutable contains first selected or header item + not selected items\n                    let (other_selected_items, items_immutable) = grouped_items.into_iter().partition::<Vec<_>, _>(|(idx, data)| data.checked && main_idx != *idx);\n\n                    let mut results: Vec<_> = items_immutable.into_iter().map(|(idx, data)| (idx, data, None)).collect();\n\n                    for (idx, data) in other_selected_items {\n                        // Stop requested, so just return items\n                        if stop_flag.load(Ordering::Relaxed) {\n                            results.push((idx, data, None));\n                            continue;\n                        }\n\n                        let rm_idx = rm_idx.fetch_add(1, Ordering::Relaxed);\n                        let size = size.fetch_add(size_idx.map(|size_idx| data.get_size(size_idx)).unwrap_or_default(), Ordering::Relaxed);\n                        let mut progress = message_type.get_base_progress();\n                        progress.entries_to_check = items_queued_to_process;\n                        progress.entries_checked = rm_idx;\n                        progress.bytes_checked = size;\n                        delayed_sender.send(progress);\n\n                        let res = process_rel(&main_item, &data);\n\n                        results.push((idx, data, Some(res)));\n                    }\n\n                    results\n                };\n\n                if force_single_threaded {\n                    grouped_results.into_iter().flat_map(fnc).collect()\n                } else {\n                    grouped_results.into_par_iter().flat_map(fnc).collect()\n                }\n            }\n        };\n\n        output.sort_by_key(|(idx, _, _)| *idx);\n\n        output\n    }\n\n    pub(crate) fn process_and_update_gui_state(\n        self,\n        weak_app: &Weak<MainWindow>,\n        stop_flag: Arc<AtomicBool>,\n        progress_sender: &Sender<ProgressData>,\n        simpler_model: Vec<(usize, SimplerSingleMainListModel)>,\n        process_fnc: &ProcessFunction,\n        message_type: MessageType,\n        force_single_threaded: bool,\n    ) {\n        weak_app\n            .upgrade_in_event_loop(move |app| {\n                app.set_processing(true); // TODO processing should be probably set in gui\n            })\n            .expect(\"Failed to update app info text\");\n\n        let items_queued_to_process = match process_fnc {\n            ProcessFunction::Simple(_) => simpler_model.iter().filter(|(_idx, e)| e.checked).count(),\n            ProcessFunction::Related(_) => {\n                let mut contains_main_item_in_group = false;\n                let mut items_number = 0;\n                for (_idx, item) in &simpler_model {\n                    if item.header_row {\n                        contains_main_item_in_group = item.filled_header_row;\n                    } else {\n                        if item.checked {\n                            if contains_main_item_in_group {\n                                items_number += 1;\n                            } else {\n                                contains_main_item_in_group = true;\n                            }\n                        }\n                    }\n                }\n                items_number\n            }\n        };\n        debug!(\"Processing {} items for {}\", items_queued_to_process, message_type.msg_type());\n        if items_queued_to_process == 0 {\n            weak_app\n                .upgrade_in_event_loop(move |app| {\n                    app.global::<GuiState>().set_info_text(message_type.get_empty_message().into());\n                    stop_flag.store(false, Ordering::Relaxed);\n                    app.set_stop_requested(false);\n                    app.set_processing(false);\n                })\n                .expect(\"Failed to update app info text\");\n            return;\n        }\n\n        let size_idx = self.active_tab.get_int_size_opt_idx();\n\n        // Sending progress data about how many items are queued to process\n        let mut base_progress = message_type.get_base_progress();\n        base_progress.entries_to_check = items_queued_to_process;\n        base_progress.bytes_to_check = match &process_fnc {\n            ProcessFunction::Simple(_) => size_idx\n                .map(|size_idx| simpler_model.iter().map(|(_idx, m)| if m.checked { m.get_size(size_idx) } else { 0 }).sum())\n                .unwrap_or_default(),\n            ProcessFunction::Related(_) => {\n                let mut contains_main_item_in_group = false;\n                let mut items_size = 0;\n                for (_idx, item) in &simpler_model {\n                    if item.header_row {\n                        contains_main_item_in_group = item.filled_header_row;\n                    } else {\n                        if item.checked {\n                            if contains_main_item_in_group {\n                                items_size += size_idx.map(|size_idx| item.get_size(size_idx)).unwrap_or_default();\n                            } else {\n                                contains_main_item_in_group = true;\n                            }\n                        }\n                    }\n                }\n                items_size\n            }\n        };\n        let _ = progress_sender.send(base_progress).map_err(|e| error!(\"Failed to send progress data: {e}\"));\n\n        let start_time = std::time::Instant::now();\n        let results = Self::process_items(\n            simpler_model,\n            items_queued_to_process,\n            progress_sender.clone(),\n            &stop_flag,\n            process_fnc,\n            message_type,\n            size_idx,\n            force_single_threaded,\n        );\n        let processing_time = start_time.elapsed();\n        let removing_items_from_model = std::time::Instant::now();\n        let (new_simple_model, errors, items_processed) = Self::remove_processed_items_from_model(results);\n        debug!(\n            \"Items processed in {processing_time:?}, removing items from model took {:?}, from all {} items, removed from list {}, failed to process {}\",\n            removing_items_from_model.elapsed(),\n            items_queued_to_process,\n            items_processed,\n            errors.len()\n        );\n        let errors_len = errors.len();\n\n        // Sending progress data at the end of processing, to indicate that processing is finished\n        base_progress.entries_checked = items_processed + errors_len;\n\n        let _ = progress_sender.send(base_progress).map_err(|e| error!(\"Failed to send progress data: {e}\"));\n\n        weak_app\n            .upgrade_in_event_loop(move |app| {\n                let mut new_model_after_removing_useless_items = self.remove_single_items_in_groups(new_simple_model.to_vec_model());\n                // Selection cache was invalidated, so we need to reset it\n                for e in &mut new_model_after_removing_useless_items {\n                    e.selected_row = false;\n                }\n                let checked_items = new_model_after_removing_useless_items.iter().filter(|e| e.checked).count();\n                self.active_tab.set_tool_model(&app, ModelRc::new(VecModel::from(new_model_after_removing_useless_items)));\n\n                app.global::<GuiState>()\n                    .set_info_text(Messages::new_from_errors(errors.clone()).create_messages_text(MessageLimit::NoLimit).into());\n\n                app.global::<GuiState>().set_preview_visible(false);\n\n                reset_selection(&app, self.active_tab, true);\n                set_number_of_enabled_items(&app, self.active_tab, checked_items as u64);\n                stop_flag.store(false, Ordering::Relaxed);\n                app.invoke_processing_ended(message_type.get_summary_message(items_processed, errors_len, items_queued_to_process).into());\n            })\n            .expect(\"Failed to update app after processing\");\n    }\n}\n"
  },
  {
    "path": "krokiet/src/set_initial_gui_info.rs",
    "content": "use czkawka_core::common::get_all_available_threads;\nuse slint::{ComponentHandle, Model, SharedString};\n\nuse crate::settings::combo_box::StringComboBoxItems;\nuse crate::{GuiState, MainWindow, Settings};\n\npub(crate) fn set_initial_gui_infos(app: &MainWindow) {\n    let threads = get_all_available_threads();\n    let settings = app.global::<Settings>();\n    app.global::<GuiState>().set_maximum_threads(threads as f32);\n\n    let collected_items = StringComboBoxItems::get_items();\n    let StringComboBoxItems {\n        languages,\n        hash_size,\n        resize_algorithm,\n        image_hash_alg,\n        duplicates_hash_type,\n        biggest_files_method,\n        audio_check_type,\n        duplicates_check_method,\n        videos_crop_detect,\n        video_optimizer_crop_type,\n        video_optimizer_mode,\n        video_optimizer_video_codec,\n    } = &*collected_items;\n\n    let languages_display_names = StringComboBoxItems::get_display_names(languages);\n    let hash_size_display_names = StringComboBoxItems::get_display_names(hash_size);\n    let resize_algorithm_display_names = StringComboBoxItems::get_display_names(resize_algorithm);\n    let image_hash_alg_display_names = StringComboBoxItems::get_display_names(image_hash_alg);\n    let duplicates_hash_type_display_names = StringComboBoxItems::get_display_names(duplicates_hash_type);\n    let biggest_files_method_display_names = StringComboBoxItems::get_display_names(biggest_files_method);\n    let audio_check_type_display_names = StringComboBoxItems::get_display_names(audio_check_type);\n    let duplicates_check_method_display_names = StringComboBoxItems::get_display_names(duplicates_check_method);\n    let videos_crop_detect_display_names = StringComboBoxItems::get_display_names(videos_crop_detect);\n    let video_optimizer_crop_type_display_names = StringComboBoxItems::get_display_names(video_optimizer_crop_type);\n    let video_optimizer_mode_display_names = StringComboBoxItems::get_display_names(video_optimizer_mode);\n    let video_optimizer_video_codec_display_names = StringComboBoxItems::get_display_names(video_optimizer_video_codec);\n\n    // Currently this is not possible due to slint bug - after 11.0 version I will try to fight with this - https://github.com/slint-ui/slint/issues/7632\n    // For now I just assert that names will be in sync with slint files\n\n    // settings.set_languages_list(VecModel::from_slice(&languages_display_names));\n    // settings.set_similar_images_sub_available_hash_size(VecModel::from_slice(&hash_size_display_names));\n    // settings.set_similar_images_sub_available_resize_algorithm(VecModel::from_slice(&resize_algorithm_display_names));\n    // settings.set_similar_images_sub_available_hash_type(VecModel::from_slice(&image_hash_alg_display_names));\n    // settings.set_biggest_files_sub_method(VecModel::from_slice(&biggest_files_method_display_names));\n    // settings.set_duplicates_sub_check_method(VecModel::from_slice(&duplicates_check_method_display_names));\n    // settings.set_duplicates_sub_available_hash_type(VecModel::from_slice(&duplicates_hash_type_display_names));\n    // settings.set_similar_music_sub_audio_check_type(VecModel::from_slice(&audio_check_type_display_names));\n    // settings.set_similar_videos_crop_detect(VecModel::from_slice(&videos_crop_detect_display_names));\n    // settings.set_video_optimizer_sub_crop_type(VecModel::from_slice(&video_optimizer_crop_type_display_names));\n    // settings.set_video_optimizer_sub_mode(VecModel::from_slice(&video_optimizer_mode_display_names));\n    // settings.set_video_optimizer_sub_video_codec_config(VecModel::from_slice(&video_optimizer_video_codec_display_names));\n\n    assert_eq!(settings.get_languages_list().iter().collect::<Vec<SharedString>>(), languages_display_names);\n    assert_eq!(\n        settings.get_similar_images_sub_available_hash_size().iter().collect::<Vec<SharedString>>(),\n        hash_size_display_names\n    );\n    assert_eq!(\n        settings.get_similar_images_sub_available_resize_algorithm().iter().collect::<Vec<SharedString>>(),\n        resize_algorithm_display_names\n    );\n    assert_eq!(\n        settings.get_similar_images_sub_available_hash_type().iter().collect::<Vec<SharedString>>(),\n        image_hash_alg_display_names\n    );\n    assert_eq!(\n        settings.get_biggest_files_sub_method().iter().collect::<Vec<SharedString>>(),\n        biggest_files_method_display_names\n    );\n    assert_eq!(\n        settings.get_duplicates_sub_check_method().iter().collect::<Vec<SharedString>>(),\n        duplicates_check_method_display_names\n    );\n    assert_eq!(\n        settings.get_duplicates_sub_available_hash_type().iter().collect::<Vec<SharedString>>(),\n        duplicates_hash_type_display_names\n    );\n    assert_eq!(\n        settings.get_similar_music_sub_audio_check_type().iter().collect::<Vec<SharedString>>(),\n        audio_check_type_display_names\n    );\n    assert_eq!(\n        settings.get_similar_videos_crop_detect().iter().collect::<Vec<SharedString>>(),\n        videos_crop_detect_display_names\n    );\n    assert_eq!(\n        settings.get_video_optimizer_sub_crop_type().iter().collect::<Vec<SharedString>>(),\n        video_optimizer_crop_type_display_names\n    );\n    assert_eq!(\n        settings.get_video_optimizer_sub_mode().iter().collect::<Vec<SharedString>>(),\n        video_optimizer_mode_display_names\n    );\n    assert_eq!(\n        settings.get_video_optimizer_sub_video_codec_config().iter().collect::<Vec<SharedString>>(),\n        video_optimizer_video_codec_display_names\n    );\n}\n"
  },
  {
    "path": "krokiet/src/set_initial_scroll_list_data_indexes.rs",
    "content": "use slint::ComponentHandle;\n\nuse crate::common::{\n    IntDataVideoOptimizer, StrDataBadExtensions, StrDataBadNames, StrDataBigFiles, StrDataBrokenFiles, StrDataDuplicateFiles, StrDataEmptyFiles, StrDataEmptyFolders,\n    StrDataExifRemover, StrDataInvalidSymlinks, StrDataSimilarImages, StrDataSimilarMusic, StrDataSimilarVideos, StrDataTemporaryFiles, StrDataVideoOptimizer,\n    create_model_from_model_vec,\n};\nuse crate::{GuiState, MainWindow};\n\ntype DataType = [i32; 6];\n\npub(crate) fn set_initial_scroll_list_data_indexes(app: &MainWindow) {\n    let gs = app.global::<GuiState>();\n\n    // [Parent Idx, File Name Idx, (Additional)Preview Idx, Rect Left Idx, Width Idx, Height Idx]\n    // Preview Idx is set only if there is non-standard preview like video\n\n    let duplicate_data: DataType = [StrDataDuplicateFiles::Path as i32, StrDataDuplicateFiles::Name as i32, -1, -1, -1, -1];\n    gs.set_duplicate_data_idx(create_model_from_model_vec(&duplicate_data));\n\n    let empty_folders_data: DataType = [StrDataEmptyFolders::Path as i32, StrDataEmptyFolders::Name as i32, -1, -1, -1, -1];\n    gs.set_empty_folders_data_idx(create_model_from_model_vec(&empty_folders_data));\n\n    let big_files_data: DataType = [StrDataBigFiles::Path as i32, StrDataBigFiles::Name as i32, -1, -1, -1, -1];\n    gs.set_big_files_data_idx(create_model_from_model_vec(&big_files_data));\n\n    let empty_files_data: DataType = [StrDataEmptyFiles::Path as i32, StrDataEmptyFiles::Name as i32, -1, -1, -1, -1];\n    gs.set_empty_files_data_idx(create_model_from_model_vec(&empty_files_data));\n\n    let temporary_files_data: DataType = [StrDataTemporaryFiles::Path as i32, StrDataTemporaryFiles::Name as i32, -1, -1, -1, -1];\n    gs.set_temporary_files_data_idx(create_model_from_model_vec(&temporary_files_data));\n\n    let similar_images_data: DataType = [StrDataSimilarImages::Path as i32, StrDataSimilarImages::Name as i32, -1, -1, -1, -1];\n    gs.set_similar_images_data_idx(create_model_from_model_vec(&similar_images_data));\n\n    let similar_videos_data: DataType = [\n        StrDataSimilarVideos::Path as i32,\n        StrDataSimilarVideos::Name as i32,\n        StrDataSimilarVideos::PreviewPath as i32,\n        -1,\n        -1,\n        -1,\n    ];\n    gs.set_similar_videos_data_idx(create_model_from_model_vec(&similar_videos_data));\n\n    let similar_music_data: DataType = [StrDataSimilarMusic::Path as i32, StrDataSimilarMusic::Name as i32, -1, -1, -1, -1];\n    gs.set_similar_music_data_idx(create_model_from_model_vec(&similar_music_data));\n\n    let invalid_symlink_data: DataType = [StrDataInvalidSymlinks::SymlinkFolder as i32, StrDataInvalidSymlinks::SymlinkName as i32, -1, -1, -1, -1];\n    gs.set_invalid_symlink_data_idx(create_model_from_model_vec(&invalid_symlink_data));\n\n    let broken_files_data: DataType = [StrDataBrokenFiles::Path as i32, StrDataBrokenFiles::Name as i32, -1, -1, -1, -1];\n    gs.set_broken_files_data_idx(create_model_from_model_vec(&broken_files_data));\n\n    let bad_extensions_data: DataType = [StrDataBadExtensions::Path as i32, StrDataBadExtensions::Name as i32, -1, -1, -1, -1];\n    gs.set_bad_extensions_data_idx(create_model_from_model_vec(&bad_extensions_data));\n\n    let exif_remover_data: DataType = [StrDataExifRemover::Path as i32, StrDataExifRemover::Name as i32, -1, -1, -1, -1];\n    gs.set_exif_remover_data_idx(create_model_from_model_vec(&exif_remover_data));\n\n    let video_optimizer_data: DataType = [\n        StrDataVideoOptimizer::Path as i32,\n        StrDataVideoOptimizer::Name as i32,\n        StrDataVideoOptimizer::PreviewPath as i32,\n        IntDataVideoOptimizer::RectLeft as i32,\n        IntDataVideoOptimizer::Width as i32,\n        IntDataVideoOptimizer::Height as i32,\n    ];\n    gs.set_video_optimizer_data_idx(create_model_from_model_vec(&video_optimizer_data));\n\n    let bad_names_data: DataType = [StrDataBadNames::Path as i32, StrDataBadNames::Name as i32, -1, -1, -1, -1];\n    gs.set_bad_names_data_idx(create_model_from_model_vec(&bad_names_data));\n}\n"
  },
  {
    "path": "krokiet/src/settings/combo_box.rs",
    "content": "use std::fmt::Debug;\nuse std::sync::{Arc, Mutex, MutexGuard};\n\nuse czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::re_exported::{Cropdetect, HashAlg};\nuse czkawka_core::tools::big_file::SearchMode;\nuse czkawka_core::tools::video_optimizer::{VideoCodec, VideoCroppingMechanism, VideoOptimizerMode};\nuse image::imageops::FilterType;\nuse log::warn;\nuse slint::SharedString;\n\nuse crate::connect_translation::LANGUAGE_LIST;\n\n#[derive(Debug, Clone)]\npub struct StringComboBoxItem<T>\nwhere\n    T: Clone + Debug,\n{\n    pub config_name: String,\n    pub display_name: String,\n    pub value: T,\n}\n\npub struct StringComboBoxItems {\n    pub languages: Vec<StringComboBoxItem<String>>,\n    pub hash_size: Vec<StringComboBoxItem<u8>>,\n    pub resize_algorithm: Vec<StringComboBoxItem<FilterType>>,\n    pub image_hash_alg: Vec<StringComboBoxItem<HashAlg>>,\n    pub duplicates_hash_type: Vec<StringComboBoxItem<HashType>>,\n    pub biggest_files_method: Vec<StringComboBoxItem<SearchMode>>,\n    pub audio_check_type: Vec<StringComboBoxItem<CheckingMethod>>,\n    pub duplicates_check_method: Vec<StringComboBoxItem<CheckingMethod>>,\n    pub videos_crop_detect: Vec<StringComboBoxItem<Cropdetect>>,\n    pub video_optimizer_crop_type: Vec<StringComboBoxItem<VideoCroppingMechanism>>,\n    pub video_optimizer_mode: Vec<StringComboBoxItem<VideoOptimizerMode>>,\n    pub video_optimizer_video_codec: Vec<StringComboBoxItem<VideoCodec>>,\n}\n\npub static STRING_COMBO_BOX_ITEMS: std::sync::LazyLock<Arc<Mutex<StringComboBoxItems>>> = std::sync::LazyLock::new(|| {\n    let l = StringComboBoxItems::regenerate_items();\n    Arc::new(Mutex::new(l))\n});\n\nimpl StringComboBoxItems {\n    pub(crate) fn get_item_and_idx_from_config_name<T>(config_name: &str, items: &Vec<StringComboBoxItem<T>>) -> (usize, Vec<SharedString>)\n    where\n        T: Clone + Debug,\n    {\n        let position = items.iter().position(|e| e.config_name == config_name).unwrap_or_else(|| {\n            warn!(\"Trying to get non existent item - \\\"{config_name}\\\" from {items:?}\");\n            0\n        });\n        let display_names = items.iter().map(|e| e.display_name.clone().into()).collect::<Vec<_>>();\n        (position, display_names)\n    }\n\n    pub(crate) fn regenerate_items() -> Self {\n        let languages = LANGUAGE_LIST\n            .iter()\n            .map(|e| StringComboBoxItem {\n                config_name: e.short_name.to_string(),\n                display_name: e.long_name.to_string(),\n                value: e.short_name.to_string(),\n            })\n            .collect();\n\n        let hash_size = Self::convert_to_combobox_items(&[(\"8\", \"8\", 8), (\"16\", \"16\", 16), (\"32\", \"32\", 32), (\"64\", \"64\", 64)]);\n        let resize_algorithm = Self::convert_to_combobox_items(&[\n            (\"lanczos3\", \"Lanczos3\", FilterType::Lanczos3),\n            (\"gaussian\", \"Gaussian\", FilterType::Gaussian),\n            (\"catmullrom\", \"CatmullRom\", FilterType::CatmullRom),\n            (\"triangle\", \"Triangle\", FilterType::Triangle),\n            (\"nearest\", \"Nearest\", FilterType::Nearest),\n        ]);\n\n        let image_hash_alg = Self::convert_to_combobox_items(&[\n            (\"mean\", \"Mean\", HashAlg::Mean),\n            (\"gradient\", \"Gradient\", HashAlg::Gradient),\n            (\"blockhash\", \"BlockHash\", HashAlg::Blockhash),\n            (\"vertgradient\", \"VertGradient\", HashAlg::VertGradient),\n            (\"doublegradient\", \"DoubleGradient\", HashAlg::DoubleGradient),\n            (\"median\", \"Median\", HashAlg::Median),\n        ]);\n\n        let duplicates_hash_type = Self::convert_to_combobox_items(&[\n            (\"blake3\", \"Blake3\", HashType::Blake3),\n            (\"crc32\", \"CRC32\", HashType::Crc32),\n            (\"xxh3\", \"XXH3\", HashType::Xxh3),\n        ]);\n\n        let biggest_files_method = Self::convert_to_combobox_items(&[\n            (\"biggest\", \"The Biggest\", SearchMode::BiggestFiles),\n            (\"smallest\", \"The Smallest\", SearchMode::SmallestFiles),\n        ]);\n\n        let audio_check_type = Self::convert_to_combobox_items(&[(\"tags\", \"Tags\", CheckingMethod::AudioTags), (\"fingerprint\", \"Fingerprint\", CheckingMethod::AudioContent)]);\n\n        let duplicates_check_method = Self::convert_to_combobox_items(&[\n            (\"hash\", \"Hash\", CheckingMethod::Hash),\n            (\"size\", \"Size\", CheckingMethod::Size),\n            (\"name\", \"Name\", CheckingMethod::Name),\n            (\"size_and_name\", \"Size and Name\", CheckingMethod::SizeName),\n        ]);\n\n        let videos_crop_detect = Self::convert_to_combobox_items(&[\n            (\"letterbox\", \"LetterBox\", Cropdetect::Letterbox),\n            (\"motion\", \"Motion\", Cropdetect::Motion),\n            (\"none\", \"None\", Cropdetect::None),\n        ]);\n\n        let video_optimizer_crop_type = Self::convert_to_combobox_items(&[\n            (\"blackbars\", \"Black Bars\", VideoCroppingMechanism::BlackBars),\n            (\"staticcontent\", \"Static Content\", VideoCroppingMechanism::StaticContent),\n        ]);\n\n        let video_optimizer_mode = Self::convert_to_combobox_items(&[\n            (\"crop\", \"Crop\", VideoOptimizerMode::VideoCrop),\n            (\"transcode\", \"Transcode\", VideoOptimizerMode::VideoTranscode),\n        ]);\n\n        let video_optimizer_video_codec = Self::convert_to_combobox_items(&[\n            (\"h265\", \"HEVC/H265\", VideoCodec::H265),\n            (\"h264\", \"H264\", VideoCodec::H264),\n            (\"vp9\", \"VP9\", VideoCodec::Vp9),\n            (\"av1\", \"AV1\", VideoCodec::Av1),\n        ]);\n\n        Self {\n            languages,\n            hash_size,\n            resize_algorithm,\n            image_hash_alg,\n            duplicates_hash_type,\n            biggest_files_method,\n            audio_check_type,\n            duplicates_check_method,\n            videos_crop_detect,\n            video_optimizer_crop_type,\n            video_optimizer_mode,\n            video_optimizer_video_codec,\n        }\n    }\n\n    fn convert_to_combobox_items<T>(input: &[(&str, &str, T)]) -> Vec<StringComboBoxItem<T>>\n    where\n        T: Clone + Debug,\n    {\n        input\n            .iter()\n            .map(|(config_name, display_name, value)| StringComboBoxItem {\n                config_name: config_name.to_string(),\n                display_name: display_name.to_string(),\n                value: value.clone(),\n            })\n            .collect()\n    }\n\n    pub(crate) fn get_items() -> MutexGuard<'static, Self> {\n        STRING_COMBO_BOX_ITEMS.lock().expect(\"Can't lock string combobox items\")\n    }\n\n    pub(crate) fn regenerate_and_set() {\n        *STRING_COMBO_BOX_ITEMS.lock().expect(\"Can't lock string combobox items\") = Self::regenerate_items();\n    }\n\n    pub(crate) fn get_display_names<T: Debug + Clone>(items: &[StringComboBoxItem<T>]) -> Vec<SharedString> {\n        items.iter().map(|e| e.display_name.clone().into()).collect()\n    }\n}\n"
  },
  {
    "path": "krokiet/src/settings/mod.rs",
    "content": "pub(crate) mod combo_box;\npub(crate) mod model;\n\nuse std::cmp::{max, min};\nuse std::collections::BTreeMap;\nuse std::path::PathBuf;\n\nuse czkawka_core::TOOLS_NUMBER;\nuse czkawka_core::common::basic_gui_cli::CliResult;\nuse czkawka_core::common::config_cache_path::get_config_cache_path;\nuse czkawka_core::common::{get_all_available_threads, set_number_of_threads};\nuse czkawka_core::tools::similar_videos::{ALLOWED_SKIP_FORWARD_AMOUNT, ALLOWED_VID_HASH_DURATION};\nuse log::{debug, error, info};\nuse serde::{Deserialize, Serialize};\nuse slint::{ComponentHandle, Model, ModelRc, PhysicalSize, VecModel, WindowSize};\n\nuse crate::common::{create_excluded_paths_model_from_pathbuf, create_included_paths_model_from_pathbuf, create_vec_model_from_vec_string};\nuse crate::connect_translation::change_language;\nuse crate::settings::combo_box::StringComboBoxItems;\nuse crate::settings::model::{\n    BasicSettings, ComboBoxItems, DEFAULT_BIGGEST_FILES, DEFAULT_MAX_VIDEO_THUMBNAIL_POSITION_PERCENT, DEFAULT_MAXIMUM_SIZE_KB, DEFAULT_MIN_VIDEO_THUMBNAIL_POSITION_PERCENT,\n    DEFAULT_MINIMUM_CACHE_SIZE, DEFAULT_MINIMUM_PREHASH_CACHE_SIZE, DEFAULT_MINIMUM_SIZE_KB, MAX_HASH_SIZE, PRESET_NAME_RESERVED, PRESET_NUMBER, RESERVER_PRESET_IDX,\n    SettingsCustom, default_video_optimizer_black_pixel_threshold, default_video_optimizer_max_samples, default_video_optimizer_min_crop_size,\n};\nuse crate::{Callabler, GuiState, MainWindow, Settings, flk};\n\npub(crate) fn connect_changing_settings_preset(app: &MainWindow) {\n    let a = app.as_weak();\n    app.global::<Callabler>().on_changed_settings_preset(move || {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let current_item = app.global::<Settings>().get_settings_preset_idx();\n        let loaded_data = load_data_from_file::<SettingsCustom>(get_config_file(current_item));\n        let base_settings = load_data_from_file::<BasicSettings>(get_base_config_file()).unwrap_or_default();\n        match loaded_data {\n            Ok(loaded_data) => {\n                set_settings_to_gui(&app, &loaded_data, &base_settings, None);\n                app.set_text_summary_text(flk!(\"rust_loaded_preset\", preset_idx = (current_item + 1)).into());\n            }\n            Err(e) => {\n                set_settings_to_gui(&app, &SettingsCustom::default(), &base_settings, None);\n                app.set_text_summary_text(flk!(\"rust_cannot_load_preset\", preset_idx = (current_item + 1), reason = (&e)).into());\n                error!(\"Failed to change preset - {e}, using default instead\");\n            }\n        }\n    });\n    let a = app.as_weak();\n    app.global::<Callabler>().on_save_current_preset(move || {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let settings = app.global::<Settings>();\n        let current_item = settings.get_settings_preset_idx();\n        let result = save_data_to_file(get_config_file(current_item), &collect_settings(&app));\n        match result {\n            Ok(()) => {\n                app.set_text_summary_text(flk!(\"rust_saved_preset\", preset_idx = (current_item + 1)).into());\n            }\n            Err(e) => {\n                app.set_text_summary_text(flk!(\"rust_cannot_save_preset\", preset_idx = (current_item + 1), reason = (&e)).into());\n                error!(\"Failed to save preset - {e}\");\n            }\n        }\n    });\n    let a = app.as_weak();\n    app.global::<Callabler>().on_reset_current_preset(move || {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let settings = app.global::<Settings>();\n        let current_item = settings.get_settings_preset_idx();\n        let base_settings = load_data_from_file::<BasicSettings>(get_base_config_file()).unwrap_or_default();\n        set_settings_to_gui(&app, &SettingsCustom::default(), &base_settings, None);\n        app.set_text_summary_text(flk!(\"rust_reset_preset\", preset_idx = (current_item + 1)).into());\n    });\n    let a = app.as_weak();\n    app.global::<Callabler>().on_load_current_preset(move || {\n        let app = a.upgrade().expect(\"Failed to upgrade app :(\");\n        let settings = app.global::<Settings>();\n        let current_item = settings.get_settings_preset_idx();\n        let custom_settings = load_data_from_file::<SettingsCustom>(get_config_file(current_item));\n        let base_settings = load_data_from_file::<BasicSettings>(get_base_config_file()).unwrap_or_default();\n        match custom_settings {\n            Ok(loaded_data) => {\n                set_settings_to_gui(&app, &loaded_data, &base_settings, None);\n                app.set_text_summary_text(flk!(\"rust_loaded_preset\", preset_idx = (current_item + 1)).into());\n            }\n            Err(e) => {\n                set_settings_to_gui(&app, &SettingsCustom::default(), &base_settings, None);\n                let err_message = flk!(\"rust_cannot_load_preset\", preset_idx = (current_item + 1), reason = (&e));\n                app.set_text_summary_text(err_message.into());\n                error!(\"Failed to load preset - {e}, using default instead\");\n            }\n        }\n    });\n}\n\npub(crate) fn create_default_settings_files() {\n    let base_config_file = get_base_config_file();\n    if let Some(base_config_file) = base_config_file\n        && !base_config_file.is_file()\n    {\n        let _ = save_data_to_file(Some(base_config_file), &BasicSettings::default());\n    }\n\n    for i in 0..PRESET_NUMBER {\n        let config_file = get_config_file(i as i32);\n        if let Some(config_file) = config_file\n            && !config_file.is_file()\n        {\n            let _ = save_data_to_file(Some(config_file), &SettingsCustom::default());\n        }\n    }\n}\n\npub(crate) fn load_initial_settings_from_file(cli_result: Option<&CliResult>) -> (BasicSettings, SettingsCustom, i32) {\n    StringComboBoxItems::regenerate_and_set();\n\n    let result_base_settings = load_data_from_file::<BasicSettings>(get_base_config_file());\n\n    let mut base_settings = if let Ok(base_settings_temp) = result_base_settings {\n        base_settings_temp\n    } else {\n        info!(\"Cannot load base settings, using default instead\");\n        BasicSettings::default()\n    };\n\n    let preset_to_load = if cli_result.is_some() { RESERVER_PRESET_IDX } else { base_settings.default_preset };\n\n    let mut custom_settings = load_data_from_file::<SettingsCustom>(get_config_file(preset_to_load)).unwrap_or_else(|e| {\n        error!(\"Cannot load custom settings for preset {preset_to_load} - {e}, using default instead\");\n        SettingsCustom::default()\n    });\n\n    base_settings.preset_names.truncate(PRESET_NUMBER);\n\n    if base_settings.preset_names.len() < PRESET_NUMBER {\n        while base_settings.preset_names.len() < PRESET_NUMBER - 1 {\n            base_settings.preset_names.push(format!(\"Preset {}\", base_settings.preset_names.len() + 1));\n        }\n        base_settings.preset_names.push(PRESET_NAME_RESERVED.to_string());\n    }\n    base_settings.default_preset = base_settings.default_preset.clamp(0, PRESET_NUMBER as i32 - 2);\n    custom_settings.thread_number = max(min(custom_settings.thread_number, get_all_available_threads() as i32), 0);\n\n    (base_settings, custom_settings, preset_to_load)\n}\n\npub(crate) fn set_initial_settings_to_gui(app: &MainWindow, base_settings: &BasicSettings, custom_settings: &SettingsCustom, cli_result: Option<CliResult>, preset_to_load: i32) {\n    set_settings_to_gui(app, custom_settings, base_settings, cli_result);\n    set_base_settings_to_gui(app, base_settings, preset_to_load);\n    set_number_of_threads(custom_settings.thread_number as usize);\n}\n\npub(crate) fn save_all_settings_to_file(app: &MainWindow, original_preset_idx: i32) {\n    save_base_settings_to_file(app, original_preset_idx);\n    save_custom_settings_to_file(app);\n\n    info!(\"Saved settings to file\");\n}\n\npub(crate) fn save_base_settings_to_file(app: &MainWindow, original_preset_idx: i32) {\n    let mut collected_config_from_file = collect_base_settings(app);\n\n    // We cannot normally start app with disallowed preset, so we restore it to original value\n    if collected_config_from_file.default_preset == PRESET_NUMBER as i32 - 1 {\n        collected_config_from_file.default_preset = original_preset_idx;\n    }\n\n    let result = save_data_to_file(get_base_config_file(), &collected_config_from_file);\n\n    if let Err(e) = result {\n        error!(\"Failed to save base settings - {e}\");\n    }\n}\n\npub(crate) fn save_custom_settings_to_file(app: &MainWindow) {\n    let current_item = app.global::<Settings>().get_settings_preset_idx();\n    let result = save_data_to_file(get_config_file(current_item), &collect_settings(app));\n\n    if let Err(e) = result {\n        error!(\"Failed to save custom settings - {e}\");\n    }\n}\n\npub(crate) fn load_data_from_file<T>(config_file: Option<PathBuf>) -> Result<T, String>\nwhere\n    for<'de> T: Deserialize<'de>,\n{\n    let current_time = std::time::Instant::now();\n    let Some(config_file) = config_file else {\n        return Err(\"Cannot get config file\".into());\n    };\n    if !config_file.is_file() {\n        return Err(format!(\"Config file \\\"{}\\\" doesn't exist\", config_file.to_string_lossy()));\n    }\n\n    let result = match std::fs::read_to_string(&config_file) {\n        Ok(serialized) => {\n            debug!(\"Loading data from file \\\"{}\\\" took {:?}\", config_file.to_string_lossy(), current_time.elapsed());\n\n            match serde_json::from_str(&serialized) {\n                Ok(custom_settings) => Ok(custom_settings),\n                Err(e) => Err(format!(\"Cannot deserialize settings: {e}\")),\n            }\n        }\n        Err(e) => Err(format!(\"Cannot read config file: {e}\")),\n    };\n\n    debug!(\n        \"Loading and converting data from file \\\"{}\\\" took {:?}\",\n        config_file.to_string_lossy(),\n        current_time.elapsed()\n    );\n\n    result\n}\n\npub(crate) fn save_data_to_file<T>(config_file: Option<PathBuf>, serializable_data: &T) -> Result<(), String>\nwhere\n    T: Serialize,\n{\n    let current_time = std::time::Instant::now();\n    let Some(config_file) = config_file else {\n        return Err(\"Cannot get config file\".into());\n    };\n    // Create dirs if not exists\n    if let Some(parent) = config_file.parent()\n        && let Err(e) = std::fs::create_dir_all(parent)\n    {\n        return Err(format!(\"Cannot create config folder \\\"{}\\\": {e}\", parent.to_string_lossy()));\n    }\n\n    match serde_json::to_string_pretty(&serializable_data) {\n        Ok(serialized) => {\n            if let Err(e) = std::fs::write(&config_file, serialized) {\n                return Err(format!(\"Cannot save config file: {e}\"));\n            }\n        }\n        Err(e) => {\n            return Err(format!(\"Cannot serialize settings: {e}\"));\n        }\n    }\n\n    debug!(\"Saving data to file {:?} took {:?}\", config_file, current_time.elapsed());\n    Ok(())\n}\n\npub(crate) fn get_base_config_file() -> Option<PathBuf> {\n    let config_folder = get_config_cache_path()?.config_folder;\n    let base_config_file = config_folder.join(\"config_general.json\");\n    Some(base_config_file)\n}\npub(crate) fn get_config_file(number: i32) -> Option<PathBuf> {\n    let config_folder = get_config_cache_path()?.config_folder;\n    let config_file = config_folder.join(format!(\"config_preset_{number}.json\"));\n    Some(config_file)\n}\n\npub(crate) fn set_base_settings_to_gui(app: &MainWindow, basic_settings: &BasicSettings, preset_idx: i32) {\n    let settings = app.global::<Settings>();\n    change_language(app);\n\n    settings.set_settings_preset_idx(preset_idx);\n    settings.set_settings_presets(ModelRc::new(create_vec_model_from_vec_string(basic_settings.preset_names.clone())));\n\n    let width = basic_settings.window_width.clamp(100, 1920 * 4);\n    let height = basic_settings.window_height.clamp(100, 1080 * 4);\n\n    if basic_settings.settings_load_windows_size_at_startup {\n        app.window().set_size(WindowSize::Physical(PhysicalSize { width, height }));\n    }\n    settings.set_dark_theme(basic_settings.dark_theme);\n    settings.set_show_only_icons(basic_settings.show_only_icons);\n    app.global::<Callabler>().invoke_theme_changed();\n    settings.set_load_tabs_sizes_at_startup(basic_settings.settings_load_tabs_sizes_at_startup);\n    settings.set_load_windows_size_at_startup(basic_settings.settings_load_windows_size_at_startup);\n    settings.set_limit_messages_lines(basic_settings.settings_limit_lines_of_messages);\n    settings.set_manual_application_scale(basic_settings.manual_application_scale);\n    settings.set_use_manual_application_scale(basic_settings.use_manual_application_scale);\n    settings.set_play_audio_on_scan_completion(basic_settings.play_audio_on_scan_completion);\n\n    set_combobox_basic_settings_items(&settings, basic_settings);\n}\n\npub(crate) fn set_combobox_basic_settings_items(settings: &Settings, basic_settings: &BasicSettings) {\n    let collected_items = StringComboBoxItems::get_items();\n\n    // Language\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&basic_settings.language, &collected_items.languages);\n    // settings.set_language_model(display_names); // TODO - replace with\n    settings.set_language_index(idx as i32);\n    settings.set_language_value(display_names[idx].clone());\n}\n\npub(crate) fn set_combobox_custom_settings_items(settings: &Settings, custom_settings: &SettingsCustom) {\n    let collected_items = StringComboBoxItems::get_items();\n\n    // Hash size\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.similar_images_sub_hash_size, &collected_items.hash_size);\n    // settings.set_similar_images_sub_hash_size_model(display_names); // TODO - replace with\n    settings.set_similar_images_sub_hash_size_index(idx as i32);\n    settings.set_similar_images_sub_hash_size_value(display_names[idx].clone());\n\n    // Hash type\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.similar_images_sub_hash_alg, &collected_items.image_hash_alg);\n    // settings.set_similar_images_sub_hash_alg_model(display_names);\n    settings.set_similar_images_sub_hash_alg_index(idx as i32);\n    settings.set_similar_images_sub_hash_alg_value(display_names[idx].clone());\n\n    // Resize algorithm\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.similar_images_sub_resize_algorithm, &collected_items.resize_algorithm);\n    // settings.set_similar_images_sub_resize_algorithm_model(display_names);\n    settings.set_similar_images_sub_resize_algorithm_index(idx as i32);\n    settings.set_similar_images_sub_resize_algorithm_value(display_names[idx].clone());\n\n    // Duplicates check method\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.duplicates_sub_check_method, &collected_items.duplicates_check_method);\n    // settings.set_duplicates_sub_check_method_model(display_names);\n    settings.set_duplicates_sub_check_method_index(idx as i32);\n    settings.set_duplicates_sub_check_method_value(display_names[idx].clone());\n\n    // Duplicates hash type\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.duplicates_sub_available_hash_type, &collected_items.duplicates_hash_type);\n    // settings.set_duplicates_sub_available_hash_type_model(display_names);\n    settings.set_duplicates_sub_available_hash_type_index(idx as i32);\n    settings.set_duplicates_sub_available_hash_type_value(display_names[idx].clone());\n\n    // Biggest files method\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.biggest_files_sub_method, &collected_items.biggest_files_method);\n    // settings.set_biggest_files_sub_method_model(display_names);\n    settings.set_biggest_files_sub_method_index(idx as i32);\n    settings.set_biggest_files_sub_method_value(display_names[idx].clone());\n\n    // Audio check type\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.similar_music_sub_audio_check_type, &collected_items.audio_check_type);\n    // settings.set_duplicates_sub_available_hash_type_model(display_names);\n    settings.set_similar_music_sub_audio_check_type_index(idx as i32);\n    settings.set_similar_music_sub_audio_check_type_value(display_names[idx].clone());\n\n    // Crop detect\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.similar_videos_crop_detect, &collected_items.videos_crop_detect);\n    // settings.set_similar_videos_crop_detect_model(display_names);\n    settings.set_similar_videos_crop_detect_index(idx as i32);\n    settings.set_similar_videos_crop_detect_value(display_names[idx].clone());\n\n    // Video Optimizer mode\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.video_optimizer_mode, &collected_items.video_optimizer_mode);\n    settings.set_video_optimizer_sub_mode_index(idx as i32);\n    settings.set_video_optimizer_sub_mode_value(display_names[idx].clone());\n\n    // Video Optimizer crop type\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.video_optimizer_crop_type, &collected_items.video_optimizer_crop_type);\n    settings.set_video_optimizer_sub_crop_type_index(idx as i32);\n    settings.set_video_optimizer_sub_crop_type_value(display_names[idx].clone());\n\n    // Video Optimizer video codec\n    let (idx, display_names) = StringComboBoxItems::get_item_and_idx_from_config_name(&custom_settings.video_optimizer_video_codec, &collected_items.video_optimizer_video_codec);\n    settings.set_video_optimizer_sub_video_codec_index(idx as i32);\n    settings.set_video_optimizer_sub_video_codec_value(display_names[idx].clone());\n}\n\npub(crate) fn set_settings_to_gui(app: &MainWindow, custom_settings: &SettingsCustom, base_settings: &BasicSettings, cli_args: Option<CliResult>) {\n    let settings = app.global::<Settings>();\n\n    let (included, referenced, excluded) = if let Some(cli_args) = cli_args {\n        let vs_to_vp = |vec: Vec<String>| vec.into_iter().map(PathBuf::from).collect::<Vec<_>>();\n        (vs_to_vp(cli_args.included_items), vs_to_vp(cli_args.referenced_items), vs_to_vp(cli_args.excluded_items))\n    } else {\n        (\n            custom_settings.included_paths.clone(),\n            custom_settings.included_paths_referenced.clone(),\n            custom_settings.excluded_paths.clone(),\n        )\n    };\n    // Included directories\n    let included_paths = create_included_paths_model_from_pathbuf(&included, &referenced);\n    settings.set_included_paths_model(included_paths);\n\n    // Excluded directories\n    let excluded_paths = create_excluded_paths_model_from_pathbuf(&excluded);\n    settings.set_excluded_paths_model(excluded_paths);\n\n    settings.set_excluded_items(custom_settings.excluded_items.clone().into());\n    settings.set_allowed_extensions(custom_settings.allowed_extensions.clone().into());\n    settings.set_excluded_extensions(custom_settings.excluded_extensions.clone().into());\n    settings.set_minimum_file_size(custom_settings.minimum_file_size.to_string().into());\n    settings.set_maximum_file_size(custom_settings.maximum_file_size.to_string().into());\n    settings.set_use_cache(custom_settings.use_cache);\n    settings.set_save_as_json(custom_settings.save_also_as_json);\n    settings.set_move_to_trash(custom_settings.move_deleted_files_to_trash);\n    settings.set_ignore_other_filesystems(custom_settings.ignore_other_file_systems);\n    settings.set_thread_number(custom_settings.thread_number as f32);\n\n    settings.set_recursive_search(custom_settings.recursive_search);\n    settings.set_duplicate_image_preview(custom_settings.duplicate_image_preview);\n    settings.set_duplicate_use_prehash(custom_settings.duplicate_use_prehash);\n    settings.set_duplicate_minimal_hash_cache_size(custom_settings.duplicate_minimal_hash_cache_size.to_string().into());\n    settings.set_duplicate_minimal_prehash_cache_size(custom_settings.duplicate_minimal_prehash_cache_size.to_string().into());\n    settings.set_delete_outdated_cache_entries(custom_settings.delete_outdated_cache_entries);\n    settings.set_hide_hard_links(custom_settings.hide_hard_links);\n    settings.set_duplicates_sub_name_case_sensitive(custom_settings.duplicates_sub_name_case_sensitive);\n    settings.set_similar_images_show_image_preview(custom_settings.similar_images_show_image_preview);\n    settings.set_video_thumbnails_preview(custom_settings.video_thumbnails_preview);\n    settings.set_video_thumbnails_unused_thumbnails(custom_settings.video_thumbnails_unused_thumbnails);\n    settings.set_similar_music_compare_fingerprints_only_with_similar_titles(custom_settings.similar_music_compare_fingerprints_only_with_similar_titles);\n\n    set_combobox_custom_settings_items(&settings, custom_settings);\n\n    settings.set_similar_images_sub_ignore_same_size(custom_settings.similar_images_sub_ignore_same_size);\n    settings.set_similar_images_sub_max_similarity(MAX_HASH_SIZE);\n    settings.set_similar_images_sub_current_similarity(custom_settings.similar_images_sub_similarity as f32);\n\n    settings.set_biggest_files_sub_number_of_files(custom_settings.biggest_files_sub_number_of_files.to_string().into());\n\n    settings.set_similar_videos_sub_ignore_same_size(custom_settings.similar_videos_sub_ignore_same_size);\n    settings.set_similar_videos_sub_current_similarity(custom_settings.similar_videos_sub_similarity as f32);\n    settings.set_similar_videos_sub_max_similarity(20.0);\n    settings.set_similar_videos_skip_forward_amount(\n        custom_settings\n            .similar_videos_skip_forward_amount\n            .clamp(*ALLOWED_SKIP_FORWARD_AMOUNT.start(), *ALLOWED_SKIP_FORWARD_AMOUNT.end()) as f32,\n    );\n    settings.set_similar_videos_skip_forward_amount_min(*ALLOWED_SKIP_FORWARD_AMOUNT.start() as f32);\n    settings.set_similar_videos_skip_forward_amount_max(*ALLOWED_SKIP_FORWARD_AMOUNT.end() as f32);\n    settings.set_similar_videos_vid_hash_duration(\n        custom_settings\n            .similar_videos_vid_hash_duration\n            .clamp(*ALLOWED_VID_HASH_DURATION.start(), *ALLOWED_VID_HASH_DURATION.end()) as f32,\n    );\n    settings.set_similar_videos_vid_hash_duration_min(*ALLOWED_VID_HASH_DURATION.start() as f32);\n    settings.set_similar_videos_vid_hash_duration_max(*ALLOWED_VID_HASH_DURATION.end() as f32);\n\n    settings.set_video_thumbnails_generate(custom_settings.video_thumbnails_generate);\n    settings.set_video_thumbnails_percentage(\n        custom_settings\n            .video_thumbnails_percentage\n            .clamp(DEFAULT_MIN_VIDEO_THUMBNAIL_POSITION_PERCENT, DEFAULT_MAX_VIDEO_THUMBNAIL_POSITION_PERCENT) as f32,\n    );\n    settings.set_video_thumbnails_percentage_min(DEFAULT_MIN_VIDEO_THUMBNAIL_POSITION_PERCENT as f32);\n    settings.set_video_thumbnails_percentage_max(DEFAULT_MAX_VIDEO_THUMBNAIL_POSITION_PERCENT as f32);\n    settings.set_video_thumbnails_generate_grid(custom_settings.video_thumbnails_generate_grid);\n    settings.set_video_thumbnails_grid_tiles_per_side(custom_settings.video_thumbnails_grid_tiles_per_side as f32);\n    settings.set_video_thumbnails_grid_tiles_per_side_min(2.0);\n    settings.set_video_thumbnails_grid_tiles_per_side_max(6.0);\n\n    settings.set_similar_music_sub_approximate_comparison(custom_settings.similar_music_sub_approximate_comparison);\n    settings.set_similar_music_sub_title(custom_settings.similar_music_sub_title);\n    settings.set_similar_music_sub_artist(custom_settings.similar_music_sub_artist);\n    settings.set_similar_music_sub_year(custom_settings.similar_music_sub_year);\n    settings.set_similar_music_sub_bitrate(custom_settings.similar_music_sub_bitrate);\n    settings.set_similar_music_sub_genre(custom_settings.similar_music_sub_genre);\n    settings.set_similar_music_sub_length(custom_settings.similar_music_sub_length);\n    settings.set_similar_music_sub_maximum_difference_value(custom_settings.similar_music_sub_maximum_difference_value);\n    settings.set_similar_music_sub_minimal_fragment_duration_value(custom_settings.similar_music_sub_minimal_fragment_duration_value);\n\n    settings.set_broken_files_sub_audio(custom_settings.broken_files_sub_audio);\n    settings.set_broken_files_sub_pdf(custom_settings.broken_files_sub_pdf);\n    settings.set_broken_files_sub_archive(custom_settings.broken_files_sub_archive);\n    settings.set_broken_files_sub_image(custom_settings.broken_files_sub_image);\n    settings.set_broken_files_sub_video(custom_settings.broken_files_sub_video);\n\n    settings.set_bad_names_sub_uppercase_extension(custom_settings.bad_names_sub_uppercase_extension);\n    settings.set_bad_names_sub_emoji_used(custom_settings.bad_names_sub_emoji_used);\n    settings.set_bad_names_sub_space_at_start_end(custom_settings.bad_names_sub_space_at_start_end);\n    settings.set_bad_names_sub_non_ascii(custom_settings.bad_names_sub_non_ascii);\n    settings.set_bad_names_sub_restricted_charset_enabled(custom_settings.bad_names_sub_restricted_charset_enabled);\n    settings.set_bad_names_sub_restricted_charset(custom_settings.bad_names_sub_restricted_charset.iter().collect::<String>().into());\n    settings.set_bad_names_sub_remove_duplicated(custom_settings.bad_names_sub_remove_duplicated);\n\n    settings.set_video_optimizer_sub_excluded_codecs(custom_settings.video_optimizer_excluded_codecs.clone().into());\n    settings.set_video_optimizer_sub_black_pixel_threshold(custom_settings.video_optimizer_black_pixel_threshold.to_string().into());\n    settings.set_video_optimizer_sub_black_bar_min_percentage(custom_settings.video_optimizer_black_bar_min_percentage.to_string().into());\n    settings.set_video_optimizer_sub_max_samples(custom_settings.video_optimizer_max_samples.to_string().into());\n    settings.set_video_optimizer_sub_min_crop_size(custom_settings.video_optimizer_min_crop_size.to_string().into());\n    settings.set_video_optimizer_sub_video_quality(custom_settings.video_optimizer_video_quality as f32);\n    settings.set_video_optimizer_sub_fail_if_bigger(custom_settings.video_optimizer_fail_if_bigger);\n    settings.set_video_optimizer_sub_overwrite_files(custom_settings.video_optimizer_overwrite_files);\n    settings.set_video_optimizer_sub_limit_video_size(custom_settings.video_optimizer_limit_video_size);\n    settings.set_video_optimizer_sub_max_width(custom_settings.video_optimizer_max_width.to_string().into());\n    settings.set_video_optimizer_sub_max_height(custom_settings.video_optimizer_max_height.to_string().into());\n    settings.set_video_optimizer_sub_image_threshold(custom_settings.video_optimizer_image_threshold as f32);\n\n    settings.set_ignored_exif_tags(custom_settings.ignored_exif_tags.clone().into());\n\n    // Popup-specific settings\n    settings.set_popup_move_preserve_folder_structure(custom_settings.popup_move_preserve_folder_structure);\n    settings.set_popup_move_copy_mode(custom_settings.popup_move_copy_mode);\n    settings.set_popup_clean_exif_overwrite_files(custom_settings.popup_clean_exif_overwrite_files);\n    settings.set_popup_reencode_video_overwrite_files(custom_settings.popup_reencode_video_overwrite_files);\n    settings.set_popup_reencode_video_quality(custom_settings.popup_reencode_video_quality as f32);\n    settings.set_popup_reencode_video_fail_if_bigger(custom_settings.popup_reencode_video_fail_if_bigger);\n    settings.set_popup_reencode_video_limit_video_size(custom_settings.popup_reencode_video_limit_video_size);\n    settings.set_popup_reencode_video_max_width(custom_settings.popup_reencode_video_max_width.to_string().into());\n    settings.set_popup_reencode_video_max_height(custom_settings.popup_reencode_video_max_height.to_string().into());\n    settings.set_popup_crop_video_overwrite_files(custom_settings.popup_crop_video_overwrite_files);\n    settings.set_popup_crop_video_reencode(custom_settings.popup_crop_video_reencode);\n    settings.set_popup_crop_video_quality(custom_settings.popup_crop_video_quality as f32);\n\n    let sel_px = 35.0;\n    let path_px = 350.0;\n    let name_px = 100.0;\n    let mod_px = 125.0;\n    let size_px = 75.0;\n\n    let fnm = |default_model: &[f32], name: &str| {\n        let model = default_model.iter().map(|s| (*s).clamp(30.0, 2500.0));\n        let model = model\n            .into_iter()\n            .enumerate()\n            .map(|(idx, data)| *custom_settings.column_sizes.get(name).cloned().unwrap_or_default().get(idx).unwrap_or(&data))\n            .collect::<Vec<_>>();\n\n        ModelRc::new(VecModel::from(model))\n    };\n\n    if base_settings.settings_load_tabs_sizes_at_startup {\n        settings.set_duplicates_column_size(fnm(&[sel_px, size_px, name_px, path_px, mod_px], \"duplicates\"));\n        settings.set_empty_folders_column_size(fnm(&[sel_px, name_px, path_px, mod_px], \"empty_folders\"));\n        settings.set_empty_files_column_size(fnm(&[sel_px, name_px, path_px, mod_px], \"empty_files\"));\n        settings.set_temporary_files_column_size(fnm(&[sel_px, name_px, path_px, mod_px], \"temporary_files\"));\n        settings.set_big_files_column_size(fnm(&[sel_px, size_px, name_px, path_px, mod_px], \"big_files\"));\n        settings.set_similar_images_column_size(fnm(&[sel_px, 80.0, 80.0, 80.0, name_px, path_px, mod_px], \"similar_images\"));\n        settings.set_similar_videos_column_size(fnm(&[sel_px, size_px, name_px, path_px, 80.0, 80.0, 80.0, 80.0, 80.0, mod_px], \"similar_videos\"));\n        settings.set_similar_music_column_size(fnm(&[sel_px, size_px, name_px, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, path_px, mod_px], \"similar_music\"));\n        settings.set_invalid_symlink_column_size(fnm(&[sel_px, name_px, path_px, path_px, mod_px], \"invalid_symlink\"));\n        settings.set_broken_files_column_size(fnm(&[sel_px, name_px, path_px, 200.0, size_px, mod_px], \"broken_files\"));\n        settings.set_bad_extensions_column_size(fnm(&[sel_px, name_px, path_px, 40.0, 200.0], \"bad_extensions\"));\n        settings.set_exif_remover_column_size(fnm(&[sel_px, size_px, name_px, path_px, 300.0, mod_px], \"exif_remover\"));\n        settings.set_video_optimizer_column_size(fnm(&[sel_px, size_px, name_px, path_px, 100.0, 120.0, 160.0, mod_px], \"video_optimizer\"));\n        settings.set_bad_names_column_size(fnm(&[sel_px, name_px, 250.0, path_px], \"bad_names\"));\n    }\n\n    // Clear text\n    app.global::<GuiState>().set_info_text(\"\".into());\n}\n\npub(crate) fn collect_settings(app: &MainWindow) -> SettingsCustom {\n    let settings = app.global::<Settings>();\n\n    let combo_box_items = collect_combo_box_settings(app);\n\n    let included_paths_model = settings.get_included_paths_model();\n    let included_paths = included_paths_model.iter().map(|model| PathBuf::from(model.path.as_str())).collect::<Vec<_>>();\n    let included_paths_referenced = included_paths_model\n        .iter()\n        .filter(|model| model.referenced_path)\n        .map(|model| PathBuf::from(model.path.as_str()))\n        .collect::<Vec<_>>();\n\n    let excluded_paths_model = settings.get_excluded_paths_model();\n    let excluded_paths = excluded_paths_model.iter().map(|model| PathBuf::from(model.path.as_str())).collect::<Vec<_>>();\n\n    let excluded_items = settings.get_excluded_items().to_string();\n    let allowed_extensions = settings.get_allowed_extensions().to_string();\n    let excluded_extensions = settings.get_excluded_extensions().to_string();\n    let minimum_file_size = settings.get_minimum_file_size().parse::<i32>().unwrap_or(DEFAULT_MINIMUM_SIZE_KB);\n    let maximum_file_size = settings.get_maximum_file_size().parse::<i32>().unwrap_or(DEFAULT_MAXIMUM_SIZE_KB);\n\n    let recursive_search = settings.get_recursive_search();\n    let use_cache = settings.get_use_cache();\n    let save_also_as_json = settings.get_save_as_json();\n    let move_deleted_files_to_trash = settings.get_move_to_trash();\n    let ignore_other_file_systems = settings.get_ignore_other_filesystems();\n    let thread_number = settings.get_thread_number().round() as i32;\n\n    let duplicate_image_preview = settings.get_duplicate_image_preview();\n    let duplicate_use_prehash = settings.get_duplicate_use_prehash();\n    let duplicate_minimal_hash_cache_size = settings.get_duplicate_minimal_hash_cache_size().parse::<i32>().unwrap_or(DEFAULT_MINIMUM_CACHE_SIZE);\n    let duplicate_minimal_prehash_cache_size = settings\n        .get_duplicate_minimal_prehash_cache_size()\n        .parse::<i32>()\n        .unwrap_or(DEFAULT_MINIMUM_PREHASH_CACHE_SIZE);\n    let delete_outdated_cache_entries = settings.get_delete_outdated_cache_entries();\n    let hide_hard_links = settings.get_hide_hard_links();\n    let duplicates_sub_name_case_sensitive = settings.get_duplicates_sub_name_case_sensitive();\n\n    let similar_images_show_image_preview = settings.get_similar_images_show_image_preview();\n\n    let video_thumbnails_preview = settings.get_video_thumbnails_preview();\n    let video_thumbnails_unused_thumbnails = settings.get_video_thumbnails_unused_thumbnails();\n\n    let similar_music_compare_fingerprints_only_with_similar_titles = settings.get_similar_music_compare_fingerprints_only_with_similar_titles();\n\n    let similar_images_sub_hash_size = combo_box_items.hash_size.config_name.clone();\n    let similar_images_sub_hash_alg = combo_box_items.image_hash_alg.config_name.clone();\n    let similar_images_sub_resize_algorithm = combo_box_items.resize_algorithm.config_name.clone();\n    let similar_images_sub_ignore_same_size = settings.get_similar_images_sub_ignore_same_size();\n    let similar_images_sub_similarity = settings.get_similar_images_sub_current_similarity().round() as i32;\n\n    let duplicates_sub_check_method = combo_box_items.duplicates_check_method.config_name.clone();\n    let duplicates_sub_available_hash_type = combo_box_items.duplicates_hash_type.config_name.clone();\n    let biggest_files_sub_method = combo_box_items.biggest_files_method.config_name.clone();\n    let biggest_files_sub_number_of_files = settings.get_biggest_files_sub_number_of_files().parse().unwrap_or(DEFAULT_BIGGEST_FILES);\n\n    let similar_videos_sub_ignore_same_size = settings.get_similar_videos_sub_ignore_same_size();\n    let similar_videos_sub_similarity = settings.get_similar_videos_sub_current_similarity().round() as i32;\n    let similar_videos_crop_detect = combo_box_items.videos_crop_detect.config_name.clone();\n    let similar_videos_skip_forward_amount = settings.get_similar_videos_skip_forward_amount() as u32;\n    let similar_videos_vid_hash_duration = settings.get_similar_videos_vid_hash_duration() as u32;\n\n    let video_thumbnails_generate = settings.get_video_thumbnails_generate();\n    let video_thumbnails_percentage = settings.get_video_thumbnails_percentage().round() as u8;\n    let video_thumbnails_generate_grid = settings.get_video_thumbnails_generate_grid();\n    let video_thumbnails_grid_tiles_per_side = settings.get_video_thumbnails_grid_tiles_per_side().round() as u8;\n\n    let similar_music_sub_audio_check_type = combo_box_items.audio_check_type.config_name.clone();\n    let similar_music_sub_approximate_comparison = settings.get_similar_music_sub_approximate_comparison();\n    let similar_music_sub_title = settings.get_similar_music_sub_title();\n    let similar_music_sub_artist = settings.get_similar_music_sub_artist();\n    let similar_music_sub_year = settings.get_similar_music_sub_year();\n    let similar_music_sub_bitrate = settings.get_similar_music_sub_bitrate();\n    let similar_music_sub_genre = settings.get_similar_music_sub_genre();\n    let similar_music_sub_length = settings.get_similar_music_sub_length();\n    let similar_music_sub_maximum_difference_value = settings.get_similar_music_sub_maximum_difference_value();\n    let similar_music_sub_minimal_fragment_duration_value = settings.get_similar_music_sub_minimal_fragment_duration_value();\n\n    let broken_files_sub_audio = settings.get_broken_files_sub_audio();\n    let broken_files_sub_pdf = settings.get_broken_files_sub_pdf();\n    let broken_files_sub_archive = settings.get_broken_files_sub_archive();\n    let broken_files_sub_image = settings.get_broken_files_sub_image();\n    let broken_files_sub_video = settings.get_broken_files_sub_video();\n\n    let bad_names_sub_uppercase_extension = settings.get_bad_names_sub_uppercase_extension();\n    let bad_names_sub_emoji_used = settings.get_bad_names_sub_emoji_used();\n    let bad_names_sub_space_at_start_end = settings.get_bad_names_sub_space_at_start_end();\n    let bad_names_sub_non_ascii = settings.get_bad_names_sub_non_ascii();\n    let bad_names_sub_restricted_charset_enabled = settings.get_bad_names_sub_restricted_charset_enabled();\n    let bad_names_sub_restricted_charset: Vec<char> = settings.get_bad_names_sub_restricted_charset().chars().collect();\n    let bad_names_sub_remove_duplicated = settings.get_bad_names_sub_remove_duplicated();\n\n    let video_optimizer_mode = combo_box_items.video_optimizer_mode.config_name.clone();\n    let video_optimizer_crop_type = combo_box_items.video_optimizer_crop_type.config_name.clone();\n    let video_optimizer_video_codec = combo_box_items.video_optimizer_video_codec.config_name;\n    let video_optimizer_excluded_codecs = settings.get_video_optimizer_sub_excluded_codecs().to_string();\n    let video_optimizer_black_pixel_threshold = settings\n        .get_video_optimizer_sub_black_pixel_threshold()\n        .parse::<u8>()\n        .unwrap_or(default_video_optimizer_black_pixel_threshold())\n        .min(128);\n    let video_optimizer_black_bar_min_percentage = settings\n        .get_video_optimizer_sub_black_bar_min_percentage()\n        .parse::<u8>()\n        .unwrap_or(default_video_optimizer_black_pixel_threshold())\n        .clamp(50, 100);\n    let video_optimizer_max_samples = settings\n        .get_video_optimizer_sub_max_samples()\n        .parse::<usize>()\n        .unwrap_or(default_video_optimizer_max_samples())\n        .clamp(5, 1000);\n    let video_optimizer_min_crop_size = settings\n        .get_video_optimizer_sub_min_crop_size()\n        .parse::<u32>()\n        .unwrap_or(default_video_optimizer_min_crop_size())\n        .clamp(1, 1000);\n    let video_optimizer_video_quality = settings.get_video_optimizer_sub_video_quality().round() as u32;\n    let video_optimizer_fail_if_bigger = settings.get_video_optimizer_sub_fail_if_bigger();\n    let video_optimizer_overwrite_files = settings.get_video_optimizer_sub_overwrite_files();\n    let video_optimizer_limit_video_size = settings.get_video_optimizer_sub_limit_video_size();\n    let video_optimizer_max_width = settings.get_video_optimizer_sub_max_width().parse::<u32>().unwrap_or(1920);\n    let video_optimizer_max_height = settings.get_video_optimizer_sub_max_height().parse::<u32>().unwrap_or(1920);\n    let video_optimizer_image_threshold = settings.get_video_optimizer_sub_image_threshold().round() as u8;\n\n    let ignored_exif_tags = settings.get_ignored_exif_tags().to_string();\n\n    let column_sizes = BTreeMap::from([\n        (\"duplicates\".to_string(), settings.get_duplicates_column_size().iter().collect::<Vec<_>>()),\n        (\"empty_folders\".to_string(), settings.get_empty_folders_column_size().iter().collect::<Vec<_>>()),\n        (\"empty_files\".to_string(), settings.get_empty_files_column_size().iter().collect::<Vec<_>>()),\n        (\"temporary_files\".to_string(), settings.get_temporary_files_column_size().iter().collect::<Vec<_>>()),\n        (\"big_files\".to_string(), settings.get_big_files_column_size().iter().collect::<Vec<_>>()),\n        (\"similar_images\".to_string(), settings.get_similar_images_column_size().iter().collect::<Vec<_>>()),\n        (\"similar_videos\".to_string(), settings.get_similar_videos_column_size().iter().collect::<Vec<_>>()),\n        (\"similar_music\".to_string(), settings.get_similar_music_column_size().iter().collect::<Vec<_>>()),\n        (\"invalid_symlink\".to_string(), settings.get_invalid_symlink_column_size().iter().collect::<Vec<_>>()),\n        (\"broken_files\".to_string(), settings.get_broken_files_column_size().iter().collect::<Vec<_>>()),\n        (\"bad_extensions\".to_string(), settings.get_bad_extensions_column_size().iter().collect::<Vec<_>>()),\n        (\"exif_remover\".to_string(), settings.get_exif_remover_column_size().iter().collect::<Vec<_>>()),\n        (\"video_optimizer\".to_string(), settings.get_video_optimizer_column_size().iter().collect::<Vec<_>>()),\n        (\"bad_names\".to_string(), settings.get_bad_names_column_size().iter().collect::<Vec<_>>()),\n    ]);\n    assert_eq!(column_sizes.len(), TOOLS_NUMBER);\n\n    SettingsCustom {\n        included_paths,\n        included_paths_referenced,\n        excluded_paths,\n        excluded_items,\n        allowed_extensions,\n        excluded_extensions,\n        minimum_file_size,\n        maximum_file_size,\n        recursive_search,\n        use_cache,\n        save_also_as_json,\n        move_deleted_files_to_trash,\n        ignore_other_file_systems,\n        thread_number,\n        duplicate_image_preview,\n        duplicate_use_prehash,\n        duplicate_minimal_hash_cache_size,\n        duplicate_minimal_prehash_cache_size,\n        delete_outdated_cache_entries,\n        hide_hard_links,\n        similar_images_show_image_preview,\n        video_thumbnails_preview,\n        video_thumbnails_unused_thumbnails,\n        similar_images_sub_hash_size,\n        similar_images_sub_hash_alg,\n        similar_images_sub_resize_algorithm,\n        similar_images_sub_ignore_same_size,\n        similar_images_sub_similarity,\n        duplicates_sub_check_method,\n        duplicates_sub_available_hash_type,\n        duplicates_sub_name_case_sensitive,\n        biggest_files_sub_method,\n        biggest_files_sub_number_of_files,\n        similar_videos_sub_ignore_same_size,\n        similar_videos_sub_similarity,\n        similar_music_sub_audio_check_type,\n        similar_music_sub_approximate_comparison,\n        similar_music_compare_fingerprints_only_with_similar_titles,\n        similar_music_sub_title,\n        similar_music_sub_artist,\n        similar_music_sub_year,\n        similar_music_sub_bitrate,\n        similar_music_sub_genre,\n        similar_music_sub_length,\n        similar_music_sub_maximum_difference_value,\n        similar_music_sub_minimal_fragment_duration_value,\n        broken_files_sub_audio,\n        broken_files_sub_pdf,\n        broken_files_sub_archive,\n        broken_files_sub_image,\n        broken_files_sub_video,\n        bad_names_sub_uppercase_extension,\n        bad_names_sub_emoji_used,\n        bad_names_sub_space_at_start_end,\n        bad_names_sub_non_ascii,\n        bad_names_sub_restricted_charset_enabled,\n        bad_names_sub_restricted_charset,\n        bad_names_sub_remove_duplicated,\n        similar_videos_skip_forward_amount,\n        similar_videos_vid_hash_duration,\n        similar_videos_crop_detect,\n        video_thumbnails_generate,\n        video_thumbnails_percentage,\n        video_thumbnails_generate_grid,\n        video_thumbnails_grid_tiles_per_side,\n        video_optimizer_mode,\n        video_optimizer_crop_type,\n        video_optimizer_black_pixel_threshold,\n        video_optimizer_black_bar_min_percentage,\n        video_optimizer_max_samples,\n        video_optimizer_min_crop_size,\n        video_optimizer_video_codec,\n        video_optimizer_excluded_codecs,\n        video_optimizer_video_quality,\n        video_optimizer_fail_if_bigger,\n        video_optimizer_overwrite_files,\n        video_optimizer_limit_video_size,\n        video_optimizer_max_width,\n        video_optimizer_max_height,\n        video_optimizer_image_threshold,\n        ignored_exif_tags,\n        column_sizes,\n        popup_move_preserve_folder_structure: settings.get_popup_move_preserve_folder_structure(),\n        popup_move_copy_mode: settings.get_popup_move_copy_mode(),\n        popup_clean_exif_overwrite_files: settings.get_popup_clean_exif_overwrite_files(),\n        popup_reencode_video_overwrite_files: settings.get_popup_reencode_video_overwrite_files(),\n        popup_reencode_video_quality: settings.get_popup_reencode_video_quality().round() as u32,\n        popup_reencode_video_fail_if_bigger: settings.get_popup_reencode_video_fail_if_bigger(),\n        popup_reencode_video_limit_video_size: settings.get_popup_reencode_video_limit_video_size(),\n        popup_reencode_video_max_width: settings.get_popup_reencode_video_max_width().parse::<u32>().unwrap_or(1920),\n        popup_reencode_video_max_height: settings.get_popup_reencode_video_max_height().parse::<u32>().unwrap_or(1920),\n        popup_crop_video_overwrite_files: settings.get_popup_crop_video_overwrite_files(),\n        popup_crop_video_reencode: settings.get_popup_crop_video_reencode(),\n        popup_crop_video_quality: settings.get_popup_crop_video_quality().round() as u32,\n    }\n}\n\npub(crate) fn collect_combo_box_settings(app: &MainWindow) -> ComboBoxItems {\n    let collected_combo_boxes = StringComboBoxItems::regenerate_items();\n    let settings = app.global::<Settings>();\n\n    let language_idx = settings.get_language_index() as usize;\n    let hash_size_idx = settings.get_similar_images_sub_hash_size_index() as usize;\n    let resize_algorithm_idx = settings.get_similar_images_sub_resize_algorithm_index() as usize;\n    let image_hash_alg_idx = settings.get_similar_images_sub_hash_alg_index() as usize;\n    let duplicates_hash_type_idx = settings.get_duplicates_sub_available_hash_type_index() as usize;\n    let biggest_files_method_idx = settings.get_biggest_files_sub_method_index() as usize;\n    let audio_check_type_idx = settings.get_similar_music_sub_audio_check_type_index() as usize;\n    let duplicates_check_method_idx = settings.get_duplicates_sub_check_method_index() as usize;\n    let videos_crop_detect_idx = settings.get_similar_videos_crop_detect_index() as usize;\n    let video_optimizer_crop_type_idx = settings.get_video_optimizer_sub_crop_type_index() as usize;\n    let video_optimizer_mode_idx = settings.get_video_optimizer_sub_mode_index() as usize;\n    let video_optimizer_video_codec_idx = settings.get_video_optimizer_sub_video_codec_index() as usize;\n\n    ComboBoxItems {\n        language: collected_combo_boxes.languages[language_idx].clone(),\n        hash_size: collected_combo_boxes.hash_size[hash_size_idx].clone(),\n        resize_algorithm: collected_combo_boxes.resize_algorithm[resize_algorithm_idx].clone(),\n        image_hash_alg: collected_combo_boxes.image_hash_alg[image_hash_alg_idx].clone(),\n        duplicates_hash_type: collected_combo_boxes.duplicates_hash_type[duplicates_hash_type_idx].clone(),\n        biggest_files_method: collected_combo_boxes.biggest_files_method[biggest_files_method_idx].clone(),\n        audio_check_type: collected_combo_boxes.audio_check_type[audio_check_type_idx].clone(),\n        duplicates_check_method: collected_combo_boxes.duplicates_check_method[duplicates_check_method_idx].clone(),\n        videos_crop_detect: collected_combo_boxes.videos_crop_detect[videos_crop_detect_idx].clone(),\n        video_optimizer_crop_type: collected_combo_boxes.video_optimizer_crop_type[video_optimizer_crop_type_idx].clone(),\n        video_optimizer_mode: collected_combo_boxes.video_optimizer_mode[video_optimizer_mode_idx].clone(),\n        video_optimizer_video_codec: collected_combo_boxes.video_optimizer_video_codec[video_optimizer_video_codec_idx].clone(),\n    }\n}\n\npub(crate) fn collect_base_settings(app: &MainWindow) -> BasicSettings {\n    let settings = app.global::<Settings>();\n    let combo_box_items = collect_combo_box_settings(app);\n\n    let default_preset = settings.get_settings_preset_idx();\n    let preset_names = settings.get_settings_presets().iter().map(|x| x.to_string()).collect::<Vec<_>>();\n    let window_width = (app.window().size().width as f32 / app.window().scale_factor()) as u32;\n    let window_height = (app.window().size().height as f32 / app.window().scale_factor()) as u32;\n\n    assert_eq!(preset_names.len(), PRESET_NUMBER);\n    let language = combo_box_items.language.config_name;\n    // let language = LANGUAGE_LIST[lang_idx as usize].short_name.to_string();\n    let dark_theme = settings.get_dark_theme();\n    let show_only_icons = settings.get_show_only_icons();\n\n    let settings_load_tabs_sizes_at_startup = settings.get_load_tabs_sizes_at_startup();\n    let settings_load_windows_size_at_startup = settings.get_load_windows_size_at_startup();\n    let settings_limit_lines_of_messages = settings.get_limit_messages_lines();\n    let manual_application_scale = settings.get_manual_application_scale().clamp(0.5, 3.0);\n    let use_manual_application_scale = settings.get_use_manual_application_scale();\n    let play_audio_on_scan_completion = settings.get_play_audio_on_scan_completion();\n    BasicSettings {\n        language,\n        default_preset,\n        preset_names,\n        window_width,\n        window_height,\n        dark_theme,\n        show_only_icons,\n        settings_load_tabs_sizes_at_startup,\n        settings_load_windows_size_at_startup,\n        settings_limit_lines_of_messages,\n        manual_application_scale,\n        use_manual_application_scale,\n        play_audio_on_scan_completion,\n    }\n}\n"
  },
  {
    "path": "krokiet/src/settings/model.rs",
    "content": "use std::collections::BTreeMap;\nuse std::env;\nuse std::path::PathBuf;\n\nuse czkawka_core::common::items::{DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_EXCLUDED_ITEMS};\nuse czkawka_core::common::model::{CheckingMethod, HashType};\nuse czkawka_core::re_exported::{Cropdetect, HashAlg};\nuse czkawka_core::tools::big_file::SearchMode;\nuse czkawka_core::tools::similar_videos::{DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION, DEFAULT_VIDEO_PERCENTAGE_FOR_THUMBNAIL};\nuse czkawka_core::tools::video_optimizer::{VideoCodec, VideoCroppingMechanism, VideoOptimizerMode};\nuse home::home_dir;\nuse image::imageops::FilterType;\nuse serde::{Deserialize, Serialize};\n\nuse crate::connect_translation::{LANGUAGE_LIST, find_the_closest_language_idx_to_system};\nuse crate::settings::combo_box::StringComboBoxItem;\n\npub const DEFAULT_MINIMUM_SIZE_KB: i32 = 16;\npub const DEFAULT_MAXIMUM_SIZE_KB: i32 = i32::MAX;\npub const DEFAULT_MINIMUM_CACHE_SIZE: i32 = 256;\npub const DEFAULT_MINIMUM_PREHASH_CACHE_SIZE: i32 = 256;\npub const DEFAULT_BIGGEST_FILES: i32 = 50;\npub const DEFAULT_IMAGE_SIMILARITY: i32 = 10;\npub const DEFAULT_VIDEO_SIMILARITY: i32 = 15;\npub const DEFAULT_HASH_SIZE: &str = \"16\";\npub const DEFAULT_MAXIMUM_DIFFERENCE_VALUE: f32 = 3.0;\npub const DEFAULT_MINIMAL_FRAGMENT_DURATION_VALUE: f32 = 5.0;\npub const MAX_HASH_SIZE: f32 = 40.0;\npub const DEFAULT_WINDOW_WIDTH: u32 = 800;\npub const DEFAULT_WINDOW_HEIGHT: u32 = 600;\npub const DEFAULT_MIN_VIDEO_THUMBNAIL_POSITION_PERCENT: u8 = 1;\npub const DEFAULT_MAX_VIDEO_THUMBNAIL_POSITION_PERCENT: u8 = 99;\n\npub const PRESET_NUMBER: usize = 11; // 10 normal presets + 1 reserved preset for custom settings\npub const RESERVER_PRESET_IDX: i32 = PRESET_NUMBER as i32 - 1; // 10 normal presets + 1 reserved preset for custom settings\npub const PRESET_NAME_RESERVED: &str = \"CLI Folders\";\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct SettingsCustom {\n    #[serde(default = \"default_included_paths\")]\n    pub included_paths: Vec<PathBuf>,\n    #[serde(default)]\n    pub included_paths_referenced: Vec<PathBuf>,\n    #[serde(default = \"default_excluded_paths\")]\n    pub excluded_paths: Vec<PathBuf>,\n    #[serde(default = \"default_excluded_items\")]\n    pub excluded_items: String,\n    #[serde(default)]\n    pub allowed_extensions: String,\n    #[serde(default)]\n    pub excluded_extensions: String,\n    #[serde(default = \"minimum_file_size\")]\n    pub minimum_file_size: i32,\n    #[serde(default = \"maximum_file_size\")]\n    pub maximum_file_size: i32,\n    #[serde(default = \"ttrue\")]\n    pub recursive_search: bool,\n    #[serde(default = \"ttrue\")]\n    pub use_cache: bool,\n    #[serde(default)]\n    pub save_also_as_json: bool,\n    #[serde(default = \"ttrue\")]\n    pub move_deleted_files_to_trash: bool,\n    #[serde(default)]\n    pub ignore_other_file_systems: bool,\n    #[serde(default)]\n    pub thread_number: i32,\n    #[serde(default = \"ttrue\")]\n    pub duplicate_image_preview: bool,\n    #[serde(default = \"ttrue\")]\n    pub duplicate_use_prehash: bool,\n    #[serde(default = \"minimal_hash_cache_size\")]\n    pub duplicate_minimal_hash_cache_size: i32,\n    #[serde(default = \"minimal_prehash_cache_size\")]\n    pub duplicate_minimal_prehash_cache_size: i32,\n    #[serde(default = \"ttrue\")]\n    pub delete_outdated_cache_entries: bool,\n    #[serde(default = \"ttrue\")]\n    pub hide_hard_links: bool,\n    #[serde(default = \"ttrue\")]\n    pub similar_images_show_image_preview: bool,\n    #[serde(default = \"ttrue\")]\n    pub video_thumbnails_preview: bool,\n    #[serde(default = \"ttrue\")]\n    pub video_thumbnails_unused_thumbnails: bool,\n    #[serde(default = \"default_sub_hash_size\")]\n    pub similar_images_sub_hash_size: String,\n    #[serde(default = \"default_hash_type\")]\n    pub similar_images_sub_hash_alg: String,\n    #[serde(default = \"default_resize_algorithm\")]\n    pub similar_images_sub_resize_algorithm: String,\n    #[serde(default)]\n    pub similar_images_sub_ignore_same_size: bool,\n    #[serde(default = \"default_image_similarity\")]\n    pub similar_images_sub_similarity: i32,\n    #[serde(default = \"default_duplicates_check_method\")]\n    pub duplicates_sub_check_method: String,\n    #[serde(default = \"default_duplicates_hash_type\")]\n    pub duplicates_sub_available_hash_type: String,\n    #[serde(default)]\n    pub duplicates_sub_name_case_sensitive: bool,\n    #[serde(default = \"default_biggest_method\")]\n    pub biggest_files_sub_method: String,\n    #[serde(default = \"default_biggest_files\")]\n    pub biggest_files_sub_number_of_files: i32,\n    #[serde(default)]\n    pub similar_videos_sub_ignore_same_size: bool,\n    #[serde(default = \"default_video_similarity\")]\n    pub similar_videos_sub_similarity: i32,\n    #[serde(default = \"default_audio_check_type\")]\n    pub similar_music_sub_audio_check_type: String,\n    #[serde(default)]\n    pub similar_music_sub_approximate_comparison: bool,\n    #[serde(default)]\n    pub similar_music_compare_fingerprints_only_with_similar_titles: bool,\n    #[serde(default = \"ttrue\")]\n    pub similar_music_sub_title: bool,\n    #[serde(default = \"ttrue\")]\n    pub similar_music_sub_artist: bool,\n    #[serde(default)]\n    pub similar_music_sub_year: bool,\n    #[serde(default)]\n    pub similar_music_sub_bitrate: bool,\n    #[serde(default)]\n    pub similar_music_sub_genre: bool,\n    #[serde(default)]\n    pub similar_music_sub_length: bool,\n    #[serde(default = \"default_maximum_difference_value\")]\n    pub similar_music_sub_maximum_difference_value: f32,\n    #[serde(default = \"default_minimal_fragment_duration_value\")]\n    pub similar_music_sub_minimal_fragment_duration_value: f32,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_sub_audio: bool,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_sub_pdf: bool,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_sub_archive: bool,\n    #[serde(default = \"ttrue\")]\n    pub broken_files_sub_image: bool,\n    #[serde(default)]\n    pub broken_files_sub_video: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_sub_uppercase_extension: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_sub_emoji_used: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_sub_space_at_start_end: bool,\n    #[serde(default = \"ttrue\")]\n    pub bad_names_sub_non_ascii: bool,\n    #[serde(default)]\n    pub bad_names_sub_restricted_charset_enabled: bool,\n    #[serde(default = \"default_bad_names_restricted_charset\")]\n    pub bad_names_sub_restricted_charset: Vec<char>,\n    #[serde(default)]\n    pub bad_names_sub_remove_duplicated: bool,\n    #[serde(default = \"default_similar_videos_skip_forward_amount\")]\n    pub similar_videos_skip_forward_amount: u32,\n    #[serde(default = \"default_similar_videos_vid_hash_duration\")]\n    pub similar_videos_vid_hash_duration: u32,\n    #[serde(default = \"default_similar_videos_crop_detect\")]\n    pub similar_videos_crop_detect: String,\n    #[serde(default)]\n    pub video_thumbnails_generate: bool,\n    #[serde(default = \"default_similar_videos_thumbnail_percentage\")]\n    pub video_thumbnails_percentage: u8,\n    #[serde(default)]\n    pub video_thumbnails_generate_grid: bool,\n    #[serde(default = \"default_video_thumbnails_grid_tiles_per_side\")]\n    pub video_thumbnails_grid_tiles_per_side: u8,\n    #[serde(default = \"default_video_optimizer_mode\")]\n    pub video_optimizer_mode: String,\n    #[serde(default = \"default_video_optimizer_crop_type\")]\n    pub video_optimizer_crop_type: String,\n    #[serde(default = \"default_video_optimizer_black_pixel_threshold\")]\n    pub video_optimizer_black_pixel_threshold: u8,\n    #[serde(default = \"default_video_optimizer_black_bar_min_percentage\")]\n    pub video_optimizer_black_bar_min_percentage: u8,\n    #[serde(default = \"default_video_optimizer_max_samples\")]\n    pub video_optimizer_max_samples: usize,\n    #[serde(default = \"default_video_optimizer_min_crop_size\")]\n    pub video_optimizer_min_crop_size: u32,\n    #[serde(default = \"default_video_optimizer_video_codec\")]\n    pub video_optimizer_video_codec: String,\n    #[serde(default = \"default_video_optimizer_excluded_codecs\")]\n    pub video_optimizer_excluded_codecs: String,\n    #[serde(default = \"default_video_optimizer_video_quality\")]\n    pub video_optimizer_video_quality: u32,\n    #[serde(default)]\n    pub video_optimizer_fail_if_bigger: bool,\n    #[serde(default)]\n    pub video_optimizer_overwrite_files: bool,\n    #[serde(default)]\n    pub video_optimizer_limit_video_size: bool,\n    #[serde(default = \"default_video_optimizer_max_width\")]\n    pub video_optimizer_max_width: u32,\n    #[serde(default = \"default_video_optimizer_max_height\")]\n    pub video_optimizer_max_height: u32,\n    #[serde(default = \"default_video_optimizer_image_threshold\")]\n    pub video_optimizer_image_threshold: u8,\n    #[serde(default = \"default_ignored_exif_tags\")]\n    pub ignored_exif_tags: String,\n    #[serde(default)]\n    pub column_sizes: BTreeMap<String, Vec<f32>>,\n\n    #[serde(default)]\n    pub popup_move_preserve_folder_structure: bool,\n    #[serde(default)]\n    pub popup_move_copy_mode: bool,\n    #[serde(default)]\n    pub popup_clean_exif_overwrite_files: bool,\n    #[serde(default)]\n    pub popup_reencode_video_overwrite_files: bool,\n    #[serde(default = \"default_video_optimizer_video_quality\")]\n    pub popup_reencode_video_quality: u32,\n    #[serde(default)]\n    pub popup_reencode_video_fail_if_bigger: bool,\n    #[serde(default)]\n    pub popup_reencode_video_limit_video_size: bool,\n    #[serde(default = \"default_video_optimizer_max_width\")]\n    pub popup_reencode_video_max_width: u32,\n    #[serde(default = \"default_video_optimizer_max_height\")]\n    pub popup_reencode_video_max_height: u32,\n    #[serde(default)]\n    pub popup_crop_video_overwrite_files: bool,\n    #[serde(default)]\n    pub popup_crop_video_reencode: bool,\n    #[serde(default = \"default_video_optimizer_video_quality\")]\n    pub popup_crop_video_quality: u32,\n}\n\nimpl Default for SettingsCustom {\n    fn default() -> Self {\n        serde_json::from_str(\"{}\").expect(\"Cannot fail creating {} from string\")\n    }\n}\n\npub struct ComboBoxItems {\n    pub language: StringComboBoxItem<String>,\n    pub hash_size: StringComboBoxItem<u8>,\n    pub resize_algorithm: StringComboBoxItem<FilterType>,\n    pub image_hash_alg: StringComboBoxItem<HashAlg>,\n    pub duplicates_hash_type: StringComboBoxItem<HashType>,\n    pub biggest_files_method: StringComboBoxItem<SearchMode>,\n    pub audio_check_type: StringComboBoxItem<CheckingMethod>,\n    pub duplicates_check_method: StringComboBoxItem<CheckingMethod>,\n    pub videos_crop_detect: StringComboBoxItem<Cropdetect>,\n    pub video_optimizer_crop_type: StringComboBoxItem<VideoCroppingMechanism>,\n    pub video_optimizer_mode: StringComboBoxItem<VideoOptimizerMode>,\n    pub video_optimizer_video_codec: StringComboBoxItem<VideoCodec>,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct BasicSettings {\n    #[serde(default)]\n    pub default_preset: i32,\n    #[serde(default = \"default_preset_names\")]\n    pub preset_names: Vec<String>,\n    #[serde(default = \"default_window_width\")]\n    pub window_width: u32,\n    #[serde(default = \"default_window_height\")]\n    pub window_height: u32,\n    #[serde(default = \"detect_language\")]\n    pub language: String,\n    #[serde(default = \"ttrue\")]\n    pub dark_theme: bool,\n    #[serde(default)]\n    pub show_only_icons: bool,\n    #[serde(default = \"ttrue\")]\n    pub settings_load_windows_size_at_startup: bool,\n    #[serde(default = \"ttrue\")]\n    pub settings_load_tabs_sizes_at_startup: bool,\n    #[serde(default = \"ttrue\")]\n    pub settings_limit_lines_of_messages: bool,\n    #[serde(default = \"default_manual_application_scale\")]\n    pub manual_application_scale: f32,\n    #[serde(default = \"default_use_manual_application_scale\")]\n    pub use_manual_application_scale: bool,\n    #[serde(default = \"ttrue\")]\n    pub play_audio_on_scan_completion: bool,\n}\n\nimpl Default for BasicSettings {\n    fn default() -> Self {\n        serde_json::from_str(\"{}\").expect(\"Cannot fail creating {} from string\")\n    }\n}\n\nfn detect_language() -> String {\n    let lang_idx = find_the_closest_language_idx_to_system();\n    LANGUAGE_LIST[lang_idx].short_name.to_string()\n}\n\nfn default_included_paths() -> Vec<PathBuf> {\n    let mut included_paths = Vec::new();\n    if let Ok(current_dir) = env::current_dir() {\n        included_paths.push(current_dir.to_string_lossy().to_string());\n    } else if let Some(home_dir) = home_dir() {\n        included_paths.push(home_dir.to_string_lossy().to_string());\n    } else if cfg!(target_family = \"unix\") {\n        included_paths.push(\"/\".to_string());\n    } else {\n        // This could be set to default\n        included_paths.push(\"C:\\\\\".to_string());\n    }\n    included_paths.sort();\n    included_paths.iter().map(PathBuf::from).collect::<Vec<_>>()\n}\n\nfn default_excluded_paths() -> Vec<PathBuf> {\n    let mut excluded_paths = DEFAULT_EXCLUDED_DIRECTORIES.iter().map(PathBuf::from).collect::<Vec<_>>();\n    excluded_paths.sort();\n    excluded_paths\n}\nfn default_similar_videos_skip_forward_amount() -> u32 {\n    DEFAULT_SKIP_FORWARD_AMOUNT\n}\nfn default_similar_videos_vid_hash_duration() -> u32 {\n    DEFAULT_VID_HASH_DURATION\n}\nfn default_similar_videos_crop_detect() -> String {\n    \"letterbox\".to_string()\n}\nfn default_similar_videos_thumbnail_percentage() -> u8 {\n    DEFAULT_VIDEO_PERCENTAGE_FOR_THUMBNAIL\n}\nfn default_video_thumbnails_grid_tiles_per_side() -> u8 {\n    2\n}\n\nfn default_duplicates_check_method() -> String {\n    \"hash\".to_string()\n}\nfn default_maximum_difference_value() -> f32 {\n    DEFAULT_MAXIMUM_DIFFERENCE_VALUE\n}\nfn default_minimal_fragment_duration_value() -> f32 {\n    DEFAULT_MINIMAL_FRAGMENT_DURATION_VALUE\n}\nfn default_duplicates_hash_type() -> String {\n    \"blake3\".to_string()\n}\nfn default_biggest_method() -> String {\n    \"biggest\".to_string()\n}\nfn default_audio_check_type() -> String {\n    \"tags\".to_string()\n}\nfn default_video_similarity() -> i32 {\n    DEFAULT_VIDEO_SIMILARITY\n}\nfn default_biggest_files() -> i32 {\n    DEFAULT_BIGGEST_FILES\n}\n\npub(crate) fn default_image_similarity() -> i32 {\n    DEFAULT_IMAGE_SIMILARITY\n}\nfn default_excluded_items() -> String {\n    DEFAULT_EXCLUDED_ITEMS.to_string()\n}\n\nfn default_bad_names_restricted_charset() -> Vec<char> {\n    vec!['_', ' ', '.', ',', '-', '(', ')', '[', ']', '!', '\\'', '\"']\n}\n\nfn default_preset_names() -> Vec<String> {\n    let mut v = (0..(PRESET_NUMBER - 1)).map(|x| format!(\"Preset {}\", x + 1)).collect::<Vec<_>>();\n    v.push(PRESET_NAME_RESERVED.to_string());\n    v\n}\n\nfn minimum_file_size() -> i32 {\n    DEFAULT_MINIMUM_SIZE_KB\n}\nfn maximum_file_size() -> i32 {\n    DEFAULT_MAXIMUM_SIZE_KB\n}\nfn ttrue() -> bool {\n    true\n}\nfn minimal_hash_cache_size() -> i32 {\n    DEFAULT_MINIMUM_CACHE_SIZE\n}\nfn minimal_prehash_cache_size() -> i32 {\n    DEFAULT_MINIMUM_PREHASH_CACHE_SIZE\n}\n\npub(crate) fn default_resize_algorithm() -> String {\n    \"lanczos3\".to_string()\n}\npub(crate) fn default_hash_type() -> String {\n    \"mean\".to_string()\n}\npub(crate) fn default_sub_hash_size() -> String {\n    DEFAULT_HASH_SIZE.to_string()\n}\npub(crate) fn default_window_width() -> u32 {\n    DEFAULT_WINDOW_WIDTH\n}\npub(crate) fn default_window_height() -> u32 {\n    DEFAULT_WINDOW_HEIGHT\n}\npub(crate) fn default_video_optimizer_mode() -> String {\n    \"transcode\".to_string()\n}\npub(crate) fn default_video_optimizer_crop_type() -> String {\n    \"blackbars\".to_string()\n}\npub(crate) fn default_video_optimizer_black_pixel_threshold() -> u8 {\n    20\n}\npub(crate) fn default_video_optimizer_black_bar_min_percentage() -> u8 {\n    90\n}\npub(crate) fn default_video_optimizer_max_samples() -> usize {\n    60\n}\npub(crate) fn default_video_optimizer_min_crop_size() -> u32 {\n    20\n}\npub(crate) fn default_video_optimizer_video_codec() -> String {\n    \"h265\".to_string()\n}\npub(crate) fn default_video_optimizer_excluded_codecs() -> String {\n    \"h265,hevc,av1,vp9\".to_string()\n}\npub(crate) fn default_video_optimizer_video_quality() -> u32 {\n    23\n}\npub(crate) fn default_video_optimizer_max_width() -> u32 {\n    1920\n}\npub(crate) fn default_video_optimizer_max_height() -> u32 {\n    1920\n}\npub(crate) fn default_video_optimizer_image_threshold() -> u8 {\n    1\n}\npub(crate) fn default_manual_application_scale() -> f32 {\n    1.0\n}\npub(crate) fn default_use_manual_application_scale() -> bool {\n    false\n}\npub(crate) fn default_ignored_exif_tags() -> String {\n    \"Orientation\".to_string()\n}\n"
  },
  {
    "path": "krokiet/src/shared_models.rs",
    "content": "use std::sync::{Arc, Mutex};\n\nuse czkawka_core::common::tool_data::CommonData;\nuse czkawka_core::common::traits::PrintResults;\nuse czkawka_core::tools::bad_extensions::BadExtensions;\nuse czkawka_core::tools::bad_names::BadNames;\nuse czkawka_core::tools::big_file::BigFile;\nuse czkawka_core::tools::broken_files::BrokenFiles;\nuse czkawka_core::tools::duplicate::DuplicateFinder;\nuse czkawka_core::tools::empty_files::EmptyFiles;\nuse czkawka_core::tools::empty_folder::EmptyFolder;\nuse czkawka_core::tools::exif_remover::ExifRemover;\nuse czkawka_core::tools::invalid_symlinks::InvalidSymlinks;\nuse czkawka_core::tools::same_music::SameMusic;\nuse czkawka_core::tools::similar_images::SimilarImages;\nuse czkawka_core::tools::similar_videos::SimilarVideos;\nuse czkawka_core::tools::temporary::Temporary;\nuse czkawka_core::tools::video_optimizer::VideoOptimizer;\n\nuse crate::ActiveTab;\n\npub struct SharedModels {\n    pub shared_duplication_state: Option<DuplicateFinder>,\n    pub shared_empty_folders_state: Option<EmptyFolder>,\n    pub shared_empty_files_state: Option<EmptyFiles>,\n    pub shared_temporary_files_state: Option<Temporary>,\n    pub shared_big_files_state: Option<BigFile>,\n    pub shared_similar_images_state: Option<SimilarImages>,\n    pub shared_similar_videos_state: Option<SimilarVideos>,\n    pub shared_same_music_state: Option<SameMusic>,\n    pub shared_same_invalid_symlinks: Option<InvalidSymlinks>,\n    pub shared_broken_files_state: Option<BrokenFiles>,\n    pub shared_bad_extensions_state: Option<BadExtensions>,\n    pub shared_bad_names_state: Option<BadNames>,\n    pub shared_exif_remover_state: Option<ExifRemover>,\n    pub shared_video_optimizer_state: Option<VideoOptimizer>,\n}\n\nimpl SharedModels {\n    pub fn new() -> Self {\n        Self {\n            shared_duplication_state: None,\n            shared_empty_folders_state: None,\n            shared_empty_files_state: None,\n            shared_temporary_files_state: None,\n            shared_big_files_state: None,\n            shared_similar_images_state: None,\n            shared_similar_videos_state: None,\n            shared_same_music_state: None,\n            shared_same_invalid_symlinks: None,\n            shared_broken_files_state: None,\n            shared_bad_extensions_state: None,\n            shared_bad_names_state: None,\n            shared_exif_remover_state: None,\n            shared_video_optimizer_state: None,\n        }\n    }\n\n    pub fn new_shared() -> Arc<Mutex<Self>> {\n        Arc::new(Mutex::new(Self::new()))\n    }\n\n    pub(crate) fn save_results(&self, active_tab: ActiveTab, chosen_dir: &str) -> Result<(), String> {\n        let cd = chosen_dir;\n        let result = match active_tab {\n            ActiveTab::DuplicateFiles => self.shared_duplication_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_duplicates\")),\n            ActiveTab::EmptyFolders => self.shared_empty_folders_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_empty_directories\")),\n            ActiveTab::EmptyFiles => self.shared_empty_files_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_empty_files\")),\n            ActiveTab::TemporaryFiles => self.shared_temporary_files_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_temporary_files\")),\n            ActiveTab::BigFiles => self.shared_big_files_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_big_files\")),\n            ActiveTab::SimilarImages => self.shared_similar_images_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_similar_images\")),\n            ActiveTab::SimilarVideos => self.shared_similar_videos_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_similar_videos\")),\n            ActiveTab::SimilarMusic => self.shared_same_music_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_same_music\")),\n            ActiveTab::InvalidSymlinks => self.shared_same_invalid_symlinks.as_ref().map(|x| x.save_all_in_one(cd, \"results_invalid_symlinks\")),\n            ActiveTab::BrokenFiles => self.shared_broken_files_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_broken_files\")),\n            ActiveTab::BadExtensions => self.shared_bad_extensions_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_bad_extensions\")),\n            ActiveTab::BadNames => self.shared_bad_names_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_bad_names\")),\n            ActiveTab::ExifRemover => self.shared_exif_remover_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_exif_remover\")),\n            ActiveTab::VideoOptimizer => self.shared_video_optimizer_state.as_ref().map(|x| x.save_all_in_one(cd, \"results_video_optimizer\")),\n            ActiveTab::Settings | ActiveTab::About => panic!(\"Cannot save results for settings or about tab\"),\n        };\n\n        let current_path = match std::env::current_dir() {\n            Ok(t) => t.to_string_lossy().to_string(),\n            Err(_) => \"<unknown>\".to_string(),\n        };\n\n        match result.expect(\"Tried to save results, without running scan(bug which needs to be fixed)\") {\n            Ok(()) => Ok(()),\n            Err(e) => Err(format!(\"Failed to save results to folder \\\"{current_path}\\\", reason {e}\")),\n        }\n    }\n\n    pub(crate) fn get_use_reference_folders(&self, active_tab: ActiveTab) -> bool {\n        let used_reference_folder = match active_tab {\n            ActiveTab::DuplicateFiles => self.shared_duplication_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::EmptyFolders => self.shared_empty_folders_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::EmptyFiles => self.shared_empty_files_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::TemporaryFiles => self.shared_temporary_files_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::BigFiles => self.shared_big_files_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::SimilarImages => self.shared_similar_images_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::SimilarVideos => self.shared_similar_videos_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::SimilarMusic => self.shared_same_music_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::InvalidSymlinks => self.shared_same_invalid_symlinks.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::BrokenFiles => self.shared_broken_files_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::BadExtensions => self.shared_bad_extensions_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::BadNames => self.shared_bad_names_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::ExifRemover => self.shared_exif_remover_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::VideoOptimizer => self.shared_video_optimizer_state.as_ref().map(|e| e.get_use_reference_folders()),\n            ActiveTab::Settings | ActiveTab::About => panic!(\"Cannot get use reference folders for settings or about tab\"),\n        }\n        .unwrap_or(false);\n\n        let tab_can_use_reference_folders = active_tab.get_is_header_mode();\n\n        tab_can_use_reference_folders && used_reference_folder\n    }\n}\n"
  },
  {
    "path": "krokiet/src/simpler_model.rs",
    "content": "use slint::{Model, ModelRc, SharedString, VecModel};\n\nuse crate::SingleMainListModel;\nuse crate::common::connect_i32_into_u64;\n\n#[derive(Clone)]\npub struct SimplerSingleMainListModel {\n    pub checked: bool,\n    pub filled_header_row: bool,\n    pub header_row: bool,\n    pub selected_row: bool,\n    pub val_int: Vec<i32>,\n    pub val_str: Vec<String>,\n}\n\nimpl SimplerSingleMainListModel {\n    pub(crate) fn get_size(&self, size_idx: usize) -> u64 {\n        connect_i32_into_u64(self.val_int[size_idx], self.val_int[size_idx + 1])\n    }\n    #[allow(clippy::allow_attributes)]\n    #[expect(clippy::print_stdout)]\n    #[allow(dead_code)] // TODO - rust with some version shows this\n    pub(crate) fn debug_print(&self) {\n        println!(\n            \"SimplerSingleMainListModel: checked: {}, filled_header_row: {}, header_row: {}, selected_row: {}, val_int: {:?}, val_str: {:?}\",\n            self.checked, self.filled_header_row, self.header_row, self.selected_row, self.val_int, self.val_str\n        );\n    }\n}\n\nimpl From<&SingleMainListModel> for SimplerSingleMainListModel {\n    fn from(model: &SingleMainListModel) -> Self {\n        Self {\n            checked: model.checked,\n            filled_header_row: model.filled_header_row,\n            header_row: model.header_row,\n            selected_row: model.selected_row,\n            val_int: model.val_int.iter().collect(),\n            val_str: model.val_str.iter().map(|e| e.to_string()).collect(),\n        }\n    }\n}\nimpl From<SimplerSingleMainListModel> for SingleMainListModel {\n    fn from(val: SimplerSingleMainListModel) -> Self {\n        Self {\n            checked: val.checked,\n            filled_header_row: val.filled_header_row,\n            header_row: val.header_row,\n            selected_row: val.selected_row,\n            val_int: ModelRc::new(VecModel::from(val.val_int)),\n            val_str: ModelRc::new(VecModel::from(val.val_str.into_iter().map(|s| s.into()).collect::<Vec<SharedString>>())),\n        }\n    }\n}\n\npub trait ToSimplerVec {\n    fn to_simpler_enumerated_vec(self) -> Vec<(usize, SimplerSingleMainListModel)>;\n}\n\nimpl ToSimplerVec for ModelRc<SingleMainListModel> {\n    fn to_simpler_enumerated_vec(self) -> Vec<(usize, SimplerSingleMainListModel)> {\n        let vec_model = self.as_any().downcast_ref::<VecModel<SingleMainListModel>>().expect(\"Only VecModel is supported\");\n        vec_model\n            .iter()\n            .enumerate()\n            .map(|(index, model)| (index, SimplerSingleMainListModel::from(&model)))\n            .collect()\n    }\n}\n\npub trait ToSlintModel {\n    fn to_vec_model(self) -> Vec<SingleMainListModel>;\n}\nimpl ToSlintModel for Vec<SimplerSingleMainListModel> {\n    fn to_vec_model(self) -> Vec<SingleMainListModel> {\n        self.into_iter().map(|model| model.into()).collect()\n    }\n}\n\npub trait DebugPrintSimplerModel {\n    #[expect(dead_code)]\n    fn debug_print_simpler_models(&self);\n}\nimpl DebugPrintSimplerModel for Vec<SimplerSingleMainListModel> {\n    #[expect(clippy::print_stdout)]\n    fn debug_print_simpler_models(&self) {\n        println!(\"=====================START DEBUG PRINT SIMPLER MODELS=====================\");\n        println!(\"Simpler Model with {} items\", self.len());\n        for item in self {\n            item.debug_print();\n        }\n        println!(\"=====================END DEBUG PRINT SIMPLER MODELS=====================\");\n    }\n}\n"
  },
  {
    "path": "krokiet/src/test_common.rs",
    "content": "use crate::SingleMainListModel;\n\npub(crate) fn get_main_list_model() -> SingleMainListModel {\n    SingleMainListModel {\n        checked: false,\n        filled_header_row: false,\n        header_row: false,\n        selected_row: false,\n        val_int: Default::default(),\n        val_str: Default::default(),\n    }\n}\npub(crate) fn get_model_vec(items: usize) -> Vec<SingleMainListModel> {\n    (0..items).map(|_| get_main_list_model()).collect::<Vec<_>>()\n}\n"
  },
  {
    "path": "krokiet/ui/about.slint",
    "content": "import { Button } from \"std-widgets.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\nexport component About inherits VerticalLayout {\n    preferred-height: 300px;\n    preferred-width: 400px;\n\n    img := Image {\n        source: @image-url(\"../icons/krokiet_logo_flag_name_vertical.png\"); // TODO simplify svg, to be able to use here svg version with text\n        image-fit: ImageFit.contain;\n    }\n\n    Text {\n        text: \"11.0.1\";\n        horizontal-alignment: center;\n        font-size: max(min(img.width / 20, FontSizes.header), FontSizes.small);\n    }\n\n    VerticalLayout {\n        spacing: 10px;\n        padding-bottom: 10px;\n        Text {\n            text: \"2020 - 2026  Rafał Mikrut(qarmin)\";\n            horizontal-alignment: center;\n            font-size: FontSizes.header;\n        }\n\n        Text {\n            text <=> Translations.motto_text;\n            horizontal-alignment: center;\n            font-size: FontSizes.normal;\n        }\n\n        Text {\n            text <=> Translations.unicorn_text;\n            horizontal-alignment: center;\n            font-size: FontSizes.normal;\n        }\n    }\n\n    HorizontalLayout {\n        spacing: 5px;\n        Button {\n            text <=> Translations.repository_text;\n            clicked => {\n                Callabler.open_link(\"https://github.com/qarmin/czkawka\");\n            }\n        }\n\n        Button {\n            text <=> Translations.instruction_text;\n            clicked => {\n                Callabler.open_link(\"https://github.com/qarmin/czkawka/blob/master/instructions/Instruction.md\");\n            }\n        }\n\n        Button {\n            text <=> Translations.donation_text;\n            clicked => {\n                Callabler.open_link(\"https://github.com/sponsors/qarmin\");\n            }\n        }\n\n        Button {\n            text <=> Translations.translation_text;\n            clicked => {\n                Callabler.open_link(\"https://crowdin.com/project/czkawka\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/action_buttons.slint",
    "content": "import { Button } from \"std-widgets.slint\";\nimport { ActiveTab, BottomPanelVisibility, PopupRequest, SingleMainListModel } from \"common.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { Translations } from \"translations.slint\";\nimport { Settings } from \"settings.slint\";\n\nexport component VisibilityButton inherits Button {\n    in-out property <BottomPanelVisibility> button_visibility;\n    in-out property <BottomPanelVisibility> bottom_panel_visibility;\n    checked: bottom_panel_visibility == button_visibility;\n    height: 30px;\n    width: 70px;\n    clicked => {\n        if (bottom_panel_visibility == button_visibility) {\n            bottom_panel_visibility = BottomPanelVisibility.NotVisible;\n        } else {\n            bottom_panel_visibility = button_visibility;\n        }\n    }\n}\n\nexport component ActionButtons inherits HorizontalLayout {\n    callback scan_stopping;\n    callback scan_starting(ActiveTab);\n    callback show_select_popup(length, length);\n    callback show_sort_popup(length, length);\n    callback show_action_popup(PopupRequest);\n\n    in-out property <[SingleMainListModel]> duplicate_files_model: [];\n    in-out property <[SingleMainListModel]> empty_folder_model: [];\n    in-out property <[SingleMainListModel]> big_files_model: [];\n    in-out property <[SingleMainListModel]> empty_files_model: [];\n    in-out property <[SingleMainListModel]> temporary_files_model: [];\n    in-out property <[SingleMainListModel]> similar_images_model: [];\n    in-out property <[SingleMainListModel]> similar_videos_model: [];\n    in-out property <[SingleMainListModel]> similar_music_model: [];\n    in-out property <[SingleMainListModel]> invalid_symlinks_model: [];\n    in-out property <[SingleMainListModel]> broken_files_model: [];\n    in-out property <[SingleMainListModel]> bad_extensions_model: [];\n    in-out property <[SingleMainListModel]> bad_names_model: [];\n    in-out property <[SingleMainListModel]> exif_remover_model: [];\n    in-out property <[SingleMainListModel]> video_optimizer_model: [];\n\n    property <ActiveTab> active_tab: GuiState.active_tab;\n\n    in-out property <BottomPanelVisibility> bottom_panel_visibility <=> GuiState.bottom_panel_visibility;\n    in-out property <bool> stop_requested: false;\n    in-out property <bool> scanning;\n    in-out property <bool> processing;\n    in-out property <bool> base_buttons_may_be_available: !scanning && !processing && results_available;\n    in-out property <bool> lists_enabled: GuiState.is_tool_tab_active;\n    out property <int> name;\n\n    in-out property <bool> checked_anything: (\n        (active_tab == ActiveTab.DuplicateFiles && (GuiState.selected_results_duplicates > 0 || GuiState.selected_results_duplicates2 > 0)) ||\n        (active_tab == ActiveTab.SimilarImages && (GuiState.selected_results_similar_images > 0 || GuiState.selected_results_similar_images2 > 0)) ||\n        (active_tab == ActiveTab.SimilarVideos && (GuiState.selected_results_similar_videos > 0 || GuiState.selected_results_similar_videos2 > 0)) ||\n        (active_tab == ActiveTab.SimilarMusic && (GuiState.selected_results_similar_music > 0 || GuiState.selected_results_similar_music2 > 0)) ||\n        (active_tab == ActiveTab.BigFiles && (GuiState.selected_results_big_files > 0 || GuiState.selected_results_big_files2 > 0)) ||\n        (active_tab == ActiveTab.BrokenFiles && (GuiState.selected_results_broken_files > 0 || GuiState.selected_results_broken_files2 > 0)) ||\n        (active_tab == ActiveTab.InvalidSymlinks && (GuiState.selected_results_invalid_symlinks > 0 || GuiState.selected_results_invalid_symlinks2 > 0)) ||\n        (active_tab == ActiveTab.EmptyFolders && (GuiState.selected_results_empty_folders > 0 || GuiState.selected_results_empty_folders2 > 0)) ||\n        (active_tab == ActiveTab.EmptyFiles && (GuiState.selected_results_empty_files > 0 || GuiState.selected_results_empty_files2 > 0)) ||\n        (active_tab == ActiveTab.TemporaryFiles && (GuiState.selected_results_temporary_files > 0 || GuiState.selected_results_temporary_files2 > 0)) ||\n        (active_tab == ActiveTab.BadExtensions && (GuiState.selected_results_bad_extensions > 0 || GuiState.selected_results_bad_extensions2 > 0)) ||\n        (active_tab == ActiveTab.BadNames && (GuiState.selected_results_bad_names > 0 || GuiState.selected_results_bad_names2 > 0)) ||\n        (active_tab == ActiveTab.ExifRemover && (GuiState.selected_results_exif_remover > 0 || GuiState.selected_results_exif_remover2 > 0)) ||\n        (active_tab == ActiveTab.VideoOptimizer && (GuiState.selected_results_video_optimizer > 0 || GuiState.selected_results_video_optimizer2 > 0))\n    );\n    in-out property <bool> results_available: (\n        (active_tab == ActiveTab.DuplicateFiles && duplicate_files_model.length > 0) ||\n        (active_tab == ActiveTab.EmptyFolders && empty_folder_model.length > 0) ||\n        (active_tab == ActiveTab.BigFiles && big_files_model.length > 0) ||\n        (active_tab == ActiveTab.EmptyFiles && empty_files_model.length > 0) ||\n        (active_tab == ActiveTab.TemporaryFiles && temporary_files_model.length > 0) ||\n        (active_tab == ActiveTab.SimilarImages && similar_images_model.length > 0) ||\n        (active_tab == ActiveTab.SimilarVideos && similar_videos_model.length > 0) ||\n        (active_tab == ActiveTab.SimilarMusic && similar_music_model.length > 0) ||\n        (active_tab == ActiveTab.InvalidSymlinks && invalid_symlinks_model.length > 0) ||\n        (active_tab == ActiveTab.BrokenFiles && broken_files_model.length > 0) ||\n        (active_tab == ActiveTab.BadExtensions && bad_extensions_model.length > 0) ||\n        (active_tab == ActiveTab.BadNames && bad_names_model.length > 0) ||\n        (active_tab == ActiveTab.ExifRemover && exif_remover_model.length > 0) ||\n        (active_tab == ActiveTab.VideoOptimizer && video_optimizer_model.length > 0)\n    );\n\n    height: 30px;\n    spacing: 4px;\n\n    if (!scanning && !processing && lists_enabled): scan_button :=  Button {\n        height: parent.height;\n        enabled: !scanning && !processing && lists_enabled;\n        text: Settings.show_only_icons ? \"\" : Translations.scan_button_text;\n        icon: @image-url(\"../icons/krokiet_search.svg\");\n        colorize-icon: true;\n        clicked => {\n            root.scanning = true;\n            root.scan_starting(GuiState.active_tab);\n        }\n    }\n\n    if (scanning || processing): stop_button := Button {\n        height: parent.height;\n        enabled: (scanning || processing) && !stop_requested && root.lists_enabled;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.stop_button_text;\n        icon: @image-url(\"../icons/krokiet_stop.svg\");\n        colorize-icon: true;\n        clicked => {\n            root.scan_stopping();\n            root.stop_requested = true;\n        }\n    }\n\n    Rectangle {\n        max-width: 5px;\n    }\n\n    select_button := Button {\n        visible: lists_enabled;\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.select_button_text;\n        icon: @image-url(\"../icons/krokiet_select.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_select_popup(self.x + self.width / 2, self.y + parent.y);\n        }\n    }\n\n    move_button := Button {\n        visible: lists_enabled;\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.move_button_text;\n        icon: @image-url(\"../icons/krokiet_move.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.Move);\n        }\n    }\n\n    delete_button := Button {\n        visible: lists_enabled;\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.delete_button_text;\n        icon: @image-url(\"../icons/krokiet_delete.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.Delete);\n        }\n    }\n\n    save_button := Button {\n        visible: lists_enabled;\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.save_button_text;\n        icon: @image-url(\"../icons/krokiet_save.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.Save);\n        }\n    }\n\n    sort_button := Button {\n        visible: lists_enabled;\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.sort_button_text;\n        icon: @image-url(\"../icons/krokiet_sort.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_sort_popup(self.x + self.width / 2, self.y + parent.y);\n        }\n    }\n\n    if lists_enabled && GuiState.active_tab == ActiveTab.BadExtensions: rename_button := Button {\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.rename_button_text;\n        icon: @image-url(\"../icons/krokiet_rename.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.RenameBadExtension);\n        }\n    }\n\n    if lists_enabled && GuiState.active_tab == ActiveTab.BadNames: rename_button_bad_names := Button {\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.rename_button_text;\n        icon: @image-url(\"../icons/krokiet_rename.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.RenameBadFileName);\n        }\n    }\n\n    if lists_enabled && GuiState.active_tab == ActiveTab.VideoOptimizer: optimize_button := Button {\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.optimize_button_text;\n        icon: @image-url(\"../icons/krokiet_optimize.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.OptimizeVideo);\n        }\n    }\n\n    if lists_enabled && GuiState.active_tab == ActiveTab.ExifRemover: clean_button := Button {\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.clean_button_text;\n        icon: @image-url(\"../icons/krokiet_clean.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.CleanExif);\n        }\n    }\n\n    if lists_enabled && GuiState.tool_with_groups: hardlink_button := Button {\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.hardlink_button_text;\n        icon: @image-url(\"../icons/krokiet_hardlink.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.Hardlink);\n        }\n    }\n\n    if lists_enabled && GuiState.tool_with_groups: softlink_button := Button {\n        height: parent.height;\n        enabled: base_buttons_may_be_available && self.visible && checked_anything;\n        text: self.visible && Settings.show_only_icons ? \"\" : Translations.softlink_button_text;\n        icon: @image-url(\"../icons/krokiet_symlink.svg\");\n        colorize-icon: true;\n        clicked => {\n            show_action_popup(PopupRequest.Symlink);\n        }\n    }\n\n    Rectangle {\n        horizontal-stretch: 0.5;\n    }\n\n    HorizontalLayout {\n        padding: 0px;\n        spacing: 0px;\n        VisibilityButton {\n            height: parent.height;\n            width: 40px;\n            button_visibility: BottomPanelVisibility.Directories;\n            bottom_panel_visibility <=> bottom_panel_visibility;\n            icon: @image-url(\"../icons/krokiet_dir.svg\");\n            colorize-icon: true;\n        }\n\n        VisibilityButton {\n            height: parent.height;\n            width: 40px;\n            button_visibility: BottomPanelVisibility.TextErrors;\n            bottom_panel_visibility <=> bottom_panel_visibility;\n            icon: @image-url(\"../icons/krokiet_info.svg\");\n            colorize-icon: true;\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/bottom_panel.slint",
    "content": "import { Button, TextEdit } from \"std-widgets.slint\";\nimport { BottomPanelVisibility } from \"common.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { ExcludedPaths, IncludedPaths } from \"included_paths.slint\";\nimport { Translations } from \"translations.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\ncomponent DirectoriesPanel inherits HorizontalLayout {\n    callback folder_choose_requested(bool);\n    callback file_choose_requested(bool);\n    callback show_manual_add_dialog(bool);\n\n    spacing: 5px;\n    // Included directories\n    VerticalLayout {\n        horizontal-stretch: 0.0;\n        spacing: 5px;\n        Rectangle {\n            vertical-stretch: 1.0;\n        }\n    }\n\n    // Included directories \n    VerticalLayout {\n        horizontal-stretch: 1.0;\n        padding-top: 5px;\n        HorizontalLayout {\n            Button {\n                icon: @image-url(\"../icons/krokiet_folder_add.svg\");\n                colorize-icon: true;\n                clicked => {\n                    folder_choose_requested(true);\n                }\n            }\n            Button {\n                icon: @image-url(\"../icons/krokiet_file_add.svg\");\n                colorize-icon: true;\n                clicked => {\n                    file_choose_requested(true);\n                }\n            }\n\n            Rectangle {\n                width: 5px;\n            }\n            \n            Rectangle {\n                horizontal-stretch: 1.0;\n                Text {\n                    text <=> Translations.included_paths_text;\n                    font-size: FontSizes.normal;\n                    font-weight: FontSizes.bold_weight;\n                }\n            }\n            \n            Rectangle {\n                width: 5px;\n            }\n\n            Button {\n                icon: @image-url(\"../icons/krokiet_manual_add.svg\");\n                colorize-icon: true;\n                clicked => {\n                    show_manual_add_dialog(true);\n                }\n            }\n        }\n\n        Rectangle {\n            height: 5px;\n        }\n\n        included_list := IncludedPaths { }\n    }\n\n    Rectangle {\n        width: 3px;\n        horizontal-stretch: 0.0;\n        background: ColorPalette.line_item_color;\n    }\n\n    // Excluded directories \n    VerticalLayout {\n        horizontal-stretch: 1.0;\n        padding-top: 5px;\n        padding-right: 5px;\n        HorizontalLayout {\n            Button {\n                icon: @image-url(\"../icons/krokiet_folder_add.svg\");\n                colorize-icon: true;\n                clicked => {\n                    folder_choose_requested(false);\n                }\n            }\n            Button {\n                icon: @image-url(\"../icons/krokiet_file_add.svg\");\n                colorize-icon: true;\n                clicked => {\n                    file_choose_requested(false);\n                }\n            }\n\n            Rectangle {\n                width: 5px;\n            }\n\n            Rectangle {\n                horizontal-stretch: 1.0;\n                Text {\n                    text <=> Translations.excluded_paths_text;\n                    font-size: FontSizes.normal;\n                    font-weight: FontSizes.bold_weight;\n                }\n            }\n\n            Rectangle {\n                width: 5px;\n            }\n\n            Button {\n                icon: @image-url(\"../icons/krokiet_manual_add.svg\");\n                colorize-icon: true;\n                clicked => {\n                    show_manual_add_dialog(false);\n                }\n            }\n        }\n\n        Rectangle {\n            height: 5px;\n        }\n\n        excluded_list := ExcludedPaths { }\n    }\n}\n\ncomponent TextErrorsPanel inherits TextEdit {\n    height: 20px;\n    read-only: true;\n    wrap: TextWrap.no-wrap;\n    text <=> GuiState.info_text;\n    font-size: FontSizes.normal;\n}\n\nexport component BottomPanel {\n    in-out property <BottomPanelVisibility> bottom_panel_visibility: BottomPanelVisibility.Directories;\n    callback folder_choose_requested(bool);\n    callback file_choose_requested(bool);\n    callback show_manual_add_dialog(bool);\n    min-height: bottom_panel_visibility == BottomPanelVisibility.NotVisible ? 0px : 150px;\n    min-width: bottom_panel_visibility == BottomPanelVisibility.NotVisible ? 0px : 400px;\n    if bottom_panel_visibility == BottomPanelVisibility.Directories: DirectoriesPanel {\n        width: parent.width;\n        height: parent.height;\n        folder_choose_requested(included_paths) => {\n            root.folder_choose_requested(included_paths)\n        }\n        file_choose_requested(included_paths) => {\n            root.file_choose_requested(included_paths)\n        }\n        show_manual_add_dialog(included_paths) => {\n            root.show_manual_add_dialog(included_paths)\n        }\n    }\n\n    if bottom_panel_visibility == BottomPanelVisibility.TextErrors: TextErrorsPanel {\n        width: parent.width;\n        height: parent.height;\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/callabler.slint",
    "content": "import { SelectMode, SortColumnMode, SortMode } from \"common.slint\";\nimport { Palette } from \"std-widgets.slint\";\nimport { Settings } from \"settings.slint\";\n\nexport global Callabler {\n    // Bottom panel operations\n    callback remove_item_paths(bool, int);\n    callback added_manual_paths(bool, string);\n\n    // Row selecting\n    callback change_number_of_checked_items(int);\n\n    callback row_select_all();\n    callback row_reverse_single_unique_item(int);\n    callback row_reverse_item_selection(int);\n    callback row_select_items_with_shift(int, int);\n\n    callback row_reverse_checked_selection();\n    callback row_open_item_with_index(int);\n    callback row_open_parent_item_with_index(int);\n\n    // Right click or middle click opener\n    callback open_item(string);\n    callback open_parent(string);\n\n    callback delete_selected_items();\n    callback select_items(SelectMode);\n    callback populate_custom_select_columns(); // Rust fills GuiState.custom_select_columns for active tab\n    callback update_custom_select_column(int, bool, string); // col_idx, enabled, filter_value\n    callback select_items_custom_columns(bool, bool, bool); // select_mode, case_sensitive, leave_one_in_group\n    callback validate_regex(string) -> bool;\n    callback sort_items(SortMode);\n\n    // Preview\n    // Parameters: path, crop_left, crop_top, crop_right, crop_bottom, original_width, original_height\n    callback load_image_preview(string, int, int, int, int, int, int);\n\n    // Settings\n    callback changed_settings_preset();\n    callback save_current_preset();\n    callback load_current_preset();\n    callback reset_current_preset();\n    callback changed_language();\n\n    callback tab_changed();\n\n    // Dialogs\n    callback save_results();\n    callback move_items(string);\n    callback rename_files();\n    callback crop_video_items();\n    callback reencode_video_items();\n    callback clean_exif_items();\n    callback hardlink_items();\n    callback softlink_items();\n\n    // Only Slint\n    callback open_config_folder();\n    callback open_cache_folder();\n\n    callback start_cache_cleaning();\n    callback stop_cache_cleaning();\n\n    callback open_link(string);\n\n    callback theme_changed();\n    theme_changed => {\n        Palette.color-scheme = Settings.dark_theme ? ColorScheme.dark : ColorScheme.light;\n    }\n\n    callback change_sort_column_mode(SortColumnMode, int);\n}\n"
  },
  {
    "path": "krokiet/ui/color_palette.slint",
    "content": "import { Settings } from \"settings.slint\";\n\nexport global ColorPalette {\n    // Tabs at left side\n    in-out property <color> tab_selected_color: Settings.dark_theme ? #353535 : #5e5e5e;\n    in-out property <color> tab_hovered_color: Settings.dark_theme ? #49494926 : #80808014;\n    // ListView\n    in-out property <color> list_view_item_color: Settings.dark_theme ? #222222 : #dddddd;\n    in-out property <color> list_view_item_hovered_color: Settings.dark_theme ? #333333 : #d2d2d2;\n    in-out property <color> list_view_item_selected_color: Settings.dark_theme ? #444444 : #cccccc;\n    in-out property <color> list_view_item_selected_hovered_color: Settings.dark_theme ? #555555 : #bbbbbb;\n\n    in-out property <color> list_view_header_color: Settings.dark_theme ? #111111 : #888888;\n    in-out property <color> list_view_clicked_header_color: Settings.dark_theme ? #1a1a1a : #808080;\n    \n    // Popup\n    in-out property <color> popup_background: Settings.dark_theme ? #353535 : #cecece;\n    in-out property <color> popup_background_border: Settings.dark_theme ? #222222 : #808080;\n    in-out property <color> popup_background_title_line: Settings.dark_theme ? #252525 : #9e9e9e;\n    in-out property <color> popup_border_color: Settings.dark_theme ? #000000 : #808080;\n\n    in-out property <color> line_item_color: Settings.dark_theme ? #222222 : #dddddd;\n\n    in-out property <color> remove_item_color_button: Settings.dark_theme ? #222222 : #dddddd;\n\n    in-out property <color> hint_color: Settings.dark_theme ? #888888 : #888888;\n\n    public pure function get_listview_color(selected: bool, hovered: bool) -> color {\n        if (selected) {\n            return hovered ? self.list_view_item_selected_hovered_color : self.list_view_item_selected_color;\n        } else {\n            return hovered ? self.list_view_item_hovered_color : self.list_view_item_color;\n        }\n    }\n    public pure function get_listview_color_with_header(selected: bool, hovered: bool, header: bool) -> color {\n        if (header) {\n            return selected ? self.list_view_clicked_header_color : self.list_view_header_color;\n        } else {\n            return self.get_listview_color(selected, hovered);\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/common.slint",
    "content": "export enum ActiveTab {\n    DuplicateFiles,\n    EmptyFolders,\n    BigFiles,\n    EmptyFiles,\n    TemporaryFiles,\n    SimilarImages,\n    SimilarVideos,\n    SimilarMusic,\n    InvalidSymlinks,\n    BrokenFiles,\n    BadExtensions,\n    BadNames,\n    ExifRemover,\n    VideoOptimizer,\n    Settings,\n    About\n}\n\nexport enum PopupRequest {\n    Delete,\n    Move,\n    CleanExif,\n    OptimizeVideo,\n    RenameBadExtension,\n    RenameBadFileName,\n    Symlink,\n    Hardlink,\n    Save\n}\n\nexport enum SortColumnMode {\n    None, // None is default mode, is set when starting scan and cannot be restored by user\n    Descending,\n    Ascending\n}\n\nexport enum TypeOfOpenedItem {\n    CurrentItem,\n    ParentItem\n}\n\nexport struct ProgressToSend {\n    current_progress: int,\n    current_progress_size: int,\n    all_progress: int,\n    step_name: string,\n}\n\nexport struct SingleMainListModel {\n    checked: bool,\n    header_row: bool,\n    filled_header_row: bool,\n    selected_row: bool,\n    val_str: [string],\n    val_int: [int]\n}\n\nexport enum BottomPanelVisibility {\n    NotVisible,\n    TextErrors,\n    Directories\n}\n\nexport struct IncludedPathsModel {\n    path: string,\n    referenced_path: bool,\n    selected_row: bool,\n}\n\nexport struct ExcludedPathsModel {\n    path: string,\n    selected_row: bool,\n}\n\nexport enum SelectMode {\n    SelectAll,\n    UnselectAll,\n    InvertSelection,\n    SelectTheBiggestSize,\n    SelectTheBiggestResolution,\n    SelectTheSmallestSize,\n    SelectTheSmallestResolution,\n    SelectNewest,\n    SelectOldest,\n    SelectShortestPath,\n    SelectLongestPath,\n    SelectCustom,\n}\n\nexport struct SelectModel {\n    data: SelectMode,\n    name: string\n}\n\nexport enum ColumnType {\n    Str,\n    Int,\n    Date,\n    FullPath,\n}\n\nexport struct CustomSelectColumnModel {\n    column_name: string,\n    column_idx: int,\n    column_type: ColumnType,\n    // For ColumnType.Int: true means the value is stored as a (Part1, Part2) u64 pair\n    // in val_int (e.g. Size, Bitrate); false means it is a plain i32.\n    is_pair: bool,\n    enabled: bool,\n    filter_value: string,\n}\n\nexport enum SortMode {\n    FullName,\n    Selection,\n    Reverse\n}\n\nexport struct SortModel {\n    data: SortMode,\n    name: string\n}\n\nexport struct CacheCleaningProgress {\n    current_cache_file: int,\n    total_cache_files: int,\n    current_file_name: string,\n    checked_entries: int,\n    all_entries: int,\n}\n\nexport struct CacheCleaningResult {\n    processed_files_text: string,\n    entries_stats_text: string,\n    size_stats_text: string,\n    time_text: string,\n    errors_count: int,\n    errors: string,\n}\n"
  },
  {
    "path": "krokiet/ui/fonts.slint",
    "content": "export global FontSizes {\n    // Main window title\n    out property <length> title: 22px;\n    // Section headers\n    out property <length> header: 17px;\n    // Default/normal text\n    out property <length> normal: 13px;\n    // Small text (hints, secondary)\n    out property <length> small: 12px;\n\n    out property <int> bold_weight: 600;\n    out property <int> bold_light: 500;\n}\n"
  },
  {
    "path": "krokiet/ui/gui_state.slint",
    "content": "import { ActiveTab, BottomPanelVisibility, CacheCleaningProgress, CacheCleaningResult, CustomSelectColumnModel, SelectMode, SelectModel, SortMode, SortModel } from \"common.slint\";\nimport { Translations } from \"translations.slint\";\n\n// State Gui state that shows the current state of the GUI\n// It extends Settings global state with settings that are not saved to the settings file\nexport global GuiState {\n    in-out property <length> app_width;\n    in-out property <length> app_height;\n\n    in-out property <string> info_text: \"Nothing to report\";\n    in-out property <bool> preview_visible;\n    in-out property <image> preview_image;\n    in-out property <string> preview_image_path;\n\n    in-out property <bool> audio_feature_enabled: false;\n\n\n    in-out property <float> maximum_threads: 40;\n\n    in-out property <bool> choosing_include_directories;\n    in-out property <bool> visible_tool_settings;\n\n    in-out property <bool> available_subsettings: active_tab == ActiveTab.BadNames || active_tab == ActiveTab.SimilarImages || active_tab == ActiveTab.DuplicateFiles || active_tab == ActiveTab.SimilarVideos || active_tab == ActiveTab.SimilarMusic || active_tab == ActiveTab.BigFiles || active_tab == ActiveTab.BrokenFiles || active_tab == ActiveTab.VideoOptimizer || active_tab == ActiveTab.ExifRemover;\n    in-out property <bool> tool_with_groups: active_tab == ActiveTab.SimilarImages || active_tab == ActiveTab.DuplicateFiles || active_tab == ActiveTab.SimilarVideos || active_tab == ActiveTab.SimilarMusic;\n    in-out property <ActiveTab> active_tab: ActiveTab.DuplicateFiles;\n    in-out property <bool> is_tool_tab_active: active_tab != ActiveTab.Settings && active_tab != ActiveTab.About;\n\n    in-out property <[SelectModel]> select_results_list: [\n        { data: SelectMode.SelectAll, name: Translations.selection_all_text },\n        { data: SelectMode.UnselectAll, name: Translations.selection_deselect_all_text },\n    ];\n\n    in-out property <[CustomSelectColumnModel]> custom_select_columns: [];\n\n    in-out property <[SortModel]> sort_results_list: [\n        { data: SortMode.FullName, name: Translations.sort_by_full_name_text },\n        { data: SortMode.Selection, name: Translations.sort_by_selection_text },\n        { data: SortMode.Reverse, name: Translations.sort_reverse_text },\n    ];\n\n    in-out property <[{name: string, tab: ActiveTab}]> tools_model: [\n        { name: Translations.tool_duplicate_files_text, tab: ActiveTab.DuplicateFiles },\n        { name: Translations.tool_empty_folders_text, tab: ActiveTab.EmptyFolders },\n        { name: Translations.tool_big_files_text, tab: ActiveTab.BigFiles },\n        { name: Translations.tool_empty_files_text, tab: ActiveTab.EmptyFiles },\n        { name: Translations.tool_temporary_files_text, tab: ActiveTab.TemporaryFiles },\n        { name: Translations.tool_similar_images_text, tab: ActiveTab.SimilarImages },\n        { name: Translations.tool_similar_videos_text, tab: ActiveTab.SimilarVideos },\n        { name: Translations.tool_music_duplicates_text, tab: ActiveTab.SimilarMusic },\n        { name: Translations.tool_invalid_symlinks_text, tab: ActiveTab.InvalidSymlinks },\n        { name: Translations.tool_broken_files_text, tab: ActiveTab.BrokenFiles },\n        { name: Translations.tool_bad_extensions_text, tab: ActiveTab.BadExtensions },\n        { name: Translations.tool_exif_remover_text, tab: ActiveTab.ExifRemover },\n        { name: Translations.tool_video_optimizer_text, tab: ActiveTab.VideoOptimizer },\n        { name: Translations.tool_bad_names_text, tab: ActiveTab.BadNames },\n    ];\n\n    in-out property <BottomPanelVisibility> bottom_panel_visibility: BottomPanelVisibility.Directories;\n\n    // Bad workaround for missing i64 support in Slint(2 i32 are used instead):\n    in-out property <int> selected_results_duplicates: 0;\n    in-out property <int> selected_results_duplicates2: 0;\n    in-out property <int> selected_results_similar_images: 0;\n    in-out property <int> selected_results_similar_images2: 0;\n    in-out property <int> selected_results_similar_videos: 0;\n    in-out property <int> selected_results_similar_videos2: 0;\n    in-out property <int> selected_results_similar_music: 0;\n    in-out property <int> selected_results_similar_music2: 0;\n    in-out property <int> selected_results_big_files: 0;\n    in-out property <int> selected_results_big_files2: 0;\n    in-out property <int> selected_results_broken_files: 0;\n    in-out property <int> selected_results_broken_files2: 0;\n    in-out property <int> selected_results_invalid_symlinks: 0;\n    in-out property <int> selected_results_invalid_symlinks2: 0;\n    in-out property <int> selected_results_empty_folders: 0;\n    in-out property <int> selected_results_empty_folders2: 0;\n    in-out property <int> selected_results_empty_files: 0;\n    in-out property <int> selected_results_empty_files2: 0;\n    in-out property <int> selected_results_temporary_files: 0;\n    in-out property <int> selected_results_temporary_files2: 0;\n    in-out property <int> selected_results_bad_extensions: 0;\n    in-out property <int> selected_results_bad_extensions2: 0;\n    in-out property <int> selected_results_exif_remover: 0;\n    in-out property <int> selected_results_exif_remover2: 0;\n    in-out property <int> selected_results_video_optimizer: 0;\n    in-out property <int> selected_results_video_optimizer2: 0;\n    in-out property <int> selected_results_bad_names: 0;\n    in-out property <int> selected_results_bad_names2: 0;\n\n    // Data index arrays for lists: [parentPathIdx, fileNameIdx, previewImageIdx]\n    in-out property <[int]> duplicate_data_idx: [3, 2, -1, -1];\n    in-out property <[int]> empty_folders_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> big_files_data_idx: [3, 2, -1, -1];\n    in-out property <[int]> empty_files_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> temporary_files_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> similar_images_data_idx: [5, 4, -1, -1];\n    in-out property <[int]> similar_videos_data_idx: [3, 2, 10, -1];\n    in-out property <[int]> similar_music_data_idx: [9, 2, -1, -1];\n    in-out property <[int]> invalid_symlink_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> broken_files_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> bad_extensions_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> exif_remover_data_idx: [2, 1, -1, -1];\n    in-out property <[int]> video_optimizer_data_idx: [2, 1, 8, -1];\n    in-out property <[int]> bad_names_data_idx: [2, 1, -1, -1];\n\n    in-out property <bool> file_dialog_open: false;\n\n    in-out property <bool> cache_cleaning_is_cleaning: false;\n    in-out property <bool> cache_cleaning_finished: false;\n    in-out property <CacheCleaningProgress> cache_cleaning_progress: {\n        current_cache_file: 0,\n        total_cache_files: 0,\n        current_file_name: \"\",\n        checked_entries: 0,\n        all_entries: 0,\n    };\n    in-out property <CacheCleaningResult> cache_cleaning_result: {\n        processed_files_text: \"\",\n        entries_stats_text: \"\",\n        size_stats_text: \"\",\n        time_text: \"\",\n        errors_count: 0,\n        errors: \"\",\n    };\n}\n"
  },
  {
    "path": "krokiet/ui/included_paths.slint",
    "content": "import { Button, CheckBox, ListView } from \"std-widgets.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { ExcludedPathsModel, IncludedPathsModel } from \"common.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { Settings } from \"settings.slint\";\nimport { Translations } from \"translations.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\nexport component IncludedPaths {\n    in-out property <[IncludedPathsModel]> model <=> Settings.included_paths_model;\n    in-out property <int> current_index <=> Settings.included_paths_model_selected_idx;\n\n    in-out property <length> size_referenced_path: 33px;\n    min-width: 50px;\n    VerticalLayout {\n        HorizontalLayout {\n            spacing: 5px;\n            Text {\n                text <=> Translations.ref_text;\n                width: size_referenced_path;\n                font-size: FontSizes.normal;\n                horizontal-alignment: center;\n                font-weight: FontSizes.bold_weight;\n            }\n\n            Text {\n                font-size: FontSizes.normal;\n                horizontal-stretch: 1.0;\n                text <=> Translations.path_text;\n                font-weight: FontSizes.bold_weight;\n            }\n        }\n\n        ListView {\n            for r[idx] in model: Rectangle {\n                height: 30px;\n                border_radius: 5px;\n                width: parent.width;\n\n                background: ColorPalette.get_listview_color(r.selected_row, touch-area.has-hover);\n                touch_area := TouchArea {\n                    clicked => {\n                        handle_click(true);\n                    }\n                    double-clicked => {\n                        Callabler.open_item(r.path);\n                        handle_click(false);\n                    }\n                    pointer-event(event) => {\n                        if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) {\n                            Callabler.open_parent(r.path);\n                        }\n                    }\n\n                    function handle_click(can_unselect: bool) {\n                        if (current_index != -1) {\n                            if (current_index != idx) {\n                                model[current_index].selected_row = false;\n                            } else if (can_unselect) {\n                                r.selected_row = false;\n                                current_index = -1;\n                                return;\n                            }\n                        }\n                        r.selected_row = true;\n                        current_index = idx;\n                    }\n                }\n\n                HorizontalLayout {\n                    padding-left: 7.5px;\n                    spacing: 0px;\n                    width: parent.width;\n\n                    CheckBox {\n                        checked: r.referenced_path;\n                        toggled => {\n                            model[idx].referenced_path = self.checked;\n                        }\n                        width: size_referenced_path;\n                    }\n\n                    Text {\n                        font-size: FontSizes.normal;\n                        horizontal-stretch: 1.0;\n                        height: parent.height;\n                        text: r.path;\n                        vertical-alignment: center;\n                        horizontal-alignment: left;\n            \n                    }\n                }\n                    \n                HorizontalLayout {\n                    width: parent.width;\n                    padding-left: 5px;\n                    Rectangle { }\n                    Rectangle {\n                        width: 50px;\n                        background: ColorPalette.remove_item_color_button;\n                        horizontal-stretch: 1.0;\n                        Button {\n                            width: 50px;\n                            icon: @image-url(\"../icons/krokiet_delete.svg\");\n                            colorize-icon: true;\n                            clicked => {\n                                Callabler.remove_item_paths(true, idx);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nexport component ExcludedPaths {\n    in-out property <[ExcludedPathsModel]> model <=> Settings.excluded_paths_model;\n    in-out property <int> current_index <=> Settings.excluded_paths_model_selected_idx;\n\n    min-width: 50px;\n    VerticalLayout {\n        HorizontalLayout {\n            spacing: 5px;\n            padding-left: 5px;\n            Text {\n                text <=> Translations.path_text;\n                font-size: FontSizes.normal;\n                font-weight: FontSizes.bold_weight;\n            }\n        }\n\n        ListView {\n            for r[idx] in model: Rectangle {\n                height: 30px;\n                border_radius: 5px;\n                width: parent.width;\n\n                background: ColorPalette.get_listview_color(r.selected_row, touch-area.has-hover);\n                touch_area := TouchArea {\n                    clicked => {\n                        handle_click(true);\n                    }\n                    double-clicked => {\n                        Callabler.open_item(r.path);\n                        handle_click(false);\n                    }\n                    pointer-event(event) => {\n                        if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) {\n                            Callabler.open_parent(r.path);\n                        }\n                    }\n\n                    function handle_click(can_unselect: bool) {\n                        if (current_index != -1) {\n                            if (current_index != idx) {\n                                model[current_index].selected_row = false;\n                            } else if (can_unselect) {\n                                r.selected_row = false;\n                                current_index = -1;\n                                return;\n                            }\n                        }\n                        r.selected_row = true;\n                        current_index = idx;\n                    }\n                }\n\n                Text {\n                    font-size: FontSizes.normal;\n                    width: parent.width;\n                    horizontal-stretch: 1.0;\n                    height: parent.height;\n                    text: r.path;\n                    vertical-alignment: center;\n                    horizontal-alignment: left;\n                    x: 5px;\n                }\n                \n                HorizontalLayout {\n                    width: parent.width;\n                    Rectangle { }\n                    Rectangle {\n                        width: 50px;\n                        background: ColorPalette.remove_item_color_button;\n                        horizontal-stretch: 1.0;\n                        Button {\n                            width: 50px;\n                            icon: @image-url(\"../icons/krokiet_delete.svg\");\n                            colorize-icon: true;\n                            clicked => {\n                                Callabler.remove_item_paths(false, idx);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/left_side_panel.slint",
    "content": "import { Button, ListView } from \"std-widgets.slint\";\nimport { ActiveTab, BottomPanelVisibility } from \"common.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { FontSizes } from \"fonts.slint\";\nimport { Translations } from \"translations.slint\";\n\ncomponent TabItem {\n    in property <string> text;\n    in property <ActiveTab> curr_tab;\n    callback changed_active_tab();\n\n    Rectangle {\n        width: parent.width;\n        horizontal-stretch: 1.0;\n        background: touch-area.has-hover ? ColorPalette.tab_hovered_color : transparent;\n        touch_area := TouchArea {\n            clicked => {\n                if (GuiState.active_tab == root.curr_tab) {\n                    return;\n                }\n                GuiState.active_tab = root.curr_tab;\n                Callabler.tab_changed();\n                changed_active_tab();\n            }\n        }\n    }\n\n    HorizontalLayout {\n        width: parent.width;\n        alignment: LayoutAlignment.start;\n        layout_rectangle := VerticalLayout {\n            empty_rectangle := Rectangle { }\n\n            current_rectangle := Rectangle {\n                visible: (GuiState.active_tab == root.curr_tab);\n                border-radius: 2px;\n                width: 5px;\n                height: 0px;\n                background: ColorPalette.tab_selected_color;\n                animate height {\n                    duration: 150ms;\n                    easing: ease;\n                }\n            }\n\n            empty_rectangle2 := Rectangle { }\n        }\n    }\n\n    Text {\n        text: root.text;\n        width: parent.width;\n        horizontal-alignment: center;\n        font-size: FontSizes.normal;\n        font-weight: FontSizes.bold_light;\n    }\n\n    states [\n        is-selected when GuiState.active_tab == root.curr_tab: {\n            current_rectangle.height: layout_rectangle.height;\n        }\n        is-not-selected when GuiState.active_tab != root.curr_tab: {\n            current_rectangle.height: 0px;\n        }\n    ]\n}\n\nexport component LeftSidePanel {\n    callback changed_active_tab();\n\n    // Hidden off-screen Text elements to measure the natural width of each tool label.\n    // The panel width is set to the widest one plus a small padding so that no manual\n    // per-language constant is needed, like it was done in previous versions\n    t_duplicate   := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_duplicate_files_text; }\n    t_empty_f     := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_empty_folders_text; }\n    t_big         := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_big_files_text; }\n    t_empty_fi    := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_empty_files_text; }\n    t_temp        := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_temporary_files_text; }\n    t_sim_img     := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_similar_images_text; }\n    t_sim_vid     := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_similar_videos_text; }\n    t_music       := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_music_duplicates_text; }\n    t_symlinks    := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_invalid_symlinks_text; }\n    t_broken      := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_broken_files_text; }\n    t_bad_ext     := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_bad_extensions_text; }\n    t_exif        := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_exif_remover_text; }\n    t_vid_opt     := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_video_optimizer_text; }\n    t_bad_names   := Text { x: -10000px; y: -10000px; height: 0; font-size: FontSizes.normal; font-weight: FontSizes.bold_light; text: Translations.tool_bad_names_text; }\n\n    property <length> max_tool_text_width:\n        max(t_duplicate.preferred-width,\n        max(t_empty_f.preferred-width,\n        max(t_big.preferred-width,\n        max(t_empty_fi.preferred-width,\n        max(t_temp.preferred-width,\n        max(t_sim_img.preferred-width,\n        max(t_sim_vid.preferred-width,\n        max(t_music.preferred-width,\n        max(t_symlinks.preferred-width,\n        max(t_broken.preferred-width,\n        max(t_bad_ext.preferred-width,\n        max(t_exif.preferred-width,\n        max(t_vid_opt.preferred-width,\n            t_bad_names.preferred-width)))))))))))));\n\n    preferred-width: max_tool_text_width + 20px;\n    VerticalLayout {\n        spacing: 2px;\n        Rectangle {\n            visible: GuiState.active_tab != ActiveTab.About;\n            height: 80px;\n            Image {\n                width: parent.height;\n                source: @image-url(\"../icons/krokiet_logo_flag.svg\");\n                image-fit: ImageFit.contain;\n            }\n\n            touch_area := TouchArea {\n                clicked => {\n                    GuiState.active_tab = ActiveTab.About;\n                    Callabler.tab_changed();\n                    root.changed_active_tab();\n                    GuiState.bottom_panel_visibility = BottomPanelVisibility.NotVisible;\n                }\n            }\n        }\n\n        ListView {\n            out property <length> element_size: 25px;\n            out property <[{name: string, tab: ActiveTab}]> tools_model <=> GuiState.tools_model;\n\n            for r[idx] in tools_model: TabItem {\n                height: parent.element_size;\n                text: r.name;\n                curr_tab: r.tab;\n                changed_active_tab() => {\n                    root.changed_active_tab();\n                }\n            }\n        }\n\n        Rectangle {\n            min-width: 90px;\n            HorizontalLayout {\n                alignment: start;\n                Button {\n                    visible: GuiState.active_tab != ActiveTab.Settings;\n                    min-width: 20px;\n                    min-height: 20px;\n                    max-height: self.width;\n                    preferred-height: self.width;\n                    icon: @image-url(\"../icons/krokiet_settings.svg\");\n                    colorize-icon: true;\n                    clicked => {\n                        GuiState.active_tab = ActiveTab.Settings;\n                        Callabler.tab_changed();\n                        root.changed_active_tab();\n                    }\n                }\n            }\n\n            HorizontalLayout {\n                alignment: end;\n                Button {\n                    checkable: true;\n                    checked: GuiState.visible_tool_settings;\n                    visible: GuiState.available_subsettings;\n                    min-width: 20px;\n                    min-height: 20px;\n                    max-height: self.width;\n                    preferred-height: self.width;\n                    icon: @image-url(\"../icons/krokiet_subsettings.svg\");\n                    colorize-icon: true;\n                    clicked => {\n                        GuiState.visible_tool_settings = !GuiState.visible_tool_settings;\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/main_lists.slint",
    "content": "import { SelectableTableView } from \"selectable_tree_view.slint\";\nimport { ActiveTab, SingleMainListModel } from \"common.slint\";\nimport { SettingsList } from \"settings_list.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { About } from \"about.slint\";\nimport { Settings } from \"settings.slint\";\n\nexport component MainList {\n    callback show_clean_cache_popup();\n\n    in-out property <bool> working: false;\n\n    in-out property <[SingleMainListModel]> duplicate_files_model: [];\n    in-out property <[SingleMainListModel]> empty_folder_model: [\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"kropkarz\", \"/Xd1\", \"24.10.2023\"], val_int: []} ,\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"witasphere\", \"/Xd1/Imagerren2\", \"25.11.1991\"], val_int: []},\n        {checked: true, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"lokkaler\", \"/Xd1/Vide2\", \"01.23.1911\"], val_int: []}\n    ];\n    in-out property <[SingleMainListModel]> big_files_model: [];\n    in-out property <[SingleMainListModel]> empty_files_model: [\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"kropkarz\", \"/Xd1\", \"24.10.2023\"], val_int: []} ,\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"witasphere\", \"/Xd1/Imagerren2\", \"25.11.1991\"], val_int: []},\n        {checked: true, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"lokkaler\", \"/Xd1/Vide2\", \"01.23.1911\"], val_int: []}\n    ];\n    in-out property <[SingleMainListModel]> temporary_files_model: [];\n    in-out property <[SingleMainListModel]> similar_images_model: [\n        {checked: false, selected_row: false, header_row: true, filled_header_row: false, val_str: [\"Original\", \"500KB\", \"100x100\", \"kropkarz\", \"/Xd1\", \"24.10.2023\"], val_int: []},\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"Similar\", \"500KB\", \"100x100\", \"witasphere\", \"/Xd1/Imagerren2\", \"25.11.1991\"], val_int: []},\n        {checked: true, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"Similar\", \"500KB\", \"100x100\", \"lokkaler\", \"/Xd1/Vide2\", \"01.23.1911\"], val_int: []}\n    ];\n    in-out property <[SingleMainListModel]> similar_videos_model: [];\n    in-out property <[SingleMainListModel]> similar_music_model: [];\n    in-out property <[SingleMainListModel]> invalid_symlinks_model: [];\n    in-out property <[SingleMainListModel]> broken_files_model: [];\n    in-out property <[SingleMainListModel]> bad_extensions_model: [];\n    in-out property <[SingleMainListModel]> bad_names_model: [];\n    in-out property <[SingleMainListModel]> exif_remover_model: [];\n    in-out property <[SingleMainListModel]> video_optimizer_model: [];\n\n    callback changed_active_tab();\n\n    duplicates := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.DuplicateFiles;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.duplicates_column_name;\n        column_sizes <=> Settings.duplicates_column_size;\n        values <=> duplicate_files_model;\n        parentPathIdx: GuiState.duplicate_data_idx[0];\n        fileNameIdx: GuiState.duplicate_data_idx[1];\n        previewImageIdx: GuiState.duplicate_data_idx[2];\n        topLeftCropIdx: GuiState.duplicate_data_idx[3];\n        originalWidthIdx: GuiState.duplicate_data_idx[4];\n        originalHeightIdx: GuiState.duplicate_data_idx[5];\n        sort_available: !working;\n    }\n\n    empty_folders := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.EmptyFolders;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.empty_folders_column_name;\n        column_sizes <=> Settings.empty_folders_column_size;\n        values <=> empty_folder_model;\n        parentPathIdx: GuiState.empty_folders_data_idx[0];\n        fileNameIdx: GuiState.empty_folders_data_idx[1];\n        previewImageIdx: GuiState.empty_folders_data_idx[2];\n        topLeftCropIdx: GuiState.empty_folders_data_idx[3];\n        originalWidthIdx: GuiState.empty_folders_data_idx[4];\n        originalHeightIdx: GuiState.empty_folders_data_idx[5];\n        sort_available: !working;\n    }\n\n    big_files := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.BigFiles;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.big_files_column_name;\n        column_sizes <=> Settings.big_files_column_size;\n        values <=> big_files_model;\n        parentPathIdx: GuiState.big_files_data_idx[0];\n        fileNameIdx: GuiState.big_files_data_idx[1];\n        previewImageIdx: GuiState.big_files_data_idx[2];\n        topLeftCropIdx: GuiState.big_files_data_idx[3];\n        originalWidthIdx: GuiState.big_files_data_idx[4];\n        originalHeightIdx: GuiState.big_files_data_idx[5];\n        sort_available: !working;\n    }\n\n    empty_files := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.EmptyFiles;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.empty_files_column_name;\n        column_sizes <=> Settings.empty_files_column_size;\n        values <=> empty_files_model;\n        parentPathIdx: GuiState.empty_files_data_idx[0];\n        fileNameIdx: GuiState.empty_files_data_idx[1];\n        previewImageIdx: GuiState.empty_files_data_idx[2];\n        topLeftCropIdx: GuiState.empty_files_data_idx[3];\n        originalWidthIdx: GuiState.empty_files_data_idx[4];\n        originalHeightIdx: GuiState.empty_files_data_idx[5];\n        sort_available: !working;\n    }\n\n    temporary_files := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.TemporaryFiles;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.temporary_files_column_name;\n        column_sizes <=> Settings.temporary_files_column_size;\n        values <=> temporary_files_model;\n        parentPathIdx: GuiState.temporary_files_data_idx[0];\n        fileNameIdx: GuiState.temporary_files_data_idx[1];\n        previewImageIdx: GuiState.temporary_files_data_idx[2];\n        topLeftCropIdx: GuiState.temporary_files_data_idx[3];\n        originalWidthIdx: GuiState.temporary_files_data_idx[4];\n        originalHeightIdx: GuiState.temporary_files_data_idx[5];\n        sort_available: !working;\n    }\n\n    similar_images := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.SimilarImages;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.similar_images_column_name;\n        column_sizes <=> Settings.similar_images_column_size;\n        values <=> similar_images_model;\n        parentPathIdx: GuiState.similar_images_data_idx[0];\n        fileNameIdx: GuiState.similar_images_data_idx[1];\n        previewImageIdx: GuiState.similar_images_data_idx[2];\n        topLeftCropIdx: GuiState.similar_images_data_idx[3];\n        originalWidthIdx: GuiState.similar_images_data_idx[4];\n        originalHeightIdx: GuiState.similar_images_data_idx[5];\n        sort_available: !working;\n    }\n\n    similar_videos := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.SimilarVideos;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.similar_videos_column_name;\n        column_sizes <=> Settings.similar_videos_column_size;\n        values <=> similar_videos_model;\n        parentPathIdx: GuiState.similar_videos_data_idx[0];\n        fileNameIdx: GuiState.similar_videos_data_idx[1];\n        previewImageIdx: GuiState.similar_videos_data_idx[2];\n        topLeftCropIdx: GuiState.similar_videos_data_idx[3];\n        originalWidthIdx: GuiState.similar_videos_data_idx[4];\n        originalHeightIdx: GuiState.similar_videos_data_idx[5];\n        sort_available: !working;\n    }\n\n    similar_music := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.SimilarMusic;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.similar_music_column_name;\n        column_sizes <=> Settings.similar_music_column_size;\n        values <=> similar_music_model;\n        parentPathIdx: GuiState.similar_music_data_idx[0];\n        fileNameIdx: GuiState.similar_music_data_idx[1];\n        previewImageIdx: GuiState.similar_music_data_idx[2];\n        topLeftCropIdx: GuiState.similar_music_data_idx[3];\n        originalWidthIdx: GuiState.similar_music_data_idx[4];\n        originalHeightIdx: GuiState.similar_music_data_idx[5];\n        sort_available: !working;\n    }\n\n    invalid_symlink := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.InvalidSymlinks;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.invalid_symlink_column_name;\n        column_sizes <=> Settings.invalid_symlink_column_size;\n        values <=> invalid_symlinks_model;\n        parentPathIdx: GuiState.invalid_symlink_data_idx[0];\n        fileNameIdx: GuiState.invalid_symlink_data_idx[1];\n        previewImageIdx: GuiState.invalid_symlink_data_idx[2];\n        topLeftCropIdx: GuiState.invalid_symlink_data_idx[3];\n        originalWidthIdx: GuiState.invalid_symlink_data_idx[4];\n        originalHeightIdx: GuiState.invalid_symlink_data_idx[5];\n        sort_available: !working;\n    }\n\n    broken_files := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.BrokenFiles;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.broken_files_column_name;\n        column_sizes <=> Settings.broken_files_column_size;\n        values <=> broken_files_model;\n        parentPathIdx: GuiState.broken_files_data_idx[0];\n        fileNameIdx: GuiState.broken_files_data_idx[1];\n        previewImageIdx: GuiState.broken_files_data_idx[2];\n        topLeftCropIdx: GuiState.broken_files_data_idx[3];\n        originalWidthIdx: GuiState.broken_files_data_idx[4];\n        originalHeightIdx: GuiState.broken_files_data_idx[5];\n        sort_available: !working;\n    }\n\n    bad_extensions := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.BadExtensions;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.bad_extensions_column_name;\n        column_sizes <=> Settings.bad_extensions_column_size;\n        values <=> bad_extensions_model;\n        parentPathIdx: GuiState.bad_extensions_data_idx[0];\n        fileNameIdx: GuiState.bad_extensions_data_idx[1];\n        previewImageIdx: GuiState.bad_extensions_data_idx[2];\n        topLeftCropIdx: GuiState.bad_extensions_data_idx[3];\n        originalWidthIdx: GuiState.bad_extensions_data_idx[4];\n        originalHeightIdx: GuiState.bad_extensions_data_idx[5];\n        sort_available: !working;\n    }\n\n    exif_remover := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.ExifRemover;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.exif_remover_column_name;\n        column_sizes <=> Settings.exif_remover_column_size;\n        values <=> exif_remover_model;\n        parentPathIdx: GuiState.exif_remover_data_idx[0];\n        fileNameIdx: GuiState.exif_remover_data_idx[1];\n        previewImageIdx: GuiState.exif_remover_data_idx[2];\n        topLeftCropIdx: GuiState.exif_remover_data_idx[3];\n        originalWidthIdx: GuiState.exif_remover_data_idx[4];\n        originalHeightIdx: GuiState.exif_remover_data_idx[5];\n        sort_available: !working;\n    }\n\n    video_optimizer := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.VideoOptimizer;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.video_optimizer_column_name;\n        column_sizes <=> Settings.video_optimizer_column_size;\n        values <=> video_optimizer_model;\n        parentPathIdx: GuiState.video_optimizer_data_idx[0];\n        fileNameIdx: GuiState.video_optimizer_data_idx[1];\n        previewImageIdx: GuiState.video_optimizer_data_idx[2];\n        topLeftCropIdx: GuiState.video_optimizer_data_idx[3];\n        originalWidthIdx: GuiState.video_optimizer_data_idx[4];\n        originalHeightIdx: GuiState.video_optimizer_data_idx[5];\n        sort_available: !working;\n    }\n\n    bad_names := SelectableTableView {\n        visible: GuiState.active_tab == ActiveTab.BadNames;\n        min-width: 200px;\n        height: parent.height;\n        columns <=> Settings.bad_names_column_name;\n        column_sizes <=> Settings.bad_names_column_size;\n        values <=> bad_names_model;\n        parentPathIdx: GuiState.bad_names_data_idx[0];\n        fileNameIdx: GuiState.bad_names_data_idx[1];\n        previewImageIdx: GuiState.bad_names_data_idx[2];\n        topLeftCropIdx: GuiState.bad_names_data_idx[3];\n        originalWidthIdx: GuiState.bad_names_data_idx[4];\n        originalHeightIdx: GuiState.bad_names_data_idx[5];\n        sort_available: !working;\n    }\n\n\n    settings_list := SettingsList {\n        visible: GuiState.active_tab == ActiveTab.Settings;\n\n        show_clean_cache_popup() => {\n            root.show_clean_cache_popup();\n        }\n    }\n\n    about_app := About {\n        visible: GuiState.active_tab == ActiveTab.About;\n    }\n\n    public function reset_selection(active_tab: ActiveTab) {\n        if (active_tab == ActiveTab.EmptyFiles) {\n            empty_files.reset_selection();\n        } else if (active_tab == ActiveTab.EmptyFolders) {\n            empty_folders.reset_selection();\n        } else if (active_tab == ActiveTab.SimilarImages) {\n            similar_images.reset_selection();\n        } else if (active_tab == ActiveTab.BadExtensions) {\n            bad_extensions.reset_selection();\n        } else if (active_tab == ActiveTab.BigFiles) {\n            big_files.reset_selection();\n        } else if (active_tab == ActiveTab.DuplicateFiles) {\n            duplicates.reset_selection();\n        } else if (active_tab == ActiveTab.TemporaryFiles) {\n            temporary_files.reset_selection();\n        } else if (active_tab == ActiveTab.SimilarVideos) {\n            similar_videos.reset_selection();\n        } else if (active_tab == ActiveTab.SimilarMusic) {\n            similar_music.reset_selection();\n        } else if (active_tab == ActiveTab.InvalidSymlinks) {\n            invalid_symlink.reset_selection();\n        } else if (active_tab == ActiveTab.BrokenFiles) {\n            broken_files.reset_selection();\n        } else if (active_tab == ActiveTab.ExifRemover) {\n            exif_remover.reset_selection();\n        } else if (active_tab == ActiveTab.VideoOptimizer) {\n            video_optimizer.reset_selection();\n        } else if (active_tab == ActiveTab.BadNames) {\n            bad_names.reset_selection();\n        } else {\n             debug(\"Non handled reset selection in main_lists.slint\");\n        }\n    }\n\n    public function scan_started() {\n        if (GuiState.active_tab == ActiveTab.EmptyFiles) {\n            empty_files.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.EmptyFolders) {\n            empty_folders.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.SimilarImages) {\n            similar_images.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.BadExtensions) {\n            bad_extensions.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.BigFiles) {\n            big_files.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.DuplicateFiles) {\n            duplicates.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.TemporaryFiles) {\n            temporary_files.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.SimilarVideos) {\n            similar_videos.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.SimilarMusic) {\n            similar_music.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.InvalidSymlinks) {\n            invalid_symlink.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.BrokenFiles) {\n            broken_files.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.ExifRemover) {\n            exif_remover.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.VideoOptimizer) {\n            video_optimizer.scan_started();\n        } else if (GuiState.active_tab == ActiveTab.BadNames) {\n            bad_names.scan_started();\n        } else {\n             debug(\"Non handled reset selection in main_lists.slint\");\n        }\n    }\n\n    changed_active_tab() => {\n        // TODO - not sure why this exists, but if needed it needs to be updated to use new functions\n        // empty_folders.deselect_selected_item();\n        // empty_files.deselect_selected_item();\n        // similar_images.deselect_selected_item();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/main_window.slint",
    "content": "import { HorizontalBox, LineEdit, Palette, VerticalBox } from \"std-widgets.slint\";\nimport { FontSizes } from \"fonts.slint\";\nimport { LeftSidePanel } from \"left_side_panel.slint\";\nimport { MainList } from \"main_lists.slint\";\nimport { ActiveTab, PopupRequest, ProgressToSend, SingleMainListModel } from \"common.slint\";\nimport { ActionButtons } from \"action_buttons.slint\";\nimport { Progress } from \"progress.slint\";\nimport { Settings } from \"settings.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { BottomPanel } from \"bottom_panel.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { Preview } from \"preview.slint\";\nimport { PopupNewDirectories } from \"popup_new_directories.slint\";\nimport { PopupDelete } from \"popup_delete.slint\";\nimport { PopupMoveFolders } from \"popup_move_folders.slint\";\nimport { PopupSelectResults } from \"popup_select_results.slint\";\nimport { PopupCustomSelect } from \"popup_custom_select.slint\";\nimport { PopupRenameBadExtensions } from \"popup_rename_bad_extensions.slint\";\nimport { PopupRenameBadFileNames } from \"popup_rename_bad_file_names.slint\";\nimport { PopupSave } from \"popup_save.slint\";\nimport { PopupSortResults } from \"popup_sort.slint\";\nimport { PopupActionConfirm } from \"popup_action_confirm.slint\";\nimport { PopupCropVideo } from \"popup_crop_video.slint\";\nimport { PopupReencodeVideo } from \"popup_optimize.slint\";\nimport { PopupCleanExif } from \"popup_clean_exif.slint\";\nimport { PopupCleanCache } from \"popup_clean_cache.slint\";\nimport { ToolSettings } from \"tool_settings.slint\";\nimport { Translations } from \"translations.slint\";\n\nexport {Settings, Callabler, GuiState, Translations, Palette}\n\nexport component MainWindow inherits Window {\n    default-font-size: FontSizes.normal;\n\n    callback scan_stopping;\n    callback scan_starting(ActiveTab);\n    callback folder_choose_requested(bool);\n    callback file_choose_requested(bool);\n    callback scan_ended(string);\n    callback reset_selection(ActiveTab);\n    callback initialize_popup_sizes();\n    callback processing_ended(string);\n\n    // Logic:\n    // Opening popup is done via Rust call\n    callback request_setup_action_popup(PopupRequest); // When opening popup we need to setup data in it, Slint -> Rust\n    callback show_action_popup(PopupRequest, string); // After setup, we can show popup - Rust -> Slint - string is choosed folder - to avoid having multiple callbacks, string will be empty for things that do not need it\n\n    title <=> Translations.main_window_title_text;\n\n    min-width: 300px;\n    preferred-width: 800px;\n    min-height: 300px;\n    preferred-height: 600px;\n    in-out property <string> text_summary_text: \"\";\n    in-out property <bool> stop_requested: false;\n    in-out property <bool> scanning: false;\n    in-out property <bool> processing: false;\n    in-out property <ProgressToSend> progress_datas: {\n        current_progress: 15,\n        all_progress: 20,\n        step_name: \"Cache\",\n    };\n    in-out property <[SingleMainListModel]> duplicate_files_model: [];\n    in-out property <[SingleMainListModel]> empty_folder_model: [];\n    in-out property <[SingleMainListModel]> big_files_model: [];\n    in-out property <[SingleMainListModel]> empty_files_model: [];\n    in-out property <[SingleMainListModel]> temporary_files_model: [];\n    in-out property <[SingleMainListModel]> similar_images_model: [];\n    in-out property <[SingleMainListModel]> similar_videos_model: [];\n    in-out property <[SingleMainListModel]> similar_music_model: [];\n    in-out property <[SingleMainListModel]> invalid_symlinks_model: [];\n    in-out property <[SingleMainListModel]> broken_files_model: [];\n    in-out property <[SingleMainListModel]> bad_extensions_model: [];\n    in-out property <[SingleMainListModel]> bad_names_model: [];\n    in-out property <[SingleMainListModel]> exif_remover_model: [];\n    in-out property <[SingleMainListModel]> video_optimizer_model: [];\n\n    VerticalBox {\n        HorizontalBox {\n            vertical-stretch: 1.0;\n            preferred-height: 300px;\n            LeftSidePanel {\n                horizontal-stretch: 0.0;\n                changed_active_tab() => {\n                    GuiState.preview_visible = false;\n                    main_list.changed_active_tab();\n                }\n            }\n\n            VerticalLayout {\n                horizontal-stretch: 1.0;\n                min_width: 300px;\n                Rectangle {\n                    vertical-stretch: 1.0;\n                    main_list := MainList {\n                        x: 0;\n                        width: preview_or_tool_settings.visible ? parent.width / 2 : parent.width;\n                        height: parent.height;\n                        horizontal-stretch: 0.5;\n                        working: root.scanning || root.processing;\n                        duplicate_files_model <=> root.duplicate_files_model;\n                        empty_folder_model <=> root.empty_folder_model;\n                        big_files_model <=> root.big_files_model;\n                        empty_files_model <=> root.empty_files_model;\n                        temporary_files_model <=> root.temporary_files_model;\n                        similar_images_model <=> root.similar_images_model;\n                        similar_videos_model <=> root.similar_videos_model;\n                        similar_music_model <=> root.similar_music_model;\n                        invalid_symlinks_model <=> root.invalid_symlinks_model;\n                        broken_files_model <=> root.broken_files_model;\n                        bad_extensions_model <=> root.bad_extensions_model;\n                        bad_names_model <=> root.bad_names_model;\n                        exif_remover_model <=> root.exif_remover_model;\n                        video_optimizer_model <=> root.video_optimizer_model;\n\n                        show_clean_cache_popup() => {\n                            clean_cache_popup_window.show_popup();\n                        }\n                    }\n\n                    preview_or_tool_settings := Rectangle {\n                        visible: (GuiState.preview_visible || tool_settings.visible) && GuiState.is_tool_tab_active;\n                        height: parent.height;\n                        x: parent.width / 2;\n                        width: self.visible ? parent.width / 2 : 0;\n                        Preview {\n                            height: parent.height;\n                            width: parent.width;\n                            visible: GuiState.preview_visible && !tool_settings.visible;\n                            source: GuiState.preview_image;\n                            image-fit: ImageFit.contain;\n                        }\n\n                        tool_settings := ToolSettings {\n                            height: parent.height;\n                            width: parent.width;\n                            visible: GuiState.visible_tool_settings && GuiState.available_subsettings;\n                        }\n                    }\n                }\n\n                if root.scanning || root.processing: Progress {\n                    horizontal-stretch: 0.0;\n                    progress_datas <=> root.progress_datas;\n                }\n            }\n        }\n\n        action_buttons := ActionButtons {\n            duplicate_files_model <=> root.duplicate_files_model;\n            empty_folder_model <=> root.empty_folder_model;\n            big_files_model <=> root.big_files_model;\n            empty_files_model <=> root.empty_files_model;\n            temporary_files_model <=> root.temporary_files_model;\n            similar_images_model <=> root.similar_images_model;\n            similar_videos_model <=> root.similar_videos_model;\n            similar_music_model <=> root.similar_music_model;\n            invalid_symlinks_model <=> root.invalid_symlinks_model;\n            broken_files_model <=> root.broken_files_model;\n            bad_extensions_model <=> root.bad_extensions_model;\n            bad_names_model <=> root.bad_names_model;\n            exif_remover_model <=> root.exif_remover_model;\n            video_optimizer_model <=> root.video_optimizer_model;\n\n            vertical-stretch: 0.0;\n            scanning <=> root.scanning;\n            processing <=> root.processing;\n            stop_requested <=> root.stop_requested;\n            scan_stopping => {\n                text_summary_text = Translations.stopping_scan_text;\n                root.scan_stopping();\n            }\n            scan_starting(item) => {\n                text_summary_text = Translations.searching_text;\n                root.scan_starting(item);\n                main_list.scan_started(); // Need to clear sorting state\n            }\n            show_select_popup(x_offset, y_offset) => {\n                select_popup_window.x_offset = x_offset;\n                select_popup_window.y_offset = y_offset;\n                select_popup_window.show_popup();\n            }\n            show_action_popup(popup_request) => {\n                request_setup_action_popup(popup_request);\n            }\n            show_sort_popup(x_offset, y_offset) => {\n                sort_popup_window.x_offset = x_offset;\n                sort_popup_window.y_offset = y_offset;\n                sort_popup_window.show_popup();\n            }\n        }\n\n        HorizontalLayout {\n            spacing: 5px;\n            text_summary := LineEdit {\n                text: text_summary_text;\n                read-only: true;\n                font-size: FontSizes.normal;\n            }\n\n            Text {\n                font-size: FontSizes.normal;\n                text: \"Krokiet\\n11.0.1\";\n                vertical-alignment: center;\n                horizontal-alignment: center;\n            }\n        }\n\n        bottom_panel := BottomPanel {\n            bottom_panel_visibility <=> action_buttons.bottom_panel_visibility;\n            vertical-stretch: 0.0;\n            folder_choose_requested(included_paths) => {\n                root.folder_choose_requested(included_paths)\n            }\n            file_choose_requested(included_paths) => {\n                root.file_choose_requested(included_paths)\n            }\n            show_manual_add_dialog(included_paths) => {\n                GuiState.choosing_include_directories = included_paths;\n                new_directory_popup_window.show_popup()\n            }\n        }\n    }\n\n    new_directory_popup_window := PopupNewDirectories {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    select_popup_window := PopupSelectResults {\n        property <length> x_offset: 0;\n        property <length> y_offset: 0;\n        x: parent.x + x_offset - self.item_width / 2.0;\n        y: parent.y + y_offset - self.all_items_height - 5px;\n        height: root.height;\n        width: root.width;\n        open_custom_select_popup() => {\n            custom_select_popup_window.show_popup();\n        }\n    }\n\n    custom_select_popup_window := PopupCustomSelect {\n        height: root.height;\n        width: root.width;\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    delete_popup_window := PopupDelete {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    move_popup_window := PopupMoveFolders {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    rename_extension_popup_window := PopupRenameBadExtensions {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    rename_bad_file_name_popup_window := PopupRenameBadFileNames {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    save_popup_window := PopupSave {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n    }\n\n    sort_popup_window := PopupSortResults {\n        property <length> x_offset: 0;\n        property <length> y_offset: 0;\n        x: parent.x + x_offset - self.item_width / 2.0;\n        y: parent.y + y_offset - self.all_items_height - 5px;\n        height: root.height;\n        width: root.width;\n    }\n\n    clean_popup_window := PopupCleanExif {\n        height: root.height;\n        width: root.width;\n        title_text: Translations.clean_text;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n\n        action_confirmed() => {\n            Callabler.clean_exif_items();\n        }\n    }\n\n    clean_cache_popup_window := PopupCleanCache {\n        height: root.height;\n        width: root.width;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n\n        start_cleaning() => {\n            Callabler.start_cache_cleaning();\n        }\n\n        stop_cleaning() => {\n            Callabler.stop_cache_cleaning();\n        }\n    }\n\n    crop_video_popup_window := PopupCropVideo {\n        height: root.height;\n        width: root.width;\n        title_text: Translations.crop_videos_text;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n\n        action_confirmed() => {\n            Callabler.crop_video_items();\n        }\n    }\n\n    reencode_video_popup_window := PopupReencodeVideo {\n        height: root.height;\n        width: root.width;\n        title_text: Translations.reencode_videos_text;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n\n        action_confirmed() => {\n            Callabler.reencode_video_items();\n        }\n    }\n\n    hardlink_popup_window := PopupActionConfirm {\n        height: root.height;\n        width: root.width;\n        title_text: Translations.hardlink_text;\n        confirmation_text: Translations.hardlink_confirmation_text;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n\n        action_confirmed => {\n            Callabler.hardlink_items();\n        }\n    }\n\n    softlink_popup_window := PopupActionConfirm {\n        height: root.height;\n        width: root.width;\n        title_text: Translations.softlink_text;\n        confirmation_text: Translations.softlink_confirmation_text;\n\n        x: parent.x + (root.width - self.popup_width) / 2.0;\n        y: parent.y + (parent.height - self.popup_height) / 2.0;\n\n        action_confirmed => {\n            Callabler.softlink_items();\n        }\n    }\n\n    if GuiState.file_dialog_open: Rectangle {\n        x: 0;\n        y: 0;\n        width: root.width;\n        height: root.height;\n        background: #808080cc;\n        z: 1000;\n\n        // Absorb all keyboard events so nothing behind the overlay can be triggered\n        fs := FocusScope {\n            x: 0; y: 0;\n            width: parent.width;\n            height: parent.height;\n            enabled: true;\n            key-pressed(event) => { EventResult.accept }\n            key-released(event) => { EventResult.accept }\n        }\n\n        // Absorb all pointer/mouse events so nothing behind the overlay can be clicked\n        TouchArea {\n            x: 0; y: 0;\n            width: parent.width;\n            height: parent.height;\n            mouse-cursor: MouseCursor.default;\n        }\n\n        Text {\n            width: parent.width;\n            height: parent.height;\n            text: Translations.file_dialog_open_text;\n            font-size: FontSizes.header;\n            color: #ffffff;\n            vertical-alignment: center;\n            horizontal-alignment: center;\n            wrap: TextWrap.word-wrap;\n        }\n\n        init => { fs.focus(); }\n    }\n\n    scan_ended(scan_text) => {\n        text_summary_text = scan_text;\n        root.scanning = false;\n        root.stop_requested = false;\n    }\n\n    processing_ended(process_text) => {\n        text_summary_text = process_text;\n        root.processing = false;\n        root.stop_requested = false;\n    }\n\n    reset_selection(active_tab) => {\n        main_list.reset_selection(active_tab);\n    }\n\n    initialize_popup_sizes => {\n        sort_popup_window.show_popup();\n        sort_popup_window.close_popup();\n        select_popup_window.show_popup();\n        select_popup_window.close_popup();\n    }\n\n    show_action_popup(request, data) => {\n        if (request == PopupRequest.Move) {\n            move_popup_window.folder_name = data;\n            move_popup_window.show_popup();\n        } else if (request == PopupRequest.Delete) {\n            delete_popup_window.show_popup();\n        } else if (request == PopupRequest.CleanExif) {\n            clean_popup_window.show_popup();\n        } else if (request == PopupRequest.OptimizeVideo) {\n            if (data == \"crop\") {\n                crop_video_popup_window.show_popup();\n            } else {\n                reencode_video_popup_window.show_popup();\n            }\n        } else if (request == PopupRequest.Symlink) {\n            softlink_popup_window.show_popup();\n        } else if (request == PopupRequest.RenameBadExtension) {\n            rename_extension_popup_window.show_popup();\n        } else if (request == PopupRequest.RenameBadFileName) {\n            rename_bad_file_name_popup_window.show_popup();\n        } else if (request == PopupRequest.Hardlink) {\n            hardlink_popup_window.show_popup();\n        } else if (request == PopupRequest.Save) {\n            save_popup_window.show_popup();\n        } else {\n            debug(\"Unsupported popup request\");\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_action_confirm.slint",
    "content": "import { PopupBase } from \"popup_base.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupActionConfirm inherits Rectangle {\n    in-out property <string> title_text;\n    in-out property <string> confirmation_text;\n    callback action_confirmed();\n\n    out property <length> popup_width: 350px;\n    out property <length> popup_height: 150px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> root.title_text;\n\n        VerticalLayout {\n            PopupCenteredText {\n                text <=> root.confirmation_text;\n            }\n        }\n\n        ok_clicked => {\n            root.action_confirmed();\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_base.slint",
    "content": "import { Button } from \"std-widgets.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { Translations } from \"translations.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\nexport component PopupBase inherits PopupWindow {\n    in-out property <string> title_text: \"TODO - needs to be changed\";\n    in-out property <string> ok_text <=> Translations.ok_button_text;\n    in-out property <string> cancel_text <=> Translations.cancel_button_text;\n    in-out property <bool> enabled_ok_button: true;\n\n    callback ok_clicked();\n    callback cancel_clicked();\n\n    close-policy: PopupClosePolicy.no-auto-close;\n    rect := Rectangle {\n        width: parent.width;\n        height: parent.height;\n        border-radius: 10px;\n        border-color: ColorPalette.popup_border_color;\n        border-width: 2px;\n        background: ColorPalette.popup_background;\n        clip: true;\n        VerticalLayout {\n            Rectangle {\n                background: ColorPalette.popup_background_title_line;\n\n                Text {\n                    vertical-stretch: 0.0;\n                    min-height: 30px;\n                    text <=> title_text;\n                    vertical-alignment: center;\n                    horizontal-alignment: center;\n                    font-size: FontSizes.normal;\n                }\n            }\n\n            @children\n\n            HorizontalLayout {\n                padding: 10px;\n                Button {\n                    enabled <=> enabled_ok_button;\n                    text <=> ok_text;\n                    primary: true;\n                    clicked => {\n                        root.close();\n                        ok_clicked();\n                    }\n                }\n\n                Rectangle { }\n\n                Button {\n                    text <=> cancel_text;\n                    clicked => {\n                        root.close();\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_centered_text.slint",
    "content": "import { FontSizes } from \"fonts.slint\";\n\nexport component PopupCenteredText inherits Text {\n    vertical-stretch: 1.0;\n    vertical-alignment: center;\n    horizontal-alignment: center;\n    font-size: FontSizes.normal;\n    wrap: TextWrap.word-wrap;\n}\n"
  },
  {
    "path": "krokiet/ui/popup_clean_cache.slint",
    "content": "import { Button, ProgressIndicator, ScrollView } from \"std-widgets.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { Translations } from \"translations.slint\";\nimport { FontSizes } from \"fonts.slint\";\nimport { GuiState } from \"gui_state.slint\";\n\nexport component PopupCleanCache inherits Rectangle {\n    callback show_popup();\n    callback start_cleaning();\n    callback stop_cleaning();\n\n    in-out property <bool> stopped_by_user: false;\n\n    out property <length> popup_width: 500px;\n    out property <length> popup_height: 300px;\n\n    popup_window := PopupWindow {\n        width: popup_width;\n        height: popup_height;\n        close-policy: PopupClosePolicy.no-auto-close;\n\n        Rectangle {\n            width: parent.width;\n            height: parent.height;\n            border-radius: 10px;\n            border-color: ColorPalette.popup_border_color;\n            border-width: 2px;\n            background: ColorPalette.popup_background;\n            clip: true;\n\n            VerticalLayout {\n                Rectangle {\n                    background: ColorPalette.popup_background_title_line;\n                    Text {\n                        vertical-stretch: 0.0;\n                        min-height: 30px;\n                        text <=> Translations.popup_clean_cache_title_text;\n                        vertical-alignment: center;\n                        horizontal-alignment: center;\n                        font-size: FontSizes.normal;\n                    }\n                }\n\n                ScrollView {\n                    VerticalLayout {\n                        padding: 15px;\n                        spacing: 10px;\n\n                        if !GuiState.cache_cleaning_is_cleaning && !GuiState.cache_cleaning_finished: VerticalLayout {\n                            spacing: 10px;\n                            Text {\n                                text <=> Translations.popup_clean_cache_confirmation_text;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                                font-size: FontSizes.normal;\n                                wrap: word-wrap;\n                            }\n                        }\n\n                        if GuiState.cache_cleaning_is_cleaning: VerticalLayout {\n                            spacing: 15px;\n\n                            Text {\n                                text: Translations.popup_clean_cache_progress_text + \" \" + GuiState.cache_cleaning_progress.current_cache_file + \"/\" + GuiState.cache_cleaning_progress.total_cache_files;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_progress.current_file_name != \"\": Text {\n                                text: Translations.popup_clean_cache_current_file_text + \" \" + GuiState.cache_cleaning_progress.current_file_name;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                                font-size: FontSizes.small;\n                                wrap: word-wrap;\n                            }\n\n                            VerticalLayout {\n                                spacing: 5px;\n                                Text {\n                                    text: Translations.popup_clean_cache_file_progress_text;\n                                    font-size: FontSizes.normal;\n                                }\n                                ProgressIndicator {\n                                    height: 10px;\n                                    progress: GuiState.cache_cleaning_progress.all_entries > 0 ? GuiState.cache_cleaning_progress.checked_entries / GuiState.cache_cleaning_progress.all_entries : 0.0;\n                                }\n                                Text {\n                                    text: GuiState.cache_cleaning_progress.checked_entries + \"/\" + GuiState.cache_cleaning_progress.all_entries;\n                                    font-size: FontSizes.small;\n                                    horizontal-alignment: TextHorizontalAlignment.center;\n                                }\n                            }\n\n                            VerticalLayout {\n                                spacing: 5px;\n                                Text {\n                                    text: Translations.popup_clean_cache_overall_progress_text;\n                                    font-size: FontSizes.normal;\n                                }\n                                ProgressIndicator {\n                                    height: 10px;\n                                    progress: GuiState.cache_cleaning_progress.total_cache_files > 0 ? GuiState.cache_cleaning_progress.current_cache_file / GuiState.cache_cleaning_progress.total_cache_files : 0.0;\n                                }\n                            }\n                        }\n\n                        if GuiState.cache_cleaning_finished: VerticalLayout {\n                            spacing: 10px;\n\n                            if root.stopped_by_user: Text {\n                                text <=> Translations.popup_clean_cache_stopped_by_user_text;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if !root.stopped_by_user: Text {\n                                text <=> Translations.popup_clean_cache_finished_text;\n                                horizontal-alignment: TextHorizontalAlignment.center;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_result.processed_files_text != \"\": Text {\n                                text: GuiState.cache_cleaning_result.processed_files_text;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_result.entries_stats_text != \"\": Text {\n                                text: GuiState.cache_cleaning_result.entries_stats_text;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_result.size_stats_text != \"\": Text {\n                                text: GuiState.cache_cleaning_result.size_stats_text;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_result.time_text != \"\": Text {\n                                text: GuiState.cache_cleaning_result.time_text;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_result.errors_count > 0: Text {\n                                text: Translations.popup_clean_cache_files_with_errors + \" \" + GuiState.cache_cleaning_result.errors_count;\n                                font-size: FontSizes.normal;\n                            }\n\n                            if GuiState.cache_cleaning_result.errors != \"\": VerticalLayout {\n                                spacing: 5px;\n                                Text {\n                                    text <=> Translations.popup_clean_cache_error_details_text;\n                                    font-size: FontSizes.normal;\n                                }\n                                Text {\n                                    text: GuiState.cache_cleaning_result.errors;\n                                    font-size: FontSizes.small;\n                                    wrap: word-wrap;\n                                }\n                            }\n                        }\n\n                        Rectangle { }\n                    }\n                }\n\n                HorizontalLayout {\n                    padding: 10px;\n                    spacing: 10px;\n\n                    if !GuiState.cache_cleaning_is_cleaning && !GuiState.cache_cleaning_finished: Button {\n                        text <=> Translations.clean_button_text;\n                        clicked => {\n                            GuiState.cache_cleaning_is_cleaning = true;\n                            GuiState.cache_cleaning_finished = false;\n                            root.stopped_by_user = false;\n                            root.start_cleaning();\n                        }\n                    }\n\n                    if GuiState.cache_cleaning_is_cleaning: Button {\n                        text <=> Translations.stop_text;\n                        clicked => {\n                            root.stopped_by_user = true;\n                            root.stop_cleaning();\n                        }\n                    }\n\n                    Rectangle { }\n\n                    Button {\n                        enabled: !GuiState.cache_cleaning_is_cleaning;\n                        text <=> Translations.cancel_button_text;\n                        clicked => {\n                            popup_window.close();\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    show_popup() => {\n        GuiState.cache_cleaning_is_cleaning = false;\n        GuiState.cache_cleaning_finished = false;\n        root.stopped_by_user = false;\n        GuiState.cache_cleaning_progress = {\n            current_cache_file: 0,\n            total_cache_files: 0,\n            current_file_name: \"\",\n            checked_entries: 0,\n            all_entries: 0,\n        };\n        GuiState.cache_cleaning_result = {\n            processed_files_text: \"\",\n            entries_stats_text: \"\",\n            size_stats_text: \"\",\n            time_text: \"\",\n            errors_count: 0,\n            errors: \"\",\n        };\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_clean_exif.slint",
    "content": "import { PopupBase } from \"popup_base.slint\";\nimport { Translations } from \"translations.slint\";\nimport { CheckBox } from \"std-widgets.slint\";\nimport { Settings } from \"settings.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupCleanExif inherits Rectangle {\n    in-out property <string> title_text;\n\n    callback action_confirmed();\n\n    out property <length> popup_width: 420px;\n    out property <length> popup_height: 200px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> root.title_text;\n\n        VerticalLayout {\n            spacing: 8px;\n\n            Rectangle { height: 8px; }\n\n            HorizontalLayout {\n                spacing: 0px;\n\n                Rectangle { width: 8px; }\n\n                VerticalLayout {\n                    spacing: 8px;\n\n                    PopupCenteredText { text: Translations.clean_confirmation_text; }\n\n                    CheckBox { text: Translations.clean_exif_overwrite_files_text; checked <=> Settings.popup_clean_exif_overwrite_files; }\n\n                    Rectangle { height: 10px; }\n                }\n\n                Rectangle { width: 8px; }\n            }\n        }\n\n        ok_clicked => {\n            root.action_confirmed();\n        }\n\n        cancel_clicked => {\n        }\n    }\n\n    show_popup() => { popup_window.show(); }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_crop_video.slint",
    "content": "import { PopupBase } from \"popup_base.slint\";\nimport { Translations } from \"translations.slint\";\nimport { CheckBox, ComboBox, Slider } from \"std-widgets.slint\";\nimport { Settings } from \"settings.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupCropVideo inherits Rectangle {\n    in-out property <string> title_text;\n\n    callback action_confirmed();\n\n    out property <length> popup_width: 420px;\n    out property <length> popup_height: 280px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> root.title_text;\n\n        VerticalLayout {\n            spacing: 8px;\n\n            Rectangle { height: 8px; }\n\n            HorizontalLayout {\n                spacing: 0px;\n\n                Rectangle { width: 8px; }\n\n                VerticalLayout {\n                    spacing: 8px;\n\n                    PopupCenteredText { text: Translations.crop_video_confirmation_text; }\n\n                    CheckBox { text: Translations.crop_reencode_video_text; checked <=> Settings.popup_crop_video_reencode; }\n\n                    HorizontalLayout {\n                        spacing: 5px;\n                        Text { text: Translations.subsettings_video_optimizer_video_codec_text; vertical_alignment: TextVerticalAlignment.center; }\n                        ComboBox {\n                            model <=> Settings.video_optimizer_sub_video_codec_config;\n                            current_index <=> Settings.video_optimizer_sub_video_codec_index;\n                            current_value <=> Settings.video_optimizer_sub_video_codec_value;\n                            enabled: Settings.popup_crop_video_reencode;\n                        }\n                    }\n\n                    HorizontalLayout {\n                        spacing: 5px;\n                        Text { text: Translations.subsettings_video_optimizer_video_quality_text; vertical_alignment: TextVerticalAlignment.center; }\n                        Slider {\n                            minimum: 0.0;\n                            maximum: 51.0;\n                            value <=> Settings.popup_crop_video_quality;\n                            enabled: Settings.popup_crop_video_reencode;\n                        }\n                        Text {\n                            text: Math.round(Settings.popup_crop_video_quality) + \" / 51\";\n                            vertical_alignment: TextVerticalAlignment.center;\n                            min-width: 50px;\n                        }\n                    }\n\n                    CheckBox { text: Translations.optimize_overwrite_files_text; checked <=> Settings.popup_crop_video_overwrite_files; }\n\n                    Rectangle { height: 10px; }\n                }\n\n                Rectangle { width: 8px; }\n            }\n        }\n\n        ok_clicked => {\n            root.action_confirmed();\n        }\n\n        cancel_clicked => {\n        }\n    }\n\n    show_popup() => { popup_window.show(); }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_custom_select.slint",
    "content": "import { Button, CheckBox, LineEdit, ScrollView } from \"std-widgets.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { FontSizes } from \"fonts.slint\";\nimport { ColumnType } from \"common.slint\";\nimport { GuiState } from \"gui_state.slint\";\n\nexport component PopupCustomSelect inherits Rectangle {\n    out property <length> popup_width: 620px;\n    out property <length> popup_height: 520px;\n    callback show_popup();\n    private property <bool> case_sensitive: false;\n    private property <bool> leave_one_in_group: true;\n\n    popup_window := PopupWindow {\n        width: popup_width;\n        height: popup_height;\n        close-policy: PopupClosePolicy.no-auto-close;\n        Rectangle {\n            width: parent.width;\n            height: parent.height;\n            border-radius: 10px;\n            border-color: ColorPalette.popup_border_color;\n            border-width: 2px;\n            background: ColorPalette.popup_background;\n            clip: true;\n            VerticalLayout {\n                // Title bar\n                Rectangle {\n                    background: ColorPalette.popup_background_title_line;\n                    height: 34px;\n                    Text {\n                        text: Translations.popup_custom_select_title_text;\n                        vertical-alignment: center;\n                        horizontal-alignment: center;\n                        font-size: FontSizes.normal;\n                    }\n                }\n                // Column names\n                HorizontalLayout {\n                    height: 24px;\n                    padding-left: 10px;\n                    padding-right: 10px;\n                    spacing: 4px;\n                    Rectangle { width: 32px; }\n                    Text {\n                        width: 180px;\n                        text: Translations.popup_custom_column_name_header_text;\n                        font-size: FontSizes.small;\n                        vertical-alignment: center;\n                        color: ColorPalette.hint_color;\n                    }\n                    Text {\n                        horizontal-stretch: 1.0;\n                        text: Translations.popup_custom_filter_value_header_text;\n                        font-size: FontSizes.small;\n                        vertical-alignment: center;\n                        color: ColorPalette.hint_color;\n                    }\n                }\n                // Scrollable column rows\n                ScrollView {\n                    vertical-stretch: 1.0;\n                    VerticalLayout {\n                        padding: 4px;\n                        spacing: 2px;\n                        for column[idx] in GuiState.custom_select_columns: HorizontalLayout {\n                            height: 30px;\n                            padding-left: 6px;\n                            padding-right: 6px;\n                            spacing: 4px;\n                            alignment: stretch;\n                            CheckBox {\n                                width: 32px;\n                                checked: column.enabled;\n                                toggled => {\n                                    Callabler.update_custom_select_column(idx, self.checked, column.filter_value);\n                                }\n                            }\n                            Text {\n                                width: 180px;\n                                text: column.column_name;\n                                vertical-alignment: center;\n                                font-size: FontSizes.normal;\n                                opacity: column.enabled ? 1.0 : 0.5;\n                            }\n                            LineEdit {\n                                horizontal-stretch: 1.0;\n                                enabled: column.enabled;\n                                text: column.filter_value;\n                                placeholder-text: column.column_type == ColumnType.Int\n                                    ? \">= 1024  or  < 512\"\n                                    : column.column_type == ColumnType.Date\n                                        ? \">= 2020-01-01 12:00 or 12:00 or < 30-12-2025\"\n                                        : column.column_type == ColumnType.FullPath\n                                            ? \"/home/user/*.rs  or  *backup*\"\n                                            : \"name*  or  *.rs\";\n                                edited(val) => {\n                                    Callabler.update_custom_select_column(idx, column.enabled, val);\n                                }\n                            }\n                        }\n                    }\n                }\n                // Alternative for non-implemented in slint popup hints\n                Rectangle {\n                    height: 84px;\n                    border-width: 1px;\n                    border-color: ColorPalette.popup_border_color;\n                    background: ColorPalette.popup_background_title_line;\n                    VerticalLayout {\n                        padding: 6px;\n                        spacing: 2px;\n                        Text {\n                            text: Translations.popup_custom_hint_str_text;\n                            font-size: FontSizes.small;\n                            color: ColorPalette.hint_color;\n                            wrap: word-wrap;\n                        }\n                        Text {\n                            text: Translations.popup_custom_hint_int_text;\n                            font-size: FontSizes.small;\n                            color: ColorPalette.hint_color;\n                            wrap: word-wrap;\n                        }\n                        Text {\n                            text: Translations.popup_custom_hint_date_text;\n                            font-size: FontSizes.small;\n                            color: ColorPalette.hint_color;\n                            wrap: word-wrap;\n                        }\n                    }\n                }\n                // All in group and case sensitive options\n                HorizontalLayout {\n                    height: 36px;\n                    padding-left: 10px;\n                    padding-right: 10px;\n                    spacing: 14px;\n                    alignment: start;\n                    CheckBox {\n                        text: Translations.popup_custom_case_sensitive_text;\n                        checked <=> case_sensitive;\n                    }\n                    if GuiState.tool_with_groups: CheckBox {\n                        text: Translations.popup_custom_leave_one_in_group_text;\n                        checked <=> leave_one_in_group;\n                    }\n                }\n                // Buttons\n                HorizontalLayout {\n                    height: 46px;\n                    padding: 8px;\n                    spacing: 8px;\n                    Button {\n                        text: Translations.popup_custom_select_button_text;\n                        primary: true;\n                        clicked => {\n                            Callabler.select_items_custom_columns(true, case_sensitive, leave_one_in_group);\n                            popup_window.close();\n                        }\n                    }\n                    Button {\n                        text: Translations.popup_custom_unselect_button_text;\n                        clicked => {\n                            Callabler.select_items_custom_columns(false, case_sensitive, leave_one_in_group);\n                            popup_window.close();\n                        }\n                    }\n                    Rectangle { horizontal-stretch: 1.0; }\n                    Button {\n                        text: Translations.cancel_button_text;\n                        clicked => { popup_window.close(); }\n                    }\n                }\n            }\n        }\n    }\n    show_popup() => {\n        case_sensitive = false;\n        leave_one_in_group = true;\n        Callabler.populate_custom_select_columns();\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_delete.slint",
    "content": "import { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { PopupBase } from \"popup_base.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupDelete inherits Rectangle {\n    out property <length> popup_width: 350px;\n    out property <length> popup_height: 150px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> Translations.delete_text;\n\n        VerticalLayout {\n            PopupCenteredText {\n                text <=> Translations.delete_confirmation_text;\n            }\n        }\n\n        ok_clicked => {\n            Callabler.delete_selected_items();\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_move_folders.slint",
    "content": "import { CheckBox } from \"std-widgets.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { PopupBase } from \"popup_base.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\nimport { Settings } from \"settings.slint\";\n\nexport component PopupMoveFolders inherits Rectangle {\n    out property <length> popup_width: 500px;\n    out property <length> popup_height: 150px;\n    in-out property <string> folder_name: \"\";\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> Translations.popup_move_title_text;\n\n        VerticalLayout {\n            PopupCenteredText {\n                text: Translations.move_confirmation_text + \"\\n\" + folder_name;\n            }\n\n            Rectangle {height: 5px;}\n\n            VerticalLayout {\n                HorizontalLayout {\n                    alignment: center;\n                    CheckBox {\n                        text <=> Translations.popup_move_copy_checkbox_text;\n                        checked <=> Settings.popup_move_copy_mode;\n                    }\n                }\n                Rectangle {height: 5px;}\n\n                HorizontalLayout {\n                    alignment: center;\n                    CheckBox {\n                        text <=> Translations.popup_move_preserve_folder_checkbox_text;\n                        checked <=> Settings.popup_move_preserve_folder_structure;\n                    }\n                }\n            }\n        }\n\n        ok_clicked => {\n            Callabler.move_items(folder_name);\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_new_directories.slint",
    "content": "import { TextEdit } from \"std-widgets.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { PopupBase } from \"popup_base.slint\";\nimport { GuiState } from \"gui_state.slint\";\n\nexport component PopupNewDirectories inherits Rectangle {\n    out property <length> popup_width: 350px;\n    out property <length> popup_height: 200px;\n    callback show_popup();\n\n    property <bool> included_paths;\n    private property <string> text_data;\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> Translations.popup_new_directories_title_text;\n        enabled_ok_button: text_data != \"\";\n\n        VerticalLayout {\n            TextEdit {\n                vertical-stretch: 1.0;\n                text <=> text_data;\n            }\n        }\n\n        ok_clicked => {\n            Callabler.added_manual_paths(GuiState.choosing_include_directories, text_data);\n        }\n    }\n\n    show_popup() => {\n        text_data = \"\";\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_optimize.slint",
    "content": "import { PopupBase } from \"popup_base.slint\";\nimport { Translations } from \"translations.slint\";\nimport { CheckBox, ComboBox, LineEdit, Slider } from \"std-widgets.slint\";\nimport { Settings } from \"settings.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupReencodeVideo inherits Rectangle {\n    in-out property <string> title_text;\n\n    callback action_confirmed();\n\n    out property <length> popup_width: 420px;\n    out property <length> popup_height: 300px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> root.title_text;\n\n        VerticalLayout {\n            spacing: 8px;\n\n            Rectangle { height: 8px; }\n\n            HorizontalLayout {\n                spacing: 0px;\n\n                Rectangle { width: 8px; }\n\n                VerticalLayout {\n                    spacing: 8px;\n\n                    PopupCenteredText { text: Translations.optimize_confirmation_text; }\n\n                    HorizontalLayout {\n                        spacing: 5px;\n                        Text { text: Translations.subsettings_video_optimizer_video_codec_text; vertical_alignment: TextVerticalAlignment.center; }\n                        ComboBox {\n                            model <=> Settings.video_optimizer_sub_video_codec_config;\n                            current_index <=> Settings.video_optimizer_sub_video_codec_index;\n                            current_value <=> Settings.video_optimizer_sub_video_codec_value;\n                        }\n                    }\n\n                    HorizontalLayout {\n                        spacing: 5px;\n                        Text { text: Translations.subsettings_video_optimizer_video_quality_text; vertical_alignment: TextVerticalAlignment.center; }\n                        Slider { minimum: 0.0; maximum: 51.0; value <=> Settings.popup_reencode_video_quality; }\n                        Text {\n                            text: Math.round(Settings.popup_reencode_video_quality) + \" / 51\";\n                            vertical_alignment: TextVerticalAlignment.center;\n                            min-width: 50px;\n                        }\n                    }\n\n                    CheckBox { text: Translations.optimize_fail_if_bigger_text; checked <=> Settings.popup_reencode_video_fail_if_bigger; }\n                    CheckBox { text: Translations.optimize_overwrite_files_text; checked <=> Settings.popup_reencode_video_overwrite_files; }\n\n                    CheckBox { text: Translations.optimize_limit_video_size_text; checked <=> Settings.popup_reencode_video_limit_video_size; }\n\n                    HorizontalLayout {\n                        spacing: 5px;\n                        Text { text: Translations.optimize_max_width_text; vertical_alignment: TextVerticalAlignment.center; }\n                        LineEdit { text <=> Settings.popup_reencode_video_max_width; enabled: Settings.popup_reencode_video_limit_video_size; }\n                        Text { text: Translations.optimize_max_height_text; vertical_alignment: TextVerticalAlignment.center; }\n                        LineEdit { text <=> Settings.popup_reencode_video_max_height; enabled: Settings.popup_reencode_video_limit_video_size; }\n                    }\n\n                    Rectangle { height: 10px; }\n                }\n\n                Rectangle { width: 8px; }\n            }\n        }\n\n        ok_clicked => {\n            root.action_confirmed();\n        }\n\n        cancel_clicked => {\n        }\n    }\n\n    show_popup() => { popup_window.show(); }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_rename_bad_extensions.slint",
    "content": "import { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { PopupBase } from \"popup_base.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupRenameBadExtensions inherits Rectangle {\n    out property <length> popup_width: 500px;\n    out property <length> popup_height: 150px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> Translations.popup_rename_title_text;\n\n        VerticalLayout {\n            PopupCenteredText {\n                text: Translations.rename_confirmation_text;\n            }\n        }\n\n        ok_clicked => {\n            Callabler.rename_files();\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_rename_bad_file_names.slint",
    "content": "import { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { PopupBase } from \"popup_base.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupRenameBadFileNames inherits Rectangle {\n    out property <length> popup_width: 500px;\n    out property <length> popup_height: 150px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> Translations.popup_rename_title_text;\n\n        VerticalLayout {\n            PopupCenteredText {\n                text: Translations.rename_confirmation_text;\n            }\n        }\n\n        ok_clicked => {\n            Callabler.rename_files();\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_save.slint",
    "content": "import { Callabler } from \"callabler.slint\";\nimport { Translations } from \"translations.slint\";\nimport { PopupBase } from \"popup_base.slint\";\nimport { PopupCenteredText } from \"popup_centered_text.slint\";\n\nexport component PopupSave inherits Rectangle {\n    out property <length> popup_width: 500px;\n    out property <length> popup_height: 150px;\n    callback show_popup();\n\n    popup_window := PopupBase {\n        width: popup_width;\n        height: popup_height;\n        title_text <=> Translations.popup_save_title_text;\n\n        VerticalLayout {\n            PopupCenteredText {\n                text: Translations.popup_save_message_text + \"\\n\\n\" + Translations.do_you_want_to_continue_text;\n            }\n        }\n\n        ok_clicked => {\n            Callabler.save_results();\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_select_results.slint",
    "content": "import { Button } from \"std-widgets.slint\";\nimport { SelectMode, SelectModel } from \"common.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { GuiState } from \"gui_state.slint\";\n\nexport component PopupSelectResults inherits Rectangle {\n    callback show_popup();\n    callback close_popup();\n    callback open_custom_select_popup();\n    property <[SelectModel]> model: GuiState.select_results_list;\n    property <length> item_height: 30px;\n    out property <length> item_width;\n    out property <length> all_items_height: item_height * model.length;\n\n    popup_window := PopupWindow {\n        width <=> item_width;\n        height: all_items_height;\n\n        close-policy: PopupClosePolicy.close-on-click-outside;\n        Rectangle {\n            width: parent.width;\n            height: parent.height;\n            border-radius: 5px;\n            background: ColorPalette.popup_background;\n            VerticalLayout {\n                for i in model: Button {\n                    text: i.name;\n                    height: item_height;\n                    clicked => {\n                        if (i.data == SelectMode.SelectCustom) {\n                            root.open_custom_select_popup();\n                        } else {\n                            Callabler.select_items(i.data);\n                        }\n                        popup_window.close();\n                    }\n                }\n            }\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n    close_popup() => {\n        popup_window.close();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/popup_sort.slint",
    "content": "import { Button } from \"std-widgets.slint\";\nimport { SortModel } from \"common.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { GuiState } from \"gui_state.slint\";\n\nexport component PopupSortResults inherits Rectangle {\n    callback show_popup();\n    callback close_popup();\n    property <[SortModel]> model: GuiState.sort_results_list;\n    property <length> item_height: 30px;\n    out property <length> item_width;\n    out property <length> all_items_height: item_height * model.length;\n\n    popup_window := PopupWindow {\n        width <=> item_width;\n        height: all_items_height;\n\n        close-policy: PopupClosePolicy.close-on-click-outside;\n        Rectangle {\n            width: parent.width;\n            height: parent.height;\n            border-radius: 5px;\n            background: ColorPalette.popup_background;\n            VerticalLayout {\n                for i in model: Button {\n                    text: i.name;\n                    height: item_height;\n                    clicked => {\n                        Callabler.sort_items(i.data);\n                        popup_window.close();\n                    }\n                }\n            }\n        }\n    }\n\n    show_popup() => {\n        popup_window.show();\n    }\n    close_popup() => {\n        popup_window.close();\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/preview.slint",
    "content": "export component Preview inherits Image {\n    image-rendering: ImageRendering.smooth;\n}\n"
  },
  {
    "path": "krokiet/ui/progress.slint",
    "content": "import { ProgressToSend } from \"common.slint\";\nimport { ProgressIndicator } from \"std-widgets.slint\";\nimport { Translations } from \"translations.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\nexport component Progress {\n    in-out property <ProgressToSend> progress_datas;\n    preferred-width: 400px;\n    preferred-height: 40px;\n    VerticalLayout {\n        padding-top: 5px;\n        Text {\n            font-size: FontSizes.normal;\n            text: progress-datas.step_name;\n            horizontal-alignment: TextHorizontalAlignment.center;\n        }\n\n        HorizontalLayout {\n            spacing: 5px;\n            VerticalLayout {\n                spacing: 5px;\n                Text {\n                    font-size: FontSizes.normal;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    text: Translations.stage_current_text;\n                }\n\n                Text {\n                    font-size: FontSizes.normal;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    text: Translations.stage_all_text;\n                }\n            }\n\n            VerticalLayout {\n                spacing: 5px;\n                VerticalLayout {\n                    alignment: LayoutAlignment.center;\n                    ProgressIndicator {\n                        visible: progress_datas.current_progress >= -0.001;\n                        height: 8px;\n                        progress: progress_datas.current_progress_size == -1 ? progress_datas.current_progress / 100.0 : progress_datas.current_progress_size / 100.0;\n                    }\n                }\n\n                VerticalLayout {\n                    alignment: LayoutAlignment.center;\n                    ProgressIndicator {\n                        height: 8px;\n                        progress: progress_datas.all_progress / 100.0;\n                    }\n                }\n            }\n\n            VerticalLayout {\n                visible: progress_datas.all_progress > -0.001;\n                spacing: 5px;\n                Text {\n                    font-size: FontSizes.normal;\n                    visible: progress_datas.current_progress >= -0.001;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    text: (progress_datas.current_progress_size == -1 ? progress_datas.current_progress : progress_datas.current_progress_size) + \"%\";\n                }\n\n                Text {\n                    font-size: FontSizes.normal;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    text: progress_datas.all_progress + \"%\";\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/selectable_tree_view.slint",
    "content": "import { CheckBox, ListView, ScrollView, VerticalBox } from \"std-widgets.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { SingleMainListModel, SortColumnMode } from \"common.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\nexport component SelectableTableView inherits Rectangle {\n    in property <[string]> columns;\n    in-out property <[SingleMainListModel]> values: [\n        {checked: false, selected_row: false, header_row: true, filled_header_row: false, val_str: [\"kropkarz\", \"/Xd1\", \"24.10.2023\"], val_int: []} ,\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"witasphere\", \"/Xd1/Imagerren2\", \"25.11.1991\"], val_int: []} ,\n        {checked: false, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"witasphere\", \"/Xd1/Imagerren2\", \"25.11.1991\"], val_int: []} ,\n        {checked: true, selected_row: false, header_row: false, filled_header_row: false, val_str: [\"lokkaler\", \"/Xd1/Vide2\", \"01.23.1911\"], val_int: []}\n    ];\n    in-out property <[length]> column_sizes: [30px, 80px, 150px, 160px];\n    private property <int> column_number: column_sizes.length + 1;\n    // This idx, starts from zero, but since first is always a checkbox, and is not in model.val values, remove 1 from idx\n    in-out property <int> parentPathIdx;\n    in-out property <int> fileNameIdx;\n    in-out property <int> previewImageIdx;\n    // Indexes into val_int for top-left crop and original dimensions\n    in-out property <int> topLeftCropIdx;\n    in-out property <int> originalWidthIdx: -1;\n    in-out property <int> originalHeightIdx: -1;\n    in-out property <int> start_shift_idx: -1;\n    in-out property <int> last_selected_idx: -1;\n    out property <length> item_height: 23px;\n    in-out property <SortColumnMode> sort_column_mode: SortColumnMode.None;\n    in-out property <int> sort_column_idx: -1;\n    in-out property <bool> sort_available: true;\n\n\n    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] + column_sizes[12]);\n\n    VerticalBox {\n        padding: 0px;\n        ScrollView {\n            height: 30px;\n            viewport-x <=> list_view.viewport-x;\n            vertical-stretch: 0;\n\n            HorizontalLayout {\n                spacing: 5px;\n                for title[idx] in root.columns: HorizontalLayout {\n                    width: root.column_sizes[idx];\n\n                    TouchArea {\n                        pointer-event(event) => {\n                            if (!sort_available) {\n                                return; // TODO - this probably should be relaxed - it should be available to sort, lists that are not processing e.g. during duplicate scan, sorting empty folders should be possible\n                            }\n                            if (event.button == PointerEventButton.left && event.kind == PointerEventKind.up) {\n                                // Clicked at header column, and already there was sorting in this column\n                                if (sort_column_idx == idx) {\n                                    if (sort_column_mode == SortColumnMode.Ascending || sort_column_mode == SortColumnMode.None) {\n                                        sort_column_mode = SortColumnMode.Descending;\n                                    } else {\n                                        sort_column_mode = SortColumnMode.Ascending;\n                                    }\n                                } else {\n                                    sort_column_mode = SortColumnMode.Descending;\n                                }\n\n                                sort_column_idx = idx;\n\n                                Callabler.change_sort_column_mode(sort_column_mode, idx);\n                            }\n                        }\n                        Text {\n                            width: parent.width;\n                            height: parent.height;\n\n                            overflow: elide;\n                            text: title;\n                            font-weight: FontSizes.bold_weight;\n                            font-size: FontSizes.normal;\n                        }\n                    }\n\n                    Rectangle {\n                        width: 1px;\n                        background: gray;\n                        TouchArea {\n                            forward-focus: focus_item;\n                            width: 8px;\n                            x: (parent.width - self.width) / 2;\n                            property <length> cached;\n                            pointer-event(event) => {\n                                if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {\n                                    self.cached = root.column_sizes[idx];\n                                }\n                            }\n                            moved => {\n                                if (self.pressed) {\n                                    root.column_sizes[idx] += (self.mouse-x - self.pressed-x);\n                                    if (root.column_sizes[idx] < 20px) {\n                                        root.column_sizes[idx] = 20px;\n                                    }\n                                }\n                            }\n                            mouse-cursor: ew-resize;\n                        }\n                    }\n                }\n            }\n        }\n\n        list_view := ListView {\n            min-width: 100px;\n            for r[idx] in root.values: Rectangle {\n                width: list_view_width;\n                border-radius: 5px;\n                height: item_height;\n                background: ColorPalette.get_listview_color_with_header(r.selected_row, touch-area.has-hover, r.header_row);\n                touch_area := TouchArea {\n                    forward-focus: focus_item;\n                    double-clicked => {\n                        if (contains_data(idx)) {\n                            Callabler.row_open_item_with_index(idx);\n                        }\n                    }\n                    pointer-event(event) => {\n                        if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) {\n                            if (contains_data(idx)) {\n                                Callabler.row_reverse_single_unique_item(idx);\n                                Callabler.row_open_parent_item_with_index(idx);\n                            }\n                        } else if (event.button == PointerEventButton.left && event.kind == PointerEventKind.up) {\n                            if (event.modifiers.control) {\n                                start_shift_idx = idx;\n                                Callabler.row_reverse_item_selection(idx);\n                                hidePreview();\n                            } else if (event.modifiers.shift) {\n                                if (start_shift_idx == -1) {\n                                    start_shift_idx = idx;\n                                }\n                                Callabler.row_select_items_with_shift(idx, start_shift_idx);\n\n                                if (start_shift_idx == idx) {\n                                    showPreview(idx);\n                                } else {\n                                    hidePreview();\n                                }\n                            } else {\n                                start_shift_idx = idx;\n                                Callabler.row_reverse_single_unique_item(idx);\n                                showPreview(idx);\n                            }\n\n                            last_selected_idx = idx;\n                        }\n                    }\n                }\n\n                HorizontalLayout {\n                    // Workaround for https://github.com/slint-ui/slint/issues/602\n                    // Alternative solution is to use TouchArea above, to steal clicks from checkbox, but it is more fishy solution\n                    CheckBox {\n                        property <bool> from_model: r.checked;\n                        visible: !r.header_row;\n                        checked: r.checked && !r.header_row;\n                        width: root.column_sizes[0];\n                        changed from-model => { self.checked = from-model; }\n                        toggled => {\n                            Callabler.change_number_of_checked_items(r.checked ? -1 : 1);\n                            r.checked = !r.checked;\n                        }\n                    }\n\n                    HorizontalLayout {\n                        spacing: 5px;\n                        for f[idx] in r.val_str: Text {\n                            width: root.column_sizes[idx + 1];\n                            text: f;\n                            font-size: FontSizes.normal;\n                            vertical-alignment: center;\n                            overflow: elide;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // Already rust code should deal with selections in the model, but not all state is available in rust code\n    public function reset_selection() {\n        start_shift_idx = -1;\n        last_selected_idx = -1;\n        hidePreview();\n    }\n\n    public function scan_started() {\n        sort_column_idx = -1;\n        sort_column_mode = SortColumnMode.None;\n        // We could here reset selections, but this is done in rust side already\n    }\n\n    function contains_data(idx: int) -> bool {\n        return root.values[idx].val_str.length > 0;\n    }\n\n    function position_to_item(idx: int) {\n        if (idx * item_height < -list_view.viewport-y) {\n            list_view.viewport-y = - (idx * item_height - 10px);\n        } else if ((idx + 1) * item_height > -list_view.viewport-y + list_view.height) {\n            list_view.viewport-y = - ((idx + 1) * item_height - list_view.height + 10px);\n        }\n    }\n\n    function get_number_of_items_to_skip() -> int {\n        list_view.height / item_height;\n    }\n\n    function jump_up(modifiers: KeyboardModifiers, items: int) {\n        if (last_selected_idx != -1) {\n            last_selected_idx = (last_selected_idx - items).max(0);\n            // If hit at header, try to skip it\n            if (!contains_data(last_selected_idx)) {\n                if (last_selected_idx == 0) {\n                    last_selected_idx = 1; // If 0 item don't have data, that means that it is header, so skip it\n                } else {\n                    last_selected_idx = (last_selected_idx - 1).max(0);\n                }\n            }\n\n            if (!modifiers.shift) {\n                start_shift_idx = last_selected_idx;\n            }\n\n            Callabler.row_select_items_with_shift(start_shift_idx, last_selected_idx);\n            if (start_shift_idx == last_selected_idx) {\n                showPreview(last_selected_idx);\n            } else {\n                hidePreview();\n            }\n\n            position_to_item(last_selected_idx);\n        }\n    }\n    function jump_down(modifiers: KeyboardModifiers, items: int) {\n        if (last_selected_idx != -1) {\n            last_selected_idx = (last_selected_idx + items).min(root.values.length - 1);\n            // If hit at header, try to skip it\n            if (!contains_data(last_selected_idx)) {\n                last_selected_idx = (last_selected_idx + 1).min(root.values.length - 1);\n            }\n\n            if (!modifiers.shift) {\n                start_shift_idx = last_selected_idx;\n            }\n\n            Callabler.row_select_items_with_shift(start_shift_idx, last_selected_idx);\n            if (start_shift_idx == last_selected_idx) {\n                showPreview(last_selected_idx);\n            } else {\n                hidePreview();\n            }\n\n            position_to_item(last_selected_idx);\n        }\n    }\n\n    public function released_key(event: KeyEvent) {\n        // debug(\"Key released: \" + event.text + \"  CTRL - \" + (event.modifiers.control ? \"true\" : \"false\") + \"  SHIFT - \" + (event.modifiers.shift ? \"true\" : \"false\") + \" ALT - \" + (event.modifiers.alt ? \"true\" : \"false\"));\n\n        if (event.text == \" \") {\n            Callabler.row_reverse_checked_selection();\n        } else if (event.text == \"\\n\") {\n           if (last_selected_idx != -1 && contains_data(last_selected_idx)) {\n                Callabler.row_open_item_with_index(last_selected_idx);\n           }\n        } else if (event.text == Key.UpArrow) {\n            jump_up(event.modifiers, 1);\n        } else if (event.text == Key.DownArrow) {\n            jump_down(event.modifiers, 1);\n        } else if (event.text == Key.PageDown) {\n            jump_down(event.modifiers, get_number_of_items_to_skip());\n        } else if (event.text == Key.PageUp) {\n            jump_up(event.modifiers, get_number_of_items_to_skip());\n        } else if (event.text == Key.Home) {\n            jump_up(event.modifiers, root.values.length);\n        } else if (event.text == Key.End) {\n            jump_down(event.modifiers, root.values.length);\n        }\n    }\n    public function pressed_key(event: KeyEvent) {\n        // debug(\"Key pressed: \" + event.text + \"  CTRL - \" + (event.modifiers.control ? \"true\" : \"false\") + \"  SHIFT - \" + (event.modifiers.shift ? \"true\" : \"false\") + \" ALT - \" + (event.modifiers.alt ? \"true\" : \"false\"));\n\n        if ((event.text == \"a\" || event.text == \"A\") && event.modifiers.control && !event.modifiers.shift && !event.modifiers.alt && !event.modifiers.meta) {\n            Callabler.row_select_all();\n        }\n    }\n\n    function showPreview(idx: int) {\n        if (root.previewImageIdx != -1) {\n            if (root.topLeftCropIdx == -1) {\n                Callabler.load_image_preview(root.values[idx].val_str[root.previewImageIdx], -1, -1, -1, -1, -1, -1);\n            } else {\n                Callabler.load_image_preview(\n                    root.values[idx].val_str[root.previewImageIdx],\n                    root.values[idx].val_int[root.topLeftCropIdx],\n                    root.values[idx].val_int[root.topLeftCropIdx+1],\n                    root.values[idx].val_int[root.topLeftCropIdx+2],\n                    root.values[idx].val_int[root.topLeftCropIdx+3],\n                    root.values[idx].val_int[root.originalWidthIdx],\n                    root.values[idx].val_int[root.originalHeightIdx],\n                );\n            }\n        } else {\n            if (root.topLeftCropIdx == -1) {\n                Callabler.load_image_preview(root.values[idx].val_str[root.parentPathIdx] + \"/\" + root.values[idx].val_str[root.fileNameIdx], -1, -1, -1, -1, -1, -1);\n            } else {\n                Callabler.load_image_preview(\n                    root.values[idx].val_str[root.parentPathIdx] + \"/\" + root.values[idx].val_str[root.fileNameIdx],\n                    root.values[idx].val_int[root.topLeftCropIdx],\n                    root.values[idx].val_int[root.topLeftCropIdx+1],\n                    root.values[idx].val_int[root.topLeftCropIdx+2],\n                    root.values[idx].val_int[root.topLeftCropIdx+3],\n                    root.values[idx].val_int[root.originalWidthIdx],\n                    root.values[idx].val_int[root.originalHeightIdx],\n                );\n            }\n        }\n    }\n\n    function hidePreview() {\n        Callabler.load_image_preview(\"NOT_AVAILABLE.NOT_AVAILABLE\", -1, -1, -1, -1, -1, -1);\n    }\n\n    focus_item := FocusScope {\n        // TODO hack works and not steal first click anymore, but key-released event is not working with it\n        // width: 0px; // Hack to not steal first click from other components - https://github.com/slint-ui/slint/issues/3503\n        // Hack not works https://github.com/slint-ui/slint/issues/3503#issuecomment-1817809834 because disables key-released event\n\n\n        key-released(event) => {\n            if (!self.visible || !self.has-focus) {\n                return accept;\n            }\n            released_key(event);\n            accept\n        }\n\n        key-pressed(event) => {\n            if (!self.visible || !self.has-focus) {\n                return accept;\n            }\n            pressed_key(event);\n            accept\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/settings.slint",
    "content": "import { ExcludedPathsModel, IncludedPathsModel } from \"common.slint\";\n\nexport global Settings {\n    in-out property <int> settings_preset_idx: 0;\n    in-out property <[string]> settings_presets: [\"Preset 1\", \"Preset 2\"];\n\n    in-out property <[string]> languages_list: [\"English\", \"Polski (Polish)\", \"Français (French)\", \"Italiano (Italian)\", \"Русский (Russian)\", \"український (Ukrainian)\", \"한국어 (Korean)\", \"Česky (Czech)\", \"Deutsch (German)\", \"日本語 (Japanese)\", \"Português (Portuguese)\", \"Português Brasileiro (Brazilian Portuguese)\", \"简体中文 (Simplified Chinese)\", \"繁體中文 (Traditional Chinese)\", \"Español (Spanish)\", \"Norsk (Norwegian)\", \"Svenska (Swedish)\", \"العربية (Arabic)\", \"Български (Bulgarian)\", \"Ελληνικά (Greek)\", \"Nederlands (Dutch)\", \"Română (Romanian)\", \"Türkçe (Turkish)\"];\n    in-out property <int> language_index: 0;\n    in-out property <string> language_value: \"English\";\n\n    in-out property <[IncludedPathsModel]> included_paths_model: [{ path: \"/home/pathssssssssssssssssssssss\", referenced_path: false, selected_row: false },{ path: \"/home/path\", referenced_path: false, selected_row: false },{ path: \"/home/path\", referenced_path: false, selected_row: false },{ path: \"/home/path\", referenced_path: false, selected_row: false }];\n    in-out property <int> included_paths_model_selected_idx: -1;\n    in-out property <[ExcludedPathsModel]> excluded_paths_model: [{ path:\"/home/pathssssssssssssssssssssssss\", selected_row: false },{ path:\"/home/path\", selected_row: false },{ path:\"/home/path\", selected_row: false },{ path:\"/home/path\", selected_row: false }, { path:\"/home/a\", selected_row: false }];\n    in-out property <int> excluded_paths_model_selected_idx: -1;\n\n    // Settings\n    in-out property <bool> dark_theme: true;\n    in-out property <bool> show_only_icons: false;\n    in-out property <bool> load_windows_size_at_startup: true;\n    in-out property <bool> load_tabs_sizes_at_startup: true;\n    in-out property <bool> limit_messages_lines: true;\n    in-out property <float> manual_application_scale: 1.0;\n    in-out property <bool> use_manual_application_scale: false;\n    in-out property <string> excluded_items: \"Excluded items\";\n    in-out property <string> allowed_extensions: \"Allowed extensions\";\n    in-out property <string> excluded_extensions: \"Excluded extensions\";\n    in-out property <string> minimum_file_size: 0;\n    in-out property <string> maximum_file_size: 0;\n    in-out property <bool> recursive_search: true;\n    in-out property <bool> use_cache: false;\n    in-out property <bool> save_as_json: false;\n    in-out property <bool> move_to_trash: false;\n    in-out property <bool> ignore_other_filesystems: false;\n    in-out property <bool> delete_outdated_cache_entries: false;\n    in-out property <bool> hide_hard_links: false;\n    in-out property <float> thread_number: 4;\n\n    in-out property <bool> duplicate_image_preview;\n    in-out property <bool> duplicate_use_prehash;\n    in-out property <string> duplicate_minimal_hash_cache_size;\n    in-out property <string> duplicate_minimal_prehash_cache_size;\n    in-out property <bool> similar_images_show_image_preview;\n\n    in-out property <bool> video_thumbnails_preview;\n    in-out property <bool> video_thumbnails_unused_thumbnails;\n\n\n    // Video Thumbnails Global Settings\n    in-out property <bool> video_thumbnails_generate: false;\n    in-out property <float> video_thumbnails_percentage: 10;\n    in-out property <float> video_thumbnails_percentage_max: 99;\n    in-out property <float> video_thumbnails_percentage_min: 1;\n    in-out property <bool> video_thumbnails_generate_grid: false;\n    in-out property <float> video_thumbnails_grid_tiles_per_side: 2;\n    in-out property <float> video_thumbnails_grid_tiles_per_side_max: 6;\n    in-out property <float> video_thumbnails_grid_tiles_per_side_min: 2;\n\n    // Allowed subsettings\n    // Similar Images\n    in-out property <[string]> similar_images_sub_available_hash_size: [\"8\", \"16\", \"32\", \"64\"];\n    in-out property <int> similar_images_sub_hash_size_index: 1;\n    in-out property <string> similar_images_sub_hash_size_value: \"16\";\n    in-out property <[string]> similar_images_sub_available_resize_algorithm: [\"Lanczos3\", \"Gaussian\", \"CatmullRom\", \"Triangle\", \"Nearest\"];\n    in-out property <int> similar_images_sub_resize_algorithm_index: 0;\n    in-out property <string> similar_images_sub_resize_algorithm_value: \"Lanczos3\";\n    in-out property <[string]> similar_images_sub_available_hash_type: [\"Mean\", \"Gradient\", \"BlockHash\", \"VertGradient\", \"DoubleGradient\", \"Median\"];\n    in-out property <int> similar_images_sub_hash_alg_index: 10;\n    in-out property <string> similar_images_sub_hash_alg_value: \"Gradient\";\n    in-out property <float> similar_images_sub_max_similarity: 40;\n    in-out property <float> similar_images_sub_current_similarity: 20;\n    in-out property <bool> similar_images_sub_ignore_same_size: false;\n\n    // Duplicates\n    in-out property <[string]> duplicates_sub_check_method: [\"Hash\", \"Size\", \"Name\", \"Size and Name\"];\n    in-out property <int> duplicates_sub_check_method_index: 0;\n    in-out property <string> duplicates_sub_check_method_value: \"Hash\";\n    in-out property <[string]> duplicates_sub_available_hash_type: [\"Blake3\", \"CRC32\", \"XXH3\"];\n    in-out property <int> duplicates_sub_available_hash_type_index: 0;\n    in-out property <string> duplicates_sub_available_hash_type_value: \"Blake3\";\n    in-out property <bool> duplicates_sub_name_case_sensitive: false;\n\n    // Big files\n    in-out property <[string]> biggest_files_sub_method: [\"The Biggest\", \"The Smallest\"];\n    in-out property <int> biggest_files_sub_method_index: 0;\n    in-out property <string> biggest_files_sub_method_value: \"The Biggest\";\n    in-out property <string> biggest_files_sub_number_of_files: 50;\n\n    // Similar Videos\n    in-out property <bool> similar_videos_sub_ignore_same_size;\n    in-out property <float> similar_videos_sub_max_similarity: 20;\n    in-out property <float> similar_videos_sub_current_similarity: 15;\n\n    in-out property <float> similar_videos_skip_forward_amount: 15;\n    in-out property <float> similar_videos_skip_forward_amount_max: 360;\n    in-out property <float> similar_videos_skip_forward_amount_min: 1;\n\n    in-out property <float> similar_videos_vid_hash_duration: 10;\n    in-out property <float> similar_videos_vid_hash_duration_max: 60;\n    in-out property <float> similar_videos_vid_hash_duration_min: 1;\n\n    in-out property <[string]> similar_videos_crop_detect: [\"LetterBox\", \"Motion\", \"None\"];\n    in-out property <string> similar_videos_crop_detect_value: \"letterbox\";\n    in-out property <int> similar_videos_crop_detect_index: 0;\n\n\n    // Same Music\n    in-out property <[string]> similar_music_sub_audio_check_type: [\"Tags\", \"Fingerprint\"];\n    in-out property <int> similar_music_sub_audio_check_type_index: 0;\n    in-out property <string> similar_music_sub_audio_check_type_value: \"Tags\";\n    in-out property <bool> similar_music_sub_approximate_comparison;\n    in-out property <bool> similar_music_sub_title: true;\n    in-out property <bool> similar_music_sub_artist: true;\n    in-out property <bool> similar_music_sub_year: false;\n    in-out property <bool> similar_music_sub_bitrate: false;\n    in-out property <bool> similar_music_sub_genre: false;\n    in-out property <bool> similar_music_sub_length: false;\n    in-out property <bool> similar_music_compare_fingerprints_only_with_similar_titles: false;\n    in-out property <float> similar_music_sub_minimal_fragment_duration_value: 5.0;\n    in-out property <float> similar_music_sub_minimal_fragment_duration_max: 180.0;\n    in-out property <float> similar_music_sub_maximum_difference_value: 3.0;\n    in-out property <float> similar_music_sub_maximum_difference_max: 10.0;\n\n    // Broken Files\n    in-out property <bool> broken_files_sub_audio: true;\n    in-out property <bool> broken_files_sub_pdf: false;\n    in-out property <bool> broken_files_sub_archive: false;\n    in-out property <bool> broken_files_sub_image: false;\n    in-out property <bool> broken_files_sub_video: false;\n\n    // Bad Names\n    in-out property <bool> bad_names_sub_uppercase_extension: true;\n    in-out property <bool> bad_names_sub_emoji_used: true;\n    in-out property <bool> bad_names_sub_space_at_start_end: true;\n    in-out property <bool> bad_names_sub_non_ascii: true;\n    in-out property <bool> bad_names_sub_restricted_charset_enabled: false;\n    in-out property <string> bad_names_sub_restricted_charset: \"_- \";\n    in-out property <bool> bad_names_sub_remove_duplicated: false;\n\n    // Video Optimizer\n    in-out property <[string]> video_optimizer_sub_mode: [\"Crop\", \"Transcode\"];\n    in-out property <int> video_optimizer_sub_mode_index: 1;\n    in-out property <string> video_optimizer_sub_mode_value: \"Transcode\";\n    in-out property <[string]> video_optimizer_sub_crop_type: [\"Black Bars\", \"Static Content\"];\n    in-out property <int> video_optimizer_sub_crop_type_index: 0;\n    in-out property <string> video_optimizer_sub_crop_type_value: \"Black Bars\";\n\n    // Video Crop scanning parameters\n    in-out property <string> video_optimizer_sub_black_pixel_threshold: \"20\";\n    in-out property <string> video_optimizer_sub_black_bar_min_percentage: \"90\";\n    in-out property <string> video_optimizer_sub_max_samples: \"60\";\n    in-out property <string> video_optimizer_sub_min_crop_size: \"20\";\n\n    in-out property <string> video_optimizer_sub_excluded_codecs: \"h265,hevc,av1,vp9\";\n    in-out property <float> video_optimizer_sub_video_quality: 23;\n    in-out property <float> video_optimizer_sub_video_quality_max: 51;\n    in-out property <float> video_optimizer_sub_video_quality_min: 0;\n    in-out property <float> video_optimizer_sub_image_threshold: 5;\n    in-out property <float> video_optimizer_sub_image_threshold_max: 255;\n    in-out property <float> video_optimizer_sub_image_threshold_min: 1;\n\n    // ComboBox model/value for optimizer codec (initialized from Rust)\n    in-out property <[string]> video_optimizer_sub_video_codec_config: [\"HEVC/H265\", \"H264\", \"VP9\", \"AV1\"];\n    in-out property <int> video_optimizer_sub_video_codec_index: 0;\n    in-out property <string> video_optimizer_sub_video_codec_value: \"h265\";\n\n    // Fail/overwrite/quality settings for optimizer (initialized from Rust)\n    in-out property <bool> video_optimizer_sub_fail_if_bigger: false;\n    in-out property <bool> video_optimizer_sub_overwrite_files: false;\n\n    // Video size limiting\n    in-out property <bool> video_optimizer_sub_limit_video_size: false;\n    in-out property <string> video_optimizer_sub_max_width: \"1920\";\n    in-out property <string> video_optimizer_sub_max_height: \"1920\";\n\n    // Exif Finder\n    in-out property <string> ignored_exif_tags: \"\";\n\n    // Move/Copy popup settings\n    in-out property <bool> popup_move_preserve_folder_structure: false;\n    in-out property <bool> popup_move_copy_mode: false;\n\n    // Clean EXIF popup settings\n    in-out property <bool> popup_clean_exif_overwrite_files: false;\n\n    // Reencode video popup settings\n    in-out property <bool> popup_reencode_video_overwrite_files: false;\n    in-out property <float> popup_reencode_video_quality: 23;\n    in-out property <bool> popup_reencode_video_fail_if_bigger: false;\n    in-out property <bool> popup_reencode_video_limit_video_size: false;\n    in-out property <string> popup_reencode_video_max_width: \"1920\";\n    in-out property <string> popup_reencode_video_max_height: \"1920\";\n\n    // Crop video popup settings\n    in-out property <bool> popup_crop_video_overwrite_files: false;\n    in-out property <bool> popup_crop_video_reencode: false;\n    in-out property <float> popup_crop_video_quality: 23;\n\n    // Audio\n    in-out property <bool> play_audio_on_scan_completion: true;\n\n    out property <length> path_px: 350px;\n    out property <length> name_px: 100px;\n    out property <length> mod_px: 125px;\n    out property <length> size_px: 75px;\n\n    in-out property <[string]> duplicates_column_name: [\"Selection\", \"Size\", \"File Name\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> duplicates_column_size: [35px, size_px, name_px, path_px, mod_px];\n    in-out property <[string]> empty_folders_column_name: [\"Selection\", \"Folder Name\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> empty_folders_column_size: [35px, name_px, path_px, mod_px];\n    in-out property <[string]> big_files_column_name: [\"Selection\", \"Size\", \"File Name\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> big_files_column_size: [35px, size_px, name_px, path_px, mod_px];\n    in-out property <[string]> empty_files_column_name: [\"Selection\", \"File Name\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> empty_files_column_size: [35px, name_px, path_px, mod_px];\n    in-out property <[string]> temporary_files_column_name: [\"Selection\", \"File Name\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> temporary_files_column_size: [35px, name_px, path_px, mod_px];\n    in-out property <[string]> similar_images_column_name: [\"Selection\", \"Similarity\", \"Size\", \"Dimensions\", \"File Name\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> similar_images_column_size: [35px, 80px, 80px, 80px, name_px, path_px, mod_px];\n    in-out property <[string]> similar_videos_column_name: [\"Selection\", \"Size\", \"File Name\", \"Path\", \"Dimensions\", \"Duration\", \"Bitrate\", \"Fps\", \"Codec\", \"Modification Date\"];\n    in-out property <[length]> similar_videos_column_size: [35px, size_px, name_px, path_px, 80px, 30px, 30px, 80px, 80px, mod_px];\n    in-out property <[string]> similar_music_column_name: [\"Selection\", \"Size\", \"File Name\", \"Title\", \"Artist\", \"Year\", \"Bitrate\", \"Length\", \"Genre\", \"Path\", \"Modification Date\"];\n    in-out property <[length]> similar_music_column_size: [35px, size_px, name_px, 80px, 80px, 80px, 80px, 80px, 80px, path_px, mod_px];\n    in-out property <[string]> invalid_symlink_column_name: [\"Selection\", \"Symlink Name\", \"Symlink Folder\", \"Destination Path\", \"Modification Date\"];\n    in-out property <[length]> invalid_symlink_column_size: [35px, name_px, path_px, path_px, mod_px];\n    in-out property <[string]> broken_files_column_name: [\"Selection\", \"File Name\", \"Path\", \"Type of Error\", \"Size\", \"Modification Date\"];\n    in-out property <[length]> broken_files_column_size: [35px, name_px, path_px, 200px, size_px, mod_px];\n    in-out property <[string]> bad_extensions_column_name: [\"Selection\", \"File Name\", \"Path\", \"Current Extension\", \"Proper Extension\"];\n    in-out property <[length]> bad_extensions_column_size: [35px, name_px, path_px, 40px, 200px];\n    in-out property <[string]> bad_names_column_name: [\"Selection\", \"File Name\", \"New Name\", \"Path\"];\n    in-out property <[length]> bad_names_column_size: [35px, name_px, 300px, path_px];\n    in-out property <[string]> exif_remover_column_name: [\"Selection\", \"Size\", \"File Name\", \"Path\", \"EXIF Tags\", \"Modification Date\"];\n    in-out property <[length]> exif_remover_column_size: [35px, size_px, name_px, path_px, 300px, mod_px];\n    in-out property <[string]> video_optimizer_column_name: [\"Selection\", \"Size\", \"File Name\", \"Path\", \"Codec\", \"Dimensions\", \"New Dimensions\", \"Modification Date\"];\n    in-out property <[length]> video_optimizer_column_size: [35px, size_px, name_px, path_px, 100px, 120px, 160px, mod_px];\n}\n"
  },
  {
    "path": "krokiet/ui/settings_list.slint",
    "content": "import { Button, CheckBox, ComboBox, LineEdit, ScrollView, Slider } from \"std-widgets.slint\";\nimport { Settings } from \"settings.slint\";\nimport { Callabler } from \"callabler.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { Translations } from \"translations.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\n// TODO use Spinbox instead LineEdit {} to be able to set only numbers\n\nglobal SettingsSize {\n    out property <length> item_height: 30px;\n}\n\nexport component TextComponent inherits HorizontalLayout {\n    in-out property <string> model;\n    in property <string> name;\n    spacing: 5px;\n    Text {\n        font-size: FontSizes.normal;\n        horizontal-stretch: 0.0;\n        vertical-alignment: TextVerticalAlignment.center;\n        text: name;\n    }\n\n    LineEdit {\n        horizontal-stretch: 1.0;\n        height: SettingsSize.item_height;\n        text <=> model;\n        font-size: FontSizes.normal;\n    }\n}\n\ncomponent CheckBoxComponent inherits HorizontalLayout {\n    in-out property <bool> model;\n    in property <string> name;\n    in-out property <bool> enabled: true;\n    callback toggled();\n    spacing: 5px;\n    CheckBox {\n        horizontal-stretch: 1.0;\n        height: SettingsSize.item_height;\n        checked <=> model;\n        enabled <=> enabled;\n        text: name;\n        toggled => {\n            root.toggled();\n        }\n    }\n    Rectangle { }\n}\n\ncomponent ThreadSliderComponent inherits HorizontalLayout {\n    in-out property <float> minimum_number;\n    in-out property <float> maximum_number;\n    in-out property <string> name;\n    spacing: 5px;\n\n    callback changed();\n    Text {\n        text <=> name;\n        vertical-alignment: TextVerticalAlignment.center;\n        height: SettingsSize.item_height;\n        font-size: FontSizes.normal;\n    }\n\n    slider := Slider {\n        enabled: true;\n        height: SettingsSize.item_height;\n        minimum: minimum_number;\n        maximum <=> maximum_number;\n        value <=> Settings.thread_number;\n        changed => root.changed();\n    }\n\n    Text {\n        height: SettingsSize.item_height;\n        vertical-alignment: TextVerticalAlignment.center;\n        text: round(slider.value) == 0 ? (\"All (\" + GuiState.maximum_threads + \"/\" + GuiState.maximum_threads + \")\") : (round(slider.value) + \"/\" + GuiState.maximum_threads);\n        font-size: FontSizes.normal;\n    }\n}\n\ncomponent ApplicationScaleComponent inherits HorizontalLayout {\n    spacing: 5px;\n    in-out property <string> name;\n    callback changed();\n    Text {\n        text <=> name;\n        vertical-alignment: TextVerticalAlignment.center;\n        height: SettingsSize.item_height;\n        font-size: FontSizes.normal;\n    }\n    slider := Slider {\n        minimum: 0.5;\n        maximum: 3.0;\n        step: 0.01;\n        value <=> Settings.manual_application_scale;\n        height: SettingsSize.item_height;\n        changed => root.changed();\n    }\n    Text {\n        height: SettingsSize.item_height;\n        vertical-alignment: TextVerticalAlignment.center;\n        text: (round(slider.value * 100.0) / 100.0);\n        font-size: FontSizes.normal;\n    }\n}\n\ncomponent MinMaxSizeComponent inherits HorizontalLayout {\n    spacing: 20px;\n    Text {\n        horizontal-stretch: 0.0;\n        text <=> Translations.settings_file_size_text;\n        vertical-alignment: TextVerticalAlignment.center;\n        font-size: FontSizes.normal;\n    }\n\n    HorizontalLayout {\n        spacing: 5px;\n        horizontal-stretch: 1.0;\n        Text {\n            text <=> Translations.settings_minimum_file_size_text;\n            vertical-alignment: TextVerticalAlignment.center;\n            font-size: FontSizes.normal;\n        }\n\n        LineEdit {\n            height: SettingsSize.item_height;\n            text <=> Settings.minimum_file_size;\n            font-size: FontSizes.normal;\n        }\n\n        Text {\n            text <=> Translations.settings_maximum_file_size_text;\n            vertical-alignment: TextVerticalAlignment.center;\n            font-size: FontSizes.normal;\n        }\n\n        LineEdit {\n            height: SettingsSize.item_height;\n            text <=> Settings.maximum_file_size;\n            font-size: FontSizes.normal;\n        }\n    }\n}\n\ncomponent Presets inherits Rectangle {\n    property <bool> edit_name;\n    property <string> current_index;\n    if !edit_name: HorizontalLayout {\n        spacing: 5px;\n        Text {\n            text <=> Translations.settings_current_preset_text;\n            vertical-alignment: TextVerticalAlignment.center;\n            font-size: FontSizes.normal;\n        }\n\n        combo_box := ComboBox {\n            current-index <=> Settings.settings_preset_idx;\n            model: Settings.settings_presets;\n            selected(item) => {\n                Settings.settings_preset_idx = self.current_index;\n                Callabler.changed_settings_preset();\n            }\n        }\n\n        Button {\n            text <=> Translations.settings_edit_name_text;\n            clicked => {\n                root.edit_name = !root.edit_name;\n            }\n        }\n    }\n    if edit_name: HorizontalLayout {\n        spacing: 5px;\n        Text {\n            text: Translations.settings_choose_name_for_prefix_text + (Settings.settings_preset_idx + 1);\n            vertical-alignment: TextVerticalAlignment.center;\n            font-size: FontSizes.normal;\n        }\n\n        current_name := LineEdit {\n            text: Settings.settings_presets[Settings.settings_preset_idx];\n            font-size: FontSizes.normal;\n        }\n\n        Button {\n            text <=> Translations.settings_save_text;\n            clicked => {\n                Settings.settings_presets[Settings.settings_preset_idx] = current_name.text;\n                edit_name = false;\n            }\n        }\n    }\n}\n\ncomponent HintText inherits Text {\n    in-out property <string> hint_text;\n    font-size: FontSizes.small;\n    color: ColorPalette.hint_color;\n    text: root.hint_text;\n    wrap: word-wrap;\n}\n\ncomponent HeaderText inherits Text {\n    font-size: FontSizes.header;\n    height: SettingsSize.item_height;\n    horizontal-alignment: TextHorizontalAlignment.center;\n    vertical-alignment: TextVerticalAlignment.center;\n}\n\ncomponent ConfigCacheButtons inherits HorizontalLayout {\n    spacing: 20px;\n    Button {\n        text <=> Translations.settings_open_config_folder_text;\n        clicked => {\n            Callabler.open_config_folder();\n        }\n    }\n\n    Button {\n        text <=> Translations.settings_open_cache_folder_text;\n        clicked => {\n            Callabler.open_cache_folder();\n        }\n    }\n}\n\ncomponent Languages inherits HorizontalLayout {\n    spacing: 5px;\n    Text {\n        text <=> Translations.settings_language_text;\n        vertical-alignment: TextVerticalAlignment.center;\n        font-size: FontSizes.normal;\n    }\n\n    combo_box := ComboBox {\n        current_index <=> Settings.language_index;\n        model <=> Settings.languages_list;\n        current-value <=> Settings.language_value;\n\n        selected(item) => {\n            Callabler.changed_language();\n        }\n    }\n}\n\nexport component SettingsList inherits VerticalLayout {\n    callback show_clean_cache_popup();\n\n    preferred-height: 300px;\n    preferred-width: 400px;\n\n    in-out property <bool> restart_required_global;\n    in-out property <bool> restart_required;\n\n    Text {\n        text <=> Translations.settings_settings_text;\n        height: SettingsSize.item_height;\n        horizontal-alignment: TextHorizontalAlignment.center;\n        font-size: FontSizes.title;\n    }\n\n    ScrollView {\n        VerticalLayout {\n            padding-right: 15px;\n            padding-bottom: 10px;\n            spacing: 5px;\n\n            HeaderText {\n                text <=> Translations.settings_global_settings_text;\n            }\n\n            Presets {\n                height: SettingsSize.item_height;\n            }\n            Languages {\n                height: SettingsSize.item_height;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_dark_theme_text;\n                model <=> Settings.dark_theme;\n                toggled => {\n                    Callabler.theme_changed();\n                }   \n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_show_only_icons_text;\n                model <=> Settings.show_only_icons;\n            }\n    \n            CheckBoxComponent {\n                name <=> Translations.settings_load_windows_size_at_startup_text;\n                model <=> Settings.load_windows_size_at_startup;\n            }\n            \n            CheckBoxComponent {\n                name <=> Translations.settings_load_tabs_sizes_at_startup_text;\n                model <=> Settings.load_tabs_sizes_at_startup;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_limit_lines_of_messages_text;\n                model <=> Settings.limit_messages_lines;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_use_manual_application_scale_text;\n                model <=> Settings.use_manual_application_scale;\n                toggled => { restart_required_global = true; }\n            }\n\n            HintText {\n                hint_text <=> Translations.settings_application_scale_hint_text;\n            }\n\n            ApplicationScaleComponent {\n                name <=> Translations.settings_application_scale_text;\n                changed => { restart_required_global = true; }\n            }\n\n            if restart_required_global: Text {\n                text <=> Translations.settings_restart_required_scale_text;\n                horizontal-alignment: TextHorizontalAlignment.center;\n                font-size: FontSizes.normal;\n            }\n\n            CheckBoxComponent {\n                enabled <=> GuiState.audio_feature_enabled;\n                name <=> Translations.settings_play_audio_on_scan_completion_text;\n                model <=> Settings.play_audio_on_scan_completion;\n            }\n\n            if !GuiState.audio_feature_enabled: HintText {\n                hint_text: Translations.settings_audio_feature_hint_text;\n            }\n\n            if GuiState.audio_feature_enabled: HintText {\n                hint_text: Translations.settings_audio_env_variable_hint_text;\n            }\n\n            HeaderText {\n                text <=> Translations.settings_general_settings_text;\n            }\n\n            TextComponent {\n                name <=> Translations.settings_excluded_items_text;\n                model <=> Settings.excluded_items;\n            }\n\n            TextComponent {\n                name <=> Translations.settings_allowed_extensions_text;\n                model <=> Settings.allowed_extensions;\n            }\n\n            TextComponent {\n                name <=> Translations.settings_excluded_extensions_text;\n                model <=> Settings.excluded_extensions;\n            }\n\n            MinMaxSizeComponent { }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_recursive_search_text;\n                model <=> Settings.recursive_search;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_use_cache_text;\n                model <=> Settings.use_cache;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_save_as_json_text;\n                model <=> Settings.save_as_json;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_move_to_trash_text;\n                model <=> Settings.move_to_trash;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_ignore_other_filesystems_text;\n                model <=> Settings.ignore_other_filesystems;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_delete_outdated_cache_entries_text;\n                model <=> Settings.delete_outdated_cache_entries;\n            }\n\n            HintText {\n                hint_text <=> Translations.settings_delete_outdated_cache_entries_hint_text;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_hide_hard_links_text;\n                model <=> Settings.hide_hard_links;\n            }\n\n            HintText {\n                hint_text <=> Translations.settings_hide_hard_links_hint_text;\n            }\n\n            ThreadSliderComponent {\n                name <=> Translations.settings_thread_number_text;\n                maximum_number <=> GuiState.maximum_threads;\n                changed => {\n                    restart_required = true;\n                }\n            }\n\n            if restart_required: Text {\n                text <=> Translations.settings_restart_required_text;\n                horizontal-alignment: TextHorizontalAlignment.center;\n                font-size: FontSizes.normal;\n            }\n            HeaderText {\n                text <=> Translations.tool_duplicate_files_text;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_duplicate_image_preview_text;\n                model <=> Settings.duplicate_image_preview;\n            }\n\n\n            TextComponent {\n                name <=> Translations.settings_duplicate_minimal_hash_cache_size_text;\n                model <=> Settings.duplicate_minimal_hash_cache_size;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_duplicate_use_prehash_text;\n                model <=> Settings.duplicate_use_prehash;\n            }\n\n            TextComponent {\n                name <=> Translations.settings_duplicate_minimal_prehash_cache_size_text;\n                model <=> Settings.duplicate_minimal_prehash_cache_size;\n            }\n\n\n            HeaderText {\n                text <=> Translations.settings_similar_images_tool_text;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_similar_images_show_image_preview_text;\n                model <=> Settings.similar_images_show_image_preview;\n            }\n\n\n\n            HeaderText {\n                text <=> Translations.settings_similar_videos_tool_text;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_similar_videos_preview_text;\n                model <=> Settings.video_thumbnails_preview;\n            }\n\n            HintText {\n                hint_text <=> Translations.settings_similar_videos_preview_hint_text;\n            }\n\n\n            HeaderText {\n                text <=> Translations.settings_video_thumbnails_header_text;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_video_thumbnails_generate_text;\n                model <=> Settings.video_thumbnails_generate;\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_video_thumbnails_clear_unused_thumbnails_text;\n                model <=> Settings.video_thumbnails_unused_thumbnails;\n            }\n\n            HorizontalLayout {\n                spacing: 5px;\n                Text {\n                    text <=> Translations.settings_video_thumbnails_position_text;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    height: SettingsSize.item_height;\n                    font-size: FontSizes.normal;\n                }\n                Slider {\n                    height: SettingsSize.item_height;\n                    minimum <=> Settings.video_thumbnails_percentage_min;\n                    maximum <=> Settings.video_thumbnails_percentage_max;\n                    value <=> Settings.video_thumbnails_percentage;\n                }\n                Text {\n                    text: \"(\" + round(Settings.video_thumbnails_percentage) + \"%)\";\n                    vertical-alignment: TextVerticalAlignment.center;\n                    height: SettingsSize.item_height;\n                    font-size: FontSizes.normal;\n                    min-width: 60px;\n                }\n            }\n\n            CheckBoxComponent {\n                name <=> Translations.settings_video_thumbnails_generate_grid_text;\n                model <=> Settings.video_thumbnails_generate_grid;\n            }\n\n            HorizontalLayout {\n                spacing: 5px;\n                Text {\n                    text <=> Translations.settings_video_thumbnails_generate_grid_hint_text;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    font-size: FontSizes.small;\n                    color: ColorPalette.hint_color;\n                    wrap: TextWrap.word-wrap;\n                }\n            }\n\n            HorizontalLayout {\n                spacing: 5px;\n                Text {\n                    text <=> Translations.settings_video_thumbnails_grid_tiles_per_side_text;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    font-size: FontSizes.normal;\n                }\n\n                Slider {\n                    minimum <=> Settings.video_thumbnails_grid_tiles_per_side_min;\n                    maximum <=> Settings.video_thumbnails_grid_tiles_per_side_max;\n                    value <=> Settings.video_thumbnails_grid_tiles_per_side;\n                }\n\n                Text {\n                    text: round(Settings.video_thumbnails_grid_tiles_per_side);\n                    vertical-alignment: TextVerticalAlignment.center;\n                    font-size: FontSizes.normal;\n                    min-width: 60px;\n                }\n            }\n\n            HorizontalLayout {\n                spacing: 5px;\n                Text {\n                    text <=> Translations.settings_video_thumbnails_grid_tiles_per_side_hint_text;\n                    vertical-alignment: TextVerticalAlignment.center;\n                    font-size: FontSizes.small;\n                    color: ColorPalette.hint_color;\n                    wrap: TextWrap.word-wrap;\n                }\n            }\n\n            HeaderText {\n                text <=> Translations.settings_cache_header_text;\n            }\n\n            Button {\n                text <=> Translations.settings_clean_cache_button_text;\n                clicked => {\n                    root.show_clean_cache_popup();\n                }\n            }\n\n            if Translations.settings_cache_number_size_text != \"\": HeaderText {\n                text <=> Translations.settings_cache_number_size_text;\n            }\n            if Translations.settings_video_thumbnails_number_size_text != \"\": HeaderText {\n                text <=> Translations.settings_video_thumbnails_number_size_text;\n            }\n            if Translations.settings_log_number_size_text != \"\": HeaderText {\n                text <=> Translations.settings_log_number_size_text;\n            }\n\n            ConfigCacheButtons { }\n        }\n    }\n\n    HorizontalLayout {\n        spacing: 5px;\n        Button {\n            text <=> Translations.settings_save_text;\n            clicked => {\n                Callabler.save_current_preset();\n            }\n        }\n\n        Button {\n            text <=> Translations.settings_load_text;\n            clicked => {\n                Callabler.load_current_preset();\n            }\n        }\n\n        Button {\n            text <=> Translations.settings_reset_text;\n            clicked => {\n                Callabler.reset_current_preset();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/tool_settings.slint",
    "content": "import { Button, CheckBox, ComboBox, LineEdit, ScrollView, Slider } from \"std-widgets.slint\";\nimport { ActiveTab } from \"common.slint\";\nimport { Settings } from \"settings.slint\";\nimport { GuiState } from \"gui_state.slint\";\nimport { TextComponent } from \"settings_list.slint\";\nimport { Translations } from \"translations.slint\";\nimport { ColorPalette } from \"color_palette.slint\";\nimport { FontSizes } from \"fonts.slint\";\n\ncomponent ComboBoxWrapper inherits HorizontalLayout {\n    in-out property <string> text;\n    in-out property <[string]> model;\n    in-out property <int> current_index;\n    in-out property <string> current_value;\n    spacing: 5px;\n    Text {\n        text <=> root.text;\n        vertical_alignment: TextVerticalAlignment.center;\n        font-size: FontSizes.normal;\n    }\n\n    ComboBox {\n        model: root.model;\n        current_index <=> root.current_index;\n        current_value <=> root.current_value;\n    }\n}\n\ncomponent CheckBoxWrapper inherits CheckBox { }\n\ncomponent SubsettingsHeader inherits Text {\n    text: Translations.subsettings_text;\n    font-size: FontSizes.normal;\n    height: 30px;\n}\n\n// Reusable text wrapper to centralize font-size and height\ncomponent LabelText inherits Text {\n    in-out property <string> label_text;\n    in-out property <length> label_font_size: FontSizes.normal;\n    in-out property <length> label_height: 25px;\n    text: root.label_text;\n    font-size: root.label_font_size;\n    height: root.label_height;\n    vertical-alignment: TextVerticalAlignment.center;\n}\n\ncomponent SliderWrapper inherits HorizontalLayout {\n    in-out property <float> maximum;\n    in-out property <float> minimum: 0;\n    in-out property <float> value;\n    in-out property <string> text;\n    in-out property <string> end_text;\n    in-out property <length> end_text_size;\n    spacing: 5px;\n    Text {\n        text: root.text;\n        font-size: FontSizes.normal;\n    }\n\n    Slider {\n        min-width: 30px;\n        minimum <=> root.minimum;\n        maximum <=> root.maximum;\n        value <=> root.value;\n    }\n\n    Text {\n        text: root.end_text;\n        width: root.end_text_size;\n        font-size: FontSizes.normal;\n    }\n}\n\ncomponent HintText inherits Text {\n    in-out property <string> hint_text;\n    font-size: FontSizes.small;\n    color: ColorPalette.hint_color;\n    text: root.hint_text;\n    wrap: TextWrap.word-wrap;\n}\n\nexport component ToolSettings {\n    ScrollView {\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.SimilarImages;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_images_hash_size_text;\n                model: Settings.similar_images_sub_available_hash_size;\n                current_index <=> Settings.similar_images_sub_hash_size_index;\n                current_value <=> Settings.similar_images_sub_hash_size_value;\n            }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_images_resize_algorithm_text;\n                model: Settings.similar_images_sub_available_resize_algorithm;\n                current_index <=> Settings.similar_images_sub_resize_algorithm_index;\n                current_value <=> Settings.similar_images_sub_resize_algorithm_value;\n            }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_images_duplicates_hash_type_text;\n                model: Settings.similar_images_sub_available_hash_type;\n                current_index <=> Settings.similar_images_sub_hash_alg_index;\n                current_value <=> Settings.similar_images_sub_hash_alg_value;\n            }\n\n            Rectangle {\n                height: 0px;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_images_ignore_same_size_text;\n                checked <=> Settings.similar_images_sub_ignore_same_size;\n            }\n\n            Rectangle {\n                height: 4px;\n            }\n\n            SliderWrapper {\n                text: Translations.subsettings_images_max_difference_text;\n                end_text: \"(\" + round(Settings.similar_images_sub_current_similarity) + \"/\" + round(Settings.similar_images_sub_max_similarity) + \")\";\n                end_text_size: 45px;\n                maximum <=> Settings.similar_images_sub_max_similarity;\n                value <=> Settings.similar_images_sub_current_similarity;\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.DuplicateFiles;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_duplicates_check_method_text;\n                model: Settings.duplicates_sub_check_method;\n                current_index <=> Settings.duplicates_sub_check_method_index;\n                current_value <=> Settings.duplicates_sub_check_method_value;\n            }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_images_duplicates_hash_type_text;\n                model: Settings.duplicates_sub_available_hash_type;\n                current_index <=> Settings.duplicates_sub_available_hash_type_index;\n                current_value <=> Settings.duplicates_sub_available_hash_type_value;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_duplicates_name_case_sensitive_text;\n                checked <=> Settings.duplicates_sub_name_case_sensitive;\n                height: 25px;\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.BigFiles;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_biggest_files_sub_method_text;\n                model: Settings.biggest_files_sub_method;\n                current_index <=> Settings.biggest_files_sub_method_index;\n                current_value <=> Settings.biggest_files_sub_method_value;\n            }\n\n            TextComponent {\n                name: Translations.subsettings_biggest_files_sub_number_of_files_text;\n                model <=> Settings.biggest_files_sub_number_of_files;\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.SimilarVideos;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n            CheckBoxWrapper {\n                text: Translations.subsettings_videos_ignore_same_size_text;\n                checked <=> Settings.similar_videos_sub_ignore_same_size;\n            }\n\n            Rectangle {\n                height: 0px;\n            }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_videos_crop_detect_text;\n                model: Settings.similar_videos_crop_detect;\n                current_index <=> Settings.similar_videos_crop_detect_index;\n                current_value <=> Settings.similar_videos_crop_detect_value;\n            }\n\n            Rectangle {\n                height: 0px;\n            }\n\n            SliderWrapper {\n                text: Translations.subsettings_videos_max_difference_text;\n                end_text: \"(\" + round(Settings.similar_videos_sub_current_similarity) + \"/\" + round(Settings.similar_videos_sub_max_similarity) + \")\";\n                end_text_size: 45px;\n                maximum <=> Settings.similar_videos_sub_max_similarity;\n                value <=> Settings.similar_videos_sub_current_similarity;\n            }\n\n            SliderWrapper {\n                text: Translations.subsettings_videos_skip_forward_amount_text;\n                end_text: \"(\" + round(Settings.similar_videos_skip_forward_amount) + \"/\" + round(Settings.similar_videos_skip_forward_amount_max) + \")\";\n                end_text_size: 60px;\n                maximum <=> Settings.similar_videos_skip_forward_amount_max;\n                minimum <=> Settings.similar_videos_skip_forward_amount_min;\n                value <=> Settings.similar_videos_skip_forward_amount;\n            }\n\n            SliderWrapper {\n                text: Translations.subsettings_videos_vid_hash_duration_text;\n                end_text: \"(\" + round(Settings.similar_videos_vid_hash_duration) + \"/\" + round(Settings.similar_videos_vid_hash_duration_max) + \")\";\n                end_text_size: 45px;\n                maximum <=> Settings.similar_videos_vid_hash_duration_max;\n                minimum <=> Settings.similar_videos_vid_hash_duration_min;\n                value <=> Settings.similar_videos_vid_hash_duration;\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.SimilarMusic;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_music_audio_check_type_text;\n                model: Settings.similar_music_sub_audio_check_type;\n                current_index <=> Settings.similar_music_sub_audio_check_type_index;\n                current_value <=> Settings.similar_music_sub_audio_check_type_value;\n            }\n            if Settings.similar_music_sub_audio_check_type_index == 0: VerticalLayout {\n                spacing: 5px;\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_approximate_comparison_text;\n                    checked <=> Settings.similar_music_sub_approximate_comparison;\n                    height: 40px;\n                }\n\n                LabelText { label_text: Translations.subsettings_music_compared_tags_text + \":\"; label_height: 20px; }\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_title_text;\n                    checked <=> Settings.similar_music_sub_title;\n                }\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_artist_text;\n                    checked <=> Settings.similar_music_sub_artist;\n                }\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_bitrate_text;\n                    checked <=> Settings.similar_music_sub_bitrate;\n                }\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_genre_text;\n                    checked <=> Settings.similar_music_sub_genre;\n                }\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_year_text;\n                    checked <=> Settings.similar_music_sub_year;\n                }\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_length_text;\n                    checked <=> Settings.similar_music_sub_length;\n                }\n\n                Rectangle {}\n            }\n            if Settings.similar_music_sub_audio_check_type_index == 1: VerticalLayout {\n                spacing: 5px;\n\n                CheckBoxWrapper {\n                    text: Translations.subsettings_music_compare_fingerprints_only_with_similar_titles_text;\n                    checked <=> Settings.similar_music_compare_fingerprints_only_with_similar_titles;\n                    height: 40px;\n                }\n                SliderWrapper {\n                    text: Translations.subsettings_music_max_difference_text;\n                    end_text: \"(\" + round(Settings.similar_music_sub_maximum_difference_value) + \"/\" + round(Settings.similar_music_sub_maximum_difference_max) + \")\";\n                    end_text_size: 45px;\n                    maximum <=> Settings.similar_music_sub_maximum_difference_max;\n                    value <=> Settings.similar_music_sub_maximum_difference_value;\n                }\n\n                SliderWrapper {\n                    text: Translations.subsettings_music_minimal_fragment_duration_text;\n                    end_text: round(Settings.similar_music_sub_minimal_fragment_duration_value);\n                    end_text_size: 45px;\n                    maximum <=> Settings.similar_music_sub_minimal_fragment_duration_max;\n                    value <=> Settings.similar_music_sub_minimal_fragment_duration_value;\n                }\n            }\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.BrokenFiles;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            LabelText { label_text: Translations.subsettings_broken_files_type_text; }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_broken_files_audio_text;\n                checked <=> Settings.broken_files_sub_audio;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_broken_files_pdf_text;\n                checked <=> Settings.broken_files_sub_pdf;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_broken_files_archive_text;\n                checked <=> Settings.broken_files_sub_archive;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_broken_files_image_text;\n                checked <=> Settings.broken_files_sub_image;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_broken_files_video_text;\n                checked <=> Settings.broken_files_sub_video;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_broken_files_video_info_text;\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.BadNames;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            LabelText { label_text: Translations.subsettings_bad_names_issues_text; }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_bad_names_uppercase_extension_text;\n                checked <=> Settings.bad_names_sub_uppercase_extension;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_bad_names_uppercase_extension_hint_text;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_bad_names_emoji_used_text;\n                checked <=> Settings.bad_names_sub_emoji_used;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_bad_names_emoji_used_hint_text;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_bad_names_space_at_start_end_text;\n                checked <=> Settings.bad_names_sub_space_at_start_end;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_bad_names_space_at_start_end_hint_text;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_bad_names_non_ascii_text;\n                checked <=> Settings.bad_names_sub_non_ascii;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_bad_names_non_ascii_hint_text;\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_bad_names_restricted_charset_text;\n                checked <=> Settings.bad_names_sub_restricted_charset_enabled;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_bad_names_restricted_charset_hint_text;\n            }\n\n            HorizontalLayout {\n                visible: Settings.bad_names_sub_restricted_charset_enabled;\n                spacing: 5px;\n                Text {\n                    text: Translations.subsettings_bad_names_allowed_chars_text;\n                    vertical-alignment: center;\n                }\n                LineEdit {\n                    text <=> Settings.bad_names_sub_restricted_charset;\n                    placeholder-text: \"_- \";\n                }\n            }\n\n            CheckBoxWrapper {\n                text: Translations.subsettings_bad_names_remove_duplicated_text;\n                checked <=> Settings.bad_names_sub_remove_duplicated;\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_bad_names_remove_duplicated_hint_text;\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.VideoOptimizer;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            ComboBoxWrapper {\n                text: Translations.subsettings_video_optimizer_mode_text;\n                model: Settings.video_optimizer_sub_mode;\n                current_index <=> Settings.video_optimizer_sub_mode_index;\n                current_value <=> Settings.video_optimizer_sub_mode_value;\n            }\n\n            if Settings.video_optimizer_sub_mode_index == 1: VerticalLayout {\n                spacing: 5px;\n\n                HorizontalLayout {\n                    spacing: 5px;\n                    Text {\n                        text: Translations.subsettings_video_optimizer_excluded_codecs_text;\n                        vertical_alignment: TextVerticalAlignment.center;\n                        font-size: FontSizes.normal;\n                    }\n\n                    LineEdit {\n                        text <=> Settings.video_optimizer_sub_excluded_codecs;\n                        font-size: FontSizes.normal;\n                    }\n\n                    Button {\n                        text: Translations.subsettings_reset_text;\n                        clicked => {\n                            Settings.video_optimizer_sub_excluded_codecs = \"h265,av1,vp9\";\n                        }\n                    }\n                }\n            }\n\n            if Settings.video_optimizer_sub_mode_index == 0: VerticalLayout {\n                spacing: 5px;\n\n                ComboBoxWrapper {\n                    text: Translations.subsettings_video_optimizer_crop_type_text;\n                    model: Settings.video_optimizer_sub_crop_type;\n                    current_index <=> Settings.video_optimizer_sub_crop_type_index;\n                    current_value <=> Settings.video_optimizer_sub_crop_type_value;\n                }\n\n                HorizontalLayout {\n                    spacing: 5px;\n                    Text {\n                        text: Translations.subsettings_video_optimizer_black_pixel_threshold_text;\n                        vertical_alignment: TextVerticalAlignment.center;\n                        font-size: FontSizes.normal;\n                    }\n\n                    LineEdit {\n                        text <=> Settings.video_optimizer_sub_black_pixel_threshold;\n                        font-size: FontSizes.normal;\n                    }\n\n                    Button {\n                        text: Translations.subsettings_reset_text;\n                        clicked => {\n                            Settings.video_optimizer_sub_black_pixel_threshold = \"20\";\n                        }\n                    }\n                }\n\n                HintText {\n                    hint_text: Translations.subsettings_video_optimizer_black_pixel_threshold_hint_text;\n                }\n\n                HorizontalLayout {\n                    spacing: 5px;\n                    Text {\n                        text: Translations.subsettings_video_optimizer_black_bar_min_percentage_text;\n                        vertical_alignment: TextVerticalAlignment.center;\n                        font-size: FontSizes.normal;\n                    }\n\n                    LineEdit {\n                        text <=> Settings.video_optimizer_sub_black_bar_min_percentage;\n                        font-size: FontSizes.normal;\n                    }\n\n                Button {\n                    text: Translations.subsettings_reset_text;\n                    clicked => {\n                        Settings.video_optimizer_sub_black_bar_min_percentage = \"90\";\n                    }\n                }\n                }\n\n                HintText {\n                    hint_text: Translations.subsettings_video_optimizer_black_bar_min_percentage_hint_text;\n                }\n\n                HorizontalLayout {\n                    spacing: 5px;\n                    Text {\n                        text: Translations.subsettings_video_optimizer_max_samples_text;\n                        vertical_alignment: TextVerticalAlignment.center;\n                        font-size: FontSizes.normal;\n                    }\n\n                    LineEdit {\n                        text <=> Settings.video_optimizer_sub_max_samples;\n                        font-size: FontSizes.normal;\n                    }\n\n                    Button {\n                        text: Translations.subsettings_reset_text;\n                        clicked => {\n                            Settings.video_optimizer_sub_max_samples = \"60\";\n                        }\n                    }\n                }\n\n                HintText {\n                    hint_text: Translations.subsettings_video_optimizer_max_samples_hint_text;\n                }\n\n                HorizontalLayout {\n                    spacing: 5px;\n                    Text {\n                        text: Translations.subsettings_video_optimizer_min_crop_size_text;\n                        vertical_alignment: TextVerticalAlignment.center;\n                        font-size: FontSizes.normal;\n                    }\n\n                    LineEdit {\n                        text <=> Settings.video_optimizer_sub_min_crop_size;\n                        font-size: FontSizes.normal;\n                    }\n\n                    Button {\n                        text: Translations.subsettings_reset_text;\n                        clicked => {\n                            Settings.video_optimizer_sub_min_crop_size = \"20\";\n                        }\n                    }\n                }\n\n                HintText {\n                    hint_text: Translations.subsettings_video_optimizer_min_crop_size_hint_text;\n                }\n            }\n\n            Rectangle { }\n        }\n\n        VerticalLayout {\n            visible: GuiState.active_tab == ActiveTab.ExifRemover;\n            spacing: 5px;\n            padding: 10px;\n            SubsettingsHeader { }\n\n            HorizontalLayout {\n                spacing: 5px;\n                Text {\n                    text: Translations.subsettings_exif_ignored_tags_text;\n                    vertical_alignment: TextVerticalAlignment.center;\n                    font-size: FontSizes.normal;\n                }\n\n                LineEdit {\n                    text <=> Settings.ignored_exif_tags;\n                    font-size: FontSizes.normal;\n                }\n            }\n\n            HintText {\n                hint_text: Translations.subsettings_exif_ignored_tags_hint_text;\n            }\n\n            Rectangle { }\n        }\n    }\n}\n"
  },
  {
    "path": "krokiet/ui/translations.slint",
    "content": "export global Translations {\n    in-out property <string> ok_button_text: \"Ok\";\n    in-out property <string> cancel_button_text: \"Cancel\";\n    in-out property <string> do_you_want_to_continue_text: \"Do you want to continue?\";\n    \n    // Main window\n    in-out property <string> main_window_title_text: \"Krokiet - Data Cleaner\";\n    in-out property <string> file_dialog_open_text: \"Close the dialog window to continue\";\n\n    // Bottom buttons\n    in-out property <string> scan_button_text: \"Scan\";\n    in-out property <string> stop_button_text: \"Stop\";\n    in-out property <string> select_button_text: \"Select\";\n    in-out property <string> move_button_text: \"Move\";\n    in-out property <string> delete_button_text: \"Delete\";\n    in-out property <string> save_button_text: \"Save\";\n    in-out property <string> sort_button_text: \"Sort\";\n    in-out property <string> rename_button_text: \"Rename\";\n    in-out property <string> optimize_button_text: \"Optimize\";\n    in-out property <string> clean_button_text: \"Clean\";\n    in-out property <string> hardlink_button_text: \"Hardlink\";\n    in-out property <string> softlink_button_text: \"Softlink\";\n\n    // About\n    in-out property <string> motto_text: \"This program is free to use and will always be.\\nSee the The MIT/GPL License for details.\";\n    in-out property <string> unicorn_text: \"You may not look at unicorn, but unicorn always looks at you.\";\n    in-out property <string> repository_text: \"Repository\";\n    in-out property <string> instruction_text: \"Instruction\";\n    in-out property <string> donation_text: \"Donation\";\n    in-out property <string> translation_text: \"Translation\";\n\n    // Bottom items\n    in-out property <string> included_paths_text: \"Included Paths\";\n    in-out property <string> excluded_paths_text: \"Excluded Paths\";\n    in-out property <string> ref_text: \"Ref\";\n    in-out property <string> path_text: \"Path\";\n\n    // Tools\n    in-out property <string> tool_duplicate_files_text: \"Duplicate Files\";\n    in-out property <string> tool_empty_folders_text: \"Empty Folders\";\n    in-out property <string> tool_big_files_text: \"Big Files\";\n    in-out property <string> tool_empty_files_text: \"Empty Files\";\n    in-out property <string> tool_temporary_files_text: \"Temporary Files\";\n    in-out property <string> tool_similar_images_text: \"Similar Images\";\n    in-out property <string> tool_similar_videos_text: \"Similar Videos\";\n    in-out property <string> tool_music_duplicates_text: \"Music Duplicates\";\n    in-out property <string> tool_invalid_symlinks_text: \"Invalid Symlinks\";\n    in-out property <string> tool_broken_files_text: \"Broken Files\";\n    in-out property <string> tool_bad_extensions_text: \"Bad Extensions\";\n    in-out property <string> tool_exif_remover_text: \"EXIF Finder\";\n    in-out property <string> tool_video_optimizer_text: \"Video Optimizer\";\n    in-out property <string> tool_bad_names_text: \"Bad Names\";\n\n    // Sorting\n    in-out property <string> sort_by_full_name_text: \"Sort by full name\";\n    in-out property <string> sort_by_selection_text: \"Sort by selection\";\n    in-out property <string> sort_reverse_text: \"Reverse sort\";\n\n    // Selection\n    in-out property <string> selection_all_text: \"Select all\";\n    in-out property <string> selection_deselect_all_text: \"Unselect all\";\n    in-out property <string> popup_custom_select_title_text: \"Custom Select / Unselect\";\n    in-out property <string> popup_custom_select_button_text: \"Select\";\n    in-out property <string> popup_custom_unselect_button_text: \"Unselect\";\n    in-out property <string> popup_custom_column_name_header_text: \"Column\";\n    in-out property <string> popup_custom_filter_value_header_text: \"Filter value (wildcard / regex)\";\n    in-out property <string> popup_custom_case_sensitive_text: \"Case sensitive\";\n    in-out property <string> popup_custom_leave_one_in_group_text: \"Leave one unselected per group\";\n    in-out property <string> popup_custom_hint_str_text: \"Text columns: wildcards  *name*  /home/*  *.rs\";\n    in-out property <string> popup_custom_hint_int_text: \"Size [KB] / numeric columns: >= 2048  < 512  = 0  (operators: >=  <=  >  <  =)\";\n    in-out property <string> popup_custom_hint_date_text: \"Date columns: DD-MM-YYYY or YYYY-MM-DD, optional time HH:MM:SS  e.g.  >= 2020-01-01  or  < 31-12-2022 23:59:59\";\n\n    // Progress\n    in-out property <string> stage_current_text: \"Current Stage:\";\n    in-out property <string> stage_all_text: \"All Stages:\";\n\n    // Subsettings\n    in-out property <string> subsettings_text: \"Subsettings\";\n    in-out property <string> subsettings_images_hash_size_text: \"Hash Size\";\n    in-out property <string> subsettings_images_resize_algorithm_text: \"Resize Algorithm\";\n    in-out property <string> subsettings_images_ignore_same_size_text: \"Ignore images with same size\";\n    in-out property <string> subsettings_images_max_difference_text: \"Max difference\";\n\n    in-out property <string> subsettings_images_duplicates_hash_type_text: \"Hash Type\";\n\n    in-out property <string> subsettings_duplicates_check_method_text: \"Check method\";\n    in-out property <string> subsettings_duplicates_name_case_sensitive_text: \"Case Sensitive(only name modes)\";\n\n    in-out property <string> subsettings_biggest_files_sub_method_text: \"Method\";\n    in-out property <string> subsettings_biggest_files_sub_number_of_files_text: \"Number of files\";\n\n    in-out property <string> subsettings_videos_max_difference_text: \"Max difference\";\n    in-out property <string> subsettings_videos_ignore_same_size_text: \"Ignore videos with same size\";\n    in-out property <string> subsettings_videos_skip_forward_amount_text: \"Skip duration [s]\";\n    in-out property <string> subsettings_videos_vid_hash_duration_text: \"Video hash duration\";\n    in-out property <string> subsettings_videos_crop_detect_text: \"Crop detect method\";\n\n    in-out property <string> subsettings_music_audio_check_type_text: \"Audio check type\";\n    in-out property <string> subsettings_music_approximate_comparison_text: \"Approximate Tag Comparison\";\n    in-out property <string> subsettings_music_compared_tags_text: \"Compared tags\";\n    in-out property <string> subsettings_music_title_text: \"Title\";\n    in-out property <string> subsettings_music_artist_text: \"Artist\";\n    in-out property <string> subsettings_music_bitrate_text: \"Bitrate\";\n    in-out property <string> subsettings_music_genre_text: \"Genre\";\n    in-out property <string> subsettings_music_year_text: \"Year\";\n    in-out property <string> subsettings_music_length_text: \"Length\";\n    in-out property <string> subsettings_music_max_difference_text: \"Max difference\";\n    in-out property <string> subsettings_music_minimal_fragment_duration_text: \"Minimal fragment duration\";\n    in-out property <string> subsettings_music_compare_fingerprints_only_with_similar_titles_text: \"Compare within groups of similar titles\";\n    in-out property <string> subsettings_broken_files_type_text: \"Type of files to check\";\n    in-out property <string> subsettings_broken_files_audio_text: \"Audio\";\n    in-out property <string> subsettings_broken_files_pdf_text: \"Pdf\";\n    in-out property <string> subsettings_broken_files_archive_text: \"Archive\";\n    in-out property <string> subsettings_broken_files_image_text: \"Image\";\n    in-out property <string> subsettings_broken_files_video_text: \"Video\";\n    in-out property <string> subsettings_broken_files_video_info_text: \"Uses ffmpeg/ffprobe. Quite slow and may detect pedantic errors even if file plays fine.\";\n\n    in-out property <string> subsettings_bad_names_issues_text: \"Filename checks\";\n    in-out property <string> subsettings_bad_names_uppercase_extension_text: \"Uppercase extension\";\n    in-out property <string> subsettings_bad_names_uppercase_extension_hint_text: \"Finds files with uppercase letters in extension\";\n    in-out property <string> subsettings_bad_names_emoji_used_text: \"Emoji in name\";\n    in-out property <string> subsettings_bad_names_emoji_used_hint_text: \"Finds files with emoji characters in name\";\n    in-out property <string> subsettings_bad_names_space_at_start_end_text: \"Leading/trailing spaces\";\n    in-out property <string> subsettings_bad_names_space_at_start_end_hint_text: \"Finds files with spaces at the start or end of the name\";\n    in-out property <string> subsettings_bad_names_non_ascii_text: \"Non-ASCII chars\";\n    in-out property <string> subsettings_bad_names_non_ascii_hint_text: \"Finds non-ASCII characters and suggests replacements or removal\";\n    in-out property <string> subsettings_bad_names_restricted_charset_text: \"Limited charset\";\n    in-out property <string> subsettings_bad_names_restricted_charset_hint_text: \"Finds files with characters outside allowed set\";\n    in-out property <string> subsettings_bad_names_allowed_chars_text: \"Allowed chars\";\n    in-out property <string> subsettings_bad_names_remove_duplicated_text: \"Duplicated chars\";\n    in-out property <string> subsettings_bad_names_remove_duplicated_hint_text: \"Finds consecutive duplicated non-alphanumeric characters\";\n\n    in-out property <string> subsettings_video_optimizer_mode_text: \"Mode\";\n    in-out property <string> subsettings_video_optimizer_crop_type_text: \"Crop Type\";\n    in-out property <string> subsettings_video_optimizer_black_pixel_threshold_text: \"Black Pixel Threshold\";\n    in-out property <string> subsettings_video_optimizer_black_pixel_threshold_hint_text: \"Maximum RGB value for each pixel channel to be considered black (0-128). Default: 20\";\n    in-out property <string> subsettings_video_optimizer_black_bar_min_percentage_text: \"Black Bar Min Percentage\";\n    in-out property <string> subsettings_video_optimizer_black_bar_min_percentage_hint_text: \"Minimum percentage of black pixels in a row/column to be considered a black bar (50-100). Default: 90\";\n    in-out property <string> subsettings_video_optimizer_max_samples_text: \"Max Samples\";\n    in-out property <string> subsettings_video_optimizer_max_samples_hint_text: \"Maximum number of frames to analyze per video (5-1000). Default: 60\";\n    in-out property <string> subsettings_video_optimizer_min_crop_size_text: \"Min Crop Size\";\n    in-out property <string> subsettings_video_optimizer_min_crop_size_hint_text: \"Minimum pixels to crop on any side (1-1000). Smaller crops are ignored. Default: 20\";\n    in-out property <string> subsettings_video_optimizer_video_codec_text: \"Video codec\";\n    in-out property <string> subsettings_video_optimizer_excluded_codecs_text: \"Excluded codecs\";\n    in-out property <string> subsettings_video_optimizer_video_quality_text: \"Video quality (CRF)\";\n    in-out property <string> subsettings_reset_text: \"Reset\";\n    in-out property <string> subsettings_exif_ignored_tags_text: \"Ignored tags:\";\n    in-out property <string> subsettings_exif_ignored_tags_hint_text: \"Comma-separated list of tags to exclude from scanning (e.g. GPS, Thumbnail). Some tags, such as ImageWidth in TIFF files, are hidden to prevent breaking the image.\";\n\n    // Settings\n    in-out property <string> settings_dark_theme_text: \"Dark theme\";\n    in-out property <string> settings_show_only_icons_text: \"Show only icons\";\n    in-out property <string> settings_excluded_items_text: \"Excluded item:\";\n    in-out property <string> settings_allowed_extensions_text: \"Allowed extensions:\";\n    in-out property <string> settings_excluded_extensions_text: \"Excluded extensions:\";\n    in-out property <string> settings_file_size_text: \"File Size(Kilobytes)\";\n    in-out property <string> settings_minimum_file_size_text: \"Min:\";\n    in-out property <string> settings_maximum_file_size_text: \"Max:\";\n    in-out property <string> settings_recursive_search_text: \"Recursive search\";\n    in-out property <string> settings_use_cache_text: \"Use cache\";\n    in-out property <string> settings_save_as_json_text: \"Also save cache as JSON file\";\n    in-out property <string> settings_move_to_trash_text: \"Move deleted files to trash\";\n    in-out property <string> settings_ignore_other_filesystems_text: \"Ignore other filesystems (only Linux)\";\n    in-out property <string> settings_delete_outdated_cache_entries_text: \"Delete automatically outdated cache entries\";\n    in-out property <string> settings_delete_outdated_cache_entries_hint_text: \"When enabled, the app will verify during cache loading (at most once per week) whether the cached records still point to existing and unmodified files/data\";\n    in-out property <string> settings_hide_hard_links_text: \"Hide hard links\";\n    in-out property <string> settings_hide_hard_links_hint_text: \"Hide hard links to same files in results\";\n    in-out property <string> settings_thread_number_text: \"Thread number\";\n    in-out property <string> settings_restart_required_text: \"---You need to restart app to apply changes in thread number---\";\n    in-out property <string> settings_restart_required_scale_text: \"---You need to restart app to apply changes in app scale---\";\n    in-out property <string> settings_application_scale_text: \"Application scale\";\n    in-out property <string> settings_use_manual_application_scale_text: \"Apply manual application scale\";\n    in-out property <string> settings_application_scale_hint_text: \"This may break HiDPI automatic detection and cause multi monitor related problems, so use it wisely\";\n    in-out property <string> settings_duplicate_image_preview_text: \"Image preview\";\n    in-out property <string> settings_duplicate_minimal_hash_cache_size_text: \"Minimal size of cached files - Hash (KB)\";\n    in-out property <string> settings_duplicate_use_prehash_text: \"Use prehash\";\n    in-out property <string> settings_duplicate_minimal_prehash_cache_size_text: \"Minimal size of cached files - Prehash (KB)\";\n    in-out property <string> settings_similar_images_show_image_preview_text: \"Image preview\";\n    in-out property <string> settings_similar_videos_preview_text: \"Image preview\";\n    in-out property <string> settings_similar_videos_preview_hint_text: \"Preview is visible only when 'Generate thumbnails' is enabled, or when the thumbnail image was already generated.\";\n    in-out property <string> settings_open_config_folder_text: \"Open config folder\";\n    in-out property <string> settings_open_cache_folder_text: \"Open cache folder\";\n    in-out property <string> settings_language_text: \"Language\";\n    in-out property <string> settings_current_preset_text: \"Current Preset:\";\n    in-out property <string> settings_edit_name_text: \"Edit name\";\n    in-out property <string> settings_choose_name_for_prefix_text: \"Choose name for prefix \";\n    in-out property <string> settings_save_text: \"Save\";\n    in-out property <string> settings_load_text: \"Load\";\n    in-out property <string> settings_reset_text: \"Reset\";\n    in-out property <string> settings_similar_videos_tool_text: \"Similar Videos tool\";\n    in-out property <string> settings_video_thumbnails_clear_unused_thumbnails_text: \"Delete unused video thumbnails older than 7 days at app startup\";\n    in-out property <string> settings_video_thumbnails_header_text: \"Video Thumbnails\";\n    in-out property <string> settings_video_thumbnails_generate_text: \"Generate thumbnails\";\n    in-out property <string> settings_video_thumbnails_position_text: \"Thumbnail position in video (%)\";\n    in-out property <string> settings_video_thumbnails_generate_grid_text: \"Generate thumbnail grid instead of single image\";\n    in-out property <string> settings_video_thumbnails_generate_grid_hint_text: \"Generating multiple images in grid is a lot slower than generating single thumbnail\";\n    in-out property <string> settings_video_thumbnails_grid_tiles_per_side_text: \"Number of tiles per side in thumbnail grid\";\n    in-out property <string> settings_video_thumbnails_grid_tiles_per_side_hint_text: \"Number of thumbnail tiles per side in the grid (e.g., 2 means 2x2 = 4 thumbnails)\";\n    in-out property <string> settings_similar_images_tool_text: \"Similar Images tool\";\n    in-out property <string> settings_general_settings_text: \"General Settings\";\n    in-out property <string> settings_global_settings_text: \"Global Settings\";\n    in-out property <string> settings_settings_text: \"Settings\";\n    in-out property <string> settings_load_windows_size_at_startup_text: \"Load windows size at startup\";\n    in-out property <string> settings_load_tabs_sizes_at_startup_text: \"Load tabs sizes at startup\";\n    in-out property <string> settings_limit_lines_of_messages_text: \"Limit messages to 500 lines\";\n\n    in-out property <string> settings_cache_number_size_text: \"\";\n    in-out property <string> settings_video_thumbnails_number_size_text: \"\";\n    in-out property <string> settings_log_number_size_text: \"\";\n    in-out property <string> settings_cache_header_text: \"Cache Settings\";\n    in-out property <string> settings_clean_cache_button_text: \"Clean outdated cache\";\n\n    in-out property <string> settings_play_audio_on_scan_completion_text: \"Play sound when scan completes successfully\";\n    in-out property <string> settings_audio_feature_hint_text: \"Available only when compiling with audio feature\";\n    in-out property <string> settings_audio_env_variable_hint_text: \"Sound can be changed, by setting KROKIET_AUDIO_STOP_FILE environment variable to a valid audio file path\";\n\n    // Popup\n\n    // Popup Save\n    in-out property <string> popup_save_title_text: \"Saving results\";\n    in-out property <string> popup_save_message_text: \"This will save results to 3 different files\";\n\n    // Popup rename\n    in-out property <string> popup_rename_title_text: \"Renaming files\";\n    in-out property <string> rename_confirmation_text: \"Are you sure you want to rename the selected items?\";\n\n    // Popup new directories\n    in-out property <string> popup_new_directories_title_text: \"Manually adding directories(one per line)\";\n\n    // Popup move folders\n    in-out property <string> popup_move_title_text: \"Moving files\";\n    in-out property <string> move_confirmation_text: \"Are you sure you want to move the selected items?\";\n    in-out property <string> popup_move_copy_checkbox_text: \"Copy files instead of moving\";\n    in-out property <string> popup_move_preserve_folder_checkbox_text: \"Preserve folder structure\";\n\n\n    // Popup delete\n    in-out property <string> delete_text: \"Delete items\";\n    in-out property <string> delete_confirmation_text: \"Are you sure you want to delete the selected items?\";\n\n    // Popup clean\n    in-out property <string> clean_text: \"Clean EXIF data\";\n    in-out property <string> clean_confirmation_text: \"Are you sure you want to remove EXIF data from the selected items?\";\n    in-out property <string> clean_exif_overwrite_files_text: \"Overwrite files\";\n\n    in-out property <string> crop_videos_text: \"Crop videos\";\n    in-out property <string> crop_video_confirmation_text: \"Are you sure you want to crop the selected videos?\";\n    in-out property <string> crop_reencode_video_text: \"Re-encode video\";\n    in-out property <string> reencode_videos_text: \"Re-encode videos\";\n    in-out property <string> optimize_confirmation_text: \"Are you sure you want to re-encode the selected videos?\";\n    in-out property <string> optimize_fail_if_bigger_text: \"Fail if optimized file is bigger\";\n    in-out property <string> optimize_overwrite_files_text: \"Overwrite files\";\n    in-out property <string> optimize_limit_video_size_text: \"Limit video size\";\n    in-out property <string> optimize_max_width_text: \"Max width:\";\n    in-out property <string> optimize_max_height_text: \"Max height:\";\n\n    // Popup hardlink\n    in-out property <string> hardlink_text: \"Create hardlinks\";\n    in-out property <string> hardlink_confirmation_text: \"Are you sure you want to create hardlinks for the selected items?\";\n\n    // Popup softlink\n    in-out property <string> softlink_text: \"Create softlinks\";\n    in-out property <string> softlink_confirmation_text: \"Are you sure you want to create softlinks (symlinks) for the selected items?\";\n\n    // Main window\n    in-out property <string> stopping_scan_text: \"Stopping scan, please wait...\";\n    in-out property <string> searching_text: \"Searching...\";\n    in-out property <string> stop_text: \"Stop\";\n\n    in-out property <string> popup_clean_cache_title_text: \"Clean Outdated Cache\";\n    in-out property <string> popup_clean_cache_confirmation_text: \"Are you sure you want to clean outdated cache entries? This will remove cache entries for files that no longer exist or have been modified.\";\n    in-out property <string> popup_clean_cache_progress_text: \"Processing cache file:\";\n    in-out property <string> popup_clean_cache_current_file_text: \"Current file:\";\n    in-out property <string> popup_clean_cache_file_progress_text: \"Current file progress:\";\n    in-out property <string> popup_clean_cache_overall_progress_text: \"Overall progress:\";\n    in-out property <string> popup_clean_cache_stopped_by_user_text: \"Cache cleaning was stopped by user\";\n    in-out property <string> popup_clean_cache_finished_text: \"Cache cleaning completed successfully!\";\n    in-out property <string> popup_clean_cache_error_details_text: \"Error details:\";\n    in-out property <string> popup_clean_cache_files_with_errors: \"Files with errors:\";\n\n\n}\n"
  },
  {
    "path": "misc/add_icon_exe/Cargo.toml",
    "content": "[package]\nname = \"add_icon_exe\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [\"czkawka <noreply@example.com>\"]\ndescription = \"Small helper to embed a PNG icon into a Windows PE executable using editpe\"\nlicense = \"MIT\"\n\n[dependencies]\neditpe = \"0.2.1\"\n"
  },
  {
    "path": "misc/ai_translate/ftl_utils.py",
    "content": "#!/usr/bin/env python3\n\nimport pathlib\nimport re\nfrom typing import Dict\n\n\nLANGUAGE_NAMES = {\n    \"ar\": \"Arabic\",\n    \"bg\": \"Bulgarian\",\n    \"cs\": \"Czech\",\n    \"de\": \"German\",\n    \"el\": \"Greek\",\n    \"en\": \"English\",\n    \"es-ES\": \"Spanish\",\n    \"fa\": \"Persian\",\n    \"fr\": \"French\",\n    \"it\": \"Italian\",\n    \"ja\": \"Japanese\",\n    \"ko\": \"Korean\",\n    \"nl\": \"Dutch\",\n    \"no\": \"Norwegian\",\n    \"pl\": \"Polish\",\n    \"pt-BR\": \"Brazilian Portuguese\",\n    \"pt-PT\": \"Portuguese\",\n    \"ro\": \"Romanian\",\n    \"ru\": \"Russian\",\n    \"sv-SE\": \"Swedish\",\n    \"tr\": \"Turkish\",\n    \"uk\": \"Ukrainian\",\n    \"zh-CN\": \"Simplified Chinese\",\n    \"zh-TW\": \"Traditional Chinese\",\n}\n\n\ndef parse_ftl_file(file_path: pathlib.Path) -> Dict[str, str]:\n    if not file_path.exists():\n        return {}\n\n    content = file_path.read_text(encoding=\"utf-8\")\n    entries = {}\n    lines = content.split(\"\\n\")\n    i = 0\n\n    while i < len(lines):\n        line = lines[i]\n\n        key_match = re.match(r\"^([\\w][\\w-]*)\\s*=\\s*(.*)\", line)\n\n        if key_match:\n            key = key_match.group(1).strip()\n            first_line_value = key_match.group(2).strip()\n\n            value_lines = []\n            if first_line_value:\n                value_lines.append(first_line_value)\n\n            i += 1\n            while i < len(lines):\n                next_line = lines[i]\n\n                if re.match(r\"^[\\w][\\w-]*\\s*=\", next_line):\n                    break\n                if next_line.startswith(\"#\"):\n                    break\n\n                if next_line and next_line[0] == \" \":\n                    value_lines.append(next_line.strip())\n                    i += 1\n                elif not next_line.strip():\n                    j = i + 1\n                    has_more_content = False\n                    while j < len(lines):\n                        if lines[j] and lines[j][0] == \" \":\n                            has_more_content = True\n                            break\n                        elif lines[j].strip() and not lines[j].startswith(\"#\"):\n                            break\n                        j += 1\n\n                    if has_more_content:\n                        value_lines.append(\"\")\n                        i += 1\n                    else:\n                        break\n                else:\n                    break\n\n            value = \"\\n\".join(value_lines) if value_lines else \"\"\n            entries[key] = value\n        else:\n            i += 1\n\n    return entries\n\n\ndef find_ftl_file_in_folder(folder: pathlib.Path) -> pathlib.Path | None:\n    if not folder.exists() or not folder.is_dir():\n        return None\n\n    ftl_files = list(folder.glob(\"*.ftl\"))\n    if len(ftl_files) == 1:\n        return ftl_files[0]\n    elif len(ftl_files) > 1:\n        print(f\"  Warning: Multiple FTL files found in {folder}, using first one: {ftl_files[0].name}\")\n        return ftl_files[0]\n\n    return None\n"
  },
  {
    "path": "misc/ai_translate/pyproject.toml",
    "content": "[project]\nname = \"i18n-ai-translate\"\nversion = \"0.1.0\"\nrequires-python = \"==3.13.*\"\ndependencies = [\n    \"fluent.syntax\",\n    \"ollama\"\n]\n"
  },
  {
    "path": "misc/ai_translate/translate.py",
    "content": "import argparse\nimport pathlib\nimport re\nimport sys\nfrom typing import Any, Dict, List, Tuple\n\nfrom ftl_utils import parse_ftl_file, find_ftl_file_in_folder, LANGUAGE_NAMES\n\n\n# DEFAULT_MODEL = \"qwen2.5:7b\"\n# DEFAULT_MODEL = \"qwen2.5:32b\"\n# DEFAULT_MODEL = \"zongwei/gemma3-translator:4b\"\nDEFAULT_MODEL = \"translategemma:12b\"\n\nIGNORED_KEYS = [\n    \"bottom_symlink_button\",\n    \"bottom_hardlink_button\",\n    \"main_tree_view_column_fps\",\n    \"main_check_box_broken_files_pdf\",\n    \"general_ok_button\",\n    \"duplicate_mode_hash_combo_box\",\n    \"compare_move_left_button\",\n    \"compare_move_right_button\",\n    \"ok_button\",\n    \"ref\",\n]\n\n\ndef serialize_ftl_entries(entries: Dict[str, str]) -> str:\n    lines = []\n    for key, value in entries.items():\n        if \"\\n\" in value:\n            lines.append(f\"{key} =\")\n            for line in value.split(\"\\n\"):\n                if line.strip():\n                    lines.append(f\"    {line}\")\n                else:\n                    lines.append(\"\")\n        else:\n            lines.append(f\"{key} = {value}\")\n\n    return \"\\n\".join(lines)\n\n\ndef read_ftl_with_structure(file_path: pathlib.Path) -> Tuple[str, Dict[str, str]]:\n    if not file_path.exists():\n        return \"\", {}\n\n    content = file_path.read_text(encoding=\"utf-8\")\n    entries = parse_ftl_file(file_path)\n\n    return content, entries\n\n\ndef translate_text(text: str, target_language: str, model: str = DEFAULT_MODEL) -> str:\n    try:\n        import ollama  # type: ignore\n    except ImportError:\n        print(\"Error: 'ollama' package not installed.\")\n        print(\"   Install it with: pip install ollama\")\n        print(\"   Or run: just prepare_translations_deps\")\n        sys.exit(1)\n\n    language_name = LANGUAGE_NAMES.get(target_language, target_language)\n\n    try:\n        response = ollama.chat(\n            model=model,\n            messages=[\n                {\n                    \"role\": \"user\",\n                    \"content\": f\"\"\"Translate the following text to {language_name}.\nKeep the same tone and style. Preserve any special formatting or placeholders.\nOnly return the translated text, no explanations or additional text.\n\nText to translate:\n{text}\"\"\",\n                }\n            ],\n        )\n\n        translated: str = str(response[\"message\"][\"content\"]).strip()\n\n        if translated.startswith('\"') and translated.endswith('\"'):\n            translated = translated[1:-1]\n        if translated.startswith(\"'\") and translated.endswith(\"'\"):\n            translated = translated[1:-1]\n\n        return translated\n\n    except Exception as e:\n        print(f\"  Translation error: {e}\")\n        return text\n\n\ndef analyze_language_file(\n    base_entries: Dict[str, str], lang_file: pathlib.Path, target_lang: str\n) -> Tuple[Dict[str, str], int, int]:\n    lang_content, lang_entries = read_ftl_with_structure(lang_file)\n\n    missing_keys = {}\n    ignored_count = 0\n\n    for key, base_value in base_entries.items():\n        if key in IGNORED_KEYS:\n            if key not in lang_entries or lang_entries[key] == base_value:\n                ignored_count += 1\n            continue\n\n        needs_translation = False\n\n        if key not in lang_entries:\n            needs_translation = True\n        elif lang_entries[key] == base_value:\n            needs_translation = True\n\n        if needs_translation:\n            missing_keys[key] = base_value\n\n    return missing_keys, len(missing_keys), ignored_count\n\n\ndef update_language_file_content(lang_file: pathlib.Path, translations: Dict[str, str]) -> None:\n    if not translations:\n        return\n\n    content = lang_file.read_text(encoding=\"utf-8\")\n    lines = content.split(\"\\n\")\n    result_lines = []\n    i = 0\n    processed_keys = set()\n\n    while i < len(lines):\n        line = lines[i]\n\n        key_match = re.match(r\"^([\\w][\\w-]*)\\s*=\", line)\n\n        if key_match:\n            key = key_match.group(1)\n            processed_keys.add(key)\n\n            if key in translations:\n                value = translations[key]\n                if \"\\n\" in value:\n                    result_lines.append(f\"{key} = \")\n                    for v_line in value.split(\"\\n\"):\n                        if v_line.strip():\n                            result_lines.append(f\"        {v_line}\")\n                        else:\n                            result_lines.append(\"\")\n                else:\n                    result_lines.append(f\"{key} = {value}\")\n\n                i += 1\n                while i < len(lines):\n                    if lines[i] and lines[i][0] == \" \":\n                        i += 1\n                    elif not lines[i].strip():\n                        if i + 1 < len(lines) and lines[i + 1] and lines[i + 1][0] == \" \":\n                            i += 1\n                        else:\n                            break\n                    else:\n                        break\n                continue\n\n        result_lines.append(line)\n        i += 1\n\n    new_keys = [k for k in translations.keys() if k not in processed_keys]\n    if new_keys:\n        if result_lines and result_lines[-1].strip():\n            result_lines.append(\"\")\n        for key in new_keys:\n            value = translations[key]\n            if \"\\n\" in value:\n                result_lines.append(f\"{key} = \")\n                for v_line in value.split(\"\\n\"):\n                    if v_line.strip():\n                        result_lines.append(f\"        {v_line}\")\n                    else:\n                        result_lines.append(\"\")\n            else:\n                result_lines.append(f\"{key} = {value}\")\n\n    lang_file.write_text(\"\\n\".join(result_lines), encoding=\"utf-8\")\n\n\ndef process_i18n_folder(\n    i18n_path: pathlib.Path,\n    model: str = DEFAULT_MODEL,\n    dry_run: bool = False,\n    target_languages: List[str] | None = None,\n) -> None:\n    print(f\"Processing i18n folder: {i18n_path}\")\n\n    en_folder = i18n_path / \"en\"\n    if not en_folder.exists():\n        print(f\"Error: English folder not found at {en_folder}\")\n        return\n\n    en_file = find_ftl_file_in_folder(en_folder)\n    if not en_file:\n        print(f\"Error: No FTL file found in {en_folder}\")\n        return\n\n    print(f\"Base file: {en_file.name}\")\n\n    base_entries = parse_ftl_file(en_file)\n    print(f\"Found {len(base_entries)} entries in base file\\n\")\n\n    lang_folders = [f for f in i18n_path.iterdir() if f.is_dir() and f.name != \"en\"]\n    lang_folders.sort()\n\n    if target_languages:\n        lang_folders = [f for f in lang_folders if f.name in target_languages]\n\n    print(\"=\" * 70)\n    print(\"ANALYSIS PHASE - Reading all files\")\n    print(\"=\" * 70)\n\n    analysis_results: Dict[str, Dict[str, Any]] = {}\n\n    for lang_folder in lang_folders:\n        lang_code = lang_folder.name\n        lang_name = LANGUAGE_NAMES.get(lang_code, \"Unknown\")\n\n        lang_file = find_ftl_file_in_folder(lang_folder)\n\n        if not lang_file:\n            lang_file = lang_folder / en_file.name\n            if not lang_file.exists():\n                lang_file.touch()\n\n        missing_keys, count, ignored_count = analyze_language_file(base_entries, lang_file, lang_code)\n        analysis_results[lang_code] = {\n            \"name\": lang_name,\n            \"file\": lang_file,\n            \"missing_keys\": missing_keys,\n            \"count\": count,\n            \"ignored_count\": ignored_count,\n        }\n\n        status = f\"{count:3} phrases to translate\"\n        if ignored_count > 0:\n            status += f\", {ignored_count:3} ignored\"\n        print(f\"{lang_code:8} ({lang_name:25}) - {status}\")\n\n    total_to_translate = sum(int(r[\"count\"]) for r in analysis_results.values())\n    total_ignored = sum(int(r[\"ignored_count\"]) for r in analysis_results.values())\n    print(f\"\\nTotal phrases to translate: {total_to_translate}\")\n    if total_ignored > 0:\n        print(f\"Total phrases ignored: {total_ignored}\")\n\n    if total_to_translate == 0:\n        print(\"\\nNo translations needed.\")\n        return\n\n    if dry_run:\n        print(\"\\n[DRY RUN] Would translate the above phrases\")\n        return\n\n    print(\"\\n\" + \"=\" * 70)\n    print(\"TRANSLATION PHASE\")\n    print(\"=\" * 70 + \"\\n\")\n\n    total_translated = 0\n\n    for lang_code, data in analysis_results.items():\n        count = int(data[\"count\"])\n        if count == 0:\n            continue\n\n        lang_name = str(data[\"name\"])\n        missing_keys = dict(data[\"missing_keys\"])\n        lang_file = pathlib.Path(data[\"file\"])\n\n        print(f\"Translating {lang_code} ({lang_name}) - {count} phrases\")\n\n        translations = {}\n        for idx, (key, base_value) in enumerate(missing_keys.items(), 1):\n            print(f\"  [{idx}/{count}] {key}: {base_value[:50]}...\")\n            translated_value = translate_text(base_value, lang_code, model)\n            translations[key] = translated_value\n\n        update_language_file_content(lang_file, translations)\n        total_translated += count\n        print(f\"  Updated {lang_file.name}\\n\")\n\n    print(\n        f\"Complete! Translated {total_translated} phrases across {len([r for r in analysis_results.values() if int(r['count']) > 0])} languages\"\n    )\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"Translate FTL files using offline AI\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=\"\"\"\nExamples:\n  python3 misc/ai_translate/translate.py czkawka_gui/i18n\n  python3 misc/ai_translate/translate.py krokiet/i18n --model qwen2.5:7b\n  python3 misc/ai_translate/translate.py czkawka_gui/i18n --dry-run\n  python3 misc/ai_translate/translate.py czkawka_gui/i18n --languages pl de fr\n        \"\"\",\n    )\n\n    parser.add_argument(\"i18n_folder\", type=str, help=\"Path to the i18n folder containing language subdirectories\")\n\n    parser.add_argument(\n        \"--model\",\n        type=str,\n        default=DEFAULT_MODEL,\n        help=f\"Ollama model to use for translation (default: {DEFAULT_MODEL})\",\n    )\n\n    parser.add_argument(\"--dry-run\", action=\"store_true\", help=\"Show what would be translated without making changes\")\n\n    parser.add_argument(\"--languages\", nargs=\"+\", help=\"Only process specific languages (e.g., --languages pl de fr)\")\n\n    args = parser.parse_args()\n\n    i18n_path = pathlib.Path(args.i18n_folder)\n    if not i18n_path.is_absolute():\n        i18n_path = pathlib.Path.cwd() / i18n_path\n\n    if not i18n_path.exists():\n        print(f\"Error: Path does not exist: {i18n_path}\")\n        sys.exit(1)\n\n    if not i18n_path.is_dir():\n        print(f\"Error: Path is not a directory: {i18n_path}\")\n        sys.exit(1)\n\n    try:\n        process_i18n_folder(i18n_path, args.model, args.dry_run, args.languages)\n    except KeyboardInterrupt:\n        print(\"\\n\\nInterrupted by user\")\n        sys.exit(1)\n    except Exception as e:\n        print(f\"\\nError: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "misc/ai_translate/validate_translations.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport pathlib\nimport re\nimport sys\nfrom typing import Any, Dict, List, Set\n\nfrom ftl_utils import parse_ftl_file, find_ftl_file_in_folder, LANGUAGE_NAMES\n\n\nclass Colors:\n    GREEN = \"\\033[92m\"\n    RED = \"\\033[91m\"\n    YELLOW = \"\\033[93m\"\n    BLUE = \"\\033[94m\"\n    RESET = \"\\033[0m\"\n    BOLD = \"\\033[1m\"\n\n\ndef extract_placeholders(text: str) -> Set[str]:\n    pattern = re.compile(r\"{\\s*\\$[\\w-]+\\s*}\")\n    matches = pattern.findall(text)\n    normalized = {re.sub(r\"\\s+\", \"\", match) for match in matches}\n    return normalized\n\n\ndef count_placeholders(text: str) -> Dict[str, int]:\n    pattern = re.compile(r\"{\\s*\\$[\\w-]+\\s*}\")\n    matches = pattern.findall(text)\n    normalized_matches = [re.sub(r\"\\s+\", \"\", match) for match in matches]\n\n    counts: Dict[str, int] = {}\n    for placeholder in normalized_matches:\n        counts[placeholder] = counts.get(placeholder, 0) + 1\n\n    return counts\n\n\ndef validate_translation(base_value: str, translated_value: str, key: str) -> List[str]:\n    errors = []\n\n    base_placeholders = extract_placeholders(base_value)\n    translated_placeholders = extract_placeholders(translated_value)\n\n    missing = base_placeholders - translated_placeholders\n    extra = translated_placeholders - base_placeholders\n\n    if missing:\n        errors.append(f\"  {Colors.RED}Missing placeholders:{Colors.RESET} {', '.join(sorted(missing))}\")\n    if extra:\n        errors.append(f\"  {Colors.RED}Extra placeholders:{Colors.RESET} {', '.join(sorted(extra))}\")\n\n    base_counts = count_placeholders(base_value)\n    translated_counts = count_placeholders(translated_value)\n\n    for placeholder in base_placeholders:\n        if placeholder in translated_placeholders:\n            base_count = base_counts.get(placeholder, 0)\n            translated_count = translated_counts.get(placeholder, 0)\n\n            if base_count != translated_count:\n                errors.append(\n                    f\"  {Colors.RED}Wrong occurrence count for {placeholder}:{Colors.RESET} expected {base_count}, found {translated_count}\"\n                )\n\n    # New: validate trailing dot presence/absence consistency\n    base_has_dot = base_value.strip().endswith(\".\")\n    translated_has_dot = translated_value.strip().endswith(\".\")\n\n    if base_has_dot != translated_has_dot:\n        if base_has_dot:\n            errors.append(\n                f\"  {Colors.RED}Trailing dot mismatch:{Colors.RESET} source ends with a dot but translation does not\"\n            )\n        else:\n            errors.append(\n                f\"  {Colors.RED}Trailing dot mismatch:{Colors.RESET} source does not end with a dot but translation does\"\n            )\n\n    return errors\n\n\ndef validate_language_file(\n    base_entries: Dict[str, str], lang_file: pathlib.Path, lang_code: str\n) -> Dict[str, List[str]]:\n    lang_entries = parse_ftl_file(lang_file)\n\n    errors_by_key = {}\n\n    for key, base_value in base_entries.items():\n        if key not in lang_entries:\n            continue\n\n        translated_value = lang_entries[key]\n\n        validation_errors = validate_translation(base_value, translated_value, key)\n\n        if validation_errors:\n            errors_by_key[key] = validation_errors\n\n    return errors_by_key\n\n\ndef fix_language_file(lang_file: pathlib.Path, keys_to_remove: Set[str]) -> int:\n    content = lang_file.read_text(encoding=\"utf-8\")\n    lines = content.split(\"\\n\")\n    result_lines = []\n    i = 0\n    removed_count = 0\n\n    while i < len(lines):\n        line = lines[i]\n\n        key_match = re.match(r\"^([\\w][\\w-]*)\\s*=\", line)\n\n        if key_match:\n            key = key_match.group(1)\n\n            if key in keys_to_remove:\n                removed_count += 1\n                i += 1\n                while i < len(lines):\n                    if lines[i] and lines[i][0] == \" \":\n                        i += 1\n                    elif not lines[i].strip():\n                        if i + 1 < len(lines) and lines[i + 1] and lines[i + 1][0] == \" \":\n                            i += 1\n                        else:\n                            break\n                    else:\n                        break\n                continue\n\n        result_lines.append(line)\n        i += 1\n\n    lang_file.write_text(\"\\n\".join(result_lines), encoding=\"utf-8\")\n    return removed_count\n\n\ndef fix_trailing_dots_in_language_file(\n    lang_file: pathlib.Path, base_entries: Dict[str, str], keys_to_fix: Set[str]\n) -> int:\n    content = lang_file.read_text(encoding=\"utf-8\")\n    lines = content.split(\"\\n\")\n    result_lines: List[str] = []\n    i = 0\n    modified_count = 0\n\n    # Parse language file entries once\n    lang_entries = parse_ftl_file(lang_file)\n\n    while i < len(lines):\n        line = lines[i]\n        key_match = re.match(r\"^([\\w][\\w-]*)\\s*=\\s*(.*)$\", line)\n\n        if key_match:\n            key = key_match.group(1)\n\n            if key in keys_to_fix and key in lang_entries and key in base_entries:\n                # Collect the block lines (initial + continuation lines starting with a space)\n                block_start = i\n                block_lines = [lines[i]]\n                j = i + 1\n                while j < len(lines) and (lines[j].startswith(\" \") or lines[j].strip() == \"\"):\n                    # include continuation or empty lines that might be part of the value\n                    # stop when next non-indented non-empty line appears\n                    if lines[j].startswith(\" \") or lines[j] == \"\":\n                        block_lines.append(lines[j])\n                        j += 1\n                    else:\n                        break\n\n                # Extract content parts for each block line (preserve whitespace around content)\n                content_parts: List[str] = []\n                indents: List[str] = []\n                for idx, bl in enumerate(block_lines):\n                    if idx == 0:\n                        m = re.match(r\"^([\\w][\\w-]*)\\s*=\\s*(.*)$\", bl)\n                        part = m.group(2) if m else \"\"\n                        content_parts.append(part)\n                        indents.append(\"\")\n                    else:\n                        # capture leading whitespace (indent) and the rest of content\n                        m = re.match(r\"^(\\s*)(.*)$\", bl)\n                        if m:\n                            indent = m.group(1)\n                            part = m.group(2)\n                        else:\n                            indent = \"\"\n                            part = bl\n                        content_parts.append(part)\n                        indents.append(indent)\n\n                # Determine last content part index (skip trailing empty continuation lines)\n                last_idx = len(content_parts) - 1\n                while last_idx >= 0 and content_parts[last_idx].strip() == \"\":\n                    last_idx -= 1\n\n                if last_idx >= 0:\n                    last_part = content_parts[last_idx]\n\n                    # split into text and trailing spaces to preserve spacing\n                    m = re.match(r\"^(.*?)(\\s*)$\", last_part, flags=re.S)\n                    text = m.group(1)  # type: ignore\n                    trailing_spaces = m.group(2)  # type: ignore\n\n                    base_has_dot = base_entries[key].strip().endswith(\".\")\n                    trans_has_dot = text.endswith(\".\")\n\n                    new_text = text\n                    if base_has_dot and not trans_has_dot:\n                        new_text = text + \".\"\n                    elif not base_has_dot and trans_has_dot:\n                        # remove all trailing dots from the textual end\n                        new_text = re.sub(r\"\\.+$\", \"\", text)\n\n                    if new_text != text:\n                        # replace last content part while keeping other parts intact\n                        content_parts[last_idx] = new_text + trailing_spaces\n\n                        # Rebuild block lines preserving original formatting\n                        new_block_lines: List[str] = []\n                        for idx, part in enumerate(content_parts):\n                            if idx == 0:\n                                new_block_lines.append(f\"{key} = {part}\")\n                            else:\n                                new_block_lines.append(f\"{indents[idx]}{part}\")\n\n                        # Append new block lines to result and advance index\n                        result_lines.extend(new_block_lines)\n                        modified_count += 1\n                        i = j\n                        continue\n\n        # default: copy original line\n        result_lines.append(line)\n        i += 1\n\n    if modified_count > 0:\n        lang_file.write_text(\"\\n\".join(result_lines), encoding=\"utf-8\")\n\n    return modified_count\n\n\ndef validate_i18n_folder(\n    i18n_path: pathlib.Path, target_languages: List[str] | None = None, fix_mode: bool = False\n) -> int:\n    print(f\"Validating i18n folder: {i18n_path}\")\n\n    en_folder = i18n_path / \"en\"\n    if not en_folder.exists():\n        print(f\"Error: English folder not found at {en_folder}\")\n        return 1\n\n    en_file = find_ftl_file_in_folder(en_folder)\n    if not en_file:\n        print(f\"Error: No FTL file found in {en_folder}\")\n        return 1\n\n    print(f\"Base file: {en_file.name}\")\n\n    base_entries = parse_ftl_file(en_file)\n    print(f\"Found {len(base_entries)} entries in base file\\n\")\n\n    lang_folders = [f for f in i18n_path.iterdir() if f.is_dir() and f.name != \"en\"]\n    lang_folders.sort()\n\n    if target_languages:\n        lang_folders = [f for f in lang_folders if f.name in target_languages]\n\n    print(\"=\" * 70)\n    print(\"VALIDATION RESULTS\")\n    print(\"=\" * 70)\n\n    total_errors = 0\n    errors_by_language: Dict[str, Dict[str, Any]] = {}\n\n    for lang_folder in lang_folders:\n        lang_code = lang_folder.name\n        lang_name = LANGUAGE_NAMES.get(lang_code, \"Unknown\")\n\n        lang_file = find_ftl_file_in_folder(lang_folder)\n\n        if not lang_file:\n            continue\n\n        errors = validate_language_file(base_entries, lang_file, lang_code)\n\n        if errors:\n            errors_by_language[lang_code] = {\"name\": lang_name, \"file\": lang_file, \"errors\": errors}\n            total_errors += len(errors)\n\n    if not errors_by_language:\n        print(\"All translations are valid!\")\n        return 0\n\n    if fix_mode:\n        print(\n            f\"\\n{Colors.YELLOW}FIX MODE: Fixing trailing-dot mismatches and removing entries with placeholder errors{Colors.RESET}\\n\"\n        )\n\n        total_removed = 0\n        total_fixed = 0\n\n        for lang_code in sorted(errors_by_language.keys()):\n            data = errors_by_language[lang_code]\n            lang_file = data[\"file\"]\n            # classify keys by type of error\n            keys_to_remove: Set[str] = set()\n            keys_to_fix_dots: Set[str] = set()\n\n            for key, msgs in data[\"errors\"].items():\n                combined = \"\\n\".join(msgs)\n                if (\n                    \"Missing placeholders\" in combined\n                    or \"Extra placeholders\" in combined\n                    or \"Wrong occurrence count\" in combined\n                ):\n                    keys_to_remove.add(key)\n                elif \"Trailing dot mismatch\" in combined:\n                    keys_to_fix_dots.add(key)\n                else:\n                    # default to removal if unknown error\n                    keys_to_remove.add(key)\n\n            removed = 0\n            fixed = 0\n\n            if keys_to_remove:\n                removed = fix_language_file(lang_file, keys_to_remove)\n\n            if keys_to_fix_dots:\n                fixed = fix_trailing_dots_in_language_file(lang_file, base_entries, keys_to_fix_dots)\n\n            total_removed += removed\n            total_fixed += fixed\n\n            print(f\"{lang_code:8} ({data['name']:25}) - removed {removed:3} entry(ies), fixed {fixed:3} entry(ies)\")\n\n        print(\n            f\"\\n{Colors.GREEN}Fixed! Removed {total_removed} entry(ies) and updated {total_fixed} translation(s) with trailing-dot mismatches{Colors.RESET}\"\n        )\n        return 0\n\n    print(f\"\\nFound errors in {len(errors_by_language)} language(s):\\n\")\n\n    for lang_code in sorted(errors_by_language.keys()):\n        data = errors_by_language[lang_code]\n        error_count = len(data[\"errors\"])\n        print(f\"{lang_code:8} ({data['name']:25}) - {error_count:3} error(s)\")\n\n    print(f\"\\nTotal errors: {total_errors}\\n\")\n    print(\"=\" * 70)\n    print(\"DETAILED ERRORS\")\n    print(\"=\" * 70 + \"\\n\")\n\n    for lang_code in sorted(errors_by_language.keys()):\n        data = errors_by_language[lang_code]\n        print(f\"\\n{Colors.BOLD}{lang_code} ({data['name']}) - {data['file'].name}{Colors.RESET}\")\n        print(\"-\" * 70)\n\n        for key, error_messages in sorted(data[\"errors\"].items()):\n            print(f\"\\n{Colors.GREEN}Key: {key}{Colors.RESET}\")\n            for error_msg in error_messages:\n                print(error_msg)\n\n    print(\"\\n\" + \"=\" * 70)\n    print(f\"Validation complete: {total_errors} error(s) found\")\n\n    return 1 if total_errors > 0 else 0\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"Validate FTL translation files for placeholder consistency\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=\"\"\"\nExamples:\n  python3 misc/ai_translate/validate_translations.py czkawka_gui/i18n\n  python3 misc/ai_translate/validate_translations.py krokiet/i18n\n  python3 misc/ai_translate/validate_translations.py czkawka_gui/i18n --languages pl de fr\n        \"\"\",\n    )\n\n    parser.add_argument(\"i18n_folder\", type=str, help=\"Path to the i18n folder containing language subdirectories\")\n\n    parser.add_argument(\"--languages\", nargs=\"+\", help=\"Only validate specific languages (e.g., --languages pl de fr)\")\n\n    parser.add_argument(\"--fix\", action=\"store_true\", help=\"Automatically remove entries with placeholder errors\")\n\n    args = parser.parse_args()\n\n    i18n_path = pathlib.Path(args.i18n_folder)\n    if not i18n_path.is_absolute():\n        i18n_path = pathlib.Path.cwd() / i18n_path\n\n    if not i18n_path.exists():\n        print(f\"Error: Path does not exist: {i18n_path}\")\n        sys.exit(1)\n\n    if not i18n_path.is_dir():\n        print(f\"Error: Path is not a directory: {i18n_path}\")\n        sys.exit(1)\n\n    try:\n        exit_code = validate_i18n_folder(i18n_path, args.languages, args.fix)\n        sys.exit(exit_code)\n    except KeyboardInterrupt:\n        print(\"\\n\\nInterrupted by user\")\n        sys.exit(1)\n    except Exception as e:\n        print(f\"\\nError: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "misc/cargo/PublishCore.sh",
    "content": "#!/bin/bash\nNUMBER=\"11.0.1\"\nCZKAWKA_PATH=\"/home/rafal\"\n\ncd \"$CZKAWKA_PATH\"\nCZKAWKA_PATH=\"$CZKAWKA_PATH/czkawka\"\nrm -rf $CZKAWKA_PATH\ngit clone https://github.com/qarmin/czkawka.git \"$CZKAWKA_PATH\"\ncd $CZKAWKA_PATH\ngit checkout \"$NUMBER\"\n\ncd \"$CZKAWKA_PATH/czkawka_core\"\ncargo package\nif [ $(echo $?) != \"0\"  ]\nthen\n  echo \"Cargo package failed CORE\"\n  exit 1\nfi\ngit reset --hard\n\ncd \"$CZKAWKA_PATH/czkawka_core\"\ncargo publish\ngit reset --hard\n\n"
  },
  {
    "path": "misc/cargo/PublishOther.sh",
    "content": "#!/bin/bash\nNUMBER=\"11.0.1\"\nCZKAWKA_PATH=\"/home/rafal\"\n\ncd \"$CZKAWKA_PATH\"\nCZKAWKA_PATH=\"$CZKAWKA_PATH/czkawka\"\nrm -rf $CZKAWKA_PATH\ngit clone https://github.com/qarmin/czkawka.git \"$CZKAWKA_PATH\"\ncd $CZKAWKA_PATH\ngit checkout \"$NUMBER\"\n\n\ncd \"$CZKAWKA_PATH/czkawka_cli\"\ncargo package\nif [ $(echo $?) != \"0\"  ]\nthen\n  echo \"Cargo package failed CLI\"\n  exit 1\nfi\ngit reset --hard\n\n\ncd \"$CZKAWKA_PATH/czkawka_gui\"\ncargo package\nif [ $(echo $?) != \"0\"  ]\nthen\n  echo \"Cargo package failed GUI\"\n  exit 1\nfi\ngit reset --hard\n\ncd \"$CZKAWKA_PATH/krokiet\"\ncargo package\nif [ $(echo $?) != \"0\"  ]\nthen\n  echo \"Cargo package failed krokiet\"\n  exit 1\nfi\ngit reset --hard\n\n\n\n\ncd \"$CZKAWKA_PATH/czkawka_cli\"\n# sed -i \"s/{ path = \\\"..\\/czkawka_core\\\" }/\\\"=$NUMBER\\\"/g\" \"$CZKAWKA_PATH/czkawka_cli/Cargo.toml\"\ncargo publish # --allow-dirty\ngit reset --hard\n\ncd \"$CZKAWKA_PATH/czkawka_gui\"\n# sed -i \"s/{ path = \\\"..\\/czkawka_core\\\" }/\\\"=$NUMBER\\\"/g\" \"$CZKAWKA_PATH/czkawka_gui/Cargo.toml\"\ncargo publish # --allow-dirty\ngit reset --hard\n\ncd \"$CZKAWKA_PATH/krokiet\"\n# sed -i \"s/{ path = \\\"..\\/czkawka_core\\\" }/\\\"=$NUMBER\\\"/g\" \"$CZKAWKA_PATH/czkawka_gui/Cargo.toml\"\ncargo publish # --allow-dirty\ngit reset --hard\n"
  },
  {
    "path": "misc/compare_files.sh",
    "content": "#!/bin/bash\nset -euo pipefail\n\n# Calculate individual MD5 hashes with file names and join them with commas\nMD5_DEBUG_1=$(md5sum czkawka_cli_debug_1 czkawka_gui_debug_1 krokiet_debug_1 | awk '{print $1}' | paste -sd \",\")\nMD5_DEBUG_2=$(md5sum czkawka_cli_debug_2 czkawka_gui_debug_2 krokiet_debug_2 | awk '{print $1}' | paste -sd \",\")\nMD5_RELEASE_1=$(md5sum czkawka_cli_release_1 czkawka_gui_release_1 krokiet_release_1 | awk '{print $1}' | paste -sd \",\")\nMD5_RELEASE_2=$(md5sum czkawka_cli_release_2 czkawka_gui_release_2 krokiet_release_2 | awk '{print $1}' | paste -sd \",\")\n\n# Print MD5 hashes\necho \"Printing CLI, GUI, and Krokiet MD5 hashes\"\necho \"\"\necho \"MD5_DEBUG_1: $MD5_DEBUG_1\"\necho \"MD5_DEBUG_2: $MD5_DEBUG_2\"\necho \"\"\necho \"MD5_RELEASE_1: $MD5_RELEASE_1\"\necho \"MD5_RELEASE_2: $MD5_RELEASE_2\"\n\nif [ \"$MD5_DEBUG_1\" == \"$MD5_DEBUG_2\" ]; then\n    echo \"DEBUG files are the same\"\nelse\n    echo \"DEBUG files are different\"\n    exit 1\nfi\nif [ \"$MD5_RELEASE_1\" == \"$MD5_RELEASE_2\" ]; then\n    echo \"RELEASE files are the same\"\nelse\n    echo \"RELEASE files are different\"\n    exit 1\nfi"
  },
  {
    "path": "misc/delete_unused_krokiet_slint_imports.py",
    "content": "import os\nimport re\nimport sys\n\nscript_path = os.path.dirname(os.path.abspath(__file__))\n\nif len(sys.argv) < 2:\n    print(\"Usage: python delete_unused_krokiet_slint_imports.py <folder>\")\n    print(\"  Example: python delete_unused_krokiet_slint_imports.py krokiet\")\n    print(\"  Example: python delete_unused_krokiet_slint_imports.py cedinia\")\n    sys.exit(1)\n\nfolder = sys.argv[1]\nui_path = f\"{script_path}/../{folder}/ui\"\n\ncollected_files = [\n    os.path.join(root, file) for root, _, files in os.walk(ui_path) for file in files if file.endswith(\".slint\")\n]\n\nfor file_path in collected_files:\n    with open(file_path, \"r\", encoding=\"utf-8\") as file:\n        content = file.read()\n        lines = content.splitlines()\n\n    non_import_lines: list[str] = []\n    imports_to_check = []\n    updated_lines = []\n\n    for line in lines:\n        if line.startswith(\"import\"):\n            imports_to_check.append(line)\n        else:\n            if len(non_import_lines) == 0 and len(line.strip()) == 0:\n                continue\n            non_import_lines.append(line)\n\n    non_imported_content = \"\\n\".join(list(non_import_lines))\n\n    imports: dict[str, set[str]] = {}\n\n    for import_line in imports_to_check:\n        imported_items = [i.strip() for i in import_line.split(\"{\")[1].split(\"}\")[0].split(\",\") if len(i.strip()) > 0]\n        if not imported_items:\n            continue\n\n        from_file = import_line.split(\"from\")[1].strip()\n\n        used_items: list[str] = []\n        for item in imported_items:\n            regex = rf\"\\b{item}\\b\"\n            if len(re.findall(regex, non_imported_content)) >= 1:\n                used_items.append(item)\n\n        if used_items:\n            imports.setdefault(from_file, set()).update(used_items)\n\n    for from_file2, used_items2 in imports.items():\n        items = list(used_items2)\n        items.sort()\n        updated_line = f\"import {{ {', '.join(items)} }} from {from_file2}\"\n        updated_line = updated_line.replace(\";;\", \";\")\n        updated_lines.append(updated_line)\n\n    if len(updated_lines) != 0:\n        updated_lines.append(\"\")\n\n    updated_lines.extend(non_import_lines)\n    if len(updated_lines) > 0 and len(updated_lines[-1].strip()) > 0:\n        updated_lines.append(\"\")\n\n    with open(file_path, \"w\", encoding=\"utf-8\") as file:\n        file.write(\"\\n\".join(updated_lines))\n"
  },
  {
    "path": "misc/docker/Dockerfile",
    "content": "FROM ubuntu:22.04\n\n# curl is needed by Rust update tool\nRUN apt-get update \\\n    && apt-get install -y curl build-essential libgtk-4-dev \\\n    && apt-get clean ; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*\n\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Download the latest stable Rust\n\nENV PATH=\"/root/.cargo/bin:${PATH}\"\n\nRUN cargo --version\n"
  },
  {
    "path": "misc/find_unused_callbacks.py",
    "content": "import os\nimport sys\nimport re\n\nexcluded = [\"theme_changed\"]  # Executed from Slint\n\n\ndef find_files(root: str, ext: str, folder: str | None) -> list[str]:\n    files = []\n    for dirpath, _, filenames in os.walk(root):\n        for f in filenames:\n            if f.endswith(ext) and (folder is None or folder in dirpath):\n                files.append(os.path.join(dirpath, f))\n    return files\n\n\ndef read_files(files: list[str]) -> str:\n    content = \"\"\n    for f in files:\n        with open(f, \"r\", encoding=\"utf-8\") as file:\n            content += file.read() + \"\\n\"\n    return content\n\n\ndef extract_callbacks(slint_path: str) -> list[str]:\n    callbacks = []\n    with open(slint_path, \"r\", encoding=\"utf-8\") as f:\n        content = f.read()\n\n    pattern = r\"callback\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(\"\n    for match in re.finditer(pattern, content):\n        callback_name = match.group(1)\n        callbacks.append(callback_name)\n\n    return callbacks\n\n\ndef format_green(text: str) -> str:\n    return f\"\\033[92m{text}\\033[0m\"\n\n\nif len(sys.argv) < 2:\n    print(\"Usage: python find_unused_callbacks.py <folder>\")\n    sys.exit(1)\n\nfolder = sys.argv[1]\ncallabler_path = f\"{folder}/ui/callabler.slint\"\n\nif not os.path.exists(callabler_path):\n    print(f\"Error: {callabler_path} not found\")\n    sys.exit(1)\n\ncallbacks = extract_callbacks(callabler_path)\nprint(f\"Found {len(callbacks)} callbacks in callabler.slint\")\n\nrust_files = find_files(f\"{folder}/src\", \".rs\", None)\nrust_content = read_files(rust_files)\n\nerrors_found = False\n\nfor callback in callbacks:\n    if callback in excluded:\n        continue\n\n    pattern = rf\"\\.on_{callback}\\(\"\n    matches = list(re.finditer(pattern, rust_content))\n\n    if len(matches) == 0:\n        print(f\"Error: Callback {format_green(callback)} has NO Rust implementation\")\n        errors_found = True\n    elif len(matches) > 1:\n        print(f\"Error: Callback {format_green(callback)} has {len(matches)} Rust implementations (expected 1)\")\n        errors_found = True\n\nif errors_found:\n    sys.exit(1)\nelse:\n    print(\"All callbacks have exactly 1 Rust implementation\")\n"
  },
  {
    "path": "misc/find_unused_fluent_translations.py",
    "content": "import os\nimport sys\n\n\ndef find_files(root: str, ext: str, folder: str | None) -> list[str]:\n    files = []\n    for dirpath, _, filenames in os.walk(root):\n        for f in filenames:\n            if f.endswith(ext) and (folder is None or folder in dirpath):\n                files.append(os.path.join(dirpath, f))\n    return files\n\n\ndef read_files(files: list[str]) -> str:\n    content = \"\"\n    for f in files:\n        with open(f, \"r\", encoding=\"utf-8\") as file:\n            content += file.read() + \"\\n\"\n    return content\n\n\ndef extract_ftl_keys(ftl_path: str) -> list[str]:\n    keys = []\n    with open(ftl_path, \"r\", encoding=\"utf-8\") as f:\n        lines = f.readlines()\n        for line in lines:\n            line = line.strip()\n            if \"=\" not in line:\n                continue\n            key = line.split(\"=\")[0].strip()\n            keys.append(key)\n\n    # Find duplicated keys\n    seen = set()\n    duplicates = set()\n    for key in keys:\n        if key in seen:\n            duplicates.add(key)\n        else:\n            seen.add(key)\n\n    if duplicates:\n        print(f\"Warning: Found duplicated keys in {format_green(ftl_path)}: {format_green(', '.join(duplicates))}\")\n        exit(1)\n\n    return keys\n\n\ndef format_green(text: str) -> str:\n    return f\"\\033[92m{text}\\033[0m\"\n\n\nif len(sys.argv) < 2:\n    print(\"Usage: python find_unused_fluent_translations.py <folder>\")\n    sys.exit(1)\n\nfolder = sys.argv[1]\nrust_files = find_files(folder, \".rs\", None)\nftl_files = find_files(folder, f\"{folder}.ftl\", \"/en\")\nrust_content = read_files(rust_files)\n\nprint(f\"Found {len(rust_files)} Rust files and {len(ftl_files)} FTL files in {folder}\")\n\nfound = False\n\nfor ftl_file in ftl_files:\n    unused = []\n    keys = extract_ftl_keys(ftl_file)\n    print(f\"Found {len(keys)} keys in {ftl_file}\")\n    for key in keys:\n        if f'\"{key}\"' not in rust_content:\n            unused.append(key)\n    if unused:\n        print(\n            f\"Unused keys in {ftl_file}(needs to bind to slint in connect_translations.rs file, if using krokiet, otherwise it needs to be removed from ftl file or added to code):\"\n        )\n        for key in unused:\n            print(f\"  {format_green(key)}\")\n        found = True\n\nif found:\n    sys.exit(1)\n"
  },
  {
    "path": "misc/find_unused_settings_properties.py",
    "content": "import re\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n\ndef extract_settings_properties(settings_file: Path) -> Dict[str, str]:\n    properties = {}\n\n    with open(settings_file, \"r\", encoding=\"utf-8\") as f:\n        content = f.read()\n\n    # Match property definitions like:\n    # in-out property <bool> dark_theme: true;\n    # in-out property <string> language_value: \"English\";\n    # out property <length> path_px: 350px;\n    property_pattern = r\"(?:in-out|out|in)\\s+property\\s+<([^>]+)>\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*:\"\n\n    for match in re.finditer(property_pattern, content):\n        prop_type = match.group(1).strip()\n        prop_name = match.group(2).strip()\n        properties[prop_name] = prop_type\n\n    return properties\n\n\ndef find_rust_files(project_root: Path, folder: str) -> List[Path]:\n    rust_files = []\n    src_dir = project_root / folder / \"src\"\n\n    if src_dir.exists():\n        for rust_file in src_dir.rglob(\"*.rs\"):\n            rust_files.append(rust_file)\n\n    return rust_files\n\n\ndef check_property_usage_in_rust(rust_files: List[Path], property_name: str) -> Tuple[bool, bool]:\n    getter_pattern = rf\"\\.get_{property_name}\\(\"\n    setter_pattern = rf\"\\.set_{property_name}\\(\"\n\n    has_getter = False\n    has_setter = False\n\n    for rust_file in rust_files:\n        try:\n            with open(rust_file, \"r\", encoding=\"utf-8\") as f:\n                content = f.read()\n\n                if re.search(getter_pattern, content):\n                    has_getter = True\n                if re.search(setter_pattern, content):\n                    has_setter = True\n\n                # Early exit if both found\n                if has_getter and has_setter:\n                    return True, True\n        except Exception as e:\n            print(f\"Warning: Could not read {rust_file}: {e}\")\n            continue\n\n    return has_getter, has_setter\n\n\ndef main() -> None:\n    script_dir = Path(__file__).parent\n    project_root = script_dir.parent\n\n    if len(sys.argv) < 2:\n        print(\"Usage: python find_unused_settings_properties.py <folder>\")\n        print(\"  Example: python find_unused_settings_properties.py krokiet\")\n        print(\"  Example: python find_unused_settings_properties.py cedinia\")\n        sys.exit(1)\n\n    folder = sys.argv[1]\n    settings_file = project_root / folder / \"ui\" / \"settings.slint\"\n\n    if not settings_file.exists():\n        print(f\"Error: Settings file not found at {settings_file}\")\n        return\n\n    print(f\"Reading properties from: {settings_file}\")\n    properties = extract_settings_properties(settings_file)\n    print(f\"Found {len(properties)} properties in settings.slint\\n\")\n\n    print(\"Finding Rust files...\")\n    rust_files = find_rust_files(project_root, folder)\n    print(f\"Found {len(rust_files)} Rust files to check\\n\")\n\n    # Check each property\n    missing_getters = []\n    missing_setters = []\n    missing_both = []\n\n    print(\"Checking property usage in Rust code...\")\n    print(\"-\" * 80)\n\n    for prop_name, prop_type in sorted(properties.items()):\n        has_getter, has_setter = check_property_usage_in_rust(rust_files, prop_name)\n\n        if not has_getter and not has_setter:\n            missing_both.append((prop_name, prop_type))\n        elif not has_getter:\n            missing_getters.append((prop_name, prop_type))\n        elif not has_setter:\n            missing_setters.append((prop_name, prop_type))\n\n    # Print results\n    print(\"\\n\" + \"=\" * 80)\n    print(\"RESULTS\")\n    print(\"=\" * 80)\n\n    if missing_both:\n        print(f\"\\nProperties with NO getter AND NO setter ({len(missing_both)}):\")\n        print(\"-\" * 80)\n        for prop_name, prop_type in missing_both:\n            print(f\"  • {prop_name:<50} <{prop_type}>\")\n\n    if missing_getters:\n        print(f\"\\nProperties with NO getter (but has setter) ({len(missing_getters)}):\")\n        print(\"-\" * 80)\n        for prop_name, prop_type in missing_getters:\n            print(f\"  • {prop_name:<50} <{prop_type}>\")\n\n    if missing_setters:\n        print(f\"\\nProperties with NO setter (but has getter) ({len(missing_setters)}):\")\n        print(\"-\" * 80)\n        for prop_name, prop_type in missing_setters:\n            print(f\"  • {prop_name:<50} <{prop_type}>\")\n\n    if not missing_both and not missing_getters and not missing_setters:\n        print(\"\\nAll properties have both getters and setters!\")\n\n    # Summary\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SUMMARY\")\n    print(\"=\" * 80)\n    print(f\"Total properties:               {len(properties)}\")\n    print(\n        f\"Properties fully used:          {len(properties) - len(missing_both) - len(missing_getters) - len(missing_setters)}\"\n    )\n    print(f\"Properties missing both:        {len(missing_both)}\")\n    print(f\"Properties missing getter only: {len(missing_getters)}\")\n    print(f\"Properties missing setter only: {len(missing_setters)}\")\n    print(\"=\" * 80)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "misc/find_unused_slint_translations.py",
    "content": "import os\nimport sys\n\n\n# Find translations in krokiet/src/translations.slint\n# Check if in other slint files they are used\n# Check if in all items from translations are used in a way f\"set_{}(\" in src/connect_translation.rs\n\n\ndef find_files(root: str, ext: str, folder: str | None) -> list[str]:\n    files = []\n    for dirpath, _, filenames in os.walk(root):\n        for f in filenames:\n            if f.endswith(ext) and (folder is None or folder in dirpath):\n                files.append(os.path.join(dirpath, f))\n    return files\n\n\ndef read_files(files: list[str]) -> str:\n    content = \"\"\n    for f in files:\n        with open(f, \"r\", encoding=\"utf-8\") as file:\n            content += file.read() + \"\\n\"\n    return content\n\n\ndef extract_ftl_keys(ftl_path: str) -> list[str]:\n    keys = []\n    with open(ftl_path, \"r\", encoding=\"utf-8\") as f:\n        lines = f.readlines()\n        for line in lines:\n            line = line.strip()\n            if \"=\" not in line:\n                continue\n            key = line.split(\"=\")[0].strip()\n            keys.append(key)\n    return keys\n\n\ndef extract_slint_properties(slint_path: str) -> list[str]:\n    properties = []\n    with open(slint_path, \"r\", encoding=\"utf-8\") as f:\n        lines = f.readlines()\n        for slint_line in lines:\n            if \"property \" not in slint_line:\n                continue\n            properties.append(slint_line.split(\">\")[1].split(\":\")[0].strip())\n    properties.sort()\n    return properties\n\n\ndef format_green(text: str) -> str:\n    return f\"\\033[92m{text}\\033[0m\"\n\n\nif len(sys.argv) < 2:\n    print(\"Usage: python find_unused_slint_translations.py <folder>\")\n    sys.exit(1)\n\nfolder = sys.argv[1]\n\n# Support both krokiet (connect_translation.rs) and cedinia (translations.rs)\ntranslation_rs_candidates = [\n    f\"{folder}/src/connect_translation.rs\",\n    f\"{folder}/src/translations.rs\",\n]\nrust_translation_file = None\nfor candidate in translation_rs_candidates:\n    if os.path.exists(candidate):\n        rust_translation_file = candidate\n        break\n\nif rust_translation_file is None:\n    print(f\"Error: Could not find a translation Rust file in {folder}/src/ (tried: {translation_rs_candidates})\")\n    sys.exit(1)\n\nrust_translation_content = open(rust_translation_file, \"r\", encoding=\"utf-8\").read()\n\nmissing_in_slint = []\n\nslint_files = find_files(folder, \".slint\", folder)\nassert any([file for file in slint_files if \"translations.slint\" in file]), (\n    \"No translations.slint found in krokiet folder\"\n)\nslint_files = [file for file in slint_files if \"translations.slint\" not in file]\nslint_files_content = read_files(slint_files)\narguments = extract_slint_properties(f\"{folder}/ui/translations.slint\")\nprint(f\"Found {len(arguments)} arguments in translations.slint\")\n\n# Check if all arguments are used in Slint files\n# Skip arguments that are intentionally set from Rust (set_xxx) and not needed in Slint directly\nfor argument in arguments:\n    if f\"Translations.{argument}\" not in slint_files_content:\n        # If the argument is set from Rust, it's intentionally Rust-side — don't flag it\n        if f\"set_{argument}(\" not in rust_translation_content:\n            missing_in_slint.append(argument)\nmissing_in_slint.sort()\n\nmissing_in_rust = []\nfor argument in arguments:\n    if f\"set_{argument}(\" not in rust_translation_content:\n        missing_in_rust.append(argument)\nmissing_in_rust.sort()\n\nif len(missing_in_rust) > 0:\n    print(\n        \"---- Arguments not used in Rust translation file: \" + \", \".join(format_green(arg) for arg in missing_in_rust)\n    )\nif len(missing_in_slint) > 0:\n    print(\n        \"---- Arguments not used in Slint files(you need to bind them in slint like Translation.translation_item or remove): \"\n        + \", \".join(format_green(arg) for arg in missing_in_slint)\n    )\n\nif len(missing_in_slint) > 0 or len(missing_in_rust) > 0:\n    sys.exit(1)\n"
  },
  {
    "path": "misc/flathub.sh",
    "content": "#!/bin/bash\nrm -rf flatpak\nuv venv -p 3.11\nuv pip install aiohttp toml tomlkit # Or sudo apt install python3-aiohttp python3-toml python3-tomlkit\nwget https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py\nmkdir flatpak\nuv run python3 flatpak-cargo-generator.py ./Cargo.lock -o flatpak/cargo-sources.json\nrm flatpak-cargo-generator.py\n"
  },
  {
    "path": "misc/gen_android_icons.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\ngen_android_icons.py – Generate cedinia Android icon files from an SVG.\n\nReads:\n  cedinia/icons/logo.svg          (or a path you pass as the first argument)\n\nWrites:\n  cedinia/res/mipmap-mdpi/ic_launcher.png        48 × 48\n  cedinia/res/mipmap-hdpi/ic_launcher.png        72 × 72\n  cedinia/res/mipmap-xhdpi/ic_launcher.png       96 × 96\n  cedinia/res/mipmap-xxhdpi/ic_launcher.png     144 × 144\n  cedinia/res/mipmap-xxxhdpi/ic_launcher.png    192 × 192\n\n  cedinia/res/drawable-nodpi/ic_launcher_fg_src.png   432 × 432\n  cedinia/res/drawable/ic_launcher_foreground.xml     (bitmap reference)\n\nThe existing ic_launcher_background.xml and mipmap-anydpi-v26/ic_launcher.xml\nare NOT modified (they already reference the correct resource names).\n\nRequires one of:\n  cairosvg   – pip install cairosvg\n  inkscape   – system package / https://inkscape.org\n\nUsage:\n  python misc/gen_android_icons.py\n  python misc/gen_android_icons.py path/to/custom.svg\n\"\"\"\n\nimport subprocess\nimport sys\nfrom pathlib import Path\n\n# Legacy launcher icon: density → size in pixels\nMIPMAP_SIZES: dict[str, int] = {\n    \"mdpi\": 48,\n    \"hdpi\": 72,\n    \"xhdpi\": 96,\n    \"xxhdpi\": 144,\n    \"xxxhdpi\": 192,\n}\n\n# Adaptive icon foreground PNG: 108 dp × 4 (xxxhdpi scale) = 432 px\nFOREGROUND_SIZE = 432\n\n# Content of the bitmap drawable that wraps our foreground PNG\nFOREGROUND_XML = \"\"\"\\\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<bitmap xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:src=\"@drawable/ic_launcher_fg_src\"\n    android:gravity=\"center\" />\n\"\"\"\n\n# ── renderers ────────────────────────────────────────────────────────────────\n\n\ndef render_cairosvg(svg: Path, out: Path, size: int) -> None:\n    import cairosvg  # type: ignore\n\n    out.parent.mkdir(parents=True, exist_ok=True)\n    cairosvg.svg2png(url=str(svg), write_to=str(out), output_width=size, output_height=size)\n\n\ndef render_inkscape(svg: Path, out: Path, size: int) -> None:\n    out.parent.mkdir(parents=True, exist_ok=True)\n    # Inkscape 1.x uses --export-filename; 0.9x used --export-png.\n    result = subprocess.run(\n        [\n            \"inkscape\",\n            str(svg),\n            f\"--export-filename={out}\",\n            f\"--export-width={size}\",\n            f\"--export-height={size}\",\n        ],\n        capture_output=True,\n        text=True,\n    )\n    if result.returncode != 0:\n        # Fallback: try the old --export-png flag (Inkscape 0.9x)\n        result2 = subprocess.run(\n            [\n                \"inkscape\",\n                str(svg),\n                f\"--export-png={out}\",\n                f\"--export-width={size}\",\n                f\"--export-height={size}\",\n            ],\n            capture_output=True,\n            text=True,\n        )\n        if result2.returncode != 0:\n            raise RuntimeError(f\"inkscape failed (exit {result.returncode}):\\n{result.stderr}\\n{result2.stderr}\")\n\n\n# ── main ─────────────────────────────────────────────────────────────────────\n\n\ndef main() -> None:\n    script_dir = Path(__file__).resolve().parent\n    project_root = script_dir.parent  # misc/../ = czkawka/\n\n    if len(sys.argv) >= 2:\n        svg = Path(sys.argv[1]).resolve()\n    else:\n        svg = project_root / \"cedinia\" / \"icons\" / \"logo.svg\"\n\n    if not svg.exists():\n        print(f\"error: SVG not found: {svg}\", file=sys.stderr)\n        print(\"Usage: python misc/gen_android_icons.py [path/to/logo.svg]\", file=sys.stderr)\n        sys.exit(1)\n\n    res_dir = project_root / \"cedinia\" / \"res\"\n\n    # Pick renderer\n    try:\n        import cairosvg\n\n        render = render_cairosvg\n        print(\"renderer: cairosvg\")\n    except ImportError:\n        render = render_inkscape\n        print(\"renderer: inkscape  (cairosvg not found – install with: pip install cairosvg)\")\n\n    print(f\"source  : {svg}\")\n    print()\n\n    # Legacy mipmap PNGs\n    for density, size in MIPMAP_SIZES.items():\n        out = res_dir / f\"mipmap-{density}\" / \"ic_launcher.png\"\n        render(svg, out, size)\n        print(f\"  {str(out.relative_to(project_root)): <60}  {size}×{size} px\")\n\n    print()\n\n    # Adaptive icon foreground PNG (drawable-nodpi so the system uses it unscaled)\n    fg_png = res_dir / \"drawable-nodpi\" / \"ic_launcher_fg_src.png\"\n    render(svg, fg_png, FOREGROUND_SIZE)\n    print(f\"  {str(fg_png.relative_to(project_root)): <60}  {FOREGROUND_SIZE}×{FOREGROUND_SIZE} px\")\n\n    # Adaptive icon foreground XML (bitmap wrapper)\n    fg_xml = res_dir / \"drawable\" / \"ic_launcher_foreground.xml\"\n    fg_xml.parent.mkdir(parents=True, exist_ok=True)\n    fg_xml.write_text(FOREGROUND_XML, encoding=\"utf-8\")\n    print(f\"  {str(fg_xml.relative_to(project_root)): <60}  (bitmap drawable)\")\n\n    print()\n    print(\"Done.\")\n    print()\n    print(\"Note: ic_launcher_background.xml and mipmap-anydpi-v26/ic_launcher.xml\")\n    print(\"      are NOT modified – they already reference the correct resource names.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "misc/nix/flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.11-small\";\n\n    rust-overlay = {\n      url = \"github:oxalica/rust-overlay\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n\n    flake-utils.url = \"github:numtide/flake-utils\";\n    crane.url = \"github:ipetkov/crane\";\n  };\n\n  outputs = { self, nixpkgs, rust-overlay, flake-utils, crane, ... }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        overlays = [ (import rust-overlay) ];\n        pkgs = import nixpkgs {\n          inherit system overlays;\n        };\n        cargoToml = (builtins.fromTOML (builtins.readFile ../../czkawka_core/Cargo.toml));\n      in\n      {\n        packages = (import ./packages.nix { \n          inherit self pkgs crane;\n          msrvRust = pkgs.rust-bin.stable.${cargoToml.package.rust-version}.minimal;\n          buildInputs = with pkgs; [\n            atk\n            cairo\n            gdk-pixbuf\n            glib\n            gtk4\n            pango\n          ];\n          nativeBuildInputs = with pkgs; [\n            pkg-config\n            gobject-introspection\n            gsettings-desktop-schemas\n            wrapGAppsHook4\n          ];\n        });\n      }\n    );\n}\n"
  },
  {
    "path": "misc/nix/packages.nix",
    "content": "{ self, pkgs, crane, msrvRust, buildInputs, nativeBuildInputs }:\nlet\n  craneLib = (crane.mkLib pkgs).overrideToolchain (p: msrvRust);\n  src = ../..;\n  doCheck = false;\nin\nrec {\n  default = czkawka-gui-wayland;\n  czkawka-gui = let\n    cargoToml = \"${self}/../../czkawka_gui/Cargo.toml\";\n    cargoTomlConfig = builtins.fromTOML (builtins.readFile cargoToml);\n    version = cargoTomlConfig.package.version;\n  in\n  craneLib.buildPackage {\n    inherit version src cargoToml buildInputs nativeBuildInputs doCheck;\n    name = \"czkawka-gui\";\n    cargoExtraArgs = \"--bin czkawka_gui\";\n    cargoArtifacts = craneLib.buildDepsOnly {\n      inherit version src cargoToml buildInputs nativeBuildInputs doCheck;\n      name = \"czkawka-gui\";\n      cargoExtraArgs  = \"--bin czkawka_gui\";\n    };\n  };\n  wrapped-czkawka-gui = pkgs.writeShellScriptBin \"wrapped-czkawka-gui\" ''\n    export GSETTINGS_SCHEMA_DIR =\"${pkgs.gtk4}/share/gsettings-schemas/gtk4-${pkgs.gtk4.version}/glib-2.0/schemas\";\n    exec ${czkawka-gui}/bin/czkawka_gui \"$@\"\n  '';\n  czkawka-gui-wayland = let\n    cargoToml = \"${self}/../../czkawka_gui/Cargo.toml\";\n    cargoTomlConfig = builtins.fromTOML (builtins.readFile cargoToml);\n    version = cargoTomlConfig.package.version;\n    waylandBuildInputs = buildInputs ++ [ pkgs.wayland ];\n  in\n  craneLib.buildPackage {\n    inherit version src cargoToml nativeBuildInputs doCheck;\n    buildInputs = waylandBuildInputs;\n    name = \"czkawka-gui\";\n    cargoExtraArgs = \"--bin czkawka_gui\";\n    cargoArtifacts = craneLib.buildDepsOnly {\n      inherit version src cargoToml nativeBuildInputs doCheck;\n      name = \"czkawka-gui\";\n      cargoExtraArgs  = \"--bin czkawka_gui\";\n    };\n  };\n  czkawka-cli = let\n    cargoToml = \"${self}/../../czkawka_cli/Cargo.toml\";\n    cargoTomlConfig = builtins.fromTOML (builtins.readFile cargoToml);\n    version = cargoTomlConfig.package.version;\n  in\n  craneLib.buildPackage {\n    inherit version src cargoToml doCheck;\n    buildInputs = [];\n    nativeBuildInputs = [];\n    name = \"czkawka-cli\";\n    cargoExtraArgs = \"--bin czkawka_cli\";\n    cargoArtifacts = craneLib.buildDepsOnly {\n      inherit version src cargoToml buildInputs nativeBuildInputs doCheck;\n      name = \"czkawka-cli\";\n      cargoExtraArgs  = \"--bin czkawka_cli\";\n    };\n  };\n}\n"
  },
  {
    "path": "misc/remove_comments.py",
    "content": "#!/usr/bin/env python3\n\nimport os\nimport sys\nfrom pathlib import Path\n\nif len(sys.argv) < 2:\n    print(\"Usage: python remove_comments.py <path>\")\n    sys.exit(1)\n\nTARGET_DIR = Path(sys.argv[1]).resolve()\n\n\ndef remove_comments_from_text(s: str) -> str:\n    lines = s.splitlines(keepends=True)\n    # Pre-pass: drop full-line // comments (first non-whitespace is //)\n    preprocessed = []\n    for line in lines:\n        stripped = line.lstrip()\n        if stripped.startswith(\"//\"):\n            # keep newline if present (preserve line count)\n            if line.endswith(\"\\n\"):\n                preprocessed.append(\"\\n\")\n            else:\n                preprocessed.append(\"\")\n        else:\n            preprocessed.append(line)\n    lines = preprocessed\n\n    out_lines = []\n    block_depth = 0\n\n    # helper to detect raw string start at position i in a line\n    def raw_start_at(line: str, i: int) -> int | None:\n        # match r#*\" or br#*\"\n        if i < 0 or i >= len(line):\n            return None\n        j = i\n        if line[j] == \"b\":\n            j += 1\n            if j >= len(line) or line[j] != \"r\":\n                return None\n        if line[j] != \"r\":\n            return None\n        j += 1\n        k = j\n        while k < len(line) and line[k] == \"#\":\n            k += 1\n        if k < len(line) and line[k] == '\"':\n            return k - j  # number of hashes\n        return None\n\n    for line in lines:\n        i = 0\n        n = len(line)\n        if block_depth > 0:\n            # we are inside a block comment that started earlier; scan for end\n            new_line_parts = []\n            while i < n:\n                if line.startswith(\"/*\", i):\n                    block_depth += 1\n                    i += 2\n                elif line.startswith(\"*/\", i):\n                    block_depth -= 1\n                    i += 2\n                    if block_depth == 0:\n                        # rest of line should be processed normally\n                        new_line_parts.append(line[i:])\n                        break\n                else:\n                    i += 1\n            out_lines.append(\"\".join(new_line_parts))\n            continue\n\n        out = []\n        while i < n:\n            ch = line[i]\n            # detect block comment start\n            if line.startswith(\"/*\", i):\n                block_depth = 1\n                i += 2\n                # consume until end of block (possibly multi-line)\n                while i < n:\n                    if line.startswith(\"/*\", i):\n                        block_depth += 1\n                        i += 2\n                    elif line.startswith(\"*/\", i):\n                        block_depth -= 1\n                        i += 2\n                        break\n                    else:\n                        i += 1\n                if block_depth > 0:\n                    # block continues to next lines; stop processing this line\n                    break\n                else:\n                    continue\n            # detect line comment\n            if line.startswith(\"//\", i):\n                # drop rest of the line\n                break\n            # detect raw string\n            raw = raw_start_at(line, i)\n            if raw is not None:\n                # copy raw string start\n                start = i\n                # find the closing \" followed by same number of hashes\n                hashes = raw\n                # move i to the char after opening quote\n                j = i\n                if line[j] == \"b\":\n                    j += 1\n                j += 1  # skip 'r'\n                while j < n and line[j] == \"#\":\n                    j += 1\n                # now j points at opening quote\n                i = j + 1\n                # scan until closing\n                while True:\n                    rest = line[i:]\n                    idx = rest.find('\"' + (\"#\" * hashes))\n                    if idx != -1:\n                        endpos = i + idx + 1 + hashes\n                        out.append(line[start:endpos])\n                        i = endpos\n                        break\n                    else:\n                        out.append(line[start:])\n                        i = n\n                        break\n                continue\n            # detect normal string\n            if ch == '\"':\n                out.append(ch)\n                i += 1\n                while i < n:\n                    out.append(line[i])\n                    if line[i] == \"\\\\\":\n                        if i + 1 < n:\n                            out.append(line[i + 1])\n                            i += 2\n                        else:\n                            i += 1\n                    elif line[i] == '\"':\n                        i += 1\n                        break\n                    else:\n                        i += 1\n                continue\n            # detect char\n            if ch == \"'\":\n                out.append(ch)\n                i += 1\n                while i < n:\n                    out.append(line[i])\n                    if line[i] == \"\\\\\":\n                        if i + 1 < n:\n                            out.append(line[i + 1])\n                            i += 2\n                        else:\n                            i += 1\n                    elif line[i] == \"'\":\n                        i += 1\n                        break\n                    else:\n                        i += 1\n                continue\n            # default copy\n            out.append(ch)\n            i += 1\n        out_lines.append(\"\".join(out))\n    return \"\".join(out_lines)\n\n\ndef process_file(path: Path) -> bool:\n    text = path.read_text(encoding=\"utf-8\")\n    new_text = remove_comments_from_text(text)\n    if new_text != text:\n        path.write_text(new_text, encoding=\"utf-8\")\n        print(f\"Updated {path} (overwritten, no backup)\")\n        return True\n    else:\n        print(f\"No changes: {path}\")\n        return False\n\n\ndef main() -> None:\n    if not TARGET_DIR.exists():\n        print(\"Target directory not found:\", TARGET_DIR)\n        sys.exit(1)\n    rs_files = list(TARGET_DIR.rglob(\"*.rs\"))\n    if not rs_files:\n        print(\"No .slint files found under\", TARGET_DIR)\n        sys.exit(0)\n    changed = 0\n    for f in rs_files:\n        try:\n            if process_file(f):\n                changed += 1\n        except Exception as e:\n            print(\"ERROR processing\", f, e)\n    print(f\"Completed. Files modified: {changed}/{len(rs_files)}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "misc/run_checks.sh",
    "content": "#!/usr/bin/env bash\n\ncmds=(\n    \"python3 misc/delete_unused_krokiet_slint_imports.py krokiet\"\n    \"python3 misc/delete_unused_krokiet_slint_imports.py cedinia\"\n    \"python3 misc/find_unused_fluent_translations.py czkawka_gui\"\n    \"python3 misc/find_unused_fluent_translations.py krokiet\"\n    \"python3 misc/find_unused_fluent_translations.py cedinia\"\n    \"python3 misc/find_unused_fluent_translations.py czkawka_core\"\n    \"python3 misc/find_unused_slint_translations.py krokiet\"\n    \"python3 misc/find_unused_slint_translations.py cedinia\"\n    \"python3 misc/find_unused_callbacks.py krokiet\"\n    \"python3 misc/find_unused_settings_properties.py krokiet\"\n    \"python3 misc/find_unused_settings_properties.py cedinia\"\n)\n\nfailed=\"\"\nfor cmd in \"${cmds[@]}\"; do\n    out=$(eval \"$cmd\" 2>&1)\n    if [ $? -ne 0 ]; then\n        failed+=\"=== FAILED: $cmd ===\\n$out\\n\\n\"\n    fi\ndone\n\nif [ -n \"$failed\" ]; then\n    echo -e \"$failed\"\n    exit 1\nfi\n\n"
  },
  {
    "path": "misc/simplify_and_minify_svg.py",
    "content": "import subprocess\nfrom pathlib import Path\nimport sys\n\n# Required apps:\n# - inkscape\n# - svgcleaner - https://crates.io/crates/svgcleaner\n\nFAKE_RUN = False\n\n\ndef run_cmd(cmd: list[str], cwd: Path | None = None) -> bool:\n    print(f\"Running: {' '.join(cmd)}\")\n    if FAKE_RUN:\n        return True\n    result = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n    if result.returncode != 0:\n        print(f\"Error: {result.stderr}\")\n    return result.returncode == 0\n\n\ndef simplify_svg(svg_path: Path) -> None:\n    run_cmd(\n        [\"inkscape\", str(svg_path), \"--export-type=svg\", \"--export-text-to-path\", \"--export-filename=\" + str(svg_path)]\n    )\n\n    cleaned_path = svg_path.with_suffix(\".cleaned.svg\")\n    if run_cmd([\"svgcleaner\", str(svg_path), str(cleaned_path)]):\n        if not FAKE_RUN:\n            svg_path.unlink()\n            cleaned_path.rename(svg_path)\n    elif cleaned_path.exists():\n        if not FAKE_RUN:\n            cleaned_path.unlink()\n\n\ndef main() -> None:\n    # First arg is folder to search for svg files\n    if len(sys.argv) < 2:\n        print(\"Usage: python simplify_and_minify_svg.py <folder>\")\n        return\n    if FAKE_RUN:\n        print(\"FAKE_RUN is enabled, no changes will be made to files.\")\n\n    svg_folder = Path(sys.argv[1])\n    svg_files = list(svg_folder.glob(\"**/*.svg\"))\n\n    disabled_contains_names = [\"krokiet_logo\"]\n    svg_files = [\n        svg_file for svg_file in svg_files if not any(name in svg_file.name for name in disabled_contains_names)\n    ]\n\n    if not svg_files:\n        print(f\"No SVG files found in the specified directory: {svg_folder}\")\n        return\n\n    for svg_file in svg_files:\n        simplify_svg(svg_file)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "misc/test_compilation_speed_size/Cargo.toml",
    "content": "[package]\nname = \"test_compilation_speed_size\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nwalkdir = \"2.5.0\"\nhumansize = \"2.1\"\nserde = { version = \"1.0.219\", features = [\"derive\"] }\nserde_json = \"1.0.142\"\nstrum = { version = \"0.27.2\", features = [\"strum_macros\"] }\nplotters = { version = \"0.3.7\", features = [\"all_elements\", \"all_series\"] }\n\n[profile.release]\npanic = \"unwind\"\noverflow-checks = true\ndebug = true"
  },
  {
    "path": "misc/test_compilation_speed_size/README.md",
    "content": "# Test compilation speed size\nThis project is used to get the compilation speed and size of generated rust binaries for different configurations.\n\n## How to use?\n- install app `cargo install --path .`\n- configure json config, just like `test.json` or `krokiet.json` - allowed values you can find in `src/model.rs`\n- go to your project root(if project is in workspace, you need to go to the workspace root)\n- run `test_compilation_speed_size config.json`\n- fix compilation errors if any happens(mixing some compilation flags can cause compilation errors)\n- install python dependencies with `sudo apt install python3-matplotlib python3-pandas python3-tabulate` or similar command\n- generate charts with `python3 generate_md_and_plots.py`\n- generated md and png files will be in `charts` folder"
  },
  {
    "path": "misc/test_compilation_speed_size/generate_md_and_plots.py",
    "content": "# sudo apt install python3-matplotlib python3-pandas python3-tabulate\nfrom typing import Any\n\nimport pandas as pd\nimport matplotlib.pyplot as plt  # type: ignore[import-not-found]\nimport matplotlib\nimport os\n\nfrom pandas import Series\n\ndf = pd.read_csv(\"compilation_results.txt\", sep=\"|\", engine=\"python\")\ndf = df.apply(lambda x: x.str.strip() if x.dtype == \"object\" else x)\n\ndf[\"Output File Size (bytes)\"] = pd.to_numeric(df[\"Output File Size(in bytes)\"], errors=\"coerce\")\ndf[\"Target Folder Size (bytes)\"] = pd.to_numeric(df[\"Target Folder Size(in bytes)\"], errors=\"coerce\")\ndf[\"Compilation Time (seconds)\"] = pd.to_numeric(df[\"Compilation Time(seconds)\"], errors=\"coerce\")\ndf[\"Rebuild Time (seconds)\"] = pd.to_numeric(df[\"Rebuild Time(seconds)\"], errors=\"coerce\")\n\nmatplotlib.rcParams[\"font.family\"] = \"Noto Sans\"\nFONT_SIZE = 13\nmatplotlib.rcParams.update(\n    {\n        \"font.size\": FONT_SIZE,\n        \"axes.titlesize\": FONT_SIZE,\n        \"axes.labelsize\": FONT_SIZE,\n        \"xtick.labelsize\": FONT_SIZE,\n        \"ytick.labelsize\": FONT_SIZE,\n        \"legend.fontsize\": FONT_SIZE,\n        \"figure.titlesize\": FONT_SIZE,\n    }\n)\n\nos.makedirs(\"charts\", exist_ok=True)\n\n\ndef bold_labels(labels: Series[Any]) -> list[str]:\n    return [\n        r\"$\\bf{\" + str(label) + \"}$\"\n        if (\"debug\" == str(label).lower() or \"release\" == str(label).lower())\n        else str(label)\n        for label in labels\n    ]\n\n\ndef plot_barh(\n    df: pd.DataFrame,\n    value_col: str,\n    xlabel: str,\n    title: str,\n    filename: str,\n    fmt: str = \"{:.1f}\",\n    unit_div: float = 1,\n    label_fmt: str | None = None,\n    dropna: bool = False,\n    color: str = \"C0\",\n    config_col: str = \"BuildConfig\",\n) -> None:\n    data = df\n    if dropna:\n        data = data.dropna(subset=[value_col])\n    data_sorted = data.sort_values(value_col, ascending=False)\n\n    plt.figure(figsize=(12, 10), dpi=300)\n    labels = bold_labels(data_sorted[config_col])\n    bars = plt.barh(labels, data_sorted[value_col] / unit_div, color=color)\n\n    ax = plt.gca()\n    max_val = (data_sorted[value_col] / unit_div).max()\n    if max_val is None or max_val == 0:\n        x_max = 1\n    else:\n        x_max = max_val * 1.15\n    ax.set_xlim(0, x_max)\n\n    plt.xlabel(xlabel)\n    plt.title(title)\n\n    if label_fmt is None:\n        label_fmt = fmt\n    plt.bar_label(bars, fmt=label_fmt, color=\"black\", padding=5)\n\n    plt.tight_layout()\n    plt.savefig(filename)\n    plt.close()\n\n\nplot_barh(\n    df,\n    \"Compilation Time (seconds)\",\n    \"Compilation Time (seconds)\",\n    \"Compilation Time by Config\",\n    \"charts/compilation_time.png\",\n    fmt=\"{:.1f}s\",\n)\nplot_barh(\n    df,\n    \"Rebuild Time (seconds)\",\n    \"Rebuild Time (seconds)\",\n    \"Rebuild Time by Config\",\n    \"charts/rebuild_time.png\",\n    fmt=\"{:.1f}s\",\n)\nplot_barh(\n    df,\n    \"Output File Size (bytes)\",\n    \"Output File Size (MB)\",\n    \"Output File Size by Config\",\n    \"charts/output_file_size.png\",\n    fmt=\"{:.1f} MB\",\n    unit_div=1024**2,\n    dropna=True,\n)\nplot_barh(\n    df,\n    \"Target Folder Size (bytes)\",\n    \"Target Folder Size (GB)\",\n    \"Target Folder Size by Config\",\n    \"charts/target_folder_size.png\",\n    fmt=\"{:.1f} GB\",\n    unit_div=1024**3,\n)\n\ncolumns = [col for col in df.columns if \"(\" not in col]\nwith open(\"charts/compilation_results.md\", \"w\") as f:\n    f.write(df[columns].to_markdown(index=False))\n"
  },
  {
    "path": "misc/test_compilation_speed_size/src/main.rs",
    "content": "mod model;\nmod new_chart;\n\nuse crate::model::{\n    BuildConfig, BuildOrCheck,  Config,  Panic, Project, Results,\n};\nuse std::fs;\nuse std::fs::OpenOptions;\nuse std::io::Write;\nuse std::path::Path;\nuse std::process::exit;\nuse walkdir::WalkDir;\n// use crate::new_chart::create_chart;\n\nconst PROFILE_NAME: &str = \"fff\";\nconst RESULTS_FILE_NAME: &str = \"compilation_results.txt\";\n\nfn main() {\n    // create_chart(); // TODO currently is broken a little\n    let Some(first_arg) = std::env::args().nth(1) else {\n        eprintln!(\"Please provide a path to the configuration json file as the first argument.\");\n        exit(1);\n    };\n\n    let cargo_toml_path = Path::new(\"Cargo.toml\");\n    if !cargo_toml_path.is_file() {\n        eprintln!(\"Cannot find Cargo.toml in the current directory. Please run this script from the root cargo directory(must be able to modify profiles).\");\n        exit(1);\n    }\n\n    clean_changes_to_project_files(\"Cargo.toml\");\n\n    let Ok(cargo_toml_content) = fs::read_to_string(&cargo_toml_path) else {\n        eprintln!(\"Could not read content of Cargo.toml file\");\n        exit(1);\n    };\n\n    let Ok(config_json_content) = fs::read_to_string(&first_arg) else {\n        eprintln!(\"Could not read content of the provided json file: {}\", first_arg);\n        exit(1);\n    };\n\n    let mut config = match serde_json::from_str::<Config>(&config_json_content) {\n        Ok(c) => c,\n        Err(e) => {\n            eprintln!(\"Could not parse content of the provided json file: {}. Error: {}\", first_arg, e);\n            exit(1);\n        }\n    };\n\n    let mut results_file = OpenOptions::new()\n        .write(true)\n        .create(true)\n        .truncate(true)\n        .open(Path::new(RESULTS_FILE_NAME))\n        .expect(\"Could not open results file\");\n\n    Results::write_header_to_file(&mut results_file).unwrap();\n\n    config.build_config_converted = config.build_config.clone().into_iter().map(|e| e.into()).collect();\n    let mut all_configs = Vec::new();\n    for build_config in &config.build_config_converted {\n        all_configs.push(build_config.clone());\n    }\n\n    println!(\"Found {} configurations to test\", all_configs.len());\n\n    // let mut results = Vec::new();\n    for build_config in all_configs {\n        let new_cargo_toml_content = format!(\"{cargo_toml_content}\\n\\n[profile.{PROFILE_NAME}]\\n{}\\n\", build_config.to_str());\n        fs::write(&cargo_toml_path, new_cargo_toml_content).expect(\"Could not write Cargo.toml file\");\n        let result = check_compilation_speed_and_size(&build_config, &config.project);\n        // results.push(result.clone());\n        result.save_to_file(&mut results_file).expect(\"Could not save results to file\");\n    }\n\n    fs::write(&cargo_toml_path, cargo_toml_content).expect(\"Could not restore content of Cargo.toml file\");\n}\n\nfn clean_cargo() {\n    println!(\"Cleaning cargo...\");\n    let output = std::process::Command::new(\"cargo\")\n        .arg(\"clean\")\n        .stdout(std::process::Stdio::inherit())\n        .stderr(std::process::Stdio::inherit())\n        .output()\n        .expect(\"Failed to execute cargo clean\");\n\n    if !output.status.success() {\n        panic!(\"Cargo clean failed: {}\", String::from_utf8_lossy(&output.stderr));\n    }\n}\n\nfn run_cargo_build(build_config: &BuildConfig, project: &Project) {\n    let build_check = if build_config.build_or_check == BuildOrCheck::Build { \"build\" } else { \"check\" };\n    let mut command = std::process::Command::new(\"cargo\");\n    command.arg(\"+nightly\");\n    if build_config.cranelift {\n        command.env(\"CARGO_PROFILE_DEV_CODEGEN_BACKEND\", \"cranelift\");\n        command.env(\"RUSTUP_TOOLCHAIN\", \"nightly\");\n        command.arg(\"-Zcodegen-backend\");\n    }\n    let mut rust_flags = None;\n    // TODO - not works currently\n    // if mold {\n    //     let to_add = \"-C link-arg=-fuse-ld=mold\";\n    //     rust_flags = match rust_flags {\n    //         None => Some(to_add.to_string()),\n    //         Some(flags) => Some(format!(\"{flags} {to_add}\")),\n    //     };\n    // }\n    if build_config.build_std {\n        if build_config.panic == Panic::Abort {\n            command.args([\"-Z\", \"build-std=std,panic_abort\"]);\n        } else {\n            command.args([\"-Z\", \"build-std=std\"]);\n        }\n    }\n    if build_config.native {\n        let to_add = \"-C target-cpu=native\";\n        rust_flags = match rust_flags {\n            None => Some(to_add.to_string()),\n            Some(flags) => Some(format!(\"{flags} {to_add}\")),\n        };\n    }\n\n    if let Some(rust_flags) = rust_flags {\n        command.env(\"RUSTFLAGS\", rust_flags);\n    }\n\n    // if threads_number > 0 {\n    // Looks that not all steps uses this variable - but I may be wrong\n    //     command.env(\"CARGO_BUILD_JOBS\", threads_number.to_string());\n    // }\n\n    command\n        .arg(build_check)\n        .arg(\"--bin\")\n        .arg(&project.name)\n        .arg(\"--profile\")\n        .arg(PROFILE_NAME)\n        .stdout(std::process::Stdio::inherit())\n        .stderr(std::process::Stdio::inherit());\n\n    println!(\"Running cargo command: {:?}\", command);\n\n    let output = command.output().expect(\"Failed to execute cargo build\");\n\n    if !output.status.success() {\n        panic!(\"Cargo build failed: {}\", String::from_utf8_lossy(&output.stderr));\n    }\n}\n\nfn clean_changes_to_project_files(path: &str) {\n    let clean_command = std::process::Command::new(\"git\")\n        .arg(\"checkout\")\n        .arg(path)\n        .stdout(std::process::Stdio::inherit())\n        .stderr(std::process::Stdio::inherit())\n        .output()\n        .expect(\"Failed to execute git checkout\");\n    if !clean_command.status.success() {\n        panic!(\"Git checkout failed: {}\", String::from_utf8_lossy(&clean_command.stderr));\n    }\n}\n\nfn add_empty_line_to_file(project: &Project) {\n    let file_path = Path::new(&project.path_to_main_rs_file);\n    let mut file = OpenOptions::new().append(true).open(&file_path).expect(\"Could not open main.rs file\");\n    if let Err(e) = writeln!(file, \"// Absolutely nothing\") {\n        panic!(\"Could not write to main.rs file: {}\", e);\n    }\n}\n\nfn check_compilation_speed_and_size(build_config: &BuildConfig, project: &Project) -> Results {\n    clean_cargo();\n    clean_changes_to_project_files(&project.path_to_clean_with_git);\n\n    let start_time = std::time::Instant::now();\n\n    println!(\"Running cargo build for project: {}\", project.name);\n    println!(\"Build_config: {}\", build_config.to_string_short());\n\n    run_cargo_build(build_config, project);\n\n    let compilation_time = start_time.elapsed();\n\n    let output_file_size = get_size_of_output_file(project);\n    let target_folder_size = get_size_of_target_folder();\n\n    add_empty_line_to_file(project);\n\n    let rebuild_time_start = std::time::Instant::now();\n    run_cargo_build(build_config, project);\n    let rebuild_time = rebuild_time_start.elapsed();\n\n    clean_cargo();\n    clean_changes_to_project_files(&project.path_to_clean_with_git);\n\n    Results {\n        output_file_size,\n        target_folder_size,\n        compilation_time,\n        build_config: build_config.clone(),\n        project: project.clone(),\n        rebuild_time,\n    }\n}\n\nfn get_size_of_output_file(project: &Project) -> u64 {\n    let output_path = Path::new(\"target\").join(PROFILE_NAME).join(&project.name);\n    if output_path.exists() {\n        output_path.metadata().map(|e| e.len()).unwrap_or_default()\n    } else {\n        0\n    }\n}\n\nfn get_size_of_target_folder() -> u64 {\n    let target_path = Path::new(\"target\");\n    get_size_of_files_in_folder(&target_path)\n}\n\nfn get_size_of_files_in_folder(folder: &Path) -> u64 {\n    WalkDir::new(folder)\n        .max_depth(999)\n        .into_iter()\n        .flatten()\n        .map(|e| e.metadata().map(|e| e.len()).unwrap_or_default())\n        .sum()\n}\n"
  },
  {
    "path": "misc/test_compilation_speed_size/src/model.rs",
    "content": "use std::io::Write;\nuse std::time::Duration;\n\nuse humansize::{BINARY, format_size};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct Config {\n    pub project: Project,\n    pub build_config: Vec<BuildConfigRead>,\n    #[serde(skip)]\n    pub build_config_converted: Vec<BuildConfig>,\n}\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct Project {\n    pub name: String,\n    pub path_to_main_rs_file: String,\n    pub path_to_clean_with_git: String,\n}\n\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct BuildConfigRead {\n    pub name: String,\n    pub rust_base_config: RustBaseConfig,\n    pub lto: Option<Lto>,\n    pub debug: Option<Debugg>,\n    pub opt_level: Option<OptLevel>,\n    pub build_or_check: Option<BuildOrCheck>,\n    pub codegen_units: Option<CodegenUnits>,\n    pub panic: Option<Panic>,\n    pub split_debug: Option<SplitDebug>,\n    pub overflow_checks: Option<OverflowChecks>,\n    pub incremental: Option<Incremental>,\n    pub build_std: Option<bool>,\n    pub native: Option<bool>,\n    pub cranelift: Option<bool>,\n}\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct BuildConfig {\n    pub name: String,\n    pub rust_base_config: RustBaseConfig,\n    pub lto: Lto,\n    pub debug: Debugg,\n    pub opt_level: OptLevel,\n    pub build_or_check: BuildOrCheck,\n    pub codegen_units: CodegenUnits,\n    pub panic: Panic,\n    pub split_debug: SplitDebug,\n    pub overflow_checks: OverflowChecks,\n    pub incremental: Incremental,\n    pub build_std: bool,\n    pub native: bool,\n    pub cranelift: bool,\n}\n\nimpl From<BuildConfigRead> for BuildConfig {\n    fn from(config: BuildConfigRead) -> Self {\n        let base_config = match config.rust_base_config {\n            RustBaseConfig::Release => BuildConfig {\n                name:\"release\".to_string(),\n                rust_base_config: RustBaseConfig::Release,\n                lto: Lto::Off,\n                debug: Debugg::None,\n                opt_level: OptLevel::Three,\n                build_or_check: BuildOrCheck::Build,\n                codegen_units: CodegenUnits::Sixteen,\n                panic: Panic::Unwind,\n                split_debug: SplitDebug::Off,\n                overflow_checks: OverflowChecks::Off,\n                incremental: Incremental::Off,\n                build_std: false,\n                native: false,\n                cranelift: false\n            },\n            RustBaseConfig::Debug => BuildConfig {\n                name: \"debug\".to_string(),\n                rust_base_config: RustBaseConfig::Debug,\n                lto: Lto::Off,\n                debug: Debugg::Full,\n                opt_level: OptLevel::Zero,\n                build_or_check: BuildOrCheck::Build,\n                codegen_units: CodegenUnits::Default,\n                panic: Panic::Unwind,\n                split_debug: SplitDebug::Off,\n                overflow_checks: OverflowChecks::On,\n                incremental: Incremental::On,\n                build_std: false,\n                native: false,\n                cranelift: false\n            }\n        };\n\n        Self {\n            name: config.name,\n            rust_base_config: base_config.rust_base_config,\n            lto: config.lto.unwrap_or(base_config.lto),\n            debug: config.debug.unwrap_or(base_config.debug),\n            opt_level: config.opt_level.unwrap_or(base_config.opt_level),\n            build_or_check: config.build_or_check.unwrap_or(base_config.build_or_check),\n            codegen_units: config.codegen_units.unwrap_or(base_config.codegen_units),\n            panic: config.panic.unwrap_or(base_config.panic),\n            split_debug: config.split_debug.unwrap_or(base_config.split_debug),\n            overflow_checks: config.overflow_checks.unwrap_or(base_config.overflow_checks),\n            incremental: config.incremental.unwrap_or(base_config.incremental),\n            build_std: config.build_std.unwrap_or(false),\n            native: config.native.unwrap_or(false),\n            cranelift: config.cranelift.unwrap_or(false),\n        }\n    }\n}\n\nimpl BuildConfig {\n    pub fn to_str(&self) -> String {\n        format!(\n            \"{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n\",\n            self.rust_base_config.to_str(),\n            self.lto.to_str(),\n            self.debug.to_str(),\n            self.opt_level.to_str(),\n            self.codegen_units.to_str(),\n            self.panic.to_str(),\n            self.split_debug.to_str(),\n            self.overflow_checks.to_str(),\n            self.incremental.to_str(),\n        )\n    }\n    pub fn to_string_short(&self) -> String {\n        format!(\n            \"LTO: {}, Debug: {}, Opt: {}, Build/Check: {}, Codegen Units: {}, Panic: {}, Split Debug: {}, Overflow Checks: {}, Incremental: {}, Build Std: {}, Cranelift: {}\",\n            self.lto.to_str(),\n            self.debug.to_str(),\n            self.opt_level.to_str(),\n            self.build_or_check.to_str(),\n            self.codegen_units.to_str(),\n            self.panic.to_str(),\n            self.split_debug.to_str(),\n            self.overflow_checks.to_str(),\n            self.incremental.to_str(),\n            self.build_std,\n            self.cranelift\n        )\n    }\n}\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub enum RustBaseConfig {\n    Release,\n    Debug\n}\n\nimpl RustBaseConfig {\n    pub fn to_str(&self) -> &'static str {\n        match self {\n            RustBaseConfig::Release => \"inherits=\\\"release\\\"\",\n            RustBaseConfig::Debug => \"inherits=\\\"dev\\\"\",\n        }\n    }\n}\n\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum Debugg {\n    None,\n    LineDirectivesOnly,\n    LineTablesOnly,\n    Limited,\n    Full,\n}\n\nimpl Debugg {\n    fn to_str(self) -> &'static str {\n        match self {\n            Debugg::None => \"debug=\\\"none\\\"\",\n            Debugg::LineDirectivesOnly => \"debug=\\\"line-directives-only\\\"\",\n            Debugg::LineTablesOnly => \"debug=\\\"line-tables-only\\\"\",\n            Debugg::Limited => \"debug=1\",\n            Debugg::Full => \"debug=2\",\n        }\n    }\n}\n\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum SplitDebug {\n    Off,\n    Packed,\n    Unpacked,\n}\nimpl SplitDebug {\n    fn to_str(self) -> &'static str {\n        match self {\n            SplitDebug::Off => \"split-debuginfo=\\\"off\\\"\",\n            SplitDebug::Packed => \"split-debuginfo=\\\"packed\\\"\",\n            SplitDebug::Unpacked => \"split-debuginfo=\\\"unpacked\\\"\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum OptLevel {\n    Zero,\n    One,\n    Two,\n    Three,\n    S,\n}\nimpl OptLevel {\n    fn to_str(self) -> &'static str {\n        match self {\n            OptLevel::Zero => \"opt-level=0\",\n            OptLevel::One => \"opt-level=1\",\n            OptLevel::Two => \"opt-level=2\",\n            OptLevel::Three => \"opt-level=3\",\n            OptLevel::S => \"opt-level=\\\"s\\\"\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum Lto {\n    Off,\n    Thin,\n    Fat,\n}\n\nimpl Lto {\n    fn to_str(self) -> &'static str {\n        match self {\n            Lto::Off => \"lto=\\\"off\\\"\",\n            Lto::Thin => \"lto=\\\"thin\\\"\",\n            Lto::Fat => \"lto=\\\"fat\\\"\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum BuildOrCheck {\n    Build,\n    Check,\n}\n\nimpl BuildOrCheck {\n    fn to_str(self) -> &'static str {\n        match self {\n            BuildOrCheck::Build => \"build\",\n            BuildOrCheck::Check => \"check\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum CodegenUnits {\n    One,\n    Sixteen,\n    Default,\n}\nimpl CodegenUnits {\n    fn to_str(self) -> &'static str {\n        match self {\n            CodegenUnits::One => \"codegen-units=1\",\n            CodegenUnits::Sixteen => \"codegen-units=16\",\n            CodegenUnits::Default => \"codegen-units=256\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum Panic {\n    Unwind,\n    Abort,\n}\nimpl Panic {\n    fn to_str(self) -> &'static str {\n        match self {\n            Panic::Unwind => \"panic=\\\"unwind\\\"\",\n            Panic::Abort => \"panic=\\\"abort\\\"\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum OverflowChecks {\n    On,\n    Off,\n}\nimpl OverflowChecks {\n    fn to_str(self) -> &'static str {\n        match self {\n            OverflowChecks::On => \"overflow-checks=true\",\n            OverflowChecks::Off => \"overflow-checks=false\",\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq,  Serialize, Deserialize)]\npub enum Incremental {\n    On,\n    Off,\n}\nimpl Incremental {\n    fn to_str(self) -> &'static str {\n        match self {\n            Incremental::On => \"incremental=true\",\n            Incremental::Off => \"incremental=false\",\n        }\n    }\n}\n\n\n\npub struct Results {\n    pub output_file_size: u64,\n    pub target_folder_size: u64,\n    pub compilation_time: Duration,\n    pub build_config: BuildConfig,\n    pub project: Project,\n    pub rebuild_time: Duration,\n}\n\nimpl Results {\n    pub fn write_header_to_file(file_writer: &mut std::fs::File) -> std::io::Result<()> {\n        writeln!(\n            file_writer,\n            \"BuildConfig|Output File Size|Output File Size(in bytes)|Target Folder Size|Target Folder Size(in bytes)|Compilation Time(seconds)|Compilation Time|Rebuild Time(seconds)|Rebuild Time\",\n        )?;\n        Ok(())\n    }\n    pub fn save_to_file(&self, file_writer: &mut std::fs::File) -> std::io::Result<()> {\n        let file_size_pretty = if self.output_file_size == 0 {\n            \"-\".to_string()\n        } else {\n            format_size(self.output_file_size, BINARY)\n        };\n        let file_size_number = if self.output_file_size == 0 {\n            \"-\".to_string()\n        } else {\n            self.output_file_size.to_string()\n        };\n\n        writeln!(\n            file_writer,\n            \"{} __ {}|{}|{}|{}|{}|{}|{}|{}|{}\",\n            self.build_config.name,\n            self.project.name,\n            file_size_pretty,\n            file_size_number,\n            format_size(self.target_folder_size, BINARY),\n            self.target_folder_size,\n            self.compilation_time.as_secs_f32(),\n            duration_to_pretty_time(self.compilation_time),\n            self.rebuild_time.as_secs_f32(),\n            duration_to_pretty_time(self.rebuild_time)\n        )?;\n        Ok(())\n    }\n}\n\nfn duration_to_pretty_time(duration: Duration) -> String {\n    let seconds = duration.as_secs();\n    let minutes = seconds / 60;\n    let hours = minutes / 60;\n    let days = hours / 24;\n\n    if days > 0 {\n        format!(\"{}d {}h {}m {}s\", days, hours % 24, minutes % 60, seconds % 60)\n    } else if hours > 0 {\n        format!(\"{}h {}m {}s\", hours, minutes % 60, seconds % 60)\n    } else if minutes > 0 {\n        format!(\"{}m {}s\", minutes, seconds % 60)\n    } else {\n        format!(\"{}s\", seconds)\n    }\n}\n"
  },
  {
    "path": "misc/test_compilation_speed_size/src/new_chart.rs",
    "content": "use plotters::prelude::*;\nuse std::fs::{self, File};\nuse std::io::{BufRead, BufReader}; use plotters::style::text_anchor::Pos;\nuse plotters::style::text_anchor::VPos; use plotters::style::text_anchor::HPos;\n\npub fn _create_chart() -> Result<(), Box<dyn std::error::Error>> {\n    // Prepare output directory\n    fs::create_dir_all(\"charts\")?;\n\n    // Open and read the file\n    let file = File::open(\"compilation_results.txt\")?;\n    let reader = BufReader::new(file);\n\n    // Read header and find column indices\n    let mut lines = reader.lines();\n    let header = lines.next().unwrap()?;\n    let headers: Vec<&str> = header.split('|').collect();\n    let config_idx = headers.iter().position(|&h| h.trim() == \"BuildConfig\").unwrap();\n    let time_idx = headers.iter().position(|&h| h.trim() == \"Compilation Time(seconds)\").unwrap();\n\n    // Parse data\n    let mut data = Vec::new();\n    for line in lines {\n        let line = line?;\n        let cols: Vec<&str> = line.split('|').collect();\n        if cols.len() <= time_idx { continue; }\n        let config = cols[config_idx].trim();\n        let time: f64 = cols[time_idx].trim().parse().unwrap_or(0.0);\n        data.push((config.to_string(), time));\n    }\n\n    // Sort by time descending\n    data.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());\n\n    // Plot\n    let root = BitMapBackend::new(\"charts/compilation_time.png\", (1200, 800)).into_drawing_area();\n    root.fill(&WHITE)?;\n\n    let max_time = data.iter().map(|(_, t)| *t).fold(0.0, f64::max);\n\n    let mut chart = ChartBuilder::on(&root)\n        .caption(\"Compilation Time by Config\", (\"Noto Sans\", 50))\n        .margin(20)\n        .x_label_area_size(80)\n        .y_label_area_size(500)\n        .build_cartesian_2d(0f64..(max_time * 1.2), 0..data.len())?;\n\n    chart\n        .configure_mesh()\n        .x_desc(\"Compilation Time (seconds)\")\n        .y_labels(data.len())\n        .y_label_formatter(&|idx| {\n            if let Some((label, _)) = data.get(*idx) {\n                label.clone()\n            } else {\n                \"\".to_string()\n            }\n        })\n        .y_label_style((\"Noto Sans\", 30).into_font().style(FontStyle::Bold))\n        .x_label_style((\"Noto Sans\", 30).into_font())\n        .y_label_offset(-150)\n        .draw()?;\n\n    chart.draw_series(\n        data.iter().enumerate().map(|(i, (_label, value))| {\n            Rectangle::new(\n                [(0.0, i), (*value, i + 1)],\n                BLUE.filled(),\n            )\n        }),\n    )?;\n\n    chart.draw_series(\n        data.iter().enumerate().map(|(i, (_label, value))| {\n            let x = *value;\n            let y = i;\n            Text::new(\n                format!(\"{:.2}\", value),\n                (x, y),\n                (\"Noto Sans\", 35).into_font().color(&RED).pos(Pos::new(HPos::Right, VPos::Center)),\n            )\n        }),\n    )?;\n\n    root.present()?;\n    Ok(())\n}"
  },
  {
    "path": "misc/test_image_perf/Cargo.toml",
    "content": "[package]\nname = \"test_image_perf\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nczkawka_core = { path = \"../../czkawka_core\" }\nwalkdir = \"2.5.0\"\nhumansize = \"2.1.3\"\nrayon = \"1.10.0\"\nstrum = { version = \"0.27.1\", features = [\"strum_macros\", \"derive\"] }\nimage_hasher = \"3.0.0\"\nlog = \"0.4.25\"\nos_info = \"3.10.0\"\nhandsome_logger = \"0.9.0\"\n\n[features]\nfast_image_resize = [\"image_hasher/fast_resize_unstable\"]\n"
  },
  {
    "path": "misc/test_image_perf/src/main.rs",
    "content": "use std::env;\nuse std::thread::available_parallelism;\nuse std::time::{Duration, Instant};\n\nuse czkawka_core::common::image::get_dynamic_image_from_path;\nuse image_hasher::{FilterType, HashAlg, HasherConfig};\nuse log::info;\nuse rayon::prelude::*;\nuse walkdir::WalkDir;\n\nconst ITERATIONS_ON_IMAGE: usize = 3;\nconst ITERATIONS: usize = 5;\nconst HASH_ALG: HashAlg = HashAlg::Gradient;\nconst FILTER_TYPE: FilterType = FilterType::Lanczos3;\nconst HASH_SIZE: u32 = 8;\n\nconst MODE: &str = \"FAST_RESIZE\";\n\nfn print_items() {\n    let debug_release = if cfg!(debug_assertions) { \"debug\" } else { \"release\" };\n\n    let processors = available_parallelism().map(|e| e.get()).unwrap_or_default();\n\n    let info = os_info::get();\n\n    #[allow(unused_mut)]\n    let mut features: Vec<&str> = Vec::new();\n\n    #[allow(unused_mut)]\n    let mut app_cpu_version = \"Baseline\";\n    let mut os_cpu_version = \"Baseline\";\n    if cfg!(target_feature = \"sse2\") {\n        app_cpu_version = \"x86-64-v1 (SSE2)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"sse2\") {\n        os_cpu_version = \"x86-64-v1 (SSE2)\";\n    }\n\n    if cfg!(target_feature = \"popcnt\") {\n        app_cpu_version = \"x86-64-v2 (SSE4.2 + POPCNT)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"popcnt\") {\n        os_cpu_version = \"x86-64-v2 (SSE4.2 + POPCNT)\";\n    }\n\n    if cfg!(target_feature = \"avx2\") {\n        app_cpu_version = \"x86-64-v3 (AVX2)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"avx2\") {\n        os_cpu_version = \"x86-64-v3 (AVX2)\";\n    }\n\n    if cfg!(target_feature = \"avx512f\") {\n        app_cpu_version = \"x86-64-v4 (AVX-512)\";\n    }\n    #[cfg(any(target_arch = \"x86\", target_arch = \"x86_64\"))]\n    if is_x86_feature_detected!(\"avx512f\") {\n        os_cpu_version = \"x86-64-v4 (AVX-512)\";\n    }\n\n    // TODO - probably needs to add arm and other architectures, need help, because I don't have access to them\n\n    info!(\n        \"App version: {debug_release} mode, os {} {} [{} {}], {processors} cpu/threads, features({}): [{}], app cpu version: {}, os cpu version: {}\",\n        info.os_type(),\n        info.version(),\n        env::consts::ARCH,\n        info.bitness(),\n        features.len(),\n        features.join(\", \"),\n        app_cpu_version,\n        os_cpu_version,\n    );\n}\n\nfn main() {\n    handsome_logger::init().expect(\"TEST\");\n    print_items();\n\n    #[cfg(unix)]\n    {\n        if !is_running_as_sudo() {\n            println!(\"Please run this program as root\");\n            return;\n        }\n\n        clean_disk_cache();\n    }\n\n    let Some(test_path) = env::args().nth(1) else {\n        println!(\"Please provide path to test images\");\n        return;\n    };\n\n    let all_files: Vec<_> = WalkDir::new(&test_path).into_iter().flatten().map(|e| e.path().to_path_buf()).collect();\n\n    let all_files_len = all_files.len();\n\n    let collected_image_files = all_files\n        .into_iter()\n        .filter_map(|e| {\n            let ext = e.extension().unwrap_or_default().to_str().unwrap_or_default().to_lowercase();\n            if [\"jpg\", \"png\", \"jpeg\", \"webp\", \"crw\", \"nef\", \"arw\", \"dng\", \"avif\", \"cr3\", \"cr2\"].contains(&ext.as_str()) {\n                return Some(e.to_str().unwrap_or_default().to_string());\n            }\n            None\n        })\n        .collect::<Vec<String>>();\n\n    println!(\n        \"Collected {} image files out of all {all_files_len} files, with mode {MODE} from {test_path}\",\n        collected_image_files.len()\n    );\n\n    let mut times = Vec::new();\n\n    for i in 0..ITERATIONS {\n        println!(\"Iteration {}\", i + 1);\n        #[cfg(unix)]\n        clean_disk_cache();\n\n        let start = std::time::Instant::now();\n\n        collected_image_files.par_iter().for_each(|e| {\n            for _ in 0..ITERATIONS_ON_IMAGE {\n                let _ = hash_image(e);\n            }\n        });\n\n        let elapsed = start.elapsed();\n        println!(\"Iteration {} took {} ms\", i + 1, elapsed.as_millis());\n        times.push(elapsed.as_micros());\n    }\n\n    let total_time = times.iter().sum::<u128>();\n    let all_iterations_time = total_time as f64 / 1000.0;\n\n    let iters_without_extremes = times.len().checked_sub(2).unwrap_or_default();\n    let total_time_without_extremes = total_time - times.iter().min().copied().unwrap_or_default() - times.iter().max().copied().unwrap_or_default();\n    let all_iterations_time_without_extremes = total_time_without_extremes as f64 / 1000.0;\n    println!(\n        \"Mode {}, {} iterations, total time: {} ms, per iteration time {} ms, total time without extremes: {} ms, per iteration time without extremes {} ms\",\n        MODE,\n        ITERATIONS,\n        all_iterations_time,\n        all_iterations_time / ITERATIONS as f64,\n        all_iterations_time_without_extremes,\n        all_iterations_time_without_extremes / iters_without_extremes as f64\n    );\n}\n\nfn hash_image(hash_image: &str) -> Result<(), String> {\n    let img = get_dynamic_image_from_path(hash_image, None)?.image;\n\n    let hasher_config = HasherConfig::new().hash_size(HASH_SIZE, HASH_SIZE).hash_alg(HASH_ALG).resize_filter(FILTER_TYPE);\n    let hasher = hasher_config.to_hasher();\n    let _hash = hasher.hash_image(&img);\n\n    Ok(())\n}\n\n#[cfg(unix)]\nfn clean_disk_cache() {\n    use std::process::Command;\n    let _sync = Command::new(\"sync\").output().expect(\"Failed to execute sync\");\n    let _drop_caches = Command::new(\"sh\")\n        .arg(\"-c\")\n        .arg(\"echo 3 > /proc/sys/vm/drop_caches\")\n        .output()\n        .expect(\"Failed to execute drop_caches\");\n}\n\n#[cfg(unix)]\nfn is_running_as_sudo() -> bool {\n    match env::var(\"EUID\") {\n        Ok(euid) => euid == \"0\",\n        Err(_) => match env::var(\"USER\") {\n            Ok(user) => user == \"root\",\n            Err(_) => false,\n        },\n    }\n}\n\npub struct Timer {\n    base: String,\n    start_time: Instant,\n    last_time: Instant,\n    times: Vec<(String, Duration)>,\n}\n\nimpl Timer {\n    pub fn new(base: &str) -> Self {\n        Self {\n            base: base.to_string(),\n            start_time: Instant::now(),\n            last_time: Instant::now(),\n            times: Vec::new(),\n        }\n    }\n\n    pub fn checkpoint(&mut self, name: &str) {\n        let elapsed = self.last_time.elapsed();\n        self.times.push((name.to_string(), elapsed));\n        self.last_time = Instant::now();\n    }\n\n    pub fn report(&mut self, in_one_line: bool) -> String {\n        let all_elapsed = self.start_time.elapsed();\n        self.times.push((\"Everything\".to_string(), all_elapsed));\n\n        let joiner = if in_one_line { \", \" } else { \", \\n\" };\n        self.times\n            .iter()\n            .map(|(name, time)| format!(\"{} - {name}: {time:?}\", self.base))\n            .collect::<Vec<_>>()\n            .join(joiner)\n    }\n}\n"
  },
  {
    "path": "misc/test_read_perf/Cargo.toml",
    "content": "[package]\nname = \"test_read_perf\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nczkawka_core = { path = \"../../czkawka_core\", default-features = false }\nwalkdir = \"2.5.0\"\nhumansize = \"2.1\"\nrayon = \"1.10.0\"\nstrum = { version = \"0.27.2\", features = [\"strum_macros\", \"derive\"] }"
  },
  {
    "path": "misc/test_read_perf/src/main.rs",
    "content": "use czkawka_core::tools::duplicate::{hash_calculation, DuplicateEntry};\nuse czkawka_core::common::model::HashType;\nuse humansize::{format_size, BINARY};\nuse rayon::prelude::*;\nuse std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::env;\nuse std::sync::Arc;\nuse std::time::UNIX_EPOCH;\nuse strum::Display;\nuse walkdir::WalkDir;\n\nconst DIR_TO_CHECK: &str = \"/home/rafal/TODO/B/Nefs and raws\";\nconst ITERATIONS: usize = 1;\n\nthread_local! {\n    static BUFFER: RefCell<Vec<u8>> = RefCell::new(vec![0u8; 1024 * 1024]);\n}\n\nstatic GLOBAL_HDD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());\n\n#[allow(unused)]\n#[derive(Copy, Clone, Display, Hash, Eq, PartialEq)]\nenum MODES {\n    ARR16,\n    ARR256,\n    VEC16,\n    VEC1024,\n    VEC1024LOCKING,\n    VEC1024THREAD,\n}\n\nfn main() {\n    if !is_running_as_sudo() {\n        println!(\"Please run this program as root\");\n        return;\n    }\n    let files = collect_files_to_test();\n\n    clean_disk_cache();\n\n    let mut hashmap: HashMap<MODES, Vec<u128>> = HashMap::new();\n\n    let modes = [MODES::VEC1024, MODES::VEC1024LOCKING];\n    for i in 0..ITERATIONS {\n        for mode in modes.iter() {\n            println!(\"Iteration {}/{} in mode {}\", i, ITERATIONS, mode);\n            clean_disk_cache();\n\n            let start = std::time::Instant::now();\n            match mode {\n                MODES::ARR16 => array16(&files),\n                MODES::ARR256 => array256(&files),\n                MODES::VEC16 => vec16(&files),\n                MODES::VEC1024 => vec1024(&files),\n                MODES::VEC1024THREAD => vec1024_thread(&files),\n                MODES::VEC1024LOCKING => vec1024_locking(&files),\n            }\n\n            println!(\"Iteration {}/{} finished in mode {mode} with time: {} ms\", i, ITERATIONS, start.elapsed().as_millis());\n\n            hashmap.entry(*mode).or_insert_with(Vec::new).push(start.elapsed().as_micros());\n        }\n    }\n\n    for (mode, times) in hashmap {\n        let results_to_remove = times.len() / 4;\n\n        let total_time = times.iter().sum::<u128>();\n        let all_iterations_time = total_time as f64 / 1000.0;\n\n        let times = if ITERATIONS > 4 {\n            times\n                .iter()\n                .enumerate()\n                .filter(|(idx, _value)| idx < &results_to_remove || idx >= &(times.len() - results_to_remove))\n                .map(|e| *e.1)\n                .collect::<Vec<_>>()\n        } else {\n            times\n        };\n\n        let total_time_without_extremes = total_time - times.iter().min().cloned().unwrap_or_default() - times.iter().max().cloned().unwrap_or_default();\n        let all_iterations_time_without_extremes = total_time_without_extremes as f64 / 1000.0;\n        println!(\n            \"Mode {}, {} iterations, total time: {} ms, per iteration time {} ms, total time without extremes: {} ms, per iteration time without extremes {} ms\",\n            mode,\n            ITERATIONS,\n            all_iterations_time,\n            all_iterations_time / ITERATIONS as f64,\n            all_iterations_time_without_extremes,\n            all_iterations_time_without_extremes / ITERATIONS as f64\n        );\n    }\n}\n\nfn array16(files: &Vec<DuplicateEntry>) {\n    files.into_par_iter().for_each(|f| {\n        let mut buffer = [0u8; 16 * 1024];\n        let _ = hash_calculation(&mut buffer, &f, HashType::Blake3, &Arc::default(), &Arc::default());\n    });\n}\nfn array256(files: &Vec<DuplicateEntry>) {\n    files.into_par_iter().for_each(|f| {\n        let mut buffer = [0u8; 256 * 1024];\n        let _ = hash_calculation(&mut buffer, &f, HashType::Blake3, &Arc::default(), &Arc::default());\n    });\n}\nfn vec16(files: &Vec<DuplicateEntry>) {\n    files.into_par_iter().for_each(|f| {\n        let mut buffer = vec![0u8; 16 * 1024];\n        let _ = hash_calculation(&mut buffer, &f, HashType::Blake3, &Arc::default(), &Arc::default());\n    });\n}\nfn vec1024(files: &Vec<DuplicateEntry>) {\n    files.into_par_iter().for_each(|f| {\n        let mut buffer = vec![0u8; 1024 * 1024];\n        let _ = hash_calculation(&mut buffer, &f, HashType::Blake3, &Arc::default(), &Arc::default());\n    });\n}\nfn vec1024_locking(files: &Vec<DuplicateEntry>) {\n    files.into_par_iter().for_each(|f| {\n        let _lock = GLOBAL_HDD_LOCK.lock().unwrap();\n        let mut buffer = vec![0u8; 1024 * 1024];\n        let _ = hash_calculation(&mut buffer, &f, HashType::Blake3, &Arc::default(), &Arc::default());\n    });\n}\nfn vec1024_thread(files: &Vec<DuplicateEntry>) {\n    files.into_par_iter().for_each(|f| {\n        BUFFER.with(|buffer| {\n            let _ = hash_calculation(&mut buffer.borrow_mut(), &f, HashType::Blake3, &Arc::default(), &Arc::default());\n        });\n    });\n}\n\nfn collect_files_to_test() -> Vec<DuplicateEntry> {\n    let files = WalkDir::new(DIR_TO_CHECK)\n        .into_iter()\n        .flatten()\n        .filter(|e| e.file_type().is_file())\n        .map(|e| DuplicateEntry {\n            path: e.path().to_path_buf(),\n            modified_date: e\n                .metadata()\n                .map(|e| e.modified().map(|e| e.duration_since(UNIX_EPOCH)).expect(\"TEST\").expect(\"TEST\").as_secs())\n                .unwrap_or_default(),\n            size: e.metadata().map(|e| e.len()).unwrap_or_default(),\n            hash: \"\".to_string(),\n        })\n        .collect::<Vec<_>>();\n    let size: u64 = files.iter().map(|f| f.size).sum();\n    let size_str = format_size(size, BINARY);\n    println!(\"Collected {} files to test, total size: {}\", files.len(), size_str);\n    files\n}\n\nfn clean_disk_cache() {\n    use std::process::Command;\n    let _sync = Command::new(\"sync\").output().expect(\"Failed to execute sync\");\n    let _drop_caches = Command::new(\"sh\")\n        .arg(\"-c\")\n        .arg(\"echo 3 > /proc/sys/vm/drop_caches\")\n        .output()\n        .expect(\"Failed to execute drop_caches\");\n}\n\nfn is_running_as_sudo() -> bool {\n    match env::var(\"EUID\") {\n        Ok(euid) => euid == \"0\",\n        Err(_) => match env::var(\"USER\") {\n            Ok(user) => user == \"root\",\n            Err(_) => false,\n        },\n    }\n}\n"
  }
]