Repository: qarmin/czkawka Branch: master Commit: 28ae8bae1ebe Files: 452 Total size: 4.4 MB Directory structure: gitextract_ixrhqrgv/ ├── .cargo/ │ └── config.toml ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── android.yml │ ├── linux.yml │ ├── mac.yml │ ├── quality.yml │ └── windows.yml ├── .gitignore ├── .mailmap ├── .rustfmt.toml ├── Cargo.toml ├── Changelog.md ├── LICENSE_CC_BY_4_ICONS ├── LICENSE_MIT_EVERYTHING_OUTSIDE_ANY_CARGO_APP_LIBRARY ├── README.md ├── cedinia/ │ ├── Cargo.toml │ ├── LICENSE_CC_BY_4_ICONS │ ├── LICENSE_GPL_APP │ ├── LICENSE_MIT_CODE │ ├── README.md │ ├── TMP_INSTALL.md │ ├── build.rs │ ├── i18n/ │ │ ├── en/ │ │ │ └── cedinia.ftl │ │ └── pl/ │ │ └── cedinia.ftl │ ├── i18n.toml │ ├── java/ │ │ ├── CediniaActivity.java │ │ └── CediniaFilePicker.java │ ├── res/ │ │ ├── drawable/ │ │ │ ├── ic_launcher_background.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ └── ic_launcher.xml │ │ └── values/ │ │ └── strings.xml │ ├── src/ │ │ ├── app.rs │ │ ├── bin/ │ │ │ └── cedinia.rs │ │ ├── callbacks/ │ │ │ ├── directories.rs │ │ │ ├── misc.rs │ │ │ ├── scan.rs │ │ │ └── selection.rs │ │ ├── callbacks.rs │ │ ├── common.rs │ │ ├── file_picker_android.rs │ │ ├── lib.rs │ │ ├── localizer_cedinia.rs │ │ ├── model.rs │ │ ├── scan_runner.rs │ │ ├── scanners.rs │ │ ├── set_initial_gui_infos.rs │ │ ├── settings/ │ │ │ ├── gui_settings_values.rs │ │ │ └── mod.rs │ │ ├── thumbnail_loader.rs │ │ ├── translations.rs │ │ └── volumes.rs │ └── ui/ │ ├── app_state.slint │ ├── bottom_nav.slint │ ├── colors.slint │ ├── common.slint │ ├── components.slint │ ├── directories_screen.slint │ ├── home_screen.slint │ ├── main_window.slint │ ├── results_list.slint │ ├── scan_progress.slint │ ├── settings_components.slint │ ├── settings_screen.slint │ ├── similar_images_gallery.slint │ ├── top_bar.slint │ └── translations.slint ├── ci_tester/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── clippy.toml ├── czkawka_cli/ │ ├── Cargo.toml │ ├── LICENSE_MIT │ ├── README.md │ └── src/ │ ├── commands.rs │ ├── main.rs │ └── progress.rs ├── czkawka_core/ │ ├── Cargo.toml │ ├── LICENSE_CC_BY_4_TEST_FILES │ ├── LICENSE_MIT │ ├── README.md │ ├── benches/ │ │ └── hash_calculation_benchmark.rs │ ├── build.rs │ ├── i18n/ │ │ ├── ar/ │ │ │ └── czkawka_core.ftl │ │ ├── bg/ │ │ │ └── czkawka_core.ftl │ │ ├── cs/ │ │ │ └── czkawka_core.ftl │ │ ├── de/ │ │ │ └── czkawka_core.ftl │ │ ├── el/ │ │ │ └── czkawka_core.ftl │ │ ├── en/ │ │ │ └── czkawka_core.ftl │ │ ├── es-ES/ │ │ │ └── czkawka_core.ftl │ │ ├── fa/ │ │ │ └── czkawka_core.ftl │ │ ├── fr/ │ │ │ └── czkawka_core.ftl │ │ ├── it/ │ │ │ └── czkawka_core.ftl │ │ ├── ja/ │ │ │ └── czkawka_core.ftl │ │ ├── ko/ │ │ │ └── czkawka_core.ftl │ │ ├── nl/ │ │ │ └── czkawka_core.ftl │ │ ├── no/ │ │ │ └── czkawka_core.ftl │ │ ├── pl/ │ │ │ └── czkawka_core.ftl │ │ ├── pt-BR/ │ │ │ └── czkawka_core.ftl │ │ ├── pt-PT/ │ │ │ └── czkawka_core.ftl │ │ ├── ro/ │ │ │ └── czkawka_core.ftl │ │ ├── ru/ │ │ │ └── czkawka_core.ftl │ │ ├── sv-SE/ │ │ │ └── czkawka_core.ftl │ │ ├── tr/ │ │ │ └── czkawka_core.ftl │ │ ├── uk/ │ │ │ └── czkawka_core.ftl │ │ ├── zh-CN/ │ │ │ └── czkawka_core.ftl │ │ └── zh-TW/ │ │ └── czkawka_core.ftl │ ├── i18n.toml │ └── src/ │ ├── common/ │ │ ├── basic_gui_cli.rs │ │ ├── cache/ │ │ │ └── cleaning.rs │ │ ├── cache.rs │ │ ├── config_cache_path.rs │ │ ├── consts.rs │ │ ├── dir_traversal.rs │ │ ├── directories.rs │ │ ├── extensions.rs │ │ ├── ffmpeg_utils.rs │ │ ├── image.rs │ │ ├── items.rs │ │ ├── logger.rs │ │ ├── mod.rs │ │ ├── model.rs │ │ ├── process_utils.rs │ │ ├── progress_data.rs │ │ ├── progress_stop_handler.rs │ │ ├── tool_data.rs │ │ ├── traits.rs │ │ └── video_utils.rs │ ├── helpers/ │ │ ├── audio_checker.rs │ │ ├── debug_timer.rs │ │ ├── delayed_sender.rs │ │ ├── ffprobe.rs │ │ ├── messages.rs │ │ └── mod.rs │ ├── lib.rs │ ├── localizer_core.rs │ └── tools/ │ ├── bad_extensions/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ ├── traits.rs │ │ └── workarounds.rs │ ├── bad_names/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── big_file/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── broken_files/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── duplicate/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── empty_files/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── empty_folder/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── exif_remover/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── invalid_symlinks/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── mod.rs │ ├── same_music/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── similar_images/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── similar_videos/ │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── traits.rs │ ├── temporary/ │ │ ├── core.rs │ │ ├── mod.rs │ │ └── traits.rs │ └── video_optimizer/ │ ├── core/ │ │ ├── video_converter.rs │ │ └── video_cropper.rs │ ├── core.rs │ ├── mod.rs │ ├── tests.rs │ └── traits.rs ├── czkawka_gui/ │ ├── Cargo.toml │ ├── LICENSE_CC_BY_4_ICONS │ ├── LICENSE_MIT_APP_CODE │ ├── LICENSE_MIT_WINDOWS_THEME │ ├── README.md │ ├── i18n/ │ │ ├── ar/ │ │ │ └── czkawka_gui.ftl │ │ ├── bg/ │ │ │ └── czkawka_gui.ftl │ │ ├── cs/ │ │ │ └── czkawka_gui.ftl │ │ ├── de/ │ │ │ └── czkawka_gui.ftl │ │ ├── el/ │ │ │ └── czkawka_gui.ftl │ │ ├── en/ │ │ │ └── czkawka_gui.ftl │ │ ├── es-ES/ │ │ │ └── czkawka_gui.ftl │ │ ├── fa/ │ │ │ └── czkawka_gui.ftl │ │ ├── fr/ │ │ │ └── czkawka_gui.ftl │ │ ├── it/ │ │ │ └── czkawka_gui.ftl │ │ ├── ja/ │ │ │ └── czkawka_gui.ftl │ │ ├── ko/ │ │ │ └── czkawka_gui.ftl │ │ ├── nl/ │ │ │ └── czkawka_gui.ftl │ │ ├── no/ │ │ │ └── czkawka_gui.ftl │ │ ├── pl/ │ │ │ └── czkawka_gui.ftl │ │ ├── pt-BR/ │ │ │ └── czkawka_gui.ftl │ │ ├── pt-PT/ │ │ │ └── czkawka_gui.ftl │ │ ├── ro/ │ │ │ └── czkawka_gui.ftl │ │ ├── ru/ │ │ │ └── czkawka_gui.ftl │ │ ├── sv-SE/ │ │ │ └── czkawka_gui.ftl │ │ ├── tr/ │ │ │ └── czkawka_gui.ftl │ │ ├── uk/ │ │ │ └── czkawka_gui.ftl │ │ ├── zh-CN/ │ │ │ └── czkawka_gui.ftl │ │ └── zh-TW/ │ │ └── czkawka_gui.ftl │ ├── i18n.toml │ ├── src/ │ │ ├── compute_results.rs │ │ ├── connect_things/ │ │ │ ├── connect_about_buttons.rs │ │ │ ├── connect_button_compare.rs │ │ │ ├── connect_button_delete.rs │ │ │ ├── connect_button_hardlink.rs │ │ │ ├── connect_button_move.rs │ │ │ ├── connect_button_save.rs │ │ │ ├── connect_button_search.rs │ │ │ ├── connect_button_select.rs │ │ │ ├── connect_button_sort.rs │ │ │ ├── connect_button_stop.rs │ │ │ ├── connect_change_language.rs │ │ │ ├── connect_duplicate_buttons.rs │ │ │ ├── connect_header_buttons.rs │ │ │ ├── connect_krokiet_info_dialog.rs │ │ │ ├── connect_notebook_tabs.rs │ │ │ ├── connect_popovers_select.rs │ │ │ ├── connect_popovers_sort.rs │ │ │ ├── connect_progress_window.rs │ │ │ ├── connect_same_music_mode_changed.rs │ │ │ ├── connect_selection_of_directories.rs │ │ │ ├── connect_settings.rs │ │ │ ├── connect_show_hide_ui.rs │ │ │ ├── connect_similar_image_size_change.rs │ │ │ ├── file_chooser_helpers.rs │ │ │ └── mod.rs │ │ ├── gtk_traits.rs │ │ ├── gui_structs/ │ │ │ ├── common_tree_view.rs │ │ │ ├── common_upper_tree_view.rs │ │ │ ├── gui_about.rs │ │ │ ├── gui_bottom_buttons.rs │ │ │ ├── gui_compare_images.rs │ │ │ ├── gui_data.rs │ │ │ ├── gui_header.rs │ │ │ ├── gui_main_notebook.rs │ │ │ ├── gui_popovers_select.rs │ │ │ ├── gui_popovers_sort.rs │ │ │ ├── gui_progress_dialog.rs │ │ │ ├── gui_settings.rs │ │ │ ├── gui_upper_notebook.rs │ │ │ └── mod.rs │ │ ├── help_combo_box.rs │ │ ├── help_functions.rs │ │ ├── helpers/ │ │ │ ├── enums.rs │ │ │ ├── image_operations.rs │ │ │ ├── list_store_operations.rs │ │ │ ├── mod.rs │ │ │ └── model_iter.rs │ │ ├── initialize_gui.rs │ │ ├── language_functions.rs │ │ ├── localizer_gui.rs │ │ ├── main.rs │ │ ├── notebook_enums.rs │ │ ├── notebook_info.rs │ │ ├── opening_selecting_records.rs │ │ ├── saving_loading.rs │ │ ├── taskbar_progress.rs │ │ ├── taskbar_progress_dummy.rs │ │ └── taskbar_progress_win.rs │ └── ui/ │ ├── about_dialog.ui │ ├── compare_images.ui │ ├── czkawka.cmb │ ├── main_window.ui │ ├── popover_right_click.ui │ ├── popover_select.ui │ ├── popover_sort.ui │ ├── progress.ui │ └── settings.ui ├── data/ │ ├── com.github.qarmin.czkawka.desktop │ ├── com.github.qarmin.czkawka.metainfo.xml │ ├── io.github.qarmin.krokiet.desktop │ └── io.github.qarmin.krokiet.metainfo.xml ├── instructions/ │ ├── Instruction.md │ └── Translations.md ├── justfile ├── krokiet/ │ ├── Cargo.toml │ ├── LICENSE_CC_BY_4_AUDIO_FILES │ ├── LICENSE_CC_BY_4_ICONS │ ├── LICENSE_GPL_APP │ ├── LICENSE_MIT_CODE │ ├── README.md │ ├── build.rs │ ├── i18n/ │ │ ├── ar/ │ │ │ └── krokiet.ftl │ │ ├── bg/ │ │ │ └── krokiet.ftl │ │ ├── cs/ │ │ │ └── krokiet.ftl │ │ ├── de/ │ │ │ └── krokiet.ftl │ │ ├── el/ │ │ │ └── krokiet.ftl │ │ ├── en/ │ │ │ └── krokiet.ftl │ │ ├── es-ES/ │ │ │ └── krokiet.ftl │ │ ├── fa/ │ │ │ └── krokiet.ftl │ │ ├── fr/ │ │ │ └── krokiet.ftl │ │ ├── it/ │ │ │ └── krokiet.ftl │ │ ├── ja/ │ │ │ └── krokiet.ftl │ │ ├── ko/ │ │ │ └── krokiet.ftl │ │ ├── nl/ │ │ │ └── krokiet.ftl │ │ ├── no/ │ │ │ └── krokiet.ftl │ │ ├── pl/ │ │ │ └── krokiet.ftl │ │ ├── pt-BR/ │ │ │ └── krokiet.ftl │ │ ├── pt-PT/ │ │ │ └── krokiet.ftl │ │ ├── ro/ │ │ │ └── krokiet.ftl │ │ ├── ru/ │ │ │ └── krokiet.ftl │ │ ├── sv-SE/ │ │ │ └── krokiet.ftl │ │ ├── tr/ │ │ │ └── krokiet.ftl │ │ ├── uk/ │ │ │ └── krokiet.ftl │ │ ├── zh-CN/ │ │ │ └── krokiet.ftl │ │ └── zh-TW/ │ │ └── krokiet.ftl │ ├── i18n.toml │ ├── src/ │ │ ├── audio_player.rs │ │ ├── clear_outdated_video_thumbnails.rs │ │ ├── common.rs │ │ ├── connect_clean_cache.rs │ │ ├── connect_directories_changes.rs │ │ ├── connect_open.rs │ │ ├── connect_progress_receiver.rs │ │ ├── connect_rfd.rs │ │ ├── connect_row_selection.rs │ │ ├── connect_save.rs │ │ ├── connect_scan/ │ │ │ ├── bad_extensions.rs │ │ │ ├── bad_names.rs │ │ │ ├── big_files.rs │ │ │ ├── broken_files.rs │ │ │ ├── duplicate.rs │ │ │ ├── empty_files.rs │ │ │ ├── empty_folders.rs │ │ │ ├── exif_remover.rs │ │ │ ├── invalid_symlinks.rs │ │ │ ├── same_music.rs │ │ │ ├── similar_images.rs │ │ │ ├── similar_videos.rs │ │ │ ├── temporary_files.rs │ │ │ └── video_optimizer.rs │ │ ├── connect_scan.rs │ │ ├── connect_select/ │ │ │ ├── custom_select.rs │ │ │ └── mod.rs │ │ ├── connect_show_confirmation.rs │ │ ├── connect_show_preview.rs │ │ ├── connect_sort.rs │ │ ├── connect_stop.rs │ │ ├── connect_tab_changed.rs │ │ ├── connect_translation.rs │ │ ├── create_calculate_task_size.rs │ │ ├── file_actions/ │ │ │ ├── connect_clean_exif.rs │ │ │ ├── connect_delete.rs │ │ │ ├── connect_hardlink.rs │ │ │ ├── connect_move.rs │ │ │ ├── connect_optimize_video.rs │ │ │ ├── connect_rename.rs │ │ │ ├── connect_symlink.rs │ │ │ └── mod.rs │ │ ├── localizer_krokiet.rs │ │ ├── main.rs │ │ ├── model_operations/ │ │ │ ├── mod.rs │ │ │ └── model_processor.rs │ │ ├── set_initial_gui_info.rs │ │ ├── set_initial_scroll_list_data_indexes.rs │ │ ├── settings/ │ │ │ ├── combo_box.rs │ │ │ ├── mod.rs │ │ │ └── model.rs │ │ ├── shared_models.rs │ │ ├── simpler_model.rs │ │ └── test_common.rs │ └── ui/ │ ├── about.slint │ ├── action_buttons.slint │ ├── bottom_panel.slint │ ├── callabler.slint │ ├── color_palette.slint │ ├── common.slint │ ├── fonts.slint │ ├── gui_state.slint │ ├── included_paths.slint │ ├── left_side_panel.slint │ ├── main_lists.slint │ ├── main_window.slint │ ├── popup_action_confirm.slint │ ├── popup_base.slint │ ├── popup_centered_text.slint │ ├── popup_clean_cache.slint │ ├── popup_clean_exif.slint │ ├── popup_crop_video.slint │ ├── popup_custom_select.slint │ ├── popup_delete.slint │ ├── popup_move_folders.slint │ ├── popup_new_directories.slint │ ├── popup_optimize.slint │ ├── popup_rename_bad_extensions.slint │ ├── popup_rename_bad_file_names.slint │ ├── popup_save.slint │ ├── popup_select_results.slint │ ├── popup_sort.slint │ ├── preview.slint │ ├── progress.slint │ ├── selectable_tree_view.slint │ ├── settings.slint │ ├── settings_list.slint │ ├── tool_settings.slint │ └── translations.slint └── misc/ ├── add_icon_exe/ │ └── Cargo.toml ├── ai_translate/ │ ├── ftl_utils.py │ ├── pyproject.toml │ ├── translate.py │ └── validate_translations.py ├── cargo/ │ ├── PublishCore.sh │ └── PublishOther.sh ├── compare_files.sh ├── delete_unused_krokiet_slint_imports.py ├── docker/ │ └── Dockerfile ├── find_unused_callbacks.py ├── find_unused_fluent_translations.py ├── find_unused_settings_properties.py ├── find_unused_slint_translations.py ├── flathub.sh ├── gen_android_icons.py ├── nix/ │ ├── flake.nix │ └── packages.nix ├── remove_comments.py ├── run_checks.sh ├── simplify_and_minify_svg.py ├── test_compilation_speed_size/ │ ├── Cargo.toml │ ├── README.md │ ├── generate_md_and_plots.py │ └── src/ │ ├── main.rs │ ├── model.rs │ └── new_chart.rs ├── test_image_perf/ │ ├── Cargo.toml │ └── src/ │ └── main.rs └── test_read_perf/ ├── Cargo.toml └── src/ └── main.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.x86_64-pc-windows-msvc] # Increase default stack size to avoid running out of stack # space in debug builds. The size matches Linux's default. rustflags = ["-C", "link-arg=/STACK:8000000"] [target.aarch64-pc-windows-msvc] # Increase default stack size to avoid running out of stack # space in debug builds. The size matches Linux's default. rustflags = ["-C", "link-arg=/STACK:8000000"] ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: qarmin # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve app title: '' labels: bug assignees: '' --- **Bug Description** **Steps to reproduce:** **Terminal output** (optional): ```
Debug log # UNCOMMENT DETAILS AND PUT LOGS HERE
``` **System** - Czkawka/Krokiet version: - OS version: - Installation method: ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Feature Description** ... ================================================ FILE: .github/workflows/android.yml ================================================ name: 🤖 Android APK on: push: branches: [ master, main ] pull_request: branches: [ master, main ] workflow_dispatch: jobs: build-apk: name: Build Android APK runs-on: ubuntu-latest env: NDK_VERSION: 26.3.11579264 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Java (required by Android SDK) uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - name: Set up Android SDK uses: android-actions/setup-android@v3 - name: Cache Android SDK uses: actions/cache@v4 with: path: | ${{ env.ANDROID_SDK_ROOT }}/cmdline-tools ${{ env.ANDROID_SDK_ROOT }}/ndk ${{ env.ANDROID_SDK_ROOT }}/platforms ${{ env.ANDROID_SDK_ROOT }}/platform-tools ${{ env.ANDROID_SDK_ROOT }}/build-tools key: ${{ runner.os }}-android-sdk-${{ env.NDK_VERSION }} restore-keys: | ${{ runner.os }}-android-sdk- - name: Install Android NDK and tools run: | rustup target add aarch64-linux-android cargo install cargo-apk || true yes | sdkmanager --install "ndk;${NDK_VERSION}" echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/${NDK_VERSION}" >> $GITHUB_ENV - name: Cache Cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-android-v1 restore-keys: | ${{ runner.os }}-cargo- - name: Generate keystores run: | sudo apt update || true sudo apt install -y just just gen_keystores - name: Build APK (release) if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }} run: | echo "VERS=release" >> $GITHUB_ENV cargo apk build -p cedinia --lib --release mv target/release/apk/cedinia.apk cedinia.apk - name: Build APK (debug) if: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }} run: | echo "VERS=debug" >> $GITHUB_ENV cargo apk build -p cedinia --lib mv target/debug/apk/cedinia.apk cedinia.apk - name: Upload APK artifact uses: actions/upload-artifact@v4 with: name: cedinia-${{ env.VERS }} path: cedinia.apk - name: Release uses: softprops/action-gh-release@v2 if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} with: tag_name: "Nightly" files: | cedinia.apk token: ${{ secrets.PAT_REPOSITORY }} ================================================ FILE: .github/workflows/linux.yml ================================================ name: 🐧 Linux on: push: pull_request: schedule: - cron: '0 0 * * 2' env: CARGO_TERM_COLOR: always CZKAWKA_OFFICIAL_BUILD: ${{ vars.CZKAWKA_OFFICIAL_BUILD }} jobs: linux-all: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-22.04, ubuntu-22.04-arm] steps: - uses: actions/checkout@v4 - name: Setup env run: | ARCHNAME=$([ "${{ runner.arch }}" = "ARM64" ] && echo arm64 || echo x86_64) echo "ARCHNAME=$ARCHNAME" >> $GITHUB_ENV - name: Install basic libraries run: sudo apt update || true; sudo apt install libheif-dev libraw-dev ffmpeg libgtk-4-dev p7zip-full -y - name: Setup rust version run: rustup default 1.92.0 - name: Build Release if: ${{ github.ref == 'refs/heads/master' }} run: | sed -i 's/#lto = /lto = /g' Cargo.toml sed -i 's/#codegen-units /codegen-units /g' Cargo.toml echo "VERS=release" >> $GITHUB_ENV cargo build --release mv target/release/czkawka_cli linux_czkawka_cli_${{ env.ARCHNAME }} mv target/release/czkawka_gui linux_czkawka_gui_${{ env.ARCHNAME }} mv target/release/krokiet linux_krokiet_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_skia_opengl,winit_software" mv target/release/krokiet linux_krokiet_skia_opengl_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software" mv target/release/krokiet linux_krokiet_skia_vulkan_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "femtovg_wgpu" mv target/release/krokiet linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu" mv target/release/krokiet linux_krokiet_all_backends_${{ env.ARCHNAME }} # Fast CI profile, to avoid out of disk space errors # I doubt that anyone would use debug builds from here, because they are slow and contains not final changes - name: Build Debug if: ${{ github.ref != 'refs/heads/master' }} run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml echo "VERS=debug" >> $GITHUB_ENV cargo build --profile fastci mv target/fastci/czkawka_cli linux_czkawka_cli_${{ env.ARCHNAME }} mv target/fastci/czkawka_gui linux_czkawka_gui_${{ env.ARCHNAME }} mv target/fastci/krokiet linux_krokiet_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "winit_skia_opengl,winit_software" --profile fastci mv target/fastci/krokiet linux_krokiet_skia_opengl_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software" --profile fastci mv target/fastci/krokiet linux_krokiet_skia_vulkan_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "femtovg_wgpu" --profile fastci mv target/fastci/krokiet linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu" --profile fastci mv target/fastci/krokiet linux_krokiet_all_backends_${{ env.ARCHNAME }} - name: Pack with 7z run: | # 7z -mx=3 in rust files, takes 40% less space but is 2x slower than zip -mx=1 # 7z -mx=3 is 8x faster than 7z -mx=5, but generates 20% bigger # So looks that -mx=3 is the best option time 7z a -t7z -mx=3 czkawka_all.7z \ linux_czkawka_cli_${{ env.ARCHNAME }} \ linux_czkawka_gui_${{ env.ARCHNAME }} \ linux_krokiet_${{ env.ARCHNAME }} \ linux_krokiet_skia_opengl_${{ env.ARCHNAME }} \ linux_krokiet_skia_vulkan_${{ env.ARCHNAME }} \ linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} \ linux_krokiet_all_backends_${{ env.ARCHNAME }} - name: Store uses: actions/upload-artifact@v4 with: name: all-${{ runner.os }}-${{ runner.arch }}-${{ env.VERS }} path: | czkawka_all.7z - name: Release if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} uses: softprops/action-gh-release@v2 with: tag_name: "Nightly" files: | linux_czkawka_cli_${{ env.ARCHNAME }} linux_czkawka_gui_${{ env.ARCHNAME }} linux_krokiet_${{ env.ARCHNAME }} linux_krokiet_skia_opengl_${{ env.ARCHNAME }} linux_krokiet_skia_vulkan_${{ env.ARCHNAME }} linux_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} linux_krokiet_all_backends_${{ env.ARCHNAME }} token: ${{ secrets.PAT_REPOSITORY }} # Some dependencies requires ubuntu 24.04 linux-all-extra: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-24.04, ubuntu-24.04-arm] steps: - uses: actions/checkout@v4 - name: Setup env run: | ARCHNAME=$([ "${{ runner.arch }}" = "ARM64" ] && echo arm64 || echo x86_64) echo "ARCHNAME=$ARCHNAME" >> $GITHUB_ENV - name: Install basic libraries run: sudo apt update || true; sudo apt install libheif-dev libraw-dev ffmpeg libgtk-4-dev p7zip-full -y - name: Setup rust version run: rustup default 1.92.0 - name: Build Release if: ${{ github.ref == 'refs/heads/master' }} run: | sed -i 's/#lto = /lto = /g' Cargo.toml sed -i 's/#codegen-units /codegen-units /g' Cargo.toml echo "VERS=release" >> $GITHUB_ENV cargo build --release --features "heif,libraw" mv target/release/czkawka_cli linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }} mv target/release/czkawka_gui linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }} mv target/release/krokiet linux_krokiet_heif_raw_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_skia_opengl,winit_software,heif,libraw" mv target/release/krokiet linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software,heif,libraw" mv target/release/krokiet linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "femtovg_wgpu,heif,libraw" mv target/release/krokiet linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libraw" mv target/release/krokiet linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }} # Fast CI profile, to avoid out of disk space errors # I doubt that anyone would use debug builds from here, because they are slow and contains not final changes - name: Build Debug if: ${{ github.ref != 'refs/heads/master' }} run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml echo "VERS=debug" >> $GITHUB_ENV cargo build --features "heif,libraw" --profile fastci mv target/fastci/czkawka_cli linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }} mv target/fastci/czkawka_gui linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }} mv target/fastci/krokiet linux_krokiet_heif_raw_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "winit_skia_opengl,winit_software,heif,libraw" --profile fastci mv target/fastci/krokiet linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software,heif,libraw" --profile fastci mv target/fastci/krokiet linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "femtovg_wgpu,heif,libraw" --profile fastci mv target/fastci/krokiet linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libraw" --profile fastci mv target/fastci/krokiet linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }} - name: Pack with 7z run: | # 7z -mx=3 in rust files, takes 40% less space but is 2x slower than zip -mx=1 # 7z -mx=3 is 8x faster than 7z -mx=5, but generates 20% bigger # So looks that -mx=3 is the best option time 7z a -t7z -mx=3 czkawka_all.7z \ linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }} \ linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }} \ linux_krokiet_heif_raw_${{ env.ARCHNAME }} \ linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }} \ linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }} \ linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }} \ linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }} - name: Store uses: actions/upload-artifact@v4 with: name: all-${{ runner.os }}-${{ runner.arch }}-${{ env.VERS }}-heif-libraw path: | czkawka_all.7z - name: Release if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} uses: softprops/action-gh-release@v2 with: tag_name: "Nightly" files: | linux_czkawka_cli_heif_raw_${{ env.ARCHNAME }} linux_czkawka_gui_heif_raw_${{ env.ARCHNAME }} linux_krokiet_heif_raw_${{ env.ARCHNAME }} linux_krokiet_heif_raw_skia_opengl_${{ env.ARCHNAME }} linux_krokiet_heif_raw_skia_vulkan_${{ env.ARCHNAME }} linux_krokiet_heif_raw_femtovg_wgpu_${{ env.ARCHNAME }} linux_krokiet_heif_raw_all_backends_${{ env.ARCHNAME }} token: ${{ secrets.PAT_REPOSITORY }} ### MUSL CLI and Krokiet Release and Debug # GUI not works with MUSL :( # https://github.com/slint-ui/slint/issues/7586 # https://github.com/rust-windowing/winit/issues/1818 linux-cli-musl: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install basic libraries run: | sudo apt update || true; sudo apt install musl-tools -y - name: Setup rust version run: | rustup default 1.92.0 rustup target add x86_64-unknown-linux-musl - name: Build Release if: ${{ github.ref == 'refs/heads/master' }} run: | sed -i 's/#lto = /lto = /g' Cargo.toml sed -i 's/#codegen-units /codegen-units /g' Cargo.toml cargo build --release --bin czkawka_cli --target x86_64-unknown-linux-musl mv target/x86_64-unknown-linux-musl/release/czkawka_cli linux_czkawka_cli_musl - name: Build Debug if: ${{ github.ref != 'refs/heads/master' }} run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml cargo build --bin czkawka_cli --target x86_64-unknown-linux-musl mv target/x86_64-unknown-linux-musl/debug/czkawka_cli linux_czkawka_cli_musl - name: Store Linux CLI uses: actions/upload-artifact@v4 with: name: czkawka_cli-${{ runner.os }}-musl path: | linux_czkawka_cli_musl - name: Release uses: softprops/action-gh-release@v2 if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} with: tag_name: "Nightly" files: | linux_czkawka_cli_musl token: ${{ secrets.PAT_REPOSITORY }} ### Below, builds that do not produce artifacts ### 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 linux-all-debug-32bit: if: ${{ github.ref != 'refs/heads/master' }} runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install basic libraries run: | sudo apt update || true sudo apt install gcc-multilib -y - name: Setup rust version and target run: | rustup default 1.92.0 rustup target add i686-unknown-linux-gnu - name: Build Debug for 32-bit run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml cargo build --target i686-unknown-linux-gnu --bin czkawka_cli --bin krokiet mv target/i686-unknown-linux-gnu/debug/czkawka_cli linux_czkawka_cli_32bit mv target/i686-unknown-linux-gnu/debug/krokiet linux_krokiet_32bit - name: Store uses: actions/upload-artifact@v4 with: name: all-32bit-${{ runner.os }}-${{ runner.arch }}-debug path: | linux_czkawka_cli_32bit linux_krokiet_32bit linux-stability: if: ${{ github.ref == 'refs/heads/master' }} # Runs only in master, because it is really time consuming runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Install basic libraries run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev -y - name: Setup rust version run: rustup default 1.92.0 - name: Build packages run: | rm -rf target || true cargo build --features "heif,libraw" mv target/debug/czkawka_cli czkawka_cli_debug_1 mv target/debug/czkawka_gui czkawka_gui_debug_1 mv target/debug/krokiet krokiet_debug_1 rm -rf target || true cargo build --release --features "heif,libraw" mv target/release/czkawka_cli czkawka_cli_release_1 mv target/release/czkawka_gui czkawka_gui_release_1 mv target/release/krokiet krokiet_release_1 rm -rf target || true cargo build --features "heif,libraw" mv target/debug/czkawka_cli czkawka_cli_debug_2 mv target/debug/czkawka_gui czkawka_gui_debug_2 mv target/debug/krokiet krokiet_debug_2 rm -rf target || true cargo build --release --features "heif,libraw" mv target/release/czkawka_cli czkawka_cli_release_2 mv target/release/czkawka_gui czkawka_gui_release_2 mv target/release/krokiet krokiet_release_2 bash misc/compare_files.sh linux-tests: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install basic libraries run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev -y - name: Setup rust version run: rustup default 1.92.0 - name: Test run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml xvfb-run cargo test linux-regression-tests-on-minimal-rust-version: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install basic libraries run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev ffmpeg -y - name: Setup rust version run: rustup default 1.92.0 - name: Build test version run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml cargo build --profile test --bin czkawka_cli - name: Linux Regression Test run: | wget -q https://github.com/qarmin/czkawka/releases/download/6.0.0/TestFiles.zip cd ci_tester cargo build --release cd .. ci_tester/target/release/ci_tester target/debug/czkawka_cli android: if: ${{ github.ref == 'refs/heads/master' }} runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Setup rust version and target run: | rustup default 1.92.0 rustup target add aarch64-linux-android - name: Check for Android run: | cd czkawka_core cargo check --target aarch64-linux-android --features "blake_pure" ================================================ FILE: .github/workflows/mac.yml ================================================ name: 🍎 MacOS on: push: pull_request: schedule: - cron: '0 0 * * 2' env: CARGO_TERM_COLOR: always CZKAWKA_OFFICIAL_BUILD: ${{ vars.CZKAWKA_OFFICIAL_BUILD }} jobs: macos: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, macos-15-intel] steps: - uses: actions/checkout@v4 - name: Setup env run: | ARCHNAME=$([ "${{ runner.arch }}" = "ARM64" ] && echo arm64 || echo x86_64) echo "ARCHNAME=$ARCHNAME" >> $GITHUB_ENV - name: Setup rust version run: rustup default 1.92.0 - name: Install Homebrew run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - name: Install GTK4 run: | brew link --overwrite python@3.13 brew install gtk4 libheif libavif dav1d || true # brew link --overwrite python@3.13 - name: Build Release if: ${{ github.ref == 'refs/heads/master' }} run: | set -e sed -i '' 's/#lto = "thin"/lto = "thin"/g' Cargo.toml sed -i '' 's/#codegen-units /codegen-units /g' Cargo.toml echo "VERS=release" >> $GITHUB_ENV export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix)/lib cargo build --release mv target/release/czkawka_cli mac_czkawka_cli_${{ env.ARCHNAME }} mv target/release/czkawka_gui mac_czkawka_gui_${{ env.ARCHNAME }} mv target/release/krokiet mac_krokiet_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software" mv target/release/krokiet mac_krokiet_skia_vulkan_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "femtovg_wgpu" mv target/release/krokiet mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu" mv target/release/krokiet mac_krokiet_all_backends_${{ env.ARCHNAME }} cargo build --release --features "heif,libavif" mv target/release/czkawka_cli mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }} mv target/release/czkawka_gui mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }} mv target/release/krokiet mac_krokiet_heif_avif_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software,heif,libavif" mv target/release/krokiet mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "femtovg_wgpu,heif,libavif" mv target/release/krokiet mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --release --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libavif" mv target/release/krokiet mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }} - name: Build Debug if: ${{ github.ref != 'refs/heads/master' }} run: | set -e sed -i '' 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i '' 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml echo "VERS=debug" >> $GITHUB_ENV export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix)/lib cargo build --profile fastci mv target/fastci/czkawka_cli mac_czkawka_cli_${{ env.ARCHNAME }} mv target/fastci/czkawka_gui mac_czkawka_gui_${{ env.ARCHNAME }} mv target/fastci/krokiet mac_krokiet_${{ env.ARCHNAME }} cargo build --profile fastci --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software" mv target/fastci/krokiet mac_krokiet_skia_vulkan_${{ env.ARCHNAME }} cargo build --profile fastci --bin krokiet --no-default-features --features "femtovg_wgpu" mv target/fastci/krokiet mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --profile fastci --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu" mv target/fastci/krokiet mac_krokiet_all_backends_${{ env.ARCHNAME }} cargo build --profile fastci --features "heif,libavif" mv target/fastci/czkawka_cli mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }} mv target/fastci/czkawka_gui mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }} mv target/fastci/krokiet mac_krokiet_heif_avif_${{ env.ARCHNAME }} cargo build --profile fastci --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software,heif,libavif" mv target/fastci/krokiet mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }} cargo build --profile fastci --bin krokiet --no-default-features --features "femtovg_wgpu,heif,libavif" mv target/fastci/krokiet mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }} cargo build --profile fastci --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu,heif,libavif" mv target/fastci/krokiet mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }} - name: Store MacOS uses: actions/upload-artifact@v4 with: name: all-${{ runner.os }}-${{ runner.arch }}-${{ env.VERS }} path: | mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }} mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }} mac_krokiet_heif_avif_${{ env.ARCHNAME }} mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }} mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }} mac_czkawka_cli_${{ env.ARCHNAME }} mac_czkawka_gui_${{ env.ARCHNAME }} mac_krokiet_${{ env.ARCHNAME }} mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }} mac_krokiet_skia_vulkan_${{ env.ARCHNAME }} mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} mac_krokiet_all_backends_${{ env.ARCHNAME }} - name: Release if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} uses: softprops/action-gh-release@v2 with: tag_name: "Nightly" files: | mac_czkawka_cli_heif_avif_${{ env.ARCHNAME }} mac_czkawka_gui_heif_avif_${{ env.ARCHNAME }} mac_krokiet_heif_avif_${{ env.ARCHNAME }} mac_krokiet_heif_avif_femtovg_wgpu_${{ env.ARCHNAME }} mac_krokiet_heif_avif_all_backends_${{ env.ARCHNAME }} mac_czkawka_cli_${{ env.ARCHNAME }} mac_czkawka_gui_${{ env.ARCHNAME }} mac_krokiet_${{ env.ARCHNAME }} mac_krokiet_skia_vulkan_heif_avif_${{ env.ARCHNAME }} mac_krokiet_skia_vulkan_${{ env.ARCHNAME }} mac_krokiet_femtovg_wgpu_${{ env.ARCHNAME }} mac_krokiet_all_backends_${{ env.ARCHNAME }} token: ${{ secrets.PAT_REPOSITORY }} ================================================ FILE: .github/workflows/quality.yml ================================================ name: 🧹 Quality on: push: pull_request: schedule: - cron: '0 0 * * 2' env: CARGO_TERM_COLOR: always jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Gtk 4 run: sudo apt update || true; sudo apt install -y libgtk-4-dev libraw-dev libheif-dev libavif-dev libdav1d-dev libasound2-dev -y - name: Setup rust version run: | rustup default 1.92.0 rustup component add rustfmt rustup component add clippy - name: Disable optimizations run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml - name: Check the format run: cargo fmt --all -- --check - name: Run clippy run: | cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets -- -D warnings - name: Check tools run: | cd misc/test_image_perf cargo check cd ../../ cd misc/test_read_perf cargo check cd ../../ ================================================ FILE: .github/workflows/windows.yml ================================================ name: 🏁 Windows on: push: pull_request: schedule: - cron: '0 0 * * 2' env: CARGO_TERM_COLOR: always CZKAWKA_OFFICIAL_BUILD: ${{ vars.CZKAWKA_OFFICIAL_BUILD }} jobs: krokiet-compiled-on-linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt update || true sudo apt install -y mingw-w64 mingw-w64-x86-64-dev wget wget2 curl wine || true - name: Setup rust version run: | rustup default 1.92.0 rustup target add x86_64-pc-windows-gnu - name: Download rcedit run: | curl -L https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -o rcedit-x64.exe - name: Compile Krokiet Release if: ${{ github.ref == 'refs/heads/master' }} run: | sed -i 's/#lto = /lto = /g' Cargo.toml sed -i 's/#codegen-units /codegen-units /g' Cargo.toml cargo build --release --target x86_64-pc-windows-gnu --bin krokiet mv target/x86_64-pc-windows-gnu/release/krokiet.exe windows_krokiet_on_linux.exe export WINEPREFIX=$(mktemp -d) wine rcedit-x64.exe windows_krokiet_on_linux.exe --set-icon krokiet/icons/krokiet_logo_flag.ico - name: Compile Krokiet Debug if: ${{ github.ref != 'refs/heads/master' }} run: | sed -i 's/^\(\[profile\.dev\.package.*\)/#\1/' Cargo.toml sed -i 's|^opt-level = 3 # OPT PACKAGES|#opt-level = 3 # OPT PACKAGES|' Cargo.toml cargo build --target x86_64-pc-windows-gnu --bin krokiet mv target/x86_64-pc-windows-gnu/debug/krokiet.exe windows_krokiet_on_linux.exe export WINEPREFIX=$(mktemp -d) wine rcedit-x64.exe windows_krokiet_on_linux.exe --set-icon krokiet/icons/krokiet_logo_flag.ico - name: Pack with 7z run: | time 7z a -t7z -mx=3 czkawka_all.7z \ windows_krokiet_on_linux.exe - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: krokiet-windows-on-linux-${{ github.sha }} path: | czkawka_all.7z if-no-files-found: error - name: Release uses: softprops/action-gh-release@v2 if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} with: tag_name: "Nightly" files: | windows_krokiet_on_linux.exe token: ${{ secrets.PAT_REPOSITORY }} # Skia not provides support for gnu toolchain, which is easy to cross-compile - https://github.com/rust-skia/rust-skia/issues/345 # So need to compile krokiet on msvc Windows krokiet-compiled-on-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Setup rust version run: | rustup default 1.92.0 - name: Download rcedit run: | curl -L https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -o rcedit-x64.exe - name: Compile Krokiet Release if: ${{ github.ref == 'refs/heads/master' }} run: | 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" cargo build --release --bin krokiet --no-default-features --features "winit_skia_opengl,winit_software" mv target/release/krokiet.exe windows_krokiet_on_windows_skia_opengl.exe cargo build --release --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software" mv target/release/krokiet.exe windows_krokiet_on_windows_skia_vulkan.exe cargo build --release --bin krokiet --no-default-features --features "femtovg_wgpu" mv target/release/krokiet.exe windows_krokiet_on_windows_femtovg_wgpu.exe cargo build --release --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu" mv target/release/krokiet.exe windows_krokiet_on_windows_all_backends.exe Get-ChildItem windows_krokiet_on_windows_*.exe | ForEach-Object { ./rcedit-x64.exe $_.Name --set-icon krokiet/icons/krokiet_logo_flag.ico } - name: Compile Krokiet Debug if: ${{ github.ref != 'refs/heads/master' }} run: | (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 cargo build --bin krokiet --no-default-features --features "winit_skia_opengl,winit_software" mv target/debug/krokiet.exe windows_krokiet_on_windows_skia_opengl.exe cargo build --bin krokiet --no-default-features --features "winit_skia_vulkan,winit_software" mv target/debug/krokiet.exe windows_krokiet_on_windows_skia_vulkan.exe cargo build --bin krokiet --no-default-features --features "femtovg_wgpu" mv target/debug/krokiet.exe windows_krokiet_on_windows_femtovg_wgpu.exe cargo build --bin krokiet --no-default-features --features "winit_femtovg,winit_skia_opengl,winit_skia_vulkan,winit_software,femtovg_wgpu" mv target/debug/krokiet.exe windows_krokiet_on_windows_all_backends.exe Get-ChildItem windows_krokiet_on_windows_*.exe | ForEach-Object { ./rcedit-x64.exe $_.Name --set-icon krokiet/icons/krokiet_logo_flag.ico } - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: krokiet-windows-on-windows-${{ github.sha }} path: | windows_krokiet_on_windows_skia_opengl.exe windows_krokiet_on_windows_skia_vulkan.exe windows_krokiet_on_windows_femtovg_wgpu.exe windows_krokiet_on_windows_all_backends.exe if-no-files-found: error - name: Release uses: softprops/action-gh-release@v2 if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} with: tag_name: "Nightly" files: | windows_krokiet_on_windows_skia_opengl.exe windows_krokiet_on_windows_skia_vulkan.exe windows_krokiet_on_windows_femtovg_wgpu.exe windows_krokiet_on_windows_all_backends.exe token: ${{ secrets.PAT_REPOSITORY }} container_4_12: runs-on: ubuntu-latest container: image: ghcr.io/mglolenstine/gtk4-cross:gtk-4.12 steps: - uses: actions/checkout@v4 - name: Install additional dependencies # gio is for the build script run: | 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 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" rustup default 1.92.0 rustup target add x86_64-pc-windows-gnu mkdir -p package curl -L https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -o rcedit-x64.exe - name: Cross compile for Windows - Release if: ${{ github.ref == 'refs/heads/master' }} run: | source "$HOME/.cargo/env" export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/share/pkgconfig:$MINGW_PREFIX/lib/pkgconfig/:/usr/x86_64-w64-mingw32/lib/pkgconfig/ cargo build --target=x86_64-pc-windows-gnu --release --locked cp target/x86_64-pc-windows-gnu/release/czkawka_gui.exe package/ cp target/x86_64-pc-windows-gnu/release/czkawka_cli.exe package/ export WINEPREFIX=$(mktemp -d) wine rcedit-x64.exe package/czkawka_gui.exe --set-icon czkawka_gui/icons/icon.ico - name: Cross compile for Windows - Debug if: ${{ github.ref != 'refs/heads/master' }} run: | source "$HOME/.cargo/env" export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/share/pkgconfig:$MINGW_PREFIX/lib/pkgconfig/:/usr/x86_64-w64-mingw32/lib/pkgconfig/ cargo build --target=x86_64-pc-windows-gnu --locked --profile fastci cp target/x86_64-pc-windows-gnu/fastci/czkawka_gui.exe package/ cp target/x86_64-pc-windows-gnu/fastci/czkawka_cli.exe package/ export WINEPREFIX=$(mktemp -d) wine rcedit-x64.exe package/czkawka_gui.exe --set-icon czkawka_gui/icons/icon.ico - name: Package run: | #!/bin/bash set -euo pipefail cp -t package $(pds -vv -f package/*.exe) # Add gdbus which is recommended on Windows (why?) cp $MINGW_PREFIX/bin/gdbus.exe package # Handle the glib schema compilation as well glib-compile-schemas $MINGW_PREFIX/share/glib-2.0/schemas/ mkdir -p package/share/glib-2.0/schemas/ cp -T $MINGW_PREFIX/share/glib-2.0/schemas/gschemas.compiled package/share/glib-2.0/schemas/gschemas.compiled # Pixbuf stuff, in order to get SVGs (scalable icons) to load mkdir -p package/lib/gdk-pixbuf-2.0 cp -rT $MINGW_PREFIX/lib/gdk-pixbuf-2.0 package/lib/gdk-pixbuf-2.0 cp -f -t package $(pds -vv -f $MINGW_PREFIX/lib/gdk-pixbuf-2.0/2.10.0/loaders/*) find package -iname "*.dll" -or -iname "*.exe" -type f -exec mingw-strip {} + cd package/share wget2 https://github.com/qarmin/czkawka/files/10832192/gtk4_theme.zip unzip gtk4_theme.zip rm gtk4_theme.zip cd ../.. wget2 https://github.com/qarmin/Automated-Fuzzer/releases/download/test/libGL.zip unzip libGL.zip mv libEGL.dll package/ mv libGLESv2.dll package/ - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: czkawka-windows-${{ github.sha }}-4.12 path: | ./package if-no-files-found: error - name: Prepare files to release run: | cd package zip -r ../windows_czkawka_gui_gtk_412.zip . cd .. - name: Release uses: softprops/action-gh-release@v2 if: ${{ github.ref == 'refs/heads/master' && vars.HAVE_PAT_REPOSITORY_TOKEN == '1' }} with: tag_name: "Nightly" files: | windows_czkawka_gui_gtk_412.zip token: ${{ secrets.PAT_REPOSITORY }} windows-tests: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Setup rust version run: | rustup default 1.92.0 # Both additional features and gtk gui are non-testable due really complicated setup of environment on Windows - name: Test run: | cargo test -p czkawka_core -p krokiet ================================================ FILE: .gitignore ================================================ /target .idea/ *.iml *~ *# results*.txt TestSuite* *.snap flatpak/ *.zip *.zst *.profraw *.profdata /lcov_report* /report ci_tester/target ci_tester/Cargo.lock krokiet/Cargo.lock krokiet/target *.json *.mm_profdata perf.data perf.data.old krokiet/ui/test.slint *.html misc/*/*.lock misc/*/target/ misc/*/.idea benchmarks TestFiles *.txt .venv charts* *__pycache__*/ result .direnv cedinia/android/keystore/*.keystore cedinia/docs ================================================ FILE: .mailmap ================================================ TheEvilSkeleton Proprietary Chrome-chan Rafał Mikrut <41945903+qarmin@users.noreply.github.com> ================================================ FILE: .rustfmt.toml ================================================ newline_style = "Unix" max_width = 180 # Enable only with nightly channel via - cargo +nightly fmt imports_granularity = "Module" group_imports = "StdExternalCrate" ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "czkawka_core", "czkawka_cli", "czkawka_gui", "krokiet", "cedinia" ] exclude = [ "misc/test_read_perf", "misc/test_image_perf", "misc/test_compilation_speed_size", "ci_tester", ] resolver = "3" # android-activity 0.6.0 panics when ANativeActivity_onCreate is called a second # time (Activity recreation) because ndk_context::initialize_android_context # asserts previous.is_none(). The fix (OnceLock, init once with Application ref) # is merged to main but not yet released. Remove this patch once 0.6.1+ ships. # Affects only android-activity ^0.6 (cedinia); Slint's ^0.5 dependency is unaffected. [patch.crates-io] android-activity = { git = "https://github.com/rust-mobile/android-activity", rev = "43de2770b91b1b8ff870f00551f89f04062216cc" } [profile.release] # panic = "unwind" in opposite to "abort", allows to catch panic!() # Since Czkawka parse different types of files with few libraries, it is possible # that some files will cause crash, so at this moment I don't recommend to use "abort" # until you are ready to occasional crashes panic = "unwind" # Should find more panics, that now are hidden from user - in long term it should decrease bugs in app # It may cause some crashes, that are not handled via panic::catch_unwind, so feel free to disable it, if you want overflow-checks = true # LTO setting is disabled by default, because release mode is usually needed to develop app and compilation with LTO would take a lot of time # But it is used to optimize release builds(and probably also in CI, where time is not so important as in local development) # Fat lto, generates a lot smaller executable than thin lto # Also using codegen-units = 1, to generate smaller binaries #lto = "fat" #codegen-units = 1 # Optimize all dependencies except application/workspaces, even in debug builds to get reasonable performance e.g. when opening images [profile.dev.package."*"] # OPT PACKAGES opt-level = 3 # OPT PACKAGES [profile.fast_release] inherits = "release" incremental = true overflow-checks = true debug = false strip = true [profile.test] debug-assertions = true # Forces to crash when there is duplicated item in cli overflow-checks = true opt-level = 3 # Fast compilation and small binary size [profile.fastci] inherits = "dev" strip = "symbols" debug = false lto = "off" [profile.rdebug] inherits = "release" debug = "full" strip = "none" # Unsafe profile, just to check, how fast and small app could be [profile.fastest] inherits = "release" panic = "abort" lto = "fat" strip = "symbols" codegen-units = 1 opt-level = 3 debug = false [workspace.lints] clippy.unreachable = "allow" # This is a legitimate use case in most places clippy.enum_variant_names = "allow" # Not always is possible to use different names clippy.too_many_arguments = "allow" # Sometimes such functions are needed clippy.type_complexity = "allow" # Sometimes such types are needed clippy.collapsible_else_if = "allow" # Sometimes it is more readable clippy.iter_on_single_items = "allow" # Allows to extend slice items, without needing to converting it when number of items change clippy.needless_range_loop = "allow" # Sometimes it is more readable clippy.doc_broken_link = "warn" clippy.ip_constant = "warn" clippy.unnecessary_semicolon = "warn" clippy.trivially_copy_pass_by_ref = "warn" clippy.indexing_slicing = "warn" clippy.non_std_lazy_statics = "warn" clippy.undocumented_unsafe_blocks = "warn" clippy.manual_midpoint = "warn" clippy.ignore_without_reason = "warn" clippy.elidable_lifetime_names = "warn" #clippy.duration_suboptimal_units = "warn" #clippy.decimal_bitwise_operands = "warn" clippy.allow_attributes = "warn" clippy.assertions_on_result_states = "warn" clippy.bool_to_int_with_if = "warn" clippy.branches_sharing_code = "warn" clippy.collection_is_never_read = "warn" clippy.dbg_macro = "warn" clippy.debug_assert_with_mut_call = "warn" clippy.empty_enum_variants_with_brackets = "warn" clippy.enum_glob_use = "warn" clippy.equatable_if_let = "warn" clippy.error_impl_error = "warn" clippy.explicit_into_iter_loop = "warn" clippy.explicit_iter_loop = "warn" clippy.expl_impl_clone_on_copy = "warn" clippy.fallible_impl_from = "warn" clippy.filter_map_next = "warn" clippy.flat_map_option = "warn" clippy.float_cmp = "warn" clippy.from_iter_instead_of_collect = "warn" clippy.ignored_unit_patterns = "warn" clippy.implicit_clone = "warn" clippy.index_refutable_slice = "warn" clippy.invalid_upcast_comparisons = "warn" clippy.iter_filter_is_ok = "warn" clippy.iter_filter_is_some = "warn" clippy.iter_on_empty_collections = "warn" clippy.iter_with_drain = "warn" clippy.large_stack_arrays = "warn" clippy.large_types_passed_by_value = "warn" clippy.literal_string_with_formatting_args = "warn" clippy.lossy_float_literal = "warn" clippy.macro_use_imports = "warn" clippy.manual_assert = "warn" clippy.manual_instant_elapsed = "warn" clippy.manual_is_variant_and = "warn" clippy.manual_let_else = "warn" clippy.manual_ok_or = "warn" clippy.map_unwrap_or = "warn" clippy.match_bool = "warn" clippy.match_same_arms = "warn" clippy.match_wildcard_for_single_variants = "warn" clippy.mutex_atomic = "warn" clippy.mutex_integer = "warn" clippy.mut_mut = "warn" clippy.needless_bitwise_bool = "warn" clippy.needless_collect = "warn" clippy.needless_continue = "warn" clippy.needless_for_each = "warn" clippy.needless_pass_by_ref_mut = "warn" clippy.needless_pass_by_value = "warn" clippy.needless_raw_strings = "warn" clippy.nonstandard_macro_braces = "warn" clippy.option_as_ref_cloned = "warn" clippy.pathbuf_init_then_push = "warn" clippy.path_buf_push_overwrite = "warn" clippy.print_stderr = "warn" clippy.print_stdout = "warn" clippy.pub_underscore_fields = "warn" clippy.question_mark = "warn" clippy.range_minus_one = "warn" clippy.range_plus_one = "warn" clippy.redundant_clone = "warn" clippy.redundant_else = "warn" clippy.ref_binding_to_reference = "warn" clippy.ref_option_ref = "warn" clippy.same_functions_in_if_condition = "warn" clippy.semicolon_if_nothing_returned = "warn" clippy.set_contains_or_insert = "warn" clippy.stable_sort_primitive = "warn" clippy.string_add_assign = "warn" clippy.string_slice = "warn" clippy.suspicious_operation_groupings = "warn" clippy.suspicious_xor_used_as_pow = "warn" clippy.todo = "warn" clippy.trait_duplication_in_bounds = "warn" clippy.trivial_regex = "warn" clippy.type_repetition_in_bounds = "warn" clippy.unimplemented = "warn" clippy.uninlined_format_args = "warn" clippy.unnecessary_box_returns = "warn" clippy.unnecessary_join = "warn" clippy.unnecessary_wraps = "warn" clippy.unnested_or_patterns = "warn" clippy.unused_async = "warn" clippy.unused_result_ok = "warn" clippy.unused_rounding = "warn" clippy.unused_self = "warn" clippy.unwrap_used = "warn" clippy.used_underscore_binding = "warn" clippy.useless_let_if_seq = "warn" clippy.use_self = "warn" clippy.verbose_file_reads = "warn" clippy.wildcard_imports = "warn" ================================================ FILE: Changelog.md ================================================ ## Version 11.0.1 - 20.02.2026r ### Core - Fixed issue with excluded folders not working on Windows - [#1808](https://github.com/qarmin/czkawka/pull/1808) ### GTK GUI - 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) - Removed warning log message shown for non-existing excluded directories - [#1795](https://github.com/qarmin/czkawka/pull/1795) - Fixed panic occurring when double-clicking a folder from the included or excluded directories list - [#1799](https://github.com/qarmin/czkawka/pull/1799) - Updated to stable gtk4-rs 0.11 - [#1808](https://github.com/qarmin/czkawka/pull/1808) ### Krokiet - Increased default maximum file size limit - [#1808](https://github.com/qarmin/czkawka/pull/1808) ## Prebuilt binaries - Added new Krokiet wgpu binaries - Added new all-in-one Krokiet binaries with all backends included ## CI - Added Windows CI job running `cargo test` ## Version 11.0.0 - 14.02.2026r ### Breaking changes #### Users - 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. - 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. - The `Similarity Preset` enum in similar images mode was replaced with an integer argument `Max Difference` in range 0-40. - 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). #### Devs - Public API functions were slightly adjusted to avoid unnecessary cloning and referencing of copyable types. - `Similarity` variables were renamed to `Difference`. - Applications must call `register_image_decoding_hooks();` at startup to enable reading HEIF and JXL images. ### Core - In similar images mode and previews, extension validation was removed in most cases - [#1623](https://github.com/qarmin/czkawka/pull/1623) - Build-time and runtime versions of Musl and Glibc are now printed to logs - [#1604](https://github.com/qarmin/czkawka/pull/1604/files) - Destination file removal during symlinking is now delayed to prevent data loss in case of failure - [#1672](https://github.com/qarmin/czkawka/pull/1672) - Fixed invalid path canonicalization on Windows - [#1604](https://github.com/qarmin/czkawka/pull/1604/files) - Comparison results are now deterministic - [#1654](https://github.com/qarmin/czkawka/pull/1654) - Built-in JPEG previews are now read from RAW images when available - [#1655](https://github.com/qarmin/czkawka/pull/1655) - Fixed silent panics when the logger could not write to the terminal - [#1658](https://github.com/qarmin/czkawka/pull/1658) - Commit hash is now included in logs - [#1672](https://github.com/qarmin/czkawka/pull/1672) - Improved and fixed logic for grouping similar images by similarity level - [#1685](https://github.com/qarmin/czkawka/pull/1685) - Added scan time measurement - [#1674](https://github.com/qarmin/czkawka/pull/1674), [#1685](https://github.com/qarmin/czkawka/pull/1685) - 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) - 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) - Added new exif remover mode to remove selected EXIF tags from files - [#1745](https://github.com/qarmin/czkawka/pull/1745) - 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) - Added ability to scan individual files, not only folders - [#1745](https://github.com/qarmin/czkawka/pull/1745) - Limited supported image size to 2000 MP - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Automatic cleanup of outdated entries now runs at most once per week - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Added a function to manually remove outdated entries from cache files - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Added video property information, bitrate, codec, FPS, dimensions, duration for similar videos tool - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Fixed double rotation of HEIF images - [#1783](https://github.com/qarmin/czkawka/pull/1783) - Fixed incorrect handling of some HEIF images by using built-in libheif-rs decoding methods - [#1783](https://github.com/qarmin/czkawka/pull/1783) ### CLI - Enabled colored terminal output by default, can be disabled via feature flag - [#1672](https://github.com/qarmin/czkawka/pull/1672) - Fixed a regression where results were not printed to the terminal - [#1672](https://github.com/qarmin/czkawka/pull/1672) - Added `dry_run` and `move_to_trash` options to most of tools - [#1685](https://github.com/qarmin/czkawka/pull/1685) - Fixed unbound `--excluded-extensions` option - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Added new modes: video optimizer, exif remover, and bad names - [#1760](https://github.com/qarmin/czkawka/pull/1760) ### GTK GUI - Restored the sort button and fixed crashes related to sorting - [#1623](https://github.com/qarmin/czkawka/pull/1623) - Configuration now uses JSON format instead of a custom one - [#1623](https://github.com/qarmin/czkawka/pull/1623) - Added multithreaded creation of hard links, symbolic links, and file removal - [#1672](https://github.com/qarmin/czkawka/pull/1672) - Fixed a GTK regression that caused image previews to appear extremely small - [#1658](https://github.com/qarmin/czkawka/pull/1658) - Added a button to easily swap between compared images - [#1658](https://github.com/qarmin/czkawka/pull/1658) - Performed refactoring to evaluate possible migration to GTK 5, currently not very feasible - [#1658](https://github.com/qarmin/czkawka/pull/1658) - Fixed sorting by size in big files mode - [#1691](https://github.com/qarmin/czkawka/pull/1691) - Fixed freezes caused by an invalid function declaration in gtk4-rs - [#1691](https://github.com/qarmin/czkawka/pull/1691) - Added an About popup informing that Krokiet is the successor application - [#1718](https://github.com/qarmin/czkawka/pull/1718) - Added `--cache` and `--config` CLI options to open cache and config paths - [#1745](https://github.com/qarmin/czkawka/pull/1745) - Added shortest and longest path selection modes - [#1738](https://github.com/qarmin/czkawka/pull/1738) ### Krokiet - Added a new logo - [#1726](https://github.com/qarmin/czkawka/pull/1726) - Added video thumbnails, single and grid view - [#1714](https://github.com/qarmin/czkawka/pull/1714) - Displayed cache, thumbnails, and logs size in settings - [#1714](https://github.com/qarmin/czkawka/pull/1714) - Added sorting by clicking column headers - [#1718](https://github.com/qarmin/czkawka/pull/1718) - Introduced a default limit of 500 message lines to prevent freezes caused by slow TextEdit performance - [#1718](https://github.com/qarmin/czkawka/pull/1718) - Slightly increased font sizes to improve readability - [#1726](https://github.com/qarmin/czkawka/pull/1726) - Added runtime application scaling, with some limitations - [#1726](https://github.com/qarmin/czkawka/pull/1726) - Cleared messages in the bottom panel when a new scan starts - [#1726](https://github.com/qarmin/czkawka/pull/1726) - Fixed a crash when clicking previous results while a new scan was in progress - [#1726](https://github.com/qarmin/czkawka/pull/1726) - Changed default behavior to move files to trash instead of permanently deleting them - [#1726](https://github.com/qarmin/czkawka/pull/1726) - Added a notification dialog when the application cannot be opened - [#1745](https://github.com/qarmin/czkawka/pull/1745) - Added `--cache` and `--config` CLI options to open cache and config paths - [#1745](https://github.com/qarmin/czkawka/pull/1745) - 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) - Modification dates are now displayed in local time instead of UTC - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Added a new menu option to manually remove outdated cache entries - [#1748](https://github.com/qarmin/czkawka/pull/1748) - Added an optional scan completion sound, hidden behind the `audio` feature flag - [#1754](https://github.com/qarmin/czkawka/pull/1754) - Fixed an issue where sort options were not updating due to multiple invalid signal connections - [#1760](https://github.com/qarmin/czkawka/pull/1760) - Added support for creating hard links and symbolic links - [#1760](https://github.com/qarmin/czkawka/pull/1760) - Added shortest and longest path selection modes - [#1738](https://github.com/qarmin/czkawka/pull/1738) - Fixed crashes caused by selection cache desynchronization - [#1783](https://github.com/qarmin/czkawka/pull/1783) ### External - 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) ### Prebuilt binaries - Krokiet Windows binaries with the Skia backend are now available, this only works with MSVC build and requires Visual C++ Redistributable - Intel Mac binaries are now built with the latest available macOS version, currently 15 - 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 - Krokiet macOS OpenGL binaries are deprecated due to outdated and broken Apple OpenGL drivers, Skia Vulkan binaries are now provided and recommended - 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 - Windows binaries now use an 8 MB stack size to match Linux, fixing stack overflows in debug builds - Windows binaries now include built-in icons ## Version 10.0.0 - 18.08.2025r ### Breaking changes #### Users - 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. - Cache files now use memory limits and are incompatible with previous versions. - Cli image filter argument changed from `faussian` to `gaussian` #### Devs - `stop_flag` is now required argument in most of the core functions - Visibility of some core functions has been reduced to `pub(crate)` - 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 ### Core - Replaced `println`/`eprintln` with logging functions - [#1478](https://github.com/qarmin/czkawka/pull/1478) - Slightly improved cache loading and saving speed - [#1478](https://github.com/qarmin/czkawka/pull/1478) - 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) - 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) - Czkawka binaries are now reproducible - [#1565](https://github.com/qarmin/czkawka/pull/1565) - Added protection against deleting a folder that is no longer empty since the scan - [#1566](https://github.com/qarmin/czkawka/pull/1566) - Replaced `pdf-rs` with the more popular `lopdf` library, which also has fewer dependencies - [#1566](https://github.com/qarmin/czkawka/pull/1566) - Replaced `imagepipe` + `rawloader` with `rawler` which is still supported and faster to decode raw files - [#1572](https://github.com/qarmin/czkawka/pull/1572) - Added more configuration options in video finder - [#1578](https://github.com/qarmin/czkawka/pull/1578) - `fast_image_resize` feature is removed and `image_hasher/fast_resize_unstable` is enabled unconditionally - [#1586](https://github.com/qarmin/czkawka/pull/1586) ### CLI - Improved logic for deleting files and added progress bar for this operation - [#1571](https://github.com/qarmin/czkawka/pull/1571) ### GTK GUI - 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) - Fixed crash when removing outdated cache - [#1508](https://github.com/qarmin/czkawka/pull/1508) - Fixed missing file and folder names for similar videos in reference folders - [#1520](https://github.com/qarmin/czkawka/pull/1520) - Fixed crashes when the SVG pixbuf loader is not available - [#1565](https://github.com/qarmin/czkawka/pull/1565) - Fixed using custom select on referenced folders - [#1581](https://github.com/qarmin/czkawka/pull/1581) ### Krokiet - Added the ability to select multiple items with mouse and keyboard - [#1478](https://github.com/qarmin/czkawka/pull/1478) - Added sort button - [#1501](https://github.com/qarmin/czkawka/pull/1501) - Window size is now remembered - [#1508](https://github.com/qarmin/czkawka/pull/1508) - Added translations - [#1508](https://github.com/qarmin/czkawka/pull/1508), [#1513](https://github.com/qarmin/czkawka/pull/1513) - Improved popup styling - [#1520](https://github.com/qarmin/czkawka/pull/1520) - Dark and light themes can now be switched at runtime - [#1520](https://github.com/qarmin/czkawka/pull/1520) - Changed icon color to white for dark theme to improve visibility - [#1520](https://github.com/qarmin/czkawka/pull/1520) - Added the ability to hide text on buttons - [#1520](https://github.com/qarmin/czkawka/pull/1520) - Multithreaded removing, moving, and renaming of files - [#1565](https://github.com/qarmin/czkawka/pull/1565) - 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) - 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) - 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) - Improved appearance of bottom directories panel - [#1569](https://github.com/qarmin/czkawka/pull/1569) - Some buttons, are disabled, when there is no files selected - [#1586](https://github.com/qarmin/czkawka/pull/1586) - Added info about the number of items selected to delete - [#1589](https://github.com/qarmin/czkawka/pull/1589) - 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) ### External - There is a new unofficial Tauri-based frontend for Czkawka - [Czkawka Tauri](https://github.com/shixinhuang99/czkawka-tauri) - 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) ### CI - Compilation for 32-bit targets is now checked in CI - Czkawka binaries are now checked for reproducibility in CI ### Prebuilt binaries - 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 - HEIF Mac binaries are now provided - CI now builds Linux binaries on Ubuntu 22.04 instead of 20.04(github removed 20.04 images) - `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` :( - Prebuilt Windows console binaries are no longer provided - logs are now saved to a file, which is easier to read than terminal output - Skia opengl and vulkan backends are provided for Krokiet on Linux(no binaries on Windows, because don't know how to replace `sed`) - Prebuilt binaries are now build with `lto fat` instead `lto thin` and `codegen-units=1` to greatly reduce binary size(~25% smaller binaries) ## Version 9.0.0 - 16.03.2025r ## Breaking changes - Video, Duplicate (smaller prehash size), and Image cache (EXIF orientation + faster resize implementation) are incompatible with previous versions and need to be regenerated. ### Core - Automatically rotating all images based on their EXIF orientation - [#1368](https://github.com/qarmin/czkawka/pull/1368) - Fixed a crash caused by negative time values on some operating systems - [#1369](https://github.com/qarmin/czkawka/pull/1369) - Updated `vid_dup_finder`; it can now detect similar videos shorter than 30 seconds - [#1425](https://github.com/qarmin/czkawka/pull/1425) - Added support for more JXL image formats (using a built-in JXL → image-rs converter) - [#1425](https://github.com/qarmin/czkawka/pull/1425) - Improved duplicate file detection by using a larger, reusable buffer for file reading - [#1425](https://github.com/qarmin/czkawka/pull/1425) - Added an option for significantly faster image resizing to speed up image hashing - [#1458](https://github.com/qarmin/czkawka/pull/1458) - Logs now include information about the operating system and compiled app features(only x86_64 versions) - [#1458](https://github.com/qarmin/czkawka/pull/1458) - Added size progress tracking in certain modes - [#1458](https://github.com/qarmin/czkawka/pull/1458), [#1464](https://github.com/qarmin/czkawka/pull/1464) - Ability to stop hash calculations for large files mid-process - [#1458](https://github.com/qarmin/czkawka/pull/1458) - Implemented multithreading to speed up filtering of hard links - [#1458](https://github.com/qarmin/czkawka/pull/1458) - Reduced prehash read file size to a maximum of 4 KB - [#1458](https://github.com/qarmin/czkawka/pull/1458) - 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) - Improved scan cancellation speed when collecting files to check - [#1460](https://github.com/qarmin/czkawka/pull/1460) - 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) - Fixed a crash in debug mode when checking broken files named `.mp3` - [#1464](https://github.com/qarmin/czkawka/pull/1464) - Catching panics from symphonia crashes in broken files mode - [#1466](https://github.com/qarmin/czkawka/pull/1466) - Printing a warning, when using `panic=abort`(that may speed up app and cause occasional crashes) - [#1466](https://github.com/qarmin/czkawka/pull/1466) ### Krokiet - Changed the default tab to "Duplicate Files" - [#1368](https://github.com/qarmin/czkawka/pull/1368) ### GTK GUI - Added a window icon in Wayland - [#1400](https://github.com/qarmin/czkawka/pull/1400) - Disabled the broken sort button - [#1400](https://github.com/qarmin/czkawka/pull/1400) ### CLI - Added `-N` and `-M` flags to suppress printing results/warnings to the console - [#1464](https://github.com/qarmin/czkawka/pull/1464) - Fixed an issue where messages were not cleared at the end of a scan - [#1464](https://github.com/qarmin/czkawka/pull/1464) - Ability to disable cache via `-H` flag(useful for benchmarking) - [#1466](https://github.com/qarmin/czkawka/pull/1466) ### Prebuild-binaries - This release is last version, that supports Ubuntu 20.04 - github actions drops this OS in its runners - Linux and Mac binaries now are provided with two options x86_64 and arm64 - Arm linux builds needs at least Ubuntu 24.04 - Gtk 4.12 is used to build windows gtk gui instead gtk 4.10 - Dropping support for snap builds - too much time-consuming to maintain and testing(also it is broken currently) - Removed native windows build krokiet version - now it is available only cross-compiled version from linux(should not be any difference) ## Version 8.0.0 - 11.10.2024r ### Breaking changes - 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) - Some CLI arguments could change short name, due fixing ambiguous names ### Known regressions - Slint 1.8 which Krokiet uses requires femtovg 0.9.2 which broke font rendering - https://github.com/slint-ui/slint/issues/6298 ### CI - Providing nightly builds - [#1360](https://github.com/qarmin/czkawka/pull/1360) - https://github.com/qarmin/czkawka/releases/tag/Nightly - Added finding duplicated options in CLI - [#1364](https://github.com/qarmin/czkawka/pull/1364) ### Core - Removed some unnecessary panics - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Simplified usage of structures when sending/receiving progress information - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Added Median hash algorithm - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Fixed compilation with Rust >=1.80 - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Extracted tool input parameters, that helped to find not used parameters - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Added new mod to find similar music only in groups with similar title tag - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Printing to file/console no longer uses two backslashes in windows paths - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Fixed panic when failed to decode raw picture - [#1355](https://github.com/qarmin/czkawka/pull/1355) - Remove useless saving/loading cache when there is no files to check - [#1358](https://github.com/qarmin/czkawka/pull/1358) - Filtering hard links on windows - [#1316](https://github.com/qarmin/czkawka/pull/1316) - Added jxl support - [#1358](https://github.com/qarmin/czkawka/pull/1358) - Added avif support(via external C library, not enabled by default) - [#1358](https://github.com/qarmin/czkawka/pull/1358) - Integer overflow are enabled by default(prepare for reporting bugs, slower performance and general unstability) - [#1358](https://github.com/qarmin/czkawka/pull/1358) - Fixed crash when loading invalid image cache - [#1230](https://github.com/qarmin/czkawka/pull/1230) ### Krokiet - Fixed invalid default hash size in similar images - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Fixed and added more input parameters to the application - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Fixed problem with loading invalid preset - [#1226](https://github.com/qarmin/czkawka/pull/1226) - Fixed crash when using 8 hash size with small similarity - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Disabling buttons when no files were found - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Changed way to close/open panel at bottom - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Modify logo a little - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Avoid errors when trying to load preview of not supported file - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Added ability to show preview of referenced folders - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Enable selecting with space and jumping over entries with arrows and opening with enter - [#1359](https://github.com/qarmin/czkawka/pull/1359) - Added button to rename files with invalid extension - [#1364](https://github.com/qarmin/czkawka/pull/1364) ### GTK GUI - Fixed and added more input parameters to the application - [#1355](https://github.com/qarmin/czkawka/pull/1355) - Added option to use external libraries instead gtk pixbuf loader for previews - [#1358](https://github.com/qarmin/czkawka/pull/1358) - Using static runtime with zstd compression in appimage - [#1350](https://github.com/qarmin/czkawka/pull/1355) - Restoring flatpak builds - [#1275](https://github.com/qarmin/czkawka/pull/1275) - [External] Mac homebrew version of app - https://formulae.brew.sh/formula/czkawka ### CLI - Added options to find/remove images by size - [#1255](https://github.com/qarmin/czkawka/pull/1255) - Fixed and added more input parameters to the application - [#1354](https://github.com/qarmin/czkawka/pull/1354) - Fixed crash when stopping scan multiple times - [#1355](https://github.com/qarmin/czkawka/pull/1355) - Print results also in debug build - [#1355](https://github.com/qarmin/czkawka/pull/1355) - Added support for selecting reference directories - [#1364](https://github.com/qarmin/czkawka/pull/1364) ## Version 7.0.0 - 19.02.2024r ### BREAKING CHANGES - Reducing size of cache files, made old cache files incompatible with new version - `-C` in CLI now saves as compact json ### GTK GUI - Added drag&drop support for included/excluded folders - [#1106](https://github.com/qarmin/czkawka/pull/1106) - Added information where are saved scan results - [#1102](https://github.com/qarmin/czkawka/pull/1102) ### CLI - Providing full static rust binary with [Eyra](https://github.com/sunfishcode/eyra) - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Fixed duplicated `-c` argument, now saving as compact json is handled via `-C` - [#1153](https://github.com/qarmin/czkawka/pull/1153) - Added scan progress bar - [#1183](https://github.com/qarmin/czkawka/pull/1183) - Clean and safe cancelling of scan - [#1183](https://github.com/qarmin/czkawka/pull/1183) - Unification of CLI arguments - [#1183](https://github.com/qarmin/czkawka/pull/1183) - Hardlink support for similar images/videos - [#1201](https://github.com/qarmin/czkawka/pull/1201) ### Krokiet GUI - Initial release of new gui - [#1102](https://github.com/qarmin/czkawka/pull/1102) ### Core - Using normal crossbeam channels instead of asyncio tokio channel - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Fixed tool type when using progress of empty directories - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Fixed missing json support when saving size and name duplicate results - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Fix cross-compiled debug windows build - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Added bigger stack size by default(fixes stack overflow in some musl apps) - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Added optional libraw dependency(better single-core performance and support more raw files) - [#1102](https://github.com/qarmin/czkawka/pull/1102) - Speedup checking for wildcards and fix invalid recognizing long excluded items - [#1152](https://github.com/qarmin/czkawka/pull/1152) - Big speedup when searching for empty folders(especially with multithreading + cached FS schema) - [#1152](https://github.com/qarmin/czkawka/pull/1152) - Collecting files for scan can be a lot of faster due lazy file metadata gathering - [#1152](https://github.com/qarmin/czkawka/pull/1152) - Fixed recognizing not accessible folders as non-empty - [#1152](https://github.com/qarmin/czkawka/pull/1152) - Unifying code for collecting files to scan - [#1159](https://github.com/qarmin/czkawka/pull/1159) - Decrease memory usage when collecting files by removing unused fields in custom file entries structs - [#1159](https://github.com/qarmin/czkawka/pull/1159) - Decrease a little size of cache by few percents and improve loading/saving speed - [#1159](https://github.com/qarmin/czkawka/pull/1159) - Added ability to remove from scan files with excluded extensions - [#1184](https://github.com/qarmin/czkawka/pull/1102) - Fixed not showing in similar images results, files with same hashes when using reference folders - [#1184](https://github.com/qarmin/czkawka/pull/1102) - Optimize release binaries with LTO(~25/50% smaller, ~5/10% faster) - [#1184](https://github.com/qarmin/czkawka/pull/1102) ## Version 6.1.0 - 15.10.2023r - BREAKING CHANGE - Changed cache saving method, deduplicated, optimized and simplified procedure(all files needs to be hashed again) - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1086](https://github.com/qarmin/czkawka/pull/1086) - Remove up to 340ms of delay when waiting for results - [#1070](https://github.com/qarmin/czkawka/pull/1070) - 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) - 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) - Updated list of bad extensions and support for finding invalid jar files - [#1070](https://github.com/qarmin/czkawka/pull/1070) - More default excluded items on Windows(like pagefile) - [#1074](https://github.com/qarmin/czkawka/pull/1074) - Unified printing/saving method to files/terminal and fixed some differences/bugs - [#1082](https://github.com/qarmin/czkawka/pull/1082) - Uses fun_time library to print how much functions take time - [#1082](https://github.com/qarmin/czkawka/pull/1082) - Added exporting results into json file format - [#1083](https://github.com/qarmin/czkawka/pull/1083) - Added new test/regression suite for CI - [#1083](https://github.com/qarmin/czkawka/pull/1083) - Added ability to use relative paths - [#1083](https://github.com/qarmin/czkawka/pull/1083) - Allowed removing similar images/videos/music from cli - [#1087](https://github.com/qarmin/czkawka/pull/1087) - Added info about saving/loading items to cache in duplicate and music mode - [#1091](https://github.com/qarmin/czkawka/pull/1091) - Fixed number of files to check in duplicate mode - [#1091](https://github.com/qarmin/czkawka/pull/1091) - Added support for qoi image format(without preview yet) - [e92a](https://github.com/qarmin/czkawka/commit/e92a8a65de9bd1250be482dbce06959125554849) - Fixed stability problem, that could remove invalid file in CLI - [#1083](https://github.com/qarmin/czkawka/pull/1083) - Fix Windows gui crashes by using gtk 4.6 instead 4.8 or 4.10 - [#992](https://github.com/qarmin/czkawka/pull/992) - Fixed printing info about duplicated music files - [#1016](https://github.com/qarmin/czkawka/pull/1016) - Fixed printing info about duplicated video files - [#1017](https://github.com/qarmin/czkawka/pull/1017) ## Version 6.0.0 - 11.06.2023r - Add finding similar audio files by content - [#970](https://github.com/qarmin/czkawka/pull/970) - Allow to find duplicates by name/size at once - [#956](https://github.com/qarmin/czkawka/pull/956) - Fix, simplify and speed up finding similar images - [#983](https://github.com/qarmin/czkawka/pull/956) - Fixed bug when cache for music tags not worked - [#970](https://github.com/qarmin/czkawka/pull/970) - Allow to set number of threads from CLI - [#972](https://github.com/qarmin/czkawka/pull/972) - Fix problem with invalid item sorting in bad extensions mode - [#972](https://github.com/qarmin/czkawka/pull/972) - 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) - Use builtin gtk webp loader for previews - [#923](https://github.com/qarmin/czkawka/pull/923) - Fixed docker build - [#947](https://github.com/qarmin/czkawka/pull/947) - Restore snap builds broken since GTk 4 port - [#965](https://github.com/qarmin/czkawka/pull/947) - 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) ## Version 5.1.0 - 19.02.2023r - Added sort button - [#894](https://github.com/qarmin/czkawka/pull/894) - Allow to set number of thread used to scan - [#839](https://github.com/qarmin/czkawka/pull/839) - Faster similar images comparing with reference folders - [#826](https://github.com/qarmin/czkawka/pull/826) - Update to clap 4 - [#878](https://github.com/qarmin/czkawka/pull/878) - Use FileChooserNative instead FileChooserDialog - [#894](https://github.com/qarmin/czkawka/pull/894) - Fix invalid music tags in music files when using reference folders - [#894](https://github.com/qarmin/czkawka/pull/894) - Updated pdf dependency(a lot of less amount of broken pdf false positives) - [#894](https://github.com/qarmin/czkawka/pull/894) - Changed strange PDF error message - "Try at" - [#894](https://github.com/qarmin/czkawka/pull/894) - Treat extensions Mp4 and m4v as identical - [#834](https://github.com/qarmin/czkawka/pull/834) - Improve thumbnail quality - [#895](https://github.com/qarmin/czkawka/pull/895) - Verify if hardlinking works, and if not, disable button with proper message - [#881](https://github.com/qarmin/czkawka/pull/881) - Apply some pydantic clippy lints on project - [#901](https://github.com/qarmin/czkawka/pull/901) ## Version 5.0.2 - 30.08.2022r - Fixed problem with missing some similar images when using similarity > 0 - [#799](https://github.com/qarmin/czkawka/pull/799) - Prebuilt Linux binaries are compiled without heif support - [24b](https://github.com/qarmin/czkawka/commit/24b64a32c65904c506b54270f0977ccbe5098cc8) - Similar videos stops to proceed video after certain amount of time(fixes freezes) - [#815](https://github.com/qarmin/czkawka/pull/815) - Add --version argument for czkawka_cli - [#806](https://github.com/qarmin/czkawka/pull/806) - Rewrite a little nonsense message about minimal file size - [#807](https://github.com/qarmin/czkawka/pull/807) ## Version 5.0.1 - 03.08.2022r - Fixed problem with removing ending slash with empty disk window path - [975](https://github.com/qarmin/czkawka/commit/97563a7b2a70fb5fcf6463f28069e6ea3b0ff5c2) - Added to CLI bad extensions mode - [#795](https://github.com/qarmin/czkawka/pull/795) - Restore default sorting method in CLI where finding biggest files - [5d7](https://github.com/qarmin/czkawka/commit/5d79dc7ccfee6d5426e37c4e6a860fa555c5927a) - Added tests to CI - [#791](https://github.com/qarmin/czkawka/pull/791) - Show error message when all directories are set as reference folders - [#795](https://github.com/qarmin/czkawka/pull/795) - Added more info about new requirements on Linux - [#795](https://github.com/qarmin/czkawka/pull/795) ## Version 5.0.0 - 28.07.2022r - GUI ported to use GTK 4 - [#466](https://github.com/qarmin/czkawka/pull/466) - Use multithreading and improved algorithm to compare image hashes - [#762](https://github.com/qarmin/czkawka/pull/762) - Resize preview with window - [#466](https://github.com/qarmin/czkawka/pull/466) - Fix removing only one item from list view - [#466](https://github.com/qarmin/czkawka/pull/466) - Fix showing help command in duplicate CLI mode - [#720](https://github.com/qarmin/czkawka/pull/720) - Fix freeze when not choosing any tag in similar music mode - [#732](https://github.com/qarmin/czkawka/pull/732) - Fix preview of files with non-lowercase extensions - [#694](https://github.com/qarmin/czkawka/pull/694) - Read more tags from music files - [#705](https://github.com/qarmin/czkawka/pull/705) - 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) - Support for finding invalid PDF files - [#705](https://github.com/qarmin/czkawka/pull/705) - Re-enable checking for broken music files(`libasound.so.2` no longer needed) - [#705](https://github.com/qarmin/czkawka/pull/705) - Fix disabled ui when using invalid settings in similar music - [#740](https://github.com/qarmin/czkawka/pull/740) - Speedup searching for invalid extensions - [#740](https://github.com/qarmin/czkawka/pull/740) - Support for finding the smallest files - [#741](https://github.com/qarmin/czkawka/pull/741) - Improved Windows CI - [#749](https://github.com/qarmin/czkawka/pull/749) - Ability to check for broken files by types - [#749](https://github.com/qarmin/czkawka/pull/749) - Add heif and Webp files support - [#750](https://github.com/qarmin/czkawka/pull/750) - Use in CLI Clap library instead StructOpt - [#759](https://github.com/qarmin/czkawka/pull/759) - Multiple directories can be added via Manual Add button - [#782](https://github.com/qarmin/czkawka/pull/782) - Option to exclude files from other filesystems in GUI(Linux) - [#776](https://github.com/qarmin/czkawka/pull/776) ## Version 4.1.0 - 24.04.2022r - New mode - finding files whose content not match with their extension - [#678](https://github.com/qarmin/czkawka/pull/678) - Builtin icons - no more invalid, theme/OS dependent icons - [#659](https://github.com/qarmin/czkawka/pull/659) - Big(usually 2x) speedup of showing previews of images(both previews in scan and compare window) - [#660](https://github.com/qarmin/czkawka/pull/660) - Fix selecting records by custom selection popup - [#632](https://github.com/qarmin/czkawka/pull/632) - Support more tags when comparing music files - [#590](https://github.com/qarmin/czkawka/pull/590) - Fix not proper selecting path - [#656](https://github.com/qarmin/czkawka/pull/656) - 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) - Custom selecting is now case-insensitive by default - [#657](https://github.com/qarmin/czkawka/pull/657) - Better approximate comparison of tags - [#641](https://github.com/qarmin/czkawka/pull/641) - Fix search problem due accumulated stop events - [#623](https://github.com/qarmin/czkawka/pull/623) - Option to ignore other filesystems in Unix OS(for now only in CLI) - [#673](https://github.com/qarmin/czkawka/pull/673) - Fix file hardlinking on Windows - [#668](https://github.com/qarmin/czkawka/pull/668) - Support for case-insensitive name grouping of files - [#669](https://github.com/qarmin/czkawka/pull/669) - Directories for search GUI can be passed by CLI - [#677](https://github.com/qarmin/czkawka/pull/677) - Prevent from getting non respond app notification from display servers - [#625](https://github.com/qarmin/czkawka/pull/625) ## Version 4.0.0 - 20.01.2022r - 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) - 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) - Add support for finding similar videos - [#460](https://github.com/qarmin/czkawka/pull/460) - GUI code refactoring and search code unification - [#462](https://github.com/qarmin/czkawka/pull/462), [#531](https://github.com/qarmin/czkawka/pull/531) - Fixed crash when trying to hard/symlink 0 files - [#462](https://github.com/qarmin/czkawka/pull/462) - 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) - Change minimal supported OS to Ubuntu 20.04(needed by GTK) - [#468](https://github.com/qarmin/czkawka/pull/468) - Increased performance by avoiding creating unnecessary image previews - [#468](https://github.com/qarmin/czkawka/pull/468) - Improved performance due caching hash of broken/not supported images/videos = [#471](https://github.com/qarmin/czkawka/pull/471) - Option to not remove cache from non-existent files(e.g. from unplugged pendrive) - [#472](https://github.com/qarmin/czkawka/pull/472) - Add multiple tooltips with helpful messages - [#472](https://github.com/qarmin/czkawka/pull/472) - Allow caching prehash - [#477](https://github.com/qarmin/czkawka/pull/477) - Improve custom selecting of records(allows to use Rust regex) - [#489](https://github.com/qarmin/czkawka/pull/478) - Remove support for finding zeroed files - [#461](https://github.com/qarmin/czkawka/pull/461) - Remove HashMB mode - [#476](https://github.com/qarmin/czkawka/pull/476) - Approximate comparison of music - [#483](https://github.com/qarmin/czkawka/pull/483) - Enable column sorting for simple treeview - [#487](https://github.com/qarmin/czkawka/pull/487) - Allow hiding upper panel - [#491](https://github.com/qarmin/czkawka/pull/491) - Make UI take less space - [#500](https://github.com/qarmin/czkawka/pull/500) - Add support for raw images(NEF, CR2, KDC...) - [#532](https://github.com/qarmin/czkawka/pull/532) - 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) - Reorganize(unify) saving/loading data from file - [#524](https://github.com/qarmin/czkawka/pull/524) - Add "reference folders" - [#516](https://github.com/qarmin/czkawka/pull/516) - Add cache for similar music files - [#558](https://github.com/qarmin/czkawka/pull/558) ## Version 3.3.1 - 22.11.2021r - Fix crash when moving buttons [#457](https://github.com/qarmin/czkawka/pull/457) - Hide move button at start [c9ca230](https://github.com/qarmin/czkawka/commit/c9ca230dfd05e2166b2d68683b091cfd45037edd) ## Version 3.3.0 - 20.11.2021r - Select files by pressing space key [#415](https://github.com/qarmin/czkawka/pull/415) - Add additional info to printed errors [#446](https://github.com/qarmin/czkawka/pull/446) - 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) - Button to move files/folders to provided location [#449](https://github.com/qarmin/czkawka/pull/449) - Add non-clickable button to fix white theme [#450](https://github.com/qarmin/czkawka/pull/450) - Fixed freeze when opening in same thread file/folder [#448](https://github.com/qarmin/czkawka/pull/448) - Tool to check performance of different image filters and hash types and sizes [#447](https://github.com/qarmin/czkawka/pull/447) - 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) - Update snap file to use builtin rust plugin and update gnome extension [8f2](https://github.com/qarmin/czkawka/commit/8f232285e5c34bee6d5da8e1453d7f40a0ffd08d) - 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) ## Version 3.2.0 - 07.08.2021r - Use checkbox instead selection to select files [#392](https://github.com/qarmin/czkawka/pull/392) - Re-enable hardlink on windows - [#410](https://github.com/qarmin/czkawka/pull/410) - Fix symlink and hardlink creating - [#409](https://github.com/qarmin/czkawka/pull/409) - Add image preview to duplicate finder [#408](https://github.com/qarmin/czkawka/pull/408) - Add setting maximum file size [#407](https://github.com/qarmin/czkawka/pull/407) - Add new grouping algorithm to similar images [#405](https://github.com/qarmin/czkawka/pull/405) - Update to Rust 1.54 [#400](https://github.com/qarmin/czkawka/pull/400) - Add webp support to similar images [#396](https://github.com/qarmin/czkawka/pull/396) - Use GtkScale instead radio buttons for similarity [#397](https://github.com/qarmin/czkawka/pull/397) - Update all dependencies [#405](https://github.com/qarmin/czkawka/pull/405), [#395](https://github.com/qarmin/czkawka/pull/395) - Split UI into multiple files [#391](https://github.com/qarmin/czkawka/pull/391) - Update to gtk-rs 0.14 [#383](https://github.com/qarmin/czkawka/pull/383) - Fix bug with moving windows [#361](https://github.com/qarmin/czkawka/pull/361) - Generate Minimal Appimage [#339](https://github.com/qarmin/czkawka/pull/339) ## Version 3.1.0 - 09.05.2021r - Clean README, by moving instructions to different files - [9aea6e9b](https://github.com/qarmin/czkawka/commit/9aea6e9b1ef5ac1e56ccd008e7456b80401179d0) - Fix excluded items on Windows - [#324](https://github.com/qarmin/czkawka/pull/324) - Center windows and add missing settings icon - [#323](https://github.com/qarmin/czkawka/pull/323) - Sort cache - [#322](https://github.com/qarmin/czkawka/pull/322) - Add desktop file to Snap - [018d5bebb](https://github.com/qarmin/czkawka/commit/018d5bebb0b297ba35529b03b8e2e68eb0a9b474), [ade2a756e2](https://github.com/qarmin/czkawka/commit/ade2a756e29c5ce5739d6268fcab7e76f59ed5f6) - Customize minimum file size of cached records - [#321](https://github.com/qarmin/czkawka/pull/321) - Update benchmarks - [2044b9185](https://github.com/qarmin/czkawka/commit/2044b91852fea89dfaf10dc1ab79c1d00e9e0c12) - Rearrange Instruction - [8e7ac4a2d7f5b0](https://github.com/qarmin/czkawka/commit/8e7ac4a2d7f5b0beba2552581fb3a0d19c2efeb5) - Add info that Czkawka and Bleachbit are not alternatives to each other - [30602a486](https://github.com/qarmin/czkawka/commit/30602a486f6ade6f9b7b91a73708225b4f4c2a7d) - Fix crashes with too small message queue - [#316](https://github.com/qarmin/czkawka/pull/316) - Fix a little unsorted results - [#304](https://github.com/qarmin/czkawka/pull/304) - Fix Appimage(external bug) - [#299](https://github.com/qarmin/czkawka/issues/299) - Fix error with saving results of name duplicates - [#307](https://github.com/qarmin/czkawka/pull/307) - Update to Rust 1.5.1 - [#302](https://github.com/qarmin/czkawka/pull/302) ## Version 3.0.0 - 11.03.2021r - Option to not ignore hardlinks - [#273](https://github.com/qarmin/czkawka/pull/273) - Hardlink support for GUI - [#276](https://github.com/qarmin/czkawka/pull/276) - New settings window - [#262](https://github.com/qarmin/czkawka/pull/262) - Unify file removing - [#278](https://github.com/qarmin/czkawka/pull/278) - Dryrun in duplicates CLI - [#277](https://github.com/qarmin/czkawka/pull/277) - Option to turn off cache - [#263](https://github.com/qarmin/czkawka/pull/263) - Update Image dependency and fix crashes - [#270](https://github.com/qarmin/czkawka/pull/270), [e3aca69](https://github.com/qarmin/czkawka/commit/e3aca69499966499413e4b7cd4d1037bec6a5d68) - Add confirmation dialog when trying to remove all files in group - [#281](https://github.com/qarmin/czkawka/pull/281) - Add confirmation dialog when removing files with delete key - [#282](https://github.com/qarmin/czkawka/pull/282) - Open file when clicking at the Enter button - [#285](https://github.com/qarmin/czkawka/pull/285) - Allow to put files to trash instead fully remove them - [#284](https://github.com/qarmin/czkawka/pull/284) ## Version 2.4.0 - 22.02.2021r - Add about dialog - [#226](https://github.com/qarmin/czkawka/pull/226) - Remove checking for ico in similar images - [#227](https://github.com/qarmin/czkawka/pull/227) - Change progress dialog to progress window - [#229](https://github.com/qarmin/czkawka/pull/229) - Restore snap confinement - [#218](https://github.com/qarmin/czkawka/pull/218), [8dcb718](https://github.com/qarmin/czkawka/commit/8dcb7188434e1c1728368642e17ccec29a4b372d) - Add support for CRC32 and XXH3 hash - [#243](https://github.com/qarmin/czkawka/pull/243) - Add delete method to replace duplicate files with hard links - [#236](https://github.com/qarmin/czkawka/pull/236) - Add checking for broken music opt-in - [#249](https://github.com/qarmin/czkawka/pull/249) - Allow to save to file similar images results - [10156ccfd3](https://github.com/qarmin/czkawka/commit/10156ccfd3ba880d26d4bbad1e025b0050d7753b) - Keep original file if replacing duplicate with hardlink fails - [#256](https://github.com/qarmin/czkawka/pull/256) - Fix Windows theme - [#265](https://github.com/qarmin/czkawka/pull/265) - Windows taskbar progress support - [#264](https://github.com/qarmin/czkawka/pull/264) - Ignore duplicates if those are hard links - [#234](https://github.com/qarmin/czkawka/pull/234) - Support the hash type parameter in the CLI - [#267](https://github.com/qarmin/czkawka/pull/267) - Use one implementation for all hash calculations - [#268](https://github.com/qarmin/czkawka/pull/268) - Disable for now broken tga and gif files - [#270](https://github.com/qarmin/czkawka/pull/270) ## Version 2.3.2 - 21.01.2021r - Add support for moving selection by keyboard to update similar image preview [#223](https://github.com/qarmin/czkawka/pull/223) This version is only needed to test flatpak build ## Version 2.3.1 - 20.01.2021r - Added flatpak support - [#203](https://github.com/qarmin/czkawka/pull/203) - Spell fixes - [#222](https://github.com/qarmin/czkawka/pull/222), [#219](https://github.com/qarmin/czkawka/pull/219) ## Version 2.3.0 - 15.01.2021r - Add cache for duplicate finder - [#205](https://github.com/qarmin/czkawka/pull/205) - Add cache for broken files - [#204](https://github.com/qarmin/czkawka/pull/204) - Decrease ram usage - [#212](https://github.com/qarmin/czkawka/pull/212) - Add support for finding broken zip and audio files - [#210](https://github.com/qarmin/czkawka/pull/210) - Sort Results by path where it is possible - [#211](https://github.com/qarmin/czkawka/pull/211) - Add missing popover info for invalid symlinks - [#209](https://github.com/qarmin/czkawka/pull/209) - Use the oldest available OS in Linux and Mac CI and the newest on Windows - [#206](https://github.com/qarmin/czkawka/pull/206) - Add broken files support - [#202](https://github.com/qarmin/czkawka/pull/202) - Remove save workaround and fix crashes when loading/saving cache - [#200](https://github.com/qarmin/czkawka/pull/200) - Fix error when closing dialog progress by X - [#199](https://github.com/qarmin/czkawka/pull/199) ## Version 2.2.0 - 11.01.2021r - Adds Mac GUI - [#160](https://github.com/qarmin/czkawka/pull/160) - Use master gtk plugin again - [#179](https://github.com/qarmin/czkawka/pull/179) - Only show preview when 1 image is selected - [#183](https://github.com/qarmin/czkawka/pull/183) - Add buffered write/read - [#186](https://github.com/qarmin/czkawka/pull/186) - Fix included/excluded files which contains commas - [#195](https://github.com/qarmin/czkawka/pull/195) - Move image cache to cache from config dir - [#197](https://github.com/qarmin/czkawka/pull/197) - 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) ## Version 2.1.0 - 31.12.2020r - Hide preview when deleting images or symlinking it - [#167](https://github.com/qarmin/czkawka/pull/167) - Add manual adding of directories - [#165](https://github.com/qarmin/czkawka/pull/165), [#168](https://github.com/qarmin/czkawka/pull/168) - Add resizable top panel - [#164](https://github.com/qarmin/czkawka/pull/164) - Add support for delete button - [#159](https://github.com/qarmin/czkawka/pull/159) - Allow to select multiple entries in File Chooser - [#154](https://github.com/qarmin/czkawka/pull/154) - Add cache support for similar images - [#139](https://github.com/qarmin/czkawka/pull/139) - Add selecting images with its size - [#138](https://github.com/qarmin/czkawka/pull/138) - Modernize popovers code and simplify later changes - [#137](https://github.com/qarmin/czkawka/pull/137) ## Version 2.0.0 - 23.12.2020r - Add Snap support - [ee3d4](https://github.com/qarmin/czkawka/commit/ee3d450552cd0c37a114b05c557ff9381ef92466) - Select longer names by default - [#113](https://github.com/qarmin/czkawka/pull/113) - Add setting for deletion confirmation dialog - [#114](https://github.com/qarmin/czkawka/pull/114) - Add button to hide/show text view errors - [#115](https://github.com/qarmin/czkawka/pull/115) - Remove console window in Windows - [#116](https://github.com/qarmin/czkawka/pull/116) - Add custom selection/unselection - [#117](https://github.com/qarmin/czkawka/pull/117) - Add Image preview to similar images - [#118](https://github.com/qarmin/czkawka/pull/118) - Remove orbtk frontend - [#119](https://github.com/qarmin/czkawka/pull/119) - Update Icon - [#120](https://github.com/qarmin/czkawka/pull/120) - Add setting button to disable/enable previews(enabled by default) - [#121](https://github.com/qarmin/czkawka/pull/121) - Add button to enable/disable in settings text view errors - [#122](https://github.com/qarmin/czkawka/pull/122) - Add support for symbolic links - [#123](https://github.com/qarmin/czkawka/pull/123) - Add support for checking for invalid symlinks - [#124](https://github.com/qarmin/czkawka/pull/124) - Add new windows dark theme - [#125](https://github.com/qarmin/czkawka/pull/125) - Fix appimage crash by adding PNG version of icon - [#126](https://github.com/qarmin/czkawka/pull/126) - Split symlink path to two path and file name - [#127](https://github.com/qarmin/czkawka/pull/127) - Add option to open folders by double right click - [#128](https://github.com/qarmin/czkawka/pull/128) - Add minimal similarity level - [#129](https://github.com/qarmin/czkawka/pull/129) - Show errors in image previewer when failed to generate it - [#130](https://github.com/qarmin/czkawka/pull/130) - 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) ## Version 1.5.1 - 08.12.2020r - Fix errors in progress bar caused by dividing by 0 - [#109](https://github.com/qarmin/czkawka/pull/109) - Add option to save file, store settings and load them - [#108](https://github.com/qarmin/czkawka/pull/108) - Center dialog to current window - [a04](https://github.com/qarmin/czkawka/commit/a047380dbe8aa4d04f9c482364469e21d231fab2) ## Version 1.5.0 - 02.12.2020r - Added progress bar - [#106](https://github.com/qarmin/czkawka/pull/106) - Removed unused buttons - [#107](https://github.com/qarmin/czkawka/pull/107) ## Version 1.4.0 - 09.11.2020r - 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) - Simplify GUI code [#96](https://github.com/qarmin/czkawka/pull/96) - Group similar images - [#97](https://github.com/qarmin/czkawka/pull/97) - Add select buttons to each type of mode - [#102](https://github.com/qarmin/czkawka/pull/102) - Fix GUI behavior in GUI when deleting similar image - [#103](https://github.com/qarmin/czkawka/pull/103) - Add new similarity level - [#104](https://github.com/qarmin/czkawka/pull/104) ## Version 1.3.0 - 02.11.2020r - Appimage support - [#77](https://github.com/qarmin/czkawka/pull/77) - Removed warnings about non-existed excluded directories - [#79](https://github.com/qarmin/czkawka/pull/79) - Updated README - [8ec](https://github.com/qarmin/czkawka/commit/8ecde0fc9adb3e6cedf432c4ba749e698b645a7a) - Added pre hash support(speedup for searching big duplicates) - [#83](https://github.com/qarmin/czkawka/pull/83) - Support for searching duplicates by file name - [#84](https://github.com/qarmin/czkawka/pull/84) - Added support for checking for zeroed file - [#88](https://github.com/qarmin/czkawka/pull/88) - Refactored GUI code to faster and safer changing/adding code - [#89](https://github.com/qarmin/czkawka/pull/89) - Added some missing options to CLI in some modes - [#90](https://github.com/qarmin/czkawka/pull/90) - Implemented finding duplicates by music tags - [#95](https://github.com/qarmin/czkawka/pull/95) ## Version 1.2.1 - 17.10.2020r - Make image similarity search significantly faster. [#72](https://github.com/qarmin/czkawka/pull/72) - Improve similar images GUI a little and add sorting to Similarity Enum [#73](https://github.com/qarmin/czkawka/pull/73) - Improve deleting files in Similar files in GUI [#75](https://github.com/qarmin/czkawka/pull/75) ## Version 1.2.0 - 15.10.2020r - Replace String with PathBuf for paths [#59](https://github.com/qarmin/czkawka/pull/59) - Add test suite to PR [#65](https://github.com/qarmin/czkawka/pull/65) - Support for finding similar images to CLI [#66](https://github.com/qarmin/czkawka/pull/66) - Fix grammar-related errors and Ponglish expressions [#62](https://github.com/qarmin/czkawka/pull/62), [#63](https://github.com/qarmin/czkawka/pull/63) - Don't delete by default files in duplicate finder in CLI - [23f203](https://github.com/qarmin/czkawka/commit/23f203a061e254275c95ca23ca4f1a78bd941f02) - Support for finding similar images to GUI [#69](https://github.com/qarmin/czkawka/pull/69) - Add support for opening files/folders from GUI with double-click [#70](https://github.com/qarmin/czkawka/pull/70) ## Version 1.1.0 - 10.10.2020r - Windows support [#58](https://github.com/qarmin/czkawka/pull/58) - Improve code quality/Simplify codebase [#52](https://github.com/qarmin/czkawka/pull/52) - Fixed skipping some correct results in specific situations [#52](https://github.com/qarmin/czkawka/pull/52#discussion_r502613895) - Added support for searching in other thread [#51](https://github.com/qarmin/czkawka/pull/51) - Divide CI across files [#48](https://github.com/qarmin/czkawka/pull/48) - Added ability to stop task from GUI [#55](https://github.com/qarmin/czkawka/pull/55) - Fixed removing directories which contains only empty directories from GUI [#57](https://github.com/qarmin/czkawka/pull/57) ## Version 1.0.1 - 06.10.2020r - Replaced default argument parser with StructOpt [#37](https://github.com/qarmin/czkawka/pull/37) - 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) - App can be downloaded also from Arch AUR and Cargo [#36](https://github.com/qarmin/czkawka/pull/36) - Fixed crash with invalid file modification date [#33](https://github.com/qarmin/czkawka/issues/33) - Upper tabs can hide and show when this is necessary [#38](https://github.com/qarmin/czkawka/pull/38) - Fixed crash when file/folder name have non Unicode character [#44](https://github.com/qarmin/czkawka/issues/44) - Added support for finding similar pictures in GUI [#69](https://github.com/qarmin/czkawka/issues/69) ## Version 1.0.0 - 02.10.2020r - Added confirmation dialog to delete button - Updated Readme - Tested a lot app, so I think that it version 1.0.0 can be freely released ## Version 0.1.4 - 01.10.2020r - Fixes -f default argument - Added save button to GUI - Cleaned a little code - Deleting files and folders i GUI - Support for all notebooks items in GUI - Support for deleting and adding directories to search and to exclude in GUI - Support for light themes in GUI - Changed SystemTime to u64 from EPOCH_TIME - Selective selecting of rows duplicate finder in GUI - Changed minimum version of GTK to 3.22 - Added save system to GUI - Added Big, Temporary and Empty folders finder to GUI ## Version 0.1.3 - 27.09.2020r - Big code refactoring - now is a lot of easier create new modules and maintain old ones - Added finding empty files - Added new option to find duplicates by checking hash max 1MB of file - Added support for finding temporary folder finder - Improved README - Simplify CLI help and improve it ## Version 0.1.2 - 26.09.2020r - Add basic search empty folders in GTK GUI - Remember place where button are placed - Read and parse more values from GUI - Print errors/warnings/messages to text field in GUI - Add upper notebook with included, excluded directories, items and extensions - Improve a little GUI - Add version argument which print version e.g. `czkawka_gui --version` - Simple Empty folder support in GUI - The biggest files support in CLI ## Version 0.1.1 - 20.09.2020r - Added images to readme - Better GTK buttons and glade file - Basic search in GTK - Cleaned core from println - Core functions doesn't use now process::exit(everything is done with help of messages/errors/warnings) - Added support for non-recursive search - Improved finding number and size of duplicated files - Saving results to file - Print how much data was read by duplicate finder(debug only) - Added GitHub CI - Only debug build prints debug information's - Clean code - Add basic idea config to misc folder ## Version 0.1.0 - 07.09.2020r - Initial Version - Duplicate file finder - Empty folder finder - Very WIP Orbtk GUI frontend - Basic GTK Frontend(without any logic) - CLI ## Initial commit - 26.08.2020r ================================================ FILE: LICENSE_CC_BY_4_ICONS ================================================ All icons, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). Copyright (c) 2020 [jannuary](https://github.com/jannuary) - data/icons/com.github.qarmin.czkawka.Devel.svg - data/icons/com.github.qarmin.czkawka.svg - data/icons/com.github.qarmin.czkawka-symbolic.svg Copyright (c) 2020-2026 Rafał Mikrut - data/icons/io.github.qarmin.krokiet.svg License: CC-BY-4.0 Creative Commons Attribution 4.0 International Public License . By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. . Section 1 -- Definitions. . a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. . b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. . c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. . d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. . e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. . f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. . g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. . h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. . i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. . j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. . k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. . Section 2 -- Scope. . a. License grant. . 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: . a. reproduce and Share the Licensed Material, in whole or in part; and . b. produce, reproduce, and Share Adapted Material. . 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. . 3. Term. The term of this Public License is specified in Section 6(a). . 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. . 5. Downstream recipients. . a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. . b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. . 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). . b. Other rights. . 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. . 2. Patent and trademark rights are not licensed under this Public License. . 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. . Section 3 -- License Conditions. . Your exercise of the Licensed Rights is expressly made subject to the following conditions. . a. Attribution. . 1. If You Share the Licensed Material (including in modified form), You must: . a. retain the following if it is supplied by the Licensor with the Licensed Material: . i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); . ii. a copyright notice; . iii. a notice that refers to this Public License; . iv. a notice that refers to the disclaimer of warranties; . v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; . b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and . c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. . 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. . 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. . 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. . Section 4 -- Sui Generis Database Rights. . Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: . a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; . b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and . c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. . For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. . Section 5 -- Disclaimer of Warranties and Limitation of Liability. . a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. . b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. . c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. . Section 6 -- Term and Termination. . a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. . b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: . 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or . 2. upon express reinstatement by the Licensor. . For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. . c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. . d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. . Section 7 -- Other Terms and Conditions. . a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. . b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. . Section 8 -- Interpretation. . a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. . b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. . c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. . d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ================================================ FILE: LICENSE_MIT_EVERYTHING_OUTSIDE_ANY_CARGO_APP_LIBRARY ================================================ MIT License Copyright (c) 2020-2026 Rafał Mikrut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
krokiet_logo
**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.
czkawka_logo
**Czkawka** (_tch•kav•ka_ (IPA: [ˈʧ̑kafka]), "hiccup" in Polish) older gtk4 GUI frontend, superseded by Krokiet, but still receiving bugfix updates. ## Features - **Written in memory-safe Rust** - almost 100% unsafe code free - **Amazingly fast** - due multithreading and efficient algorithms - **Free, Open Source without any ads** - **Multiplatform** - runs on Linux, Windows, macOS, FreeBSD, x86, ARM, RISC-V and even Android - **Cache support** - second and further scans should be much faster than the first one - **Easy to run, easy to compile** - minimal runtime and build dependencies, portable version available - **CLI frontend** - for easy automation - **GUI frontend** - uses Slint or GTK 4 frameworks - **Core library** - allows to reuse functionality in other apps - **Android app** - experimental touch-friendly frontend for Android devices - **No spying** - Czkawka does not have access to the Internet, nor does it collect any user information or statistics - **Multilingual** - support multiple languages like Polish, English or Italian - **Multiple tools to use**: - **Duplicates** - Finds duplicates based on file name, size or hash - **Empty Folders** - Finds empty folders with the help of an advanced algorithm - **Big Files** - Finds the provided number of the biggest files in given location - **Empty Files** - Looks for empty files across the drive - **Temporary Files** - Finds temporary files - **Similar Images** - Finds images which are not exactly the same (different resolution, watermarks) - **Similar Videos** - Looks for visually similar videos - **Same Music** - Searches for similar music by tags or by reading content and comparing it - **Invalid Symbolic Links** - Shows symbolic links which point to non-existent files/directories - **Broken Files** - Finds files that are invalid or corrupted - **Bad Extensions** - Lists files whose content not match with their extension - **Exif Remover** - Removes Exif metadata from various file types - **Video Optimizer** - Crops from static parts and converts videos to more efficient formats - **Bad Names** - Finds files with names that may be not wanted (e.g., containing special characters) ![Krokiet](https://github.com/user-attachments/assets/3cc7ec6a-3d6a-42cb-9d33-4b0f0c547af6) ![Czkawka](https://github.com/user-attachments/assets/b0409515-1bec-4e13-8fac-7bdfa15f5848) Changelog about each version can be found in [CHANGELOG.md](Changelog.md). New 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) You can read more about the 11.0.0 release, its new features, and the issues that were fixed in the following articles: - English article – https://medium.com/@qarmin/czkawka-krokiet-11-0-0f6cea385934 - Polish article – https://medium.com/@qarmin/czkawka-krokiet-11-0-c95ee35eccc2 ## Usage, installation, compilation, requirements, license Each tool uses different technologies, so you can find instructions for each of them in the appropriate file: - [Krokiet GUI (Slint frontend)](krokiet/README.md)
- [Czkawka GUI (GTK frontend)](czkawka_gui/README.md)
- [Czkawka CLI](czkawka_cli/README.md)
- [Czkawka Core](czkawka_core/README.md)
- [Cedinia](cedinia/README.md)
## Comparison to other tools In this comparison remember, that even if app have same features they may work different(e.g. one app may have more options to choose than other). | | Krokiet | Czkawka | FSlint | DupeGuru | Bleachbit | |:-------------------------:|:-----------:|:----------------:|:------:|:-----------------:|:-----------:| | Language | Rust | Rust | Python | Python/Obj-C | Python | | Framework base language | Rust | C | C | C/C++/Obj-C/Swift | C | | Framework | Slint | GTK 4 | PyGTK2 | Qt 5 (PyQt)/Cocoa | PyGTK3 | | OS | Lin,Mac,Win | Lin,Mac,Win | Lin | Lin,Mac,Win | Lin,Mac,Win | | Duplicate finder | ✔ | ✔ | ✔ | ✔ | | | Empty files | ✔ | ✔ | ✔ | | | | Empty folders | ✔ | ✔ | ✔ | | | | Temporary files | ✔ | ✔ | ✔ | | ✔ | | Big files | ✔ | ✔ | | | | | Similar images | ✔ | ✔ | | ✔ | | | Similar videos | ✔ | ✔ | | | | | Music duplicates(tags) | ✔ | ✔ | | ✔ | | | Music duplicates(content) | ✔ | ✔ | | | | | Invalid symlinks | ✔ | ✔ | ✔ | | | | Broken files | ✔ | ✔ | | | | | Invalid names/extensions | ✔ | ✔ | ✔ | | | | Exif cleaner | ✔ | | | | | | Video optimizer | ✔ | | | | | | Bad Names | ✔ | | | | | | Names conflict | | | ✔ | | | | Installed packages | | | ✔ | | | | Bad ID | | | ✔ | | | | Non stripped binaries | | | ✔ | | | | Redundant whitespace | | | ✔ | | | | Overwriting files | | | ✔ | | ✔ | | Portable version | ✔ | ✔ | | | ✔ | | Multiple languages | ✔ | ✔ | ✔ | ✔ | ✔ | | Cache support | ✔ | ✔ | | ✔ | | | In active development | Yes | Yes** | No | No* | Yes |

* Few small commits added recently and last version released in 2023

** Czkawka GTK is in maintenance mode receiving only bugfixes

## Other apps There are many similar applications to Czkawka on the Internet, which do some things better and some things worse: ### GUI - [DupeGuru](https://github.com/arsenetar/dupeguru) - Many options to customize; great photo compare tool - [FSlint](https://github.com/pixelb/fslint) - A little outdated, but still have some tools not available in Czkawka - [AntiDupl.NET](https://github.com/ermig1979/AntiDupl) - Shows a lot of metadata of compared images - [Video Duplicate Finder](https://github.com/0x90d/videoduplicatefinder) - Finds similar videos(surprising, isn't it) ### CLI Due to limited time, the biggest emphasis is on the GUI version so if you are looking for really good and feature-packed console apps, then take a look at these: - [Fclones](https://github.com/pkolaczk/fclones) - One of the fastest tools to find duplicates; it is written also in Rust - [Rmlint](https://github.com/sahib/rmlint) - Nice console interface and also is feature packed - [RdFind](https://github.com/pauldreik/rdfind) - Fast, but written in C++ ¯\\\_(ツ)\_/¯ ## Projects using Czkawka Czkawka exposes its common functionality through a crate called **`czkawka_core`**, which can be reused by other projects. It is written in Rust and is used by all Czkawka frontends (`czkawka_gui`, `czkawka_cli`, `krokiet`, `cedinia`). It is also used by external projects, such as: - **Czkawka Tauri** - https://github.com/shixinhuang99/czkawka-tauri - A Tauri-based GUI frontend for Czkawka. - **page-dewarp** – https://github.com/lmmx/page-dewarp - A library for dewarping document images using a cubic sheet model. Bindings are also available for: - **Python** – https://pypi.org/project/czkawka/ Some projects work as wrappers around `czkawka_cli`. Without directly depending on `czkawka_core`, they allow simple scanning and retrieving results in JSON format: - **Schluckauf** – https://github.com/fadykuzman/schluckauf ## Thanks Big thanks to Pádraig Brady, creator of fantastic FSlint, because without his work I wouldn't create this tool. Thanks also to all the people who contributed to the project in every possible way Also, 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. ## How to help? - **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. - **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. - **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. - **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. - **Creating articles, videos, tutorials, etc.** - Any material that helps people better understand this program and its capabilities is welcome. - **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: **S** - Someone **Y** - You ### Situation 1: - **S** - Hey Anon, I have a lot of junk on my disk, what should I do? - **Y** - Download Krokiet/Czkawka. They are completely free and works on almost every system. - **S** - Thanks man! ### Situation 2: - **S** - I am so thirsty... - **Y** - Have you heard about Krokiet/Czkawka? - **S** - Wait, what? - **Y** - Krokiet and Czkawka, in case you did not know, let you clean unnecessary files from your disk. They are completely free... - **S** - That is nice, but I am thirsty... - **Y** - ...they work on Windows, Linux and macOS, and some people even port them to FreeBSD and Android... ## AI Policy The 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. That 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. ## Officially Supported Projects Only 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. Czkawka does not have an official website, so do not trust any sites that claim to be the official one. If you use packages from unofficial sources, make sure they are safe. ## License The entire code in this repository is licensed under the [MIT](https://mit-license.org/) license. All images and audio files are licensed under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. The 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. ## Donations If you are using the app, I would appreciate a donation for its further development, which can be done [here](https://github.com/sponsors/qarmin). ================================================ FILE: cedinia/Cargo.toml ================================================ [package] name = "cedinia" version = "11.0.1" authors = ["Rafał Mikrut "] edition = "2024" rust-version = "1.92.0" description = "Android touch-friendly GUI for Czkawka Core – named after the Battle of Cedynia (972 AD)" license = "GPL-3.0-only" homepage = "https://github.com/qarmin/czkawka" repository = "https://github.com/qarmin/czkawka" build = "build.rs" # Android requires a cdylib crate type [lib] name = "cedinia" crate-type = ["cdylib", "rlib"] [dependencies] czkawka_core = { version = "11.0.1", path = "../czkawka_core" } crossbeam-channel = "0.5" log = "0.4" humansize = "2.1" filetime = "0.2" serde = "1.0" serde_json = "1.0" image = { version = "0.25" } fast_image_resize = { version = "6.0.0", features = ["image"] } slint = { version = "1.15.0", default-features = false, features = [ "std", "compat-1-2", ] } # Translations i18n-embed = { version = "0.16", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.10" rust-embed = { version = "8.5", features = ["debug-embed"] } [target.'cfg(target_os = "android")'.dependencies] slint = { version = "1.15.0", default-features = false, features = [ "std", "compat-1-2", "backend-android-activity-06", ] } android-activity = { version = "0.6", features = ["native-activity"] } # JNI bindings needed by file_picker_android.rs jni = { version = "0.22.1", default-features = false } # Log to Android logcat android_logger = "0.15.1" [target.'cfg(not(target_os = "android"))'.dependencies] rfd = { version = "0.17", default-features = false, features = ["xdg-portal"] } trash = "5.2.5" slint = { version = "1.15.0", default-features = false, features = [ "std", "compat-1-2", "backend-winit", "renderer-winit-femtovg", "renderer-winit-software", ] } [build-dependencies] slint-build = "1.15" # Used by build.rs to compile Java helpers to DEX when cross-compiling for Android. # The build.rs itself guards on TARGET containing "android" so this is a no-op on desktop. android-build = "0.1.2" [features] default = [] # ── Android packaging metadata (used by cargo-apk) ─────────────────────────── [package.metadata.android] package = "io.github.qarmin.cedinia" label = "Cedinia" resources = "res" [package.metadata.android.sdk] min_sdk_version = 21 target_sdk_version = 34 [package.metadata.android.application] label = "@string/app_name" icon = "@mipmap/ic_launcher" # Use our NativeActivity subclass so that onActivityResult is intercepted # and forwarded to CediniaFilePicker / the Rust JNI callback. [package.metadata.android.application.activity] name = "android.app.NativeActivity" # Prevent Android from destroying and recreating the Activity on common # configuration changes (nav-bar inset, keyboard, rotation, locale, …). # Without this, setSystemUiVisibility(0) in setupNavBar() causes a # screenSize change which Android defers until foreground → causes the # "every-other-time restart" pattern. config_changes = "orientation|keyboardHidden|screenSize|smallestScreenSize|navigation|uiMode|screenLayout|locale|density|fontScale" # Full external storage access (needed to scan /sdcard root on Android 11+) [[package.metadata.android.uses_permission]] name = "android.permission.MANAGE_EXTERNAL_STORAGE" [[package.metadata.android.uses_permission]] name = "android.permission.READ_EXTERNAL_STORAGE" [[package.metadata.android.uses_permission]] name = "android.permission.WRITE_EXTERNAL_STORAGE" [package.metadata.android.signing.dev] path = "android/keystore/debug.keystore" keystore_password = "123456" [package.metadata.android.signing.release] path = "android/keystore/release.keystore" keystore_password = "123456" [lints] workspace = true ================================================ FILE: cedinia/LICENSE_CC_BY_4_ICONS ================================================ Icons icons/* res/* ui/icons/* Copyright (c) 2020-2026 Rafał Mikrut License: CC-BY-4.0 Creative Commons Attribution 4.0 International Public License . By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. . Section 1 -- Definitions. . a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. . b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. . c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. . d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. . e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. . f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. . g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. . h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. . i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. . j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. . k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. . Section 2 -- Scope. . a. License grant. . 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: . a. reproduce and Share the Licensed Material, in whole or in part; and . b. produce, reproduce, and Share Adapted Material. . 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. . 3. Term. The term of this Public License is specified in Section 6(a). . 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. . 5. Downstream recipients. . a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. . b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. . 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). . b. Other rights. . 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. . 2. Patent and trademark rights are not licensed under this Public License. . 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. . Section 3 -- License Conditions. . Your exercise of the Licensed Rights is expressly made subject to the following conditions. . a. Attribution. . 1. If You Share the Licensed Material (including in modified form), You must: . a. retain the following if it is supplied by the Licensor with the Licensed Material: . i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); . ii. a copyright notice; . iii. a notice that refers to this Public License; . iv. a notice that refers to the disclaimer of warranties; . v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; . b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and . c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. . 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. . 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. . 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. . Section 4 -- Sui Generis Database Rights. . Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: . a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; . b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and . c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. . For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. . Section 5 -- Disclaimer of Warranties and Limitation of Liability. . a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. . b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. . c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. . Section 6 -- Term and Termination. . a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. . b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: . 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or . 2. upon express reinstatement by the Licensor. . For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. . c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. . d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. . Section 7 -- Other Terms and Conditions. . a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. . b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. . Section 8 -- Interpretation. . a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. . b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. . c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. . d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ================================================ FILE: cedinia/LICENSE_GPL_APP ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The 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. When 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. To 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. For 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. Developers 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. For 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. Some 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. Finally, 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. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. “This License” refers to version 3 of the GNU General Public License. “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. “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. To “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. A “covered work” means either the unmodified Program or a work based on the Program. To “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. To “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. An 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. 1. Source Code. The “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. A “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. The “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. The “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. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All 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. You 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. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No 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. When 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. 4. Conveying Verbatim Copies. You 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. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You 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: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 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”. 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. 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. A 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. 6. Conveying Non-Source Forms. You 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: 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. 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. 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. 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. 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. A 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. A “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. “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. If 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). The 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. Corresponding 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. 7. Additional Terms. “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. When 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. Notwithstanding 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: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 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 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 d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 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. All 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. If 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. Additional 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. 8. Termination. You 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). However, 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. Moreover, 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. Termination 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. 9. Acceptance Not Required for Having Copies. You 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. 10. Automatic Licensing of Downstream Recipients. Each 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. An “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. You 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. 11. Patents. A “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”. A 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. Each 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. In 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. If 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. If, 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. A 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. Nothing 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. 12. No Surrender of Others' Freedom. If 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. 13. Use with the GNU Affero General Public License. Notwithstanding 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. 14. Revised Versions of this License. The 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. Each 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. If 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. Later 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. 15. Disclaimer of Warranty. THERE 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. 16. Limitation of Liability. IN 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. 17. Interpretation of Sections 15 and 16. If 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. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If 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. To 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. Copyright (C) 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. 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. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Krokiet Copyright (C) 2024 Rafał Mikrut This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The 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”. You 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 . The 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 . ================================================ FILE: cedinia/LICENSE_MIT_CODE ================================================ MIT License Copyright (c) 2020-2026 Rafał Mikrut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: cedinia/README.md ================================================ # Cedinia Cedinia is an experimental Android touch friendly GUI frontend for Czkawka Core, built with Slint. The name refers to the Battle of Cedynia in 972, a victory significant to the early Polish state. ## Installation Probably the easiest way is to install it from prebuilt APKs, which can be found in the releases section. ## Compilation/Setup Quite complicated - look for now at the CI or TMP_INSTALL.md for basic and not fully detailed instructions. ## AI usage Because 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. AI 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. I 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. ================================================ FILE: cedinia/TMP_INSTALL.md ================================================ ```shell export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 # must be JDK 17, see note above export ANDROID_HOME=$HOME/android-sdk export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.3.11579264 export ANDROID_BUILD_TOOLS_VERSION=35.0.0 # set to 35 if you use JDK 21+ export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin export PATH=$PATH:$ANDROID_HOME/platform-tools # Ubuntu / Debian - install Java (if you don't have it already) sudo apt install openjdk-17-jdk # Desktop (Rust) build/run cargo build -p cedinia cargo run -p cedinia # Linux example – Android command-line tools setup (adjust paths to taste) ANDROID_HOME=$HOME/android-sdk mkdir -p $ANDROID_HOME/cmdline-tools cd $ANDROID_HOME/cmdline-tools # Download from https://developer.android.com/studio#command-tools, e.g. https://dl.google.com/android/repository/commandlinetools-linux-14742923_latest.zip # unzip commandlinetools-linux-*.zip # mv cmdline-tools latest # Accept licences & install required packages $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \ "platform-tools" \ "platforms;android-30" \ "platforms;android-34" \ "build-tools;34.0.0" \ "build-tools;35.0.0" \ "ndk;26.3.11579264" # Reload the shell (if you added the exports to ~/.bashrc or ~/.zshrc) source ~/.bashrc # Install cargo-apk or cargo-xbuild cargo install cargo-apk # Or for an alternative toolchain cargo install xbuild # Add Android Rust targets rustup target add \ aarch64-linux-android \ armv7-linux-androideabi \ x86_64-linux-android \ i686-linux-android # Build the APK # Debug APK cargo apk build -p cedinia --lib # Release APK (requires a signing key) cargo apk build -p cedinia --lib --release # Check connected devices adb devices # Install and launch on a connected device adb install -r target/debug/apk/cedinia.apk adb shell am start -n io.github.qarmin.cedinia/android.app.NativeActivity # One-liner: build → install → launch cargo apk build -p cedinia --lib && \ adb install -r target/debug/apk/cedinia.apk && \ adb shell am start -n io.github.qarmin.cedinia/android.app.NativeActivity # Debugging: Live logs (Rust stdout/stderr, panics) adb logcat -s RustStdoutStderr:V # Full logcat filtered to Cedinia adb logcat -s RustStdoutStderr:V io.github.qarmin.cedinia:V AndroidRuntime:E # Crash / panic backtrace: set RUST_BACKTRACE=1 before building RUST_BACKTRACE=1 cargo apk build -p cedinia --lib # Then check adb logcat -s RustStdoutStderr:V # Uninstall adb uninstall io.github.qarmin.cedinia ``` ================================================ FILE: cedinia/build.rs ================================================ use std::env; use std::path::PathBuf; fn main() { slint_build::compile_with_config("ui/main_window.slint", slint_build::CompilerConfiguration::new().with_style("material".into())) .expect("Unable to compile Slint UI files for Cedinia"); // Compile Java helper classes to DEX when building for Android. // This follows the same pattern used by the Slint android-activity backend. if env::var("TARGET").unwrap_or_default().contains("android") { compile_android_java(); } } fn compile_android_java() { use android_build::{Dexer, JavaBuild}; let java_files = ["java/CediniaActivity.java", "java/CediniaFilePicker.java"]; let out_dir: PathBuf = env::var_os("OUT_DIR").expect("OUT_DIR environment variable not set").into(); let out_class_dir = out_dir.join("java_classes"); if out_class_dir.try_exists().unwrap_or(false) { let _ = std::fs::remove_dir_all(&out_class_dir); } std::fs::create_dir_all(&out_class_dir).unwrap_or_else(|e| panic!("Cannot create output directory {out_class_dir:?} - {e}")); let android_jar = android_build::android_jar(None).expect("No Android platform SDK found; install SDK"); let release_mode = env::var("PROFILE").as_ref().map(|s| s.as_str()) == Ok("release"); // Compile all Java sources to .class files. let mut build = JavaBuild::new(); for f in &java_files { build.file(f); } let status = build .class_path(&android_jar) .classes_out_dir(&out_class_dir) .java_source_version(8) .java_target_version(8) .debug_info(android_build::DebugInfo { line_numbers: !release_mode, variables: !release_mode, source_files: !release_mode, }) .command() .unwrap_or_else(|e| panic!("Could not generate javac command: {e}")) .args(["-encoding", "UTF-8"]) .output() .unwrap_or_else(|e| panic!("Could not run javac: {e}")); assert!(status.status.success(), "Java compilation failed:\n{}", String::from_utf8_lossy(&status.stderr)); // Convert .class files to a single classes.dex. let dex_out = Dexer::new() .android_jar(&android_jar) .class_path(&out_class_dir) .collect_classes(&out_class_dir) .expect("Failed to collect compiled Java classes for DEX conversion") .release(release_mode) .android_min_api(21) .out_dir(&out_dir) .command() .unwrap_or_else(|e| panic!("Could not generate D8 command: {e}")) .output() .unwrap_or_else(|e| panic!("Error running D8: {e}")); assert!(dex_out.status.success(), "Dex conversion failed:\n{}", String::from_utf8_lossy(&dex_out.stderr)); for f in &java_files { println!("cargo:rerun-if-changed={f}"); } } ================================================ FILE: cedinia/i18n/en/cedinia.ftl ================================================ # Cedinia – English (fallback) # App / top bar titles app_name = Cedinia tool_duplicate_files = Duplicates tool_empty_folders = Empty Folders tool_similar_images = Similar Images tool_empty_files = Empty Files tool_temporary_files = Temporary Files tool_big_files = Biggest Files tool_broken_files = Broken Files tool_bad_extensions = Bad Extensions tool_same_music = Music Duplicates tool_bad_names = Bad Names tool_exif_remover = EXIF Data tool_directories = Directories tool_settings = Settings # Home screen tool card descriptions home_dup_description = Find files with the same content home_empty_folders_description = Directories without content home_similar_images_description = Find visually similar photos home_empty_files_description = Files with zero size home_temp_files_description = Temporary and cached files home_big_files_description = Biggest/Smallest files on disk home_broken_files_description = PDF, audio, images, archives home_bad_extensions_description = Files with invalid extension home_same_music_description = Similar audio files by tags home_bad_names_description = Files with problematic characters in name home_exif_description = Images with EXIF metadata # Results list scanning = Scanning in progress... stopping = Stopping... no_results = No results press_start = Press START to scan select_label = Sel. deselect_label = Desel. list_label = List gallery_label = Gal. # Selection popup selection_popup_title = Select select_all = Select all select_except_one = Select all except one select_except_largest = Select all except largest select_except_smallest = Select all except smallest select_largest = Select largest select_smallest = Select smallest select_except_highest_res = Select all except highest resolution select_except_lowest_res = Select all except lowest resolution select_highest_res = Select highest resolution select_lowest_res = Select lowest resolution invert_selection = Invert selection close = Close # Deselection popup deselection_popup_title = Deselect deselect_all = Deselect all deselect_except_one = Deselect all except one # Confirm popup cancel = Cancel delete = Delete rename = Rename # Delete errors popup delete_errors_title = Failed to delete some files: ok = OK # Stopping overlay stopping_overlay_title = ■ Stopping stopping_overlay_body = Finishing current scan…\nPlease wait. # Permission popup permission_title = 🔒 File Access permission_body = To scan files, the app needs access to device storage. Without this permission, scanning will not be possible. grant = Grant no_permission_scan_warning = No file access – grant permission to scan # Settings screen tabs settings_tab_general = General settings_tab_tools = Tools settings_tab_diagnostics = Info # Settings — General tab settings_use_cache = Use cache settings_use_cache_desc = Speeds up subsequent scans (hash/images) settings_ignore_hidden = Ignore hidden files settings_ignore_hidden_desc = Files and folders starting with '.' settings_scan_label = SCAN settings_filters_label = FILTERS (some tools) settings_min_file_size = Min. file size settings_max_file_size = Max. file size settings_language = Language settings_language_restart = Requires app restart settings_common_label = COMMON SETTINGS settings_excluded_items = EXCLUDED ITEMS (glob patterns, comma separated) settings_excluded_items_placeholder = e.g. *.tmp, */.git/*, */node_modules/* settings_allowed_extensions = ALLOWED EXTENSIONS (empty = all) settings_allowed_extensions_placeholder = e.g. jpg, png, mp4 settings_excluded_extensions = EXCLUDED EXTENSIONS settings_excluded_extensions_placeholder = e.g. bak, tmp, log # Settings — Tools section labels settings_duplicates_header = DUPLICATES settings_check_method_label = COMPARISON METHOD settings_check_method = Method settings_hash_type_label = HASH TYPE settings_hash_type = Hash type settings_hash_type_desc = Blake3 – fastest; CRC32/xxH3 – alternatives settings_similar_images_header = SIMILAR IMAGES settings_similarity_preset = Similarity threshold settings_similarity_desc = Very High = only near-identical settings_hash_size = Hash size settings_hash_size_desc = Larger = more accurate, slower settings_hash_alg = Hash algorithm settings_image_filter = Resize filter settings_ignore_same_size = Ignore images with the same dimensions settings_big_files_header = BIGGEST FILES settings_search_mode = Search mode settings_file_count = File count settings_same_music_header = MUSIC DUPLICATES settings_music_check_method = Comparison mode settings_music_compare_tags_label = COMPARED TAGS settings_music_title = Title settings_music_artist = Artist settings_music_year = Year settings_music_length = Length settings_music_genre = Genre settings_music_bitrate = Bitrate settings_music_approx = Approximate tag comparison settings_broken_files_header = BROKEN FILES settings_broken_files_types_label = CHECKED TYPES settings_broken_audio = Audio settings_broken_pdf = PDF settings_broken_archive = Archive settings_broken_image = Image settings_bad_names_header = BAD NAMES settings_bad_names_checks_label = CHECKS settings_bad_names_uppercase_ext = Uppercase extension settings_bad_names_emoji = Emoji in name settings_bad_names_space = Spaces at start/end settings_bad_names_non_ascii = Non-ASCII characters settings_bad_names_duplicated = Repeated characters # Settings — Diagnostics tab diagnostics_header = DIAGNOSTICS diagnostics_thumbnails = Thumbnail cache diagnostics_app_cache = App cache diagnostics_refresh = Refresh diagnostics_clear_thumbnails = Clear thumbnails diagnostics_clear_cache = Clear cache diagnostics_collect_test = Scan test diagnostics_collect_test_desc = Scans each volume recursively diagnostics_collect_test_run = Run diagnostics_collect_test_stop = Stop about_repo = Repository about_translate = Translations about_donate = Support # Collect-test result popup collect_test_title = 📊 Test results collect_test_volumes = 💾 Volumes: collect_test_folders = 📁 Folders: collect_test_files = 📄 Files: collect_test_time = ⏱ Time: collect_test_ms = " ms" # Directories screen directories_include_header = Directories to scan directories_exclude_header = Excluded directories directories_add = + Add no_paths = No paths – add below directories_volume_header = Volumes directories_volume_refresh = Refresh directories_volume_add = Add # Bottom navigation nav_home = Start nav_dirs = Directories nav_settings = Settings # Status messages set from Rust status_ready = Ready status_stopped = Stopped status_no_results = No results status_deleted_selected = Deleted selected status_deleted_with_errors = Deleted with errors scan_not_started = Scan not started found_items_prefix = Found found_items_suffix = items deleted_items_prefix = Deleted deleted_items_suffix = items deleted_errors_suffix = errors renamed_prefix = Renamed renamed_files_suffix = files renamed_errors_suffix = errors cleaned_exif_prefix = Cleaned EXIF from cleaned_exif_suffix = files cleaned_exif_errors_suffix = errors and_more_prefix = …and and_more_suffix = more # Gallery / delete popups gallery_delete_button = Delete gallery_back = Back gallery_confirm_delete = Yes, delete deleting_files = Deleting files… stop = Stop files_suffix = files scanning_fallback = Scanning… app_subtitle = In honour of the Battle of Cedynia (972 CE) app_license = Frontend for Czkawka Core • GPL-3.0 about_app_label = ABOUT cache_label = CACHE ================================================ FILE: cedinia/i18n/pl/cedinia.ftl ================================================ # Cedinia – Polski (Polish) # App / top bar titles app_name = Cedinia tool_duplicate_files = Duplikaty tool_empty_folders = Puste foldery tool_similar_images = Podobne obrazy tool_empty_files = Puste pliki tool_temporary_files = Pliki tymczasowe tool_big_files = Największe pliki tool_broken_files = Uszkodzone pliki tool_bad_extensions = Złe rozszerzenia tool_same_music = Duplikaty muzyki tool_bad_names = Złe nazwy tool_exif_remover = Dane EXIF tool_directories = Katalogi tool_settings = Ustawienia # Home screen tool card descriptions home_dup_description = Znajdź pliki o tej samej zawartości home_empty_folders_description = Katalogi bez zawartości home_similar_images_description = Znajdź wizualnie podobne zdjęcia home_empty_files_description = Pliki o zerowym rozmiarze home_temp_files_description = Tymczasowe i cache'owane pliki home_big_files_description = 50 największych plików na dysku home_broken_files_description = Pliki PDF, audio, obrazy, archiwa home_bad_extensions_description = Pliki z nieprawidłowym rozszerzeniem home_same_music_description = Podobne pliki audio wg tagów home_bad_names_description = Pliki z problematycznymi znakami w nazwie home_exif_description = Obrazy z metadanymi EXIF # Results list scanning = Skanowanie w toku... stopping = Zatrzymywanie... no_results = Brak wynikow press_start = Nacisnij START aby skanowac select_label = Zaz. deselect_label = Odzn. list_label = Lista gallery_label = Gal. # Selection popup selection_popup_title = Zaznaczanie select_all = Zaznacz wszystko select_except_one = Zaznacz poza jednym select_except_largest = Zaznacz poza największym select_except_smallest = Zaznacz poza najmniejszym select_largest = Zaznacz największy select_smallest = Zaznacz najmniejszy select_except_highest_res = Zaznacz poza największą rozdzielczością select_except_lowest_res = Zaznacz poza najmniejszą rozdzielczością select_highest_res = Zaznacz największą rozdzielczość select_lowest_res = Zaznacz najmniejszą rozdzielczość invert_selection = Odwroc zaznaczenie close = Zamknij # Deselection popup deselection_popup_title = Odznaczanie deselect_all = Odznacz wszystko deselect_except_one = Odznacz poza jednym # Confirm popup cancel = Anuluj delete = Usuń rename = Zmień nazwę # Delete errors popup delete_errors_title = Nie udalo sie usunac niektorych plikow: ok = OK # Stopping overlay stopping_overlay_title = ■ Zatrzymywanie stopping_overlay_body = Kończenie bieżącego skanu…\nProszę czekać. # Permission popup permission_title = 🔒 Dostęp do plików permission_body = Aby skanować pliki, aplikacja potrzebuje dostępu do pamięci urządzenia. Bez tego uprawnienia skanowanie nie będzie możliwe. grant = Przyznaj no_permission_scan_warning = Brak uprawnień do plików – przyznaj dostęp aby skanować # Settings screen tabs settings_tab_general = Ogólne settings_tab_tools = Narzędzia settings_tab_diagnostics = Info # Settings — General tab settings_use_cache = Użyj pamięci podręcznej settings_use_cache_desc = Przyspiesza kolejne skany (hash/obrazy) settings_ignore_hidden = Ignoruj ukryte pliki settings_ignore_hidden_desc = Pliki i foldery zaczynające się od '.' settings_scan_label = SKANOWANIE settings_filters_label = FILTRY (niektóre narzędzia) settings_min_file_size = Min. rozmiar pliku settings_max_file_size = Maks. rozmiar pliku settings_language = Język settings_excluded_items = WYKLUCZONE ELEMENTY (wzorce glob, oddzielone przecinkiem) settings_excluded_items_placeholder = np. *.tmp, */.git/*, */node_modules/* settings_allowed_extensions = DOZWOLONE ROZSZERZENIA (puste = wszystkie) settings_allowed_extensions_placeholder = np. jpg, png, mp4 settings_excluded_extensions = WYKLUCZONE ROZSZERZENIA settings_excluded_extensions_placeholder = np. bak, tmp, log # Settings — Tools section labels settings_duplicates_header = DUPLIKATY settings_check_method_label = METODA PORÓWNANIA settings_check_method = Metoda settings_hash_type_label = TYP HASHA settings_hash_type = Typ hasha settings_similar_images_header = PODOBNE OBRAZY settings_similarity_preset = Próg podobieństwa settings_hash_size = Rozmiar hasha settings_hash_alg = Algorytm hasha settings_image_filter = Filtr zmiany rozmiaru settings_ignore_same_size = Ignoruj obrazy o tych samych wymiarach settings_big_files_header = NAJWIĘKSZE PLIKI settings_search_mode = Tryb wyszukiwania settings_file_count = Liczba plików settings_same_music_header = DUPLIKATY MUZYKI settings_music_check_method = Tryb porównania settings_music_compare_tags_label = PORÓWNYWANE TAGI settings_music_title = Tytuł settings_music_artist = Artysta settings_music_year = Rok settings_music_length = Długość settings_music_genre = Gatunek settings_music_bitrate = Przepływność settings_music_approx = Przybliżone porównywanie tagów settings_broken_files_header = USZKODZONE PLIKI settings_broken_files_types_label = SPRAWDZANE TYPY settings_broken_audio = Dźwięk settings_broken_pdf = PDF settings_broken_archive = Archiwum settings_broken_image = Obraz settings_bad_names_header = ZŁE NAZWY settings_bad_names_checks_label = SPRAWDZENIA settings_bad_names_uppercase_ext = Wielkie litery w rozszerzeniu settings_bad_names_emoji = Emoji w nazwie settings_bad_names_space = Spacje na początku/końcu settings_bad_names_non_ascii = Znaki spoza ASCII settings_bad_names_duplicated = Powtarzające się znaki # Settings — Diagnostics tab diagnostics_header = DIAGNOSTYKA diagnostics_thumbnails = Pamięć miniatur diagnostics_app_cache = Pamięć aplikacji diagnostics_refresh = Odśwież diagnostics_clear_thumbnails = Wyczyść miniatury diagnostics_clear_cache = Wyczyść cache diagnostics_collect_test = Test skanowania diagnostics_collect_test_run = Uruchom diagnostics_collect_test_stop = Stop about_repo = Repozytorium about_translate = Tłumaczenia about_donate = Wesprzyj # Collect-test result popup collect_test_title = 📊 Wyniki testu collect_test_volumes = 💾 Woluminy: collect_test_folders = 📁 Foldery: collect_test_files = 📄 Pliki: collect_test_time = ⏱ Czas: collect_test_ms = " ms" # Directories screen directories_include_header = Katalogi do skanowania directories_exclude_header = Katalogi wykluczone directories_add = + Dodaj directories_volume_header = Woluminy directories_volume_refresh = Odśwież directories_volume_add = Dodaj # Bottom navigation nav_home = Start nav_dirs = Katalogi nav_settings = Ustawienia # Status messages set from Rust status_ready = Gotowy / Ready status_stopped = Zatrzymano status_no_results = Brak wyników status_deleted_selected = Usunięto zaznaczone status_deleted_with_errors = Usunięto z błędami scan_not_started = Nie uruchomiono skanu found_items_prefix = Znaleziono found_items_suffix = elementów deleted_items_prefix = Usunięto deleted_items_suffix = elementów deleted_errors_suffix = błędów renamed_prefix = Zmieniono nazwy renamed_files_suffix = plików renamed_errors_suffix = błędów cleaned_exif_prefix = Wyczyszczono EXIF z cleaned_exif_suffix = plików cleaned_exif_errors_suffix = błędów and_more_prefix = …i and_more_suffix = więcej ================================================ FILE: cedinia/i18n.toml ================================================ fallback_language = "en" [fluent] assets_dir = "i18n" ================================================ FILE: cedinia/java/CediniaActivity.java ================================================ // CediniaActivity.java // Thin NativeActivity subclass that intercepts onActivityResult and forwards // it to the CediniaFilePicker helper, which then calls back into Rust via JNI. // // Usage: set android:name=".CediniaActivity" in the manifest activity element. // cargo-apk injects the package name; we declare the package-less class here // because the DEX is loaded dynamically (via InMemoryDexClassLoader) and the // real package name is resolved at load time by the Rust side. import android.app.NativeActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.WindowManager; public class CediniaActivity extends NativeActivity { @Override protected void onCreate(Bundle savedInstanceState) { // Prevent fullscreen mode from hiding the system navigation bar. // NativeActivity (and some GPU renderers) may set FLAG_FULLSCREEN; // FLAG_FORCE_NOT_FULLSCREEN overrides it before super.onCreate runs. getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); super.onCreate(savedInstanceState); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { // Reset system UI flags so status bar and nav bar stay visible. // Passing 0 clears SYSTEM_UI_FLAG_HIDE_NAVIGATION, // SYSTEM_UI_FLAG_FULLSCREEN, SYSTEM_UI_FLAG_IMMERSIVE, etc. getWindow().getDecorView().setSystemUiVisibility(0); } } @Override public void onBackPressed() { // Move the task to the background instead of finishing the Activity. // This mirrors the Home-button behaviour: tapping the launcher icon // will resume the existing instance rather than recreating it from // scratch (which is the default NativeActivity behaviour when back // is pressed on the root activity). moveTaskToBack(true); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); CediniaFilePicker.handleActivityResult(this, requestCode, resultCode, data); } } ================================================ FILE: cedinia/java/CediniaFilePicker.java ================================================ // CediniaFilePicker.java // Launches the Android Storage Access Framework folder picker and calls back into // Rust once the user makes a selection. // // For NativeActivity (no ComponentActivity): shows an AlertDialog with an EditText // so the user can type/paste a path. The SAF picker via startActivityForResult is // also attempted if the manifest activity is CediniaActivity. import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.Settings; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.LinearLayout; public class CediniaFilePicker { private static final String TAG = "CediniaFilePicker"; // Request codes used with startActivityForResult (CediniaActivity path) static final int REQ_INCLUDE = 0x4345_0001; static final int REQ_EXCLUDE = 0x4345_0002; // ── permission helpers ──────────────────────────────────────────────── /** * Returns true if the app currently has broad file read access. * Android 11+: checks MANAGE_EXTERNAL_STORAGE. * Android 6-10: checks READ_EXTERNAL_STORAGE. * Below Android 6: always true. */ public static boolean hasStoragePermission(Activity activity) { if (Build.VERSION.SDK_INT >= 30) { // Android 11+ – MANAGE_EXTERNAL_STORAGE return Environment.isExternalStorageManager(); } else if (Build.VERSION.SDK_INT >= 23) { // Android 6-10 – runtime READ_EXTERNAL_STORAGE return activity.checkSelfPermission("android.permission.READ_EXTERNAL_STORAGE") == PackageManager.PERMISSION_GRANTED; } else { return true; } } /** * Opens the appropriate system UI to grant storage permission: * - Android 11+: Settings > Special app access > All files access * - Android 6-10: requestPermissions for READ/WRITE_EXTERNAL_STORAGE */ public static void requestStoragePermission(final Activity activity) { Log.i(TAG, "requestStoragePermission: API=" + Build.VERSION.SDK_INT); if (Build.VERSION.SDK_INT >= 30) { // Must send user to the special settings screen for MANAGE_EXTERNAL_STORAGE activity.runOnUiThread(new Runnable() { @Override public void run() { try { Intent intent = new Intent( Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + activity.getPackageName())); activity.startActivity(intent); } catch (Exception e) { // Fallback: open the general "all files" settings page Log.w(TAG, "requestStoragePermission: direct intent failed, opening generic: " + e); try { activity.startActivity( new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)); } catch (Exception e2) { Log.e(TAG, "requestStoragePermission: fallback also failed: " + e2); } } } }); } else if (Build.VERSION.SDK_INT >= 23) { activity.requestPermissions(new String[]{ "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE" }, 0x4345_0010); } // Below API 23: permissions are granted at install time, nothing to do } // ── nav bar visibility ──────────────────────────────────────────────── /** * Clears immersive / hide-navigation flags so the system nav bar (Back, * Home) stays visible. Re-applies the clear whenever Slint's renderer * would hide it again. Call once after Slint has been initialised. */ public static void setupNavBar(final Activity activity) { activity.runOnUiThread(new Runnable() { @Override public void run() { final View decorView = activity.getWindow().getDecorView(); decorView.setSystemUiVisibility(0); decorView.setOnSystemUiVisibilityChangeListener( new View.OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0) { decorView.setSystemUiVisibility(0); } } } ); Log.i(TAG, "setupNavBar: nav bar listener registered"); } }); } // ── called from Rust ────────────────────────────────────────────────── /** Launch the folder picker for an "include" directory. */ public static void pickIncludeDirectory(Activity activity) { Log.i(TAG, "pickIncludeDirectory called, API=" + Build.VERSION.SDK_INT + " activity=" + activity.getClass().getName()); launchPicker(activity, true); } /** Launch the folder picker for an "exclude" directory. */ public static void pickExcludeDirectory(Activity activity) { Log.i(TAG, "pickExcludeDirectory called, API=" + Build.VERSION.SDK_INT + " activity=" + activity.getClass().getName()); launchPicker(activity, false); } // ── internal ────────────────────────────────────────────────────────── private static void launchPicker(final Activity activity, final boolean isInclude) { // Try the SAF picker via startActivityForResult only when running in // a CediniaActivity subclass that can intercept onActivityResult. String activityClass = activity.getClass().getName(); if (activityClass.contains("CediniaActivity")) { Log.i(TAG, "launchPicker: using SAF startActivityForResult"); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); activity.startActivityForResult(intent, isInclude ? REQ_INCLUDE : REQ_EXCLUDE); } else { // NativeActivity does not intercept onActivityResult via Rust, // so we fall back to a simple path-entry dialog. Log.w(TAG, "launchPicker: NativeActivity detected – using path-entry dialog"); showPathDialog(activity, isInclude); } } /** Text-entry fallback for NativeActivity where SAF result cannot be received. */ static void showPathDialog(final Activity activity, final boolean isInclude) { Log.i(TAG, "showPathDialog: isInclude=" + isInclude); activity.runOnUiThread(new Runnable() { @Override public void run() { final EditText et = new EditText(activity); et.setHint(isInclude ? "/sdcard/DCIM" : "/sdcard/Android"); et.setSingleLine(true); LinearLayout layout = new LinearLayout(activity); layout.setOrientation(LinearLayout.VERTICAL); int pad = (int)(16 * activity.getResources().getDisplayMetrics().density); layout.setPadding(pad, pad, pad, pad); layout.addView(et); new AlertDialog.Builder(activity) .setTitle(isInclude ? "Add scan directory" : "Add exclude directory") .setMessage("Enter a full path (e.g. /sdcard/DCIM):") .setView(layout) .setPositiveButton("Add", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String path = et.getText().toString().trim(); Log.i(TAG, "showPathDialog confirmed: path='" + path + "' isInclude=" + isInclude); if (!path.isEmpty()) { onDirectoryPicked(path, isInclude); } else { Log.w(TAG, "showPathDialog: empty path, ignoring"); } } }) .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Log.i(TAG, "showPathDialog: user cancelled"); } }) .show(); } }); } /** * Called by CediniaActivity.onActivityResult (SAF path). */ public static void handleActivityResult( Activity activity, int requestCode, int resultCode, Intent data) { Log.i(TAG, "handleActivityResult: requestCode=0x" + Integer.toHexString(requestCode) + " resultCode=" + resultCode + " data=" + data); if (requestCode != REQ_INCLUDE && requestCode != REQ_EXCLUDE) { Log.d(TAG, "handleActivityResult: not our request, ignoring"); return; } if (resultCode != Activity.RESULT_OK || data == null) { Log.i(TAG, "handleActivityResult: cancelled or null data – resultCode=" + resultCode); return; } Uri treeUri = data.getData(); if (treeUri == null) { Log.w(TAG, "handleActivityResult: null tree URI"); return; } persistPermission(activity, treeUri); String path = resolveTreeUriToPath(treeUri); boolean isInclude = (requestCode == REQ_INCLUDE); Log.i(TAG, "handleActivityResult: resolved path='" + path + "' isInclude=" + isInclude); onDirectoryPicked(path, isInclude); } private static void persistPermission(Activity activity, Uri treeUri) { try { int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION; activity.getContentResolver().takePersistableUriPermission(treeUri, flags); Log.d(TAG, "persistPermission: ok for " + treeUri); } catch (SecurityException e) { Log.d(TAG, "persistPermission: provider does not support persistable perms – " + e.getMessage()); } } /** * Best-effort conversion of a SAF tree URI to a filesystem path. */ private static String resolveTreeUriToPath(Uri treeUri) { Log.d(TAG, "resolveTreeUriToPath: input=" + treeUri); try { Uri docUri = DocumentsContract.buildDocumentUriUsingTree( treeUri, DocumentsContract.getTreeDocumentId(treeUri)); String docId = DocumentsContract.getDocumentId(docUri); Log.d(TAG, "resolveTreeUriToPath: docId=" + docId); if (docId == null) { return treeUri.toString(); } String[] parts = docId.split(":", 2); if (parts.length < 2) { return treeUri.toString(); } String volume = parts[0]; String subPath = parts[1]; String root; if ("primary".equalsIgnoreCase(volume) || "home".equalsIgnoreCase(volume)) { root = "/sdcard"; } else { root = "/storage/" + volume; } String result = subPath.isEmpty() ? root : root + "/" + subPath; Log.i(TAG, "resolveTreeUriToPath: result=" + result); return result; } catch (Exception e) { Log.w(TAG, "resolveTreeUriToPath: exception, returning raw URI string: " + e); return treeUri.toString(); } } // ── native callback registered by Rust via register_native_methods ──── /** * Called after the user picks a directory (either SAF or dialog). * Rust registers the native implementation at startup. */ public static native void onDirectoryPicked(String path, boolean isInclude); } ================================================ FILE: cedinia/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: cedinia/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: cedinia/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: cedinia/res/values/strings.xml ================================================ Cedinia ================================================ FILE: cedinia/src/app.rs ================================================ use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use czkawka_core::common::config_cache_path::{print_infos_and_warnings, set_config_cache_path}; use czkawka_core::common::image::register_image_decoding_hooks; use czkawka_core::common::logger::{filtering_messages, print_version_mode, setup_logger}; use slint::{ComponentHandle, Model, ModelRc, SharedString, Timer, TimerMode, VecModel, Weak}; use crate::callbacks::{ 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, wire_save_settings_now, wire_scan, wire_selection, }; use crate::model::make_file_model; use crate::scan_runner::{FileItem, ScanResult, ScanResultHandler, start_worker}; use crate::set_initial_gui_infos::set_initial_gui_infos; use crate::settings::{apply_settings_to_gui, collect_settings_from_gui, load_dirs, load_settings, save_dirs, save_settings}; use crate::thumbnail_loader::{ThumbnailData, collect_thumb_tasks, make_placeholder_image, rgba_to_slint_image, spawn_thumbnail_loader}; use crate::translations::translate_items; use crate::volumes::home_dir; use crate::{AppState, FileEntry, MainWindow, ProgressData, ScanState, SimilarGroupCard, SimilarImageItem}; #[cfg(target_os = "android")] thread_local! { static DIR_STATE: std::cell::RefCell, Rc>>, Rc>>, )>> = const { std::cell::RefCell::new(None) }; } #[cfg(target_os = "android")] pub fn on_directory_picked(path: String, is_include: bool) { log::info!("on_directory_picked: path='{}' is_include={}", path, is_include); DIR_STATE.with(|cell| { let guard = cell.borrow(); if let Some((weak, inc, exc)) = guard.as_ref() { if let Some(win) = weak.upgrade() { if is_include { inc.borrow_mut().push(PathBuf::from(&path)); } else { exc.borrow_mut().push(PathBuf::from(&path)); } win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); let settings = crate::settings::collect_settings_from_gui(&win); crate::settings::save_settings(&settings); crate::settings::save_dirs(&inc.borrow(), &exc.borrow()); } } }); } pub fn run_app() { setup_logger_cache(); #[cfg(target_os = "android")] unreachable!("use android_main"); #[cfg(not(target_os = "android"))] run_app_with_insets(0.0, 1.0, ()); } #[cfg(target_os = "android")] pub fn run_app_with_insets(inset_bottom_px: f32, scale: f32, android_app: slint::android::AndroidApp) { run_app_inner(inset_bottom_px, scale, Some(android_app)); } #[cfg(not(target_os = "android"))] pub fn run_app_with_insets(inset_bottom_px: f32, scale: f32, _unused: ()) { run_app_inner(inset_bottom_px, scale, None::<()>); } fn build_gallery_groups(items: &[FileItem], placeholder: &slint::Image) -> Vec { use slint::{ModelRc, SharedString, VecModel}; use crate::common::{STR_IDX_NAME, STR_IDX_PATH, STR_IDX_SIZE}; let mut groups: Vec = Vec::new(); let mut cur_label = String::new(); let mut cur_items: Vec = Vec::new(); for (flat_idx, item) in items.iter().enumerate() { if item.is_header { if !cur_items.is_empty() { groups.push(SimilarGroupCard { label: SharedString::from(&cur_label), items: ModelRc::new(VecModel::from(std::mem::take(&mut cur_items))), }); } cur_label = item.val_str[STR_IDX_NAME].clone(); } else { let name = &item.val_str[STR_IDX_NAME]; let path = &item.val_str[STR_IDX_PATH]; let size = &item.val_str[STR_IDX_SIZE]; let full_path = if path.is_empty() { name.clone() } else { format!("{path}/{name}") }; cur_items.push(SimilarImageItem { full_path: SharedString::from(full_path), name: SharedString::from(name), size: SharedString::from(size), val_str: ModelRc::new(VecModel::from(item.val_str.iter().map(|s| SharedString::from(s.as_str())).collect::>())), flat_idx: flat_idx as i32, thumbnail: placeholder.clone(), checked: false, }); } } if !cur_items.is_empty() { groups.push(SimilarGroupCard { label: SharedString::from(&cur_label), items: ModelRc::new(VecModel::from(cur_items)), }); } groups } fn show_delete_errors(win: &MainWindow, errors: &[String]) { let mut msg = errors.iter().take(10).cloned().collect::>().join("\n\n"); if errors.len() > 10 { msg.push_str(&format!("\n\n{} {} {}", crate::flc!("and_more_prefix"), errors.len() - 10, crate::flc!("and_more_suffix"))); } win.global::().set_delete_errors_text(SharedString::from(msg)); win.global::().set_delete_errors_visible(true); } fn rebuild_similar_images_after_delete(win: &MainWindow, deleted: &std::collections::HashSet) { let groups = win.get_similar_images_groups(); let mut new_groups: Vec = Vec::new(); let mut new_flat: Vec = Vec::new(); for gi in 0..groups.row_count() { if let Some(group) = groups.row_data(gi) { let surviving: Vec<_> = (0..group.items.row_count()) .filter_map(|ii| group.items.row_data(ii)) .filter(|item| !deleted.contains(item.full_path.as_str())) .map(|mut item| { item.checked = false; item }) .collect(); if surviving.is_empty() { continue; } new_flat.push(FileEntry { checked: false, is_header: true, val_str: ModelRc::new(VecModel::from(vec![ group.label.clone(), SharedString::default(), SharedString::default(), SharedString::default(), ])), val_int: ModelRc::new(VecModel::from(vec![])), }); let mut final_items: Vec = Vec::new(); for mut item in surviving { item.flat_idx = new_flat.len() as i32; new_flat.push(FileEntry { checked: false, is_header: false, val_str: item.val_str.clone(), val_int: ModelRc::new(VecModel::from(vec![])), }); final_items.push(item); } new_groups.push(SimilarGroupCard { label: group.label.clone(), items: ModelRc::new(VecModel::from(final_items)), }); } } win.set_similar_images_model(ModelRc::new(VecModel::from(new_flat))); win.set_similar_images_groups(ModelRc::new(VecModel::from(new_groups))); win.global::().set_selected_count(0); } struct GuiHandler { weak: Weak, scan_gen: Arc, thumb_tx: std::sync::mpsc::Sender, thumb_cancel: Arc>>, } impl ScanResultHandler for GuiHandler { fn on_result(&self, result: ScanResult) { let weak = self.weak.clone(); let current_gen = self.scan_gen.load(Ordering::SeqCst); match result { ScanResult::Progress(p) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); if p.scan_id != current_gen { return; } let pd = ProgressData { step_name: SharedString::from(p.step_name), current_progress: p.current, all_progress: p.all, is_indeterminate: p.is_indeterminate, }; win.global::().set_progress(pd); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::DuplicateFiles(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_duplicate_files_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::EmptyFolders(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_empty_folder_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::SimilarImages(items) => { let thumb_tx = self.thumb_tx.clone(); let thumb_cancel = Arc::clone(&self.thumb_cancel); slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); let tasks = collect_thumb_tasks(&items); let ph = make_placeholder_image(); let groups = build_gallery_groups(&items, &ph); win.set_similar_images_model(make_file_model(items)); win.set_similar_images_groups(ModelRc::new(VecModel::from(groups))); let mut cancel_guard = thumb_cancel.lock().unwrap(); cancel_guard.store(true, Ordering::Relaxed); let new_cancel = Arc::new(AtomicBool::new(false)); *cancel_guard = new_cancel.clone(); drop(cancel_guard); spawn_thumbnail_loader(tasks, thumb_tx, new_cancel, current_gen); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::EmptyFiles(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_empty_files_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::TemporaryFiles(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_temporary_files_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::BigFiles(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_big_files_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::BrokenFiles(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_broken_files_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::BadExtensions(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_bad_extensions_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::SameMusic(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_same_music_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::BadNames(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_bad_names_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::ExifRemover(items) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); win.set_exif_remover_model(make_file_model(items)); }) .expect("Failed to invoke progress update in event loop"); } ScanResult::Finished(id) => { slint::invoke_from_event_loop(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); if id != current_gen { return; } let was_stopping = win.global::().get_scan_state() == ScanState::Stopping; if was_stopping { win.global::().set_scan_state(ScanState::Stopped); win.global::().set_status_message(SharedString::from(crate::flc!("status_stopped"))); } else { win.global::().set_scan_state(ScanState::Done); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); let file_count = (0..model.row_count()).filter(|&i| model.row_data(i).is_some_and(|e| !e.is_header)).count(); let status = if file_count > 0 { format!("{} {file_count} {}", crate::flc!("found_items_prefix"), crate::flc!("found_items_suffix")) } else { crate::flc!("status_no_results") }; win.global::().set_status_message(SharedString::from(status)); } }) .expect("Failed to invoke progress update in event loop"); } } } } fn run_app_inner( inset_bottom_px: f32, scale: f32, #[cfg(target_os = "android")] android_app: Option, #[cfg(not(target_os = "android"))] _android_app: Option<()>, ) { std::thread::spawn(crate::thumbnail_loader::cleanup_old_thumbnails); let window = MainWindow::new().expect("Failed to create MainWindow"); let loaded_settings = load_settings(); crate::localizer_cedinia::apply_language_preference(&loaded_settings.language); apply_settings_to_gui(&window, &loaded_settings); set_initial_gui_infos(&window); translate_items(&window); window.global::().set_status_message(SharedString::from(crate::flc!("status_ready"))); let bot_lp = inset_bottom_px / scale; window.global::().set_inset_bottom(bot_lp); #[cfg(target_os = "android")] { if let Some(app) = android_app { let weak = window.as_weak(); let inset_timer = Rc::new(Timer::default()); let inset_timer_clone = inset_timer.clone(); inset_timer.start(TimerMode::Repeated, std::time::Duration::from_millis(50), move || { let rect = app.content_rect(); if rect.bottom > 0 { if let Some(win) = weak.upgrade() { let window_height = win.window().size().height as f32; let nav_bar_px = window_height - rect.bottom as f32; if nav_bar_px > 0.0 { win.global::().set_inset_bottom(nav_bar_px / scale); } } inset_timer_clone.stop(); } }); } } let (saved_included, saved_excluded) = load_dirs(); let included_dirs = Rc::new(std::cell::RefCell::new(if saved_included.is_empty() { vec![home_dir()] } else { saved_included })); let excluded_dirs: Rc>> = Rc::new(std::cell::RefCell::new(saved_excluded)); let scan_gen: Arc = Arc::new(AtomicU32::new(0)); let (thumb_tx, thumb_rx) = std::sync::mpsc::channel::(); let thumb_cancel: Arc>> = Arc::new(std::sync::Mutex::new(Arc::new(AtomicBool::new(false)))); let placeholder: Rc> = Rc::new(std::cell::OnceCell::new()); let handler = GuiHandler { weak: window.as_weak(), scan_gen: Arc::clone(&scan_gen), thumb_tx, thumb_cancel: Arc::clone(&thumb_cancel), }; let (scan_tx_inner, stop_flag) = start_worker(handler); let scan_tx = Rc::new(scan_tx_inner); #[cfg(target_os = "android")] DIR_STATE.with(|cell| { *cell.borrow_mut() = Some((window.as_weak(), included_dirs.clone(), excluded_dirs.clone())); }); window.set_directories_model(build_dir_model(&included_dirs.borrow(), &excluded_dirs.borrow())); let (delete_tx, delete_rx) = std::sync::mpsc::channel::(); let delete_rx = Rc::new(std::cell::RefCell::new(delete_rx)); let delete_stop: Rc>> = Rc::new(std::cell::RefCell::new(Arc::new(AtomicBool::new(false)))); wire_scan(&window, stop_flag, scan_tx, included_dirs.clone(), scan_gen.clone()); wire_permission(&window); wire_selection(&window, delete_tx, Rc::clone(&delete_stop)); wire_directories(&window, included_dirs.clone(), excluded_dirs.clone()); wire_collect_test(&window); wire_open_path(&window); wire_language_change(&window); wire_open_url(&window); wire_cache_info(&window); wire_save_settings_now(&window, included_dirs.clone(), excluded_dirs.clone()); let weak = window.as_weak(); let thumb_rx = Rc::new(std::cell::RefCell::new(thumb_rx)); let scan_gen_poll = scan_gen; let delete_rx_poll = delete_rx; let timer = Timer::default(); #[cfg(target_os = "android")] let mut perm_poll_counter: u32 = 0; timer.start(TimerMode::Repeated, std::time::Duration::from_millis(50), move || { let win = weak.upgrade().expect("Failed to upgrade MainWindow weak reference in timer"); let current_gen = scan_gen_poll.load(Ordering::SeqCst); { let rx = thumb_rx.borrow(); while let Ok(tr) = rx.try_recv() { if tr.scan_id != current_gen { continue; } let img = match tr.data { ThumbnailData::Loaded(rgba, w, h) => rgba_to_slint_image(&rgba, w, h), ThumbnailData::Placeholder => placeholder.get_or_init(make_placeholder_image).clone(), }; let groups = win.get_similar_images_groups(); if let Some(group) = groups.row_data(tr.group_idx) && let Some(mut item) = group.items.row_data(tr.item_idx) { item.thumbnail = img; group.items.set_row_data(tr.item_idx, item); } } } { let rx = delete_rx_poll.borrow(); while let Ok(event) = rx.try_recv() { match event { DeleteEvent::Progress(done, total) => { win.global::().set_delete_progress_text(SharedString::from(format!("{done} / {total}"))); } DeleteEvent::Finished(deleted, errors) => { win.global::().set_delete_running(false); if !deleted.is_empty() { let del_set: std::collections::HashSet = deleted.into_iter().collect(); rebuild_similar_images_after_delete(&win, &del_set); } let status = if errors.is_empty() { crate::flc!("status_deleted_selected").to_string() } else { crate::flc!("status_deleted_with_errors").to_string() }; win.global::().set_status_message(SharedString::from(status)); if !errors.is_empty() { show_delete_errors(&win, &errors); } } DeleteEvent::ListDeleteFinished(deleted, errors) => { win.global::().set_delete_running(false); let del_set: std::collections::HashSet = deleted.iter().cloned().collect(); if !del_set.is_empty() { let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); if let Some(vm) = model.as_any().downcast_ref::>() { let mut items: Vec = vm.iter().collect(); items.retain(|e| { if e.is_header { return true; } let name = e.val_str.row_data(0).map(|s| s.to_string()).unwrap_or_default(); let path = e.val_str.row_data(1).map(|s| s.to_string()).unwrap_or_default(); let full = if path.is_empty() { name } else { format!("{path}/{name}") }; !del_set.contains(&full) }); loop { let mut removed = false; let mut i = 0; while i < items.len() { if items[i].is_header { let group_len = items[i + 1..].iter().take_while(|e| !e.is_header).count(); if group_len <= 1 { let end = i + 1 + group_len; items.drain(i..end); removed = true; continue; } } i += 1; } if !removed { break; } } vm.set_vec(items); win.global::().set_selected_count(0); } rebuild_similar_images_after_delete(&win, &del_set); } let status = if errors.is_empty() { format!("{} {} {}", crate::flc!("deleted_items_prefix"), deleted.len(), crate::flc!("deleted_items_suffix")) } else { format!( "{} {} {}, {} {}", crate::flc!("deleted_items_prefix"), deleted.len(), crate::flc!("deleted_items_suffix"), errors.len(), crate::flc!("deleted_errors_suffix") ) }; win.global::().set_status_message(SharedString::from(status)); if !errors.is_empty() { show_delete_errors(&win, &errors); } } DeleteEvent::ListRenameFinished(renamed, errors) => { win.global::().set_delete_running(false); let model = win.get_bad_extensions_model(); if let Some(vm) = model.as_any().downcast_ref::>() { let items: Vec = vm.iter().filter(|e| !e.checked).collect(); vm.set_vec(items); win.global::().set_selected_count(0); } let status = if errors.is_empty() { format!("{} {renamed} {}", crate::flc!("renamed_prefix"), crate::flc!("renamed_files_suffix")) } else { format!( "{} {} {}, {} {}", crate::flc!("renamed_prefix"), renamed, crate::flc!("renamed_files_suffix"), errors.len(), crate::flc!("renamed_errors_suffix") ) }; win.global::().set_status_message(SharedString::from(status)); if !errors.is_empty() { show_delete_errors(&win, &errors); } } DeleteEvent::ExifCleanFinished(cleaned, errors) => { win.global::().set_delete_running(false); let cleaned_set: std::collections::HashSet = cleaned.iter().cloned().collect(); if !cleaned_set.is_empty() { let model = win.get_exif_remover_model(); if let Some(vm) = model.as_any().downcast_ref::>() { let items: Vec = vm .iter() .filter(|e| { if e.is_header { return true; } let name = e.val_str.row_data(0).map(|s| s.to_string()).unwrap_or_default(); let path = e.val_str.row_data(1).map(|s| s.to_string()).unwrap_or_default(); let full = if path.is_empty() { name } else { format!("{path}/{name}") }; !cleaned_set.contains(&full) }) .collect(); vm.set_vec(items); win.global::().set_selected_count(0); } } let status = if errors.is_empty() { format!("{} {} {}", crate::flc!("cleaned_exif_prefix"), cleaned.len(), crate::flc!("cleaned_exif_suffix")) } else { format!( "{} {} {}, {} {}", crate::flc!("cleaned_exif_prefix"), cleaned.len(), crate::flc!("cleaned_exif_suffix"), errors.len(), crate::flc!("cleaned_exif_errors_suffix") ) }; win.global::().set_status_message(SharedString::from(status)); if !errors.is_empty() { show_delete_errors(&win, &errors); } } } } } #[cfg(target_os = "android")] { perm_poll_counter += 1; if perm_poll_counter >= 40 { perm_poll_counter = 0; let granted = crate::file_picker_android::check_storage_permission(); win.global::().set_storage_permission_granted(granted); } } }); window.run().expect("Failed to run MainWindow"); let current_settings = collect_settings_from_gui(&window); save_settings(¤t_settings); save_dirs(&included_dirs.borrow(), &excluded_dirs.borrow()); } pub(crate) fn setup_logger_cache() { static INIT_DONE: std::sync::OnceLock<()> = std::sync::OnceLock::new(); if INIT_DONE.set(()).is_err() { log::info!("setup_logger_cache: already initialized, skipping"); return; } register_image_decoding_hooks(); let config_cache_path_set_result = set_config_cache_path("cedinia", "cedinia"); setup_logger(false, "cedinia", filtering_messages); print_version_mode("Cedinia"); print_infos_and_warnings(config_cache_path_set_result.infos, config_cache_path_set_result.warnings); } ================================================ FILE: cedinia/src/bin/cedinia.rs ================================================ fn main() { cedinia::run_app(); } ================================================ FILE: cedinia/src/callbacks/directories.rs ================================================ use std::path::PathBuf; use std::rc::Rc; use slint::{ComponentHandle, ModelRc, SharedString, VecModel}; use crate::volumes::{detect_storage_volumes, refresh_volumes_flags}; use crate::{AppState, DirectoryEntry, MainWindow, VolumeEntry}; pub(crate) fn wire_directories(window: &MainWindow, included_dirs: Rc>>, excluded_dirs: Rc>>) { { let weak = window.as_weak(); let inc = included_dirs.clone(); let exc = excluded_dirs.clone(); window.global::().on_pick_include_dir(move || { #[cfg(not(target_os = "android"))] { let win = weak.unwrap(); if let Some(path) = rfd::FileDialog::new().pick_folder() { inc.borrow_mut().push(path); win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); } } #[cfg(target_os = "android")] { let _ = (&weak, &inc, &exc); crate::file_picker_android::launch_pick_directory(true); } }); } { let weak = window.as_weak(); let inc = included_dirs.clone(); let exc = excluded_dirs.clone(); window.global::().on_pick_exclude_dir(move || { #[cfg(not(target_os = "android"))] { let win = weak.unwrap(); if let Some(path) = rfd::FileDialog::new().pick_folder() { exc.borrow_mut().push(path); win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); } } #[cfg(target_os = "android")] { let _ = (&weak, &inc, &exc); crate::file_picker_android::launch_pick_directory(false); } }); } { let weak = window.as_weak(); let inc = included_dirs.clone(); let exc = excluded_dirs.clone(); window.global::().on_add_include_dir(move |path| { let win = weak.unwrap(); inc.borrow_mut().push(PathBuf::from(path.as_str())); win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow()); }); } { let weak = window.as_weak(); let inc = included_dirs.clone(); let exc = excluded_dirs.clone(); window.global::().on_remove_include_dir(move |path| { let win = weak.unwrap(); inc.borrow_mut().retain(|p| p.to_string_lossy() != path.as_str()); win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow()); }); } { let weak = window.as_weak(); let inc = included_dirs.clone(); let exc = excluded_dirs.clone(); window.global::().on_add_exclude_dir(move |path| { let win = weak.unwrap(); exc.borrow_mut().push(PathBuf::from(path.as_str())); win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow()); }); } { let weak = window.as_weak(); let inc = included_dirs.clone(); let exc = excluded_dirs.clone(); window.global::().on_remove_exclude_dir(move |path| { let win = weak.unwrap(); exc.borrow_mut().retain(|p| p.to_string_lossy() != path.as_str()); win.set_directories_model(build_dir_model(&inc.borrow(), &exc.borrow())); refresh_volumes_flags(&win, &inc.borrow(), &exc.borrow()); }); } { let weak = window.as_weak(); let inc = included_dirs; let exc = excluded_dirs; window.global::().on_list_storage_volumes(move || { let raw = detect_storage_volumes(); let inc_set: Vec = inc.borrow().iter().map(|p| p.to_string_lossy().to_string()).collect(); let exc_set: Vec = exc.borrow().iter().map(|p| p.to_string_lossy().to_string()).collect(); let volumes: Vec = raw .into_iter() .map(|mut v| { let path = v.path.to_string(); v.is_included = inc_set.contains(&path); v.is_excluded = exc_set.contains(&path); v }) .collect(); if let Some(win) = weak.upgrade() { win.global::().set_storage_volumes(ModelRc::new(VecModel::from(volumes))); } }); } } pub(crate) fn build_dir_model(included: &[PathBuf], excluded: &[PathBuf]) -> ModelRc { let mut entries: Vec = included .iter() .map(|p| DirectoryEntry { path: SharedString::from(p.to_string_lossy().to_string()), is_included: true, }) .collect(); for p in excluded { entries.push(DirectoryEntry { path: SharedString::from(p.to_string_lossy().to_string()), is_included: false, }); } ModelRc::new(VecModel::from(entries)) } ================================================ FILE: cedinia/src/callbacks/misc.rs ================================================ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use slint::ComponentHandle; use crate::settings::{collect_settings_from_gui, save_settings}; use crate::thumbnail_loader::thumbnail_cache_dir; use crate::volumes::{count_files_and_dirs_stoppable, detect_storage_volumes}; use crate::{AppState, CollectTestResult, GeneralSettings, MainWindow}; pub(crate) fn wire_open_path(window: &MainWindow) { #[cfg(not(target_os = "android"))] { window.global::().on_open_path(|path| { let _ = std::process::Command::new("xdg-open").arg(path.as_str()).spawn(); }); window.global::().on_open_parent_folder(|path| { if !path.is_empty() { let _ = std::process::Command::new("xdg-open").arg(path.as_str()).spawn(); } }); } #[cfg(target_os = "android")] { window.global::().on_open_path(|_| {}); window.global::().on_open_parent_folder(|_| {}); } } pub(crate) fn wire_permission(window: &MainWindow) { #[cfg(target_os = "android")] { let perm = crate::file_picker_android::check_storage_permission(); window.global::().set_storage_permission_granted(perm); if !perm { window.global::().set_show_permission_popup(true); } window.global::().on_request_storage_permission(move || { crate::file_picker_android::request_storage_permission(); }); } #[cfg(not(target_os = "android"))] { window.global::().on_request_storage_permission(|| {}); } } pub(crate) fn wire_collect_test(window: &MainWindow) { let collect_stop_flag: Arc = Arc::new(AtomicBool::new(false)); { let weak = window.as_weak(); let stop = collect_stop_flag.clone(); window.global::().on_run_collect_test(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); stop.store(false, Ordering::Relaxed); win.global::().set_collect_test_running(true); win.global::().set_collect_test_done(false); let weak2 = win.as_weak(); let stop2 = stop.clone(); std::thread::spawn(move || { let start = std::time::Instant::now(); let volumes = detect_storage_volumes(); let volume_count = volumes.len() as i32; let mut total_files: i32 = 0; let mut total_folders: i32 = 0; let mut stopped = false; 'outer: for vol in &volumes { let root = std::path::Path::new(vol.path.as_str()); let (f, d) = count_files_and_dirs_stoppable(root, &stop2, &mut stopped); total_files = total_files.saturating_add(f); total_folders = total_folders.saturating_add(d); if stopped { break 'outer; } } let elapsed_ms = start.elapsed().as_millis() as i32; let result = CollectTestResult { volumes: volume_count, files: total_files, folders: total_folders, elapsed_ms, }; let _ = slint::invoke_from_event_loop(move || { if let Some(win) = weak2.upgrade() { win.global::().set_collect_test_result(result); win.global::().set_collect_test_running(false); if !stopped { win.global::().set_collect_test_done(true); } } }); }); }); } { let weak = window.as_weak(); let stop = collect_stop_flag; window.global::().on_stop_collect_test(move || { stop.store(true, Ordering::Relaxed); if let Some(win) = weak.upgrade() { win.global::().set_collect_test_running(false); } }); } } fn dir_size_recursive(path: &Path) -> u64 { std::fs::read_dir(path).ok().map_or(0, |entries| { entries .flatten() .map(|e| { let p = e.path(); if p.is_dir() { dir_size_recursive(&p) } else { e.metadata().map(|m| m.len()).unwrap_or(0) } }) .sum() }) } pub(crate) fn wire_cache_info(window: &MainWindow) { { let weak = window.as_weak(); window.global::().on_refresh_diag_cache_info(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); if win.global::().get_diag_refresh_running() { return; } win.global::().set_diag_refresh_running(true); let weak2 = win.as_weak(); std::thread::spawn(move || { let thumb_dir = thumbnail_cache_dir(); let thumb_size = dir_size_recursive(&thumb_dir); let app_cache_size = czkawka_core::common::config_cache_path::get_config_cache_path().map_or(0, |p| dir_size_recursive(&p.cache_folder)); let _ = slint::invoke_from_event_loop(move || { if let Some(win) = weak2.upgrade() { win.global::() .set_diag_thumbnails_size(humansize::format_size(thumb_size, humansize::BINARY).into()); win.global::() .set_diag_app_cache_size(humansize::format_size(app_cache_size, humansize::BINARY).into()); win.global::().set_diag_refresh_running(false); } }); }); }); } { let weak = window.as_weak(); window.global::().on_clear_thumbnails_cache(move || { let thumb_dir = thumbnail_cache_dir(); if let Ok(entries) = std::fs::read_dir(&thumb_dir) { for entry in entries.flatten() { let _ = std::fs::remove_file(entry.path()); } } if let Some(win) = weak.upgrade() { win.global::().set_diag_thumbnails_size("0 B".into()); } }); } { let weak = window.as_weak(); window.global::().on_clear_app_cache(move || { if let Some(cache_path) = czkawka_core::common::config_cache_path::get_config_cache_path() { let _ = std::fs::remove_dir_all(&cache_path.cache_folder); } if let Some(win) = weak.upgrade() { win.global::().set_diag_app_cache_size("0 B".into()); } }); } } pub(crate) fn wire_language_change(window: &MainWindow) { let weak = window.as_weak(); window.global::().on_apply_language_change(move || { let win = weak.upgrade().expect("MainWindow dropped in on_apply_language_change"); let idx = win.global::().get_language_idx(); let lang = if idx == 1 { "pl" } else { "en" }; crate::localizer_cedinia::apply_language_preference(lang); crate::translations::translate_items(&win); }); } pub(crate) fn wire_open_url(window: &MainWindow) { #[cfg(not(target_os = "android"))] { window.global::().on_open_url(|url| { let _ = std::process::Command::new("xdg-open").arg(url.as_str()).spawn(); }); } #[cfg(target_os = "android")] { window.global::().on_open_url(|_| {}); } } pub(crate) fn wire_save_settings_now(window: &MainWindow, included_dirs: Rc>>, excluded_dirs: Rc>>) { let weak = window.as_weak(); window.global::().on_save_settings_now(move || { let win = weak.upgrade().expect("Failed to upgrade app :("); let settings = collect_settings_from_gui(&win); save_settings(&settings); crate::settings::save_dirs(&included_dirs.borrow(), &excluded_dirs.borrow()); }); } ================================================ FILE: cedinia/src/callbacks/scan.rs ================================================ use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use crossbeam_channel::Sender; use czkawka_core::common::model::{CheckingMethod, HashType}; use czkawka_core::re_exported::{FilterType, HashAlg}; use czkawka_core::tools::big_file::SearchMode; use czkawka_core::tools::similar_images::SimilarityPreset; use slint::{ComponentHandle, ModelRc, SharedString, VecModel}; use crate::scan_runner::{CommonFilters, ScanRequest}; use crate::settings::gui_settings_values::StringComboBoxItems; use crate::{ ActiveTool, AppState, BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, FileEntry, GeneralSettings, MainWindow, SameMusicSettings, ScanState, SimilarGroupCard, SimilarImagesSettings, }; pub(crate) fn wire_scan( window: &MainWindow, stop_flag: Arc, scan_tx: Rc>, included_dirs: Rc>>, scan_gen: Arc, ) { { let weak = window.as_weak(); let inc = included_dirs; let stop = stop_flag.clone(); let tx = scan_tx.clone(); let scan_gen2 = scan_gen; window.global::().on_scan_requested(move || { let win = weak.upgrade().expect("MainWindow dropped in on_scan_requested"); scan_gen2.fetch_add(1, Ordering::SeqCst); win.global::().set_scan_state(ScanState::Scanning); win.global::().set_status_message(SharedString::from("Skanowanie…")); stop.store(false, Ordering::Relaxed); let dirs = inc.borrow().clone(); let tool = win.global::().get_active_tool(); clear_tool_results(&win, tool); let req = build_scan_request(&win, tool, dirs); let _ = tx.send(req); }); } { let weak = window.as_weak(); let stop = stop_flag; let tx = scan_tx; window.global::().on_stop_requested(move || { let win = weak.upgrade().expect("MainWindow dropped in on_stop_requested"); stop.store(true, Ordering::Relaxed); let _ = tx.send(ScanRequest::Stop); win.global::().set_scan_state(ScanState::Stopping); }); } { let weak = window.as_weak(); window.global::().on_tool_changed(move |_| { let win = weak.upgrade().expect("MainWindow dropped in on_tool_changed"); win.global::().set_selected_count(0); win.global::().set_status_message(SharedString::default()); }); } } fn empty_entries() -> ModelRc { ModelRc::new(VecModel::from(vec![])) } fn clear_tool_results(win: &MainWindow, tool: ActiveTool) { match tool { ActiveTool::DuplicateFiles => win.set_duplicate_files_model(empty_entries()), ActiveTool::EmptyFolders => win.set_empty_folder_model(empty_entries()), ActiveTool::SimilarImages => { win.set_similar_images_model(empty_entries()); win.set_similar_images_groups(ModelRc::new(VecModel::::from(vec![]))); } ActiveTool::EmptyFiles => win.set_empty_files_model(empty_entries()), ActiveTool::TemporaryFiles => win.set_temporary_files_model(empty_entries()), ActiveTool::BigFiles => win.set_big_files_model(empty_entries()), ActiveTool::BrokenFiles => win.set_broken_files_model(empty_entries()), ActiveTool::BadExtensions => win.set_bad_extensions_model(empty_entries()), ActiveTool::SameMusic => win.set_same_music_model(empty_entries()), ActiveTool::BadNames => win.set_bad_names_model(empty_entries()), ActiveTool::ExifRemover => win.set_exif_remover_model(empty_entries()), ActiveTool::Home | ActiveTool::Directories | ActiveTool::Settings => {} } win.global::().set_selected_count(0); } fn build_common_filters(win: &MainWindow) -> CommonFilters { let g = win.global::(); let items = StringComboBoxItems::new(); 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()); 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()); let split_csv = |s: slint::SharedString| -> Vec { s.as_str().split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect() }; let mut excluded_items = split_csv(g.get_excluded_items()); let cache_dir = crate::thumbnail_loader::thumbnail_cache_dir(); if let Some(s) = cache_dir.to_str() { excluded_items.push(format!("{s}/*")); } if g.get_ignore_hidden() { excluded_items.push("*/.*".to_string()); excluded_items.push("*/.*/*".to_string()); } CommonFilters { excluded_items, allowed_extensions: split_csv(g.get_allowed_extensions()), excluded_extensions: split_csv(g.get_excluded_extensions()), min_file_size_bytes, max_file_size_bytes, } } fn build_scan_request(win: &MainWindow, tool: ActiveTool, dirs: Vec) -> ScanRequest { let filters = build_common_filters(win); let items = StringComboBoxItems::new(); let duplicate_request = || { let d = win.global::(); ScanRequest::DuplicateFiles { dirs: dirs.clone(), check_method: StringComboBoxItems::value_from_idx(&items.duplicates_check_method, d.get_check_method(), CheckingMethod::Hash), hash_type: StringComboBoxItems::value_from_idx(&items.duplicates_hash_type, d.get_hash_type(), HashType::Blake3), use_cache: win.global::().get_use_cache(), filters: filters.clone(), } }; match tool { ActiveTool::DuplicateFiles => duplicate_request(), ActiveTool::EmptyFolders => ScanRequest::EmptyFolders { dirs, filters }, ActiveTool::SimilarImages => { let s = win.global::(); ScanRequest::SimilarImages { dirs, similarity_preset: StringComboBoxItems::value_from_idx(&items.similarity_preset, s.get_similarity_preset(), SimilarityPreset::Medium), hash_size: StringComboBoxItems::value_from_idx(&items.hash_size, s.get_hash_size_idx(), 16), hash_alg: StringComboBoxItems::value_from_idx(&items.hash_alg, s.get_hash_alg_idx(), HashAlg::Mean), image_filter: StringComboBoxItems::value_from_idx(&items.image_filter, s.get_image_filter_idx(), FilterType::Triangle), ignore_same_size: s.get_ignore_same_size(), filters, } } ActiveTool::EmptyFiles => ScanRequest::EmptyFiles { dirs, filters }, ActiveTool::TemporaryFiles => ScanRequest::TemporaryFiles { dirs, filters }, ActiveTool::BigFiles => { let b = win.global::(); ScanRequest::BigFiles { dirs, search_mode: StringComboBoxItems::value_from_idx(&items.biggest_files_method, b.get_search_mode_idx(), SearchMode::BiggestFiles), count: StringComboBoxItems::value_from_idx(&items.big_files_count, b.get_count_idx(), 50), filters, } } ActiveTool::BrokenFiles => { let b = win.global::(); use czkawka_core::tools::broken_files::CheckedTypes; let mut types = CheckedTypes::empty(); if b.get_check_audio() { types |= CheckedTypes::AUDIO; } if b.get_check_pdf() { types |= CheckedTypes::PDF; } if b.get_check_archive() { types |= CheckedTypes::ARCHIVE; } if b.get_check_image() { types |= CheckedTypes::IMAGE; } ScanRequest::BrokenFiles { dirs, filters, checked_types: types.bits(), } } ActiveTool::BadExtensions => ScanRequest::BadExtensions { dirs, filters }, ActiveTool::SameMusic => { let m = win.global::(); use czkawka_core::tools::same_music::MusicSimilarity; let mut sim = MusicSimilarity::NONE; if m.get_title() { sim |= MusicSimilarity::TRACK_TITLE; } if m.get_artist() { sim |= MusicSimilarity::TRACK_ARTIST; } if m.get_year() { sim |= MusicSimilarity::YEAR; } if m.get_length() { sim |= MusicSimilarity::LENGTH; } if m.get_genre() { sim |= MusicSimilarity::GENRE; } if m.get_bitrate() { sim |= MusicSimilarity::BITRATE; } if sim.is_empty() { sim = MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST; } ScanRequest::SameMusic { dirs, filters, music_similarity: sim.bits(), approximate: m.get_approximate(), check_method: StringComboBoxItems::value_from_idx(&items.same_music_check_method, m.get_check_method_idx(), CheckingMethod::AudioTags), } } ActiveTool::BadNames => { let bn = win.global::(); ScanRequest::BadNames { dirs, filters, uppercase_extension: bn.get_uppercase_extension(), emoji_used: bn.get_emoji_used(), space_at_start_or_end: bn.get_space_at_start_or_end(), non_ascii_graphical: bn.get_non_ascii_graphical(), remove_duplicated_non_alpha: bn.get_remove_duplicated_non_alpha(), } } ActiveTool::ExifRemover => ScanRequest::ExifRemover { dirs, filters }, ActiveTool::Home | ActiveTool::Directories | ActiveTool::Settings => { unreachable!("scan cannot be triggered from Home/Directories/Settings tab") } } } ================================================ FILE: cedinia/src/callbacks/selection.rs ================================================ use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use slint::{ComponentHandle, Model, ModelRc, VecModel}; use crate::common::{INT_IDX_SIZE_HI, INT_IDX_SIZE_LO, IntDataSimilarImages, STR_IDX_NAME, STR_IDX_PATH, StrDataBadExtensions, StrDataBadNames}; use crate::model::{count_checked, toggle_row}; use crate::{ActiveTool, AppState, FileEntry, MainWindow, SimilarGroupCard, SimilarImageItem}; #[cfg(not(target_os = "android"))] fn delete_path(path: &str) -> Result<(), String> { trash::delete(path).map_err(|e| e.to_string()) } #[cfg(target_os = "android")] fn delete_path(path: &str) -> Result<(), String> { std::fs::remove_file(path).or_else(|_| std::fs::remove_dir_all(path)).map_err(|e| e.to_string()) } pub(crate) enum DeleteEvent { Progress(usize, usize), Finished(Vec, Vec), ListDeleteFinished(Vec, Vec), ListRenameFinished(usize, Vec), ExifCleanFinished(Vec, Vec), } fn vm_of(model: &ModelRc) -> Option<&VecModel> { model.as_any().downcast_ref::>() } fn size_from_entry(e: &FileEntry) -> u64 { let hi = get_val_int(e, INT_IDX_SIZE_HI) as u64; let lo = get_val_int(e, INT_IDX_SIZE_LO) as u64; (hi << 32) | (lo & 0xFFFF_FFFF) } fn get_val_str(e: &FileEntry, idx: usize) -> String { e.val_str.row_data(idx).map(|s| s.to_string()).unwrap_or_default() } fn get_val_int(e: &FileEntry, idx: usize) -> i32 { e.val_int.row_data(idx).unwrap_or(0) } fn full_path_of(e: &FileEntry) -> String { let name = get_val_str(e, STR_IDX_NAME); let path = get_val_str(e, STR_IDX_PATH); if path.is_empty() { name } else { format!("{path}/{name}") } } fn execute_delete_selected(win: &MainWindow, tx: std::sync::mpsc::Sender) { let tool = win.global::().get_active_tool(); let model = get_model_for_tool(win, tool); let Some(vm) = vm_of(&model) else { return }; let items: Vec = vm.iter().collect(); let to_delete: Vec<(usize, String)> = items .iter() .enumerate() .filter(|(_, e)| e.checked && !e.is_header) .map(|(i, e)| (i, full_path_of(e))) .collect(); if to_delete.is_empty() { return; } let total = to_delete.len(); win.global::().set_delete_running(true); win.global::().set_delete_progress_text(slint::SharedString::from(format!("0 / {total}"))); std::thread::spawn(move || { let mut deleted_paths: Vec = Vec::new(); let mut errors: Vec = Vec::new(); for (i, (_idx, path)) in to_delete.iter().enumerate() { match delete_path(path) { Ok(()) => { deleted_paths.push(path.clone()); } Err(err) => errors.push(format!("{path}\n {err}")), } if i % 5 == 4 || i + 1 == total { let _ = tx.send(DeleteEvent::Progress(i + 1, total)); } } let _ = tx.send(DeleteEvent::ListDeleteFinished(deleted_paths, errors)); }); } fn execute_rename_selected(win: &MainWindow, tx: std::sync::mpsc::Sender) { let model = win.get_bad_extensions_model(); let Some(vm) = vm_of(&model) else { return }; let items: Vec = vm.iter().collect(); let to_rename: Vec<(usize, String, String)> = items .iter() .enumerate() .filter(|(_, e)| e.checked && !e.is_header) .filter_map(|(i, e)| { let full = full_path_of(e); let proper_ext = get_val_str(e, StrDataBadExtensions::ProperExtension as usize); if proper_ext.is_empty() { return None; } Some((i, full, proper_ext)) }) .collect(); if to_rename.is_empty() { return; } let total = to_rename.len(); win.global::().set_delete_running(true); win.global::().set_delete_progress_text(slint::SharedString::from(format!("0 / {total}"))); std::thread::spawn(move || { let mut renamed_indices: Vec = Vec::new(); let mut errors: Vec = Vec::new(); for (i, (idx, full, proper_ext)) in to_rename.iter().enumerate() { let src = std::path::Path::new(full.as_str()); let new_path = match src.file_stem() { Some(stem) => { let parent = src.parent().unwrap_or(std::path::Path::new("")); parent.join(format!("{}.{}", stem.to_string_lossy(), proper_ext)) } None => { errors.push(format!("{full}\n Nie można odczytać nazwy pliku")); continue; } }; match std::fs::rename(full, &new_path) { Ok(()) => renamed_indices.push(*idx), Err(err) => errors.push(format!("{full}\n {err}")), } if i % 5 == 4 || i + 1 == total { let _ = tx.send(DeleteEvent::Progress(i + 1, total)); } } let renamed = renamed_indices.len(); let _ = tx.send(DeleteEvent::ListRenameFinished(renamed, errors)); }); } fn execute_rename_bad_names(win: &MainWindow, tx: std::sync::mpsc::Sender) { let model = win.get_bad_names_model(); let Some(vm) = vm_of(&model) else { return }; let items: Vec = vm.iter().collect(); let to_rename: Vec<(usize, String, String)> = items .iter() .enumerate() .filter(|(_, e)| e.checked && !e.is_header) .filter_map(|(i, e)| { let new_name = get_val_str(e, StrDataBadNames::NewName as usize); if new_name.is_empty() { return None; } let full = full_path_of(e); Some((i, full, new_name)) }) .collect(); if to_rename.is_empty() { return; } let total = to_rename.len(); win.global::().set_delete_running(true); win.global::().set_delete_progress_text(slint::SharedString::from(format!("0 / {total}"))); std::thread::spawn(move || { let mut renamed_count = 0usize; let mut errors: Vec = Vec::new(); for (i, (_idx, full, new_name)) in to_rename.iter().enumerate() { let src = std::path::Path::new(full.as_str()); let new_path = match src.parent() { Some(parent) => parent.join(new_name), None => { errors.push(format!("{full}\n Nie można odczytać katalogu")); continue; } }; match std::fs::rename(full, &new_path) { Ok(()) => renamed_count += 1, Err(err) => errors.push(format!("{full}\n {err}")), } if i % 5 == 4 || i + 1 == total { let _ = tx.send(DeleteEvent::Progress(i + 1, total)); } } let _ = tx.send(DeleteEvent::ListRenameFinished(renamed_count, errors)); }); } fn execute_clean_exif_selected(win: &MainWindow, tx: std::sync::mpsc::Sender) { let model = win.get_exif_remover_model(); let Some(vm) = vm_of(&model) else { return }; let items: Vec = vm.iter().collect(); let to_clean: Vec<(usize, String)> = items .iter() .enumerate() .filter(|(_, e)| e.checked && !e.is_header) .map(|(i, e)| (i, full_path_of(e))) .collect(); if to_clean.is_empty() { return; } let total = to_clean.len(); win.global::().set_delete_running(true); win.global::().set_delete_progress_text(slint::SharedString::from(format!("0 / {total}"))); std::thread::spawn(move || { let mut cleaned_paths: Vec = Vec::new(); let mut errors: Vec = Vec::new(); for (i, (_idx, path)) in to_clean.iter().enumerate() { match clean_exif_all_tags(path) { Ok(()) => cleaned_paths.push(path.clone()), Err(e) => errors.push(format!("{path}\n {e}")), } if i % 5 == 4 || i + 1 == total { let _ = tx.send(DeleteEvent::Progress(i + 1, total)); } } let _ = tx.send(DeleteEvent::ExifCleanFinished(cleaned_paths, errors)); }); } fn clean_exif_all_tags(path: &str) -> Result<(), String> { use czkawka_core::tools::exif_remover::core::{clean_exif_tags, extract_exif_tags_public}; let tags = extract_exif_tags_public(std::path::Path::new(path))?; clean_exif_tags(path, &tags, true).map(|_| ()) } pub(crate) fn wire_selection(window: &MainWindow, delete_tx: std::sync::mpsc::Sender, delete_stop: Rc>>) { { let weak = window.as_weak(); let tx = delete_tx.clone(); window.global::().on_clean_exif_selected(move || { let win = weak.upgrade().expect("MainWindow dropped in on_clean_exif_selected"); let model = win.get_exif_remover_model(); let n = count_checked(&model); if n == 0 { return; } let state = win.global::(); state.set_confirm_popup_message(slint::SharedString::from(format!("Czy na pewno chcesz wyczyścić tagi EXIF z {n} zaznaczonych plików?"))); state.set_confirm_popup_action(slint::SharedString::from("clean_exif")); state.set_confirm_popup_visible(true); let _ = tx.clone(); }); } { let weak = window.as_weak(); let tx = delete_tx.clone(); window.global::().on_delete_selected(move || { let win = weak.upgrade().expect("MainWindow dropped in on_delete_selected"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); let n = count_checked(&model); if n == 0 { return; } let state = win.global::(); state.set_confirm_popup_message(slint::SharedString::from(format!("Czy na pewno chcesz usunąć {n} zaznaczonych elementów?"))); state.set_confirm_popup_action(slint::SharedString::from("delete")); state.set_confirm_popup_visible(true); let _ = tx.clone(); }); } { let weak = window.as_weak(); window.global::().on_select_all(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_all"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); set_all_checked(&model, true); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_deselect_all(move || { let win = weak.upgrade().expect("MainWindow dropped in on_deselect_all"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); set_all_checked(&model, false); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(0); }); } { let weak = window.as_weak(); window.global::().on_select_all_except_one(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_all_except_one"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_except_one_per_group(&model, true); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_deselect_all_except_one(move || { let win = weak.upgrade().expect("MainWindow dropped in on_deselect_all_except_one"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_except_one_per_group(&model, false); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_invert_selection(move || { let win = weak.upgrade().expect("MainWindow dropped in on_invert_selection"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); if let Some(vm) = vm_of(&model) { let mut items: Vec = vm.iter().collect::>(); for e in &mut items { if !e.is_header { e.checked = !e.checked; } } vm.set_vec(items); } sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_largest_per_group(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_largest_per_group"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_largest_per_group(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_all_except_largest(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_all_except_largest"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_all_except_largest(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_smallest_per_group(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_smallest_per_group"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_smallest_per_group(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_all_except_smallest(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_all_except_smallest"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_all_except_smallest(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_highest_resolution_per_group(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_highest_resolution_per_group"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_highest_resolution_per_group(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_all_except_highest_resolution(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_all_except_highest_resolution"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_all_except_highest_resolution(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_lowest_resolution_per_group(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_lowest_resolution_per_group"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_lowest_resolution_per_group(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_select_all_except_lowest_resolution(move || { let win = weak.upgrade().expect("MainWindow dropped in on_select_all_except_lowest_resolution"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); select_all_except_lowest_resolution(&model); sync_gallery_if_similar(&win, tool); win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_toggle_file_checked(move |idx| { let win = weak.upgrade().expect("MainWindow dropped in on_toggle_file_checked"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); toggle_row(&model, idx as usize); if tool == ActiveTool::SimilarImages { sync_gallery_checked_from_flat(&win); } win.global::().set_selected_count(count_checked(&model)); }); } { let weak = window.as_weak(); window.global::().on_request_gallery_delete(move || { let win = weak.upgrade().expect("MainWindow dropped in on_request_gallery_delete"); let groups: Vec = win.get_similar_images_groups().iter().collect::>(); let mut total_images = 0i32; let mut total_groups = 0i32; let mut unsafe_groups = 0i32; for group in &groups { let items: Vec = group.items.iter().collect::>(); let checked = items.iter().filter(|it| it.checked).count(); if checked > 0 { total_groups += 1; total_images += checked as i32; if checked == items.len() { unsafe_groups += 1; } } } let msg = slint::SharedString::from(format!("Zamierzasz usunąć {total_images} obrazów w {total_groups} grupach?")); let warn = if unsafe_groups > 0 { slint::SharedString::from(format!("⚠ W {unsafe_groups} grupach zaznaczono wszystkie elementy!")) } else { slint::SharedString::default() }; let state = win.global::(); state.set_gallery_delete_message(msg); state.set_gallery_delete_warning(warn); state.set_gallery_delete_popup_visible(true); }); } { let weak = window.as_weak(); let tx = delete_tx.clone(); let stop_cell = Rc::clone(&delete_stop); window.global::().on_confirm_gallery_delete(move || { let win = weak.upgrade().expect("MainWindow dropped in on_confirm_gallery_delete"); let files: Vec = win .get_similar_images_groups() .iter() .flat_map(|g: SimilarGroupCard| { g.items .iter() .filter(|it: &SimilarImageItem| it.checked) .map(|it: SimilarImageItem| it.full_path.to_string()) .collect::>() }) .collect::>(); if files.is_empty() { win.global::().set_gallery_delete_popup_visible(false); return; } let new_stop = Arc::new(AtomicBool::new(false)); *stop_cell.borrow_mut() = new_stop.clone(); let state = win.global::(); state.set_gallery_delete_popup_visible(false); state.set_delete_running(true); state.set_delete_progress_text(slint::SharedString::from(format!("0 / {}", files.len()))); let tx = tx.clone(); let total = files.len(); std::thread::spawn(move || { let mut deleted: Vec = Vec::new(); let mut errors: Vec = Vec::new(); for (i, path) in files.iter().enumerate() { if new_stop.load(Ordering::Relaxed) { break; } match delete_path(path) { Ok(()) => deleted.push(path.clone()), Err(e) => errors.push(format!("{path}\n {e}")), } if i % 5 == 4 || i + 1 == total { let _ = tx.send(DeleteEvent::Progress(i + 1, total)); } } let _ = tx.send(DeleteEvent::Finished(deleted, errors)); }); }); } { window.global::().on_delete_stop_requested(move || { delete_stop.borrow().store(true, Ordering::Relaxed); }); } { let weak = window.as_weak(); let tx = delete_tx.clone(); window.global::().on_rename_selected(move || { let win = weak.upgrade().expect("MainWindow dropped in on_rename_selected"); let tool = win.global::().get_active_tool(); let model = get_model_for_tool(&win, tool); let n = count_checked(&model); if n == 0 { return; } let state = win.global::(); state.set_confirm_popup_message(slint::SharedString::from(format!("Czy na pewno chcesz zmienić nazwy {n} zaznaczonych plików?"))); let action = if tool == ActiveTool::BadNames { "rename_bad_names" } else { "rename" }; state.set_confirm_popup_action(slint::SharedString::from(action)); state.set_confirm_popup_visible(true); let _ = tx.clone(); }); } { let weak = window.as_weak(); let tx_confirm = delete_tx; window.global::().on_confirm_popup_ok(move || { let win = weak.upgrade().expect("MainWindow dropped in on_confirm_popup_ok"); let action = win.global::().get_confirm_popup_action().to_string(); win.global::().set_confirm_popup_visible(false); match action.as_str() { "delete" => execute_delete_selected(&win, tx_confirm.clone()), "rename" => execute_rename_selected(&win, tx_confirm.clone()), "rename_bad_names" => execute_rename_bad_names(&win, tx_confirm.clone()), "clean_exif" => execute_clean_exif_selected(&win, tx_confirm.clone()), _ => {} } }); } { let weak = window.as_weak(); window.global::().on_confirm_popup_cancel(move || { weak.upgrade() .expect("MainWindow dropped in on_confirm_popup_cancel") .global::() .set_confirm_popup_visible(false); }); } } pub(crate) fn get_model_for_tool(win: &MainWindow, tool: ActiveTool) -> ModelRc { match tool { ActiveTool::DuplicateFiles => win.get_duplicate_files_model(), ActiveTool::EmptyFolders => win.get_empty_folder_model(), ActiveTool::SimilarImages => win.get_similar_images_model(), ActiveTool::EmptyFiles => win.get_empty_files_model(), ActiveTool::TemporaryFiles => win.get_temporary_files_model(), ActiveTool::BigFiles => win.get_big_files_model(), ActiveTool::BrokenFiles => win.get_broken_files_model(), ActiveTool::BadExtensions => win.get_bad_extensions_model(), ActiveTool::SameMusic => win.get_same_music_model(), ActiveTool::BadNames => win.get_bad_names_model(), ActiveTool::ExifRemover => win.get_exif_remover_model(), ActiveTool::Home | ActiveTool::Directories | ActiveTool::Settings => ModelRc::new(VecModel::from(vec![])), } } pub(crate) fn set_all_checked(model: &ModelRc, state: bool) { if let Some(vm) = vm_of(model) { let mut items: Vec = vm.iter().collect::>(); for e in &mut items { if !e.is_header { e.checked = state; } } vm.set_vec(items); } } pub(crate) fn select_except_one_per_group(model: &ModelRc, select: bool) { let Some(vm) = vm_of(model) else { return }; let mut items: Vec = vm.iter().collect::>(); let has_headers = items.iter().any(|e| e.is_header); if !has_headers { for e in &mut items { if !e.is_header { e.checked = select; } } vm.set_vec(items); return; } if select { let mut first_in_group = false; for e in &mut items { if e.is_header { first_in_group = true; continue; } e.checked = if first_in_group { first_in_group = false; false } else { true }; } } else { let mut i = 0; while i < items.len() { if items[i].is_header { let group_end = items[i + 1..].iter().position(|e| e.is_header).map_or(items.len(), |p| i + 1 + p); let checked_count = items[i + 1..group_end].iter().filter(|e| e.checked).count(); if checked_count >= 2 { let mut kept = false; for j in i + 1..group_end { if items[j].checked { if kept { items[j].checked = false; } else { kept = true; } } } } i = group_end; continue; } i += 1; } } vm.set_vec(items); } fn sync_gallery_if_similar(win: &MainWindow, tool: ActiveTool) { if tool == ActiveTool::SimilarImages { sync_gallery_checked_from_flat(win); } } pub(crate) fn select_largest_per_group(model: &ModelRc) { select_by_size_per_group(model, true, true); } pub(crate) fn select_all_except_largest(model: &ModelRc) { select_by_size_per_group(model, true, false); } pub(crate) fn select_smallest_per_group(model: &ModelRc) { select_by_size_per_group(model, false, true); } pub(crate) fn select_all_except_smallest(model: &ModelRc) { select_by_size_per_group(model, false, false); } fn select_by_size_per_group(model: &ModelRc, largest: bool, select_target: bool) { let Some(vm) = vm_of(model) else { return }; let mut items: Vec = vm.iter().collect(); let mut i = 0; while i < items.len() { if items[i].is_header { let group_end = items[i + 1..].iter().position(|e| e.is_header).map_or(items.len(), |p| i + 1 + p); let target_idx = if largest { items[i + 1..group_end].iter().enumerate().max_by_key(|(_, e)| size_from_entry(e)).map(|(j, _)| i + 1 + j) } else { items[i + 1..group_end].iter().enumerate().min_by_key(|(_, e)| size_from_entry(e)).map(|(j, _)| i + 1 + j) }; for j in i + 1..group_end { let is_target = target_idx == Some(j); items[j].checked = if select_target { is_target } else { !is_target }; } i = group_end; continue; } i += 1; } vm.set_vec(items); } fn resolution_from_entry(e: &FileEntry) -> u64 { let w = get_val_int(e, IntDataSimilarImages::Width as usize) as u64; let h = get_val_int(e, IntDataSimilarImages::Height as usize) as u64; w * h } fn select_by_resolution_per_group(model: &ModelRc, highest: bool, select_target: bool) { let Some(vm) = vm_of(model) else { return }; let mut items: Vec = vm.iter().collect(); let mut i = 0; while i < items.len() { if items[i].is_header { let group_end = items[i + 1..].iter().position(|e| e.is_header).map_or(items.len(), |p| i + 1 + p); let target_idx = if highest { items[i + 1..group_end] .iter() .enumerate() .max_by_key(|(_, e)| resolution_from_entry(e)) .map(|(j, _)| i + 1 + j) } else { items[i + 1..group_end] .iter() .enumerate() .min_by_key(|(_, e)| resolution_from_entry(e)) .map(|(j, _)| i + 1 + j) }; for j in i + 1..group_end { let is_target = target_idx == Some(j); items[j].checked = if select_target { is_target } else { !is_target }; } i = group_end; continue; } i += 1; } vm.set_vec(items); } pub(crate) fn select_highest_resolution_per_group(model: &ModelRc) { select_by_resolution_per_group(model, true, true); } pub(crate) fn select_all_except_highest_resolution(model: &ModelRc) { select_by_resolution_per_group(model, true, false); } pub(crate) fn select_lowest_resolution_per_group(model: &ModelRc) { select_by_resolution_per_group(model, false, true); } pub(crate) fn select_all_except_lowest_resolution(model: &ModelRc) { select_by_resolution_per_group(model, false, false); } pub(crate) fn sync_gallery_checked_from_flat(win: &MainWindow) { let flat: Vec = win.get_similar_images_model().iter().collect::>(); let groups: Vec = win.get_similar_images_groups().iter().collect::>(); for group in &groups { let mut items: Vec = group.items.iter().collect::>(); let mut changed = false; for item in &mut items { if let Some(entry) = flat.get(item.flat_idx as usize) && item.checked != entry.checked { item.checked = entry.checked; changed = true; } } if changed && let Some(vm) = group.items.as_any().downcast_ref::>() { vm.set_vec(items); } } } ================================================ FILE: cedinia/src/callbacks.rs ================================================ mod directories; mod misc; mod scan; mod selection; pub(crate) use directories::{build_dir_model, wire_directories}; pub(crate) use misc::{wire_cache_info, wire_collect_test, wire_language_change, wire_open_path, wire_open_url, wire_permission, wire_save_settings_now}; pub(crate) use scan::wire_scan; pub(crate) use selection::{DeleteEvent, get_model_for_tool, wire_selection}; ================================================ FILE: cedinia/src/common.rs ================================================ pub const STR_IDX_NAME: usize = 0; pub const STR_IDX_PATH: usize = 1; pub const STR_IDX_SIZE: usize = 2; pub const STR_IDX_MODIFIED: usize = 3; pub const STR_BASE_COUNT: usize = 4; pub const INT_IDX_MOD_HI: usize = 0; pub const INT_IDX_MOD_LO: usize = 1; pub const INT_IDX_SIZE_HI: usize = 2; pub const INT_IDX_SIZE_LO: usize = 3; pub const INT_BASE_COUNT: usize = 4; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum StrDataSimilarImages { DimsDisplay = STR_BASE_COUNT, } pub const MAX_STR_DATA_SIMILAR_IMAGES: usize = StrDataSimilarImages::DimsDisplay as usize + 1; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum IntDataSimilarImages { Width = INT_BASE_COUNT, Height, Diff, } pub const MAX_INT_DATA_SIMILAR_IMAGES: usize = IntDataSimilarImages::Diff as usize + 1; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum StrDataBrokenFiles { ErrorString = STR_BASE_COUNT, } pub const MAX_STR_DATA_BROKEN_FILES: usize = StrDataBrokenFiles::ErrorString as usize + 1; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum StrDataBadExtensions { Display = STR_BASE_COUNT, ProperExtension, } pub const MAX_STR_DATA_BAD_EXTENSIONS: usize = StrDataBadExtensions::ProperExtension as usize + 1; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum StrDataSameMusic { Display = STR_BASE_COUNT, Title, } pub const MAX_STR_DATA_SAME_MUSIC: usize = StrDataSameMusic::Title as usize + 1; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum StrDataBadNames { NewName = STR_BASE_COUNT, } pub const MAX_STR_DATA_BAD_NAMES: usize = StrDataBadNames::NewName as usize + 1; #[repr(usize)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum IntDataExifRemover { ExifTagCount = INT_BASE_COUNT, } pub const MAX_INT_DATA_EXIF_REMOVER: usize = IntDataExifRemover::ExifTagCount as usize + 1; ================================================ FILE: cedinia/src/file_picker_android.rs ================================================ use std::ffi::c_void; use std::sync::{Arc, Mutex}; use android_activity::AndroidApp; use jni::objects::{Global, JClass, JObject, JString, JValue}; use jni::sys::jboolean; use jni::{EnvUnowned, jni_sig, jni_str}; use slint::invoke_from_event_loop; static PENDING_PICK: std::sync::OnceLock>>> = std::sync::OnceLock::new(); fn pending_pick() -> &'static Arc>> { PENDING_PICK.get_or_init(|| Arc::new(Mutex::new(None))) } #[unsafe(no_mangle)] #[allow(non_snake_case)] unsafe extern "system" fn Java_CediniaFilePicker_onDirectoryPicked(mut unowned: EnvUnowned<'_>, _class: JClass<'_>, path: JString<'_>, is_include: jboolean) { log::info!("Java_CediniaFilePicker_onDirectoryPicked: entered, is_include={}", is_include); let outcome = unowned.with_env_no_catch(|env| path.try_to_string(env)); let path_str: String = match outcome.into_outcome() { jni::Outcome::Ok(s) => s, jni::Outcome::Err(e) => { log::error!("onDirectoryPicked: path read error: {:?}", e); return; } jni::Outcome::Panic(_) => { log::error!("onDirectoryPicked: panic reading path"); return; } }; if path_str.is_empty() { log::error!("onDirectoryPicked: empty path, aborting"); return; } log::info!("onDirectoryPicked: path='{}' include={}", path_str, is_include); if let Ok(mut guard) = pending_pick().lock() { *guard = Some((path_str.clone(), is_include)); } let pending = pending_pick().clone(); let dispatch_result = invoke_from_event_loop(move || match pending.lock().ok().and_then(|mut g| g.take()) { Some((p, inc)) => { log::info!("invoke_from_event_loop: on_directory_picked path='{}' inc={}", p, inc); crate::app::on_directory_picked(p, inc); } None => log::warn!("invoke_from_event_loop: PENDING_PICK was empty"), }); if let Err(e) = dispatch_result { log::error!("onDirectoryPicked: invoke_from_event_loop failed: {:?}", e); } } pub fn init(app: &AndroidApp) { log::info!("file_picker_android::init: starting"); APP_HANDLE.get_or_init(|| app.clone()); let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }; let dex_data: &'static [u8] = include_bytes!(concat!(env!("OUT_DIR"), "/classes.dex")); log::info!("file_picker_android::init: DEX size={}", dex_data.len()); vm.attach_current_thread(|env| -> jni::errors::Result<()> { log::info!("file_picker_android::init: JVM attached"); let dex_buffer = unsafe { env.new_direct_byte_buffer(dex_data.as_ptr() as *mut _, dex_data.len()) }?; let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) }; let parent_class_loader = env .call_method(&native_activity, jni_str!("getClassLoader"), jni_sig!(() -> java.lang.ClassLoader), &[])? .l()?; let dex_loader = match env.new_object( jni_str!("dalvik/system/InMemoryDexClassLoader"), jni_sig!((buf: java.nio.ByteBuffer, loader: java.lang.ClassLoader) -> void), &[JValue::Object(&dex_buffer), JValue::Object(&parent_class_loader)], ) { Ok(obj) => { log::info!("file_picker_android::init: using InMemoryDexClassLoader"); env.exception_clear(); obj } Err(e) => { log::info!("file_picker_android::init: InMemoryDexClassLoader failed ({:?}), trying DexClassLoader", e); env.exception_clear(); let code_cache_dir = env.call_method(&native_activity, jni_str!("getCodeCacheDir"), jni_sig!(() -> java.io.File), &[])?.l()?; let path_obj = env.call_method(&code_cache_dir, jni_str!("getAbsolutePath"), jni_sig!(() -> java.lang.String), &[])?.l()?; let j_path = unsafe { JString::from_raw(env, path_obj.as_raw()) }; let path_str = j_path.try_to_string(env)?; let dex_path = format!("{}/cedinia_picker.dex", path_str); std::fs::write(&dex_path, dex_data).expect("write dex"); let oats_path = format!("{}/oats", path_str); let _ = std::fs::create_dir(&oats_path); let j_dex = env.new_string(&dex_path)?; let j_oats = env.new_string(&oats_path)?; env.new_object( jni_str!("dalvik/system/DexClassLoader"), jni_sig!((dexPath: java.lang.String, optimizedDirectory: java.lang.String, librarySearchPath: java.lang.String, parent: java.lang.ClassLoader) -> void), &[ JValue::Object(&j_dex), JValue::Object(&j_oats), JValue::Object(&JObject::null()), JValue::Object(&parent_class_loader), ], )? } }; let class_name = env.new_string("CediniaFilePicker")?; let picker_class_obj = env .call_method( &dex_loader, jni_str!("findClass"), jni_sig!((name: java.lang.String) -> java.lang.Class), &[JValue::Object(&class_name)], )? .l()?; let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) }; let native_methods = [unsafe { jni::NativeMethod::from_raw_parts( jni_str!("onDirectoryPicked"), jni_str!("(Ljava/lang/String;Z)V"), Java_CediniaFilePicker_onDirectoryPicked as *mut c_void, ) }]; unsafe { env.register_native_methods(&picker_class, &native_methods) }?; log::info!("file_picker_android::init: native method registered"); let loader_global: Global> = env.new_global_ref(dex_loader)?; let _ = DEX_LOADER_REF.set(loader_global); log::info!("file_picker_android::init: complete"); Ok(()) }) .expect("init JNI attachment failed"); } static APP_HANDLE: std::sync::OnceLock = std::sync::OnceLock::new(); static DEX_LOADER_REF: std::sync::OnceLock>> = std::sync::OnceLock::new(); pub fn launch_pick_directory(is_include: bool) { log::info!("launch_pick_directory: is_include={}", is_include); let Some(app) = APP_HANDLE.get() else { log::error!("launch_pick_directory: AndroidApp not initialised"); return; }; let Some(loader_ref) = DEX_LOADER_REF.get() else { log::error!("launch_pick_directory: DEX loader not initialised"); return; }; let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }; vm.attach_current_thread(|env| -> jni::errors::Result<()> { let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) }; let class_name = env.new_string("CediniaFilePicker")?; let picker_class_obj = env .call_method( loader_ref.as_obj(), jni_str!("findClass"), jni_sig!((name: java.lang.String) -> java.lang.Class), &[JValue::Object(&class_name)], )? .l()?; let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) }; let method = if is_include { jni_str!("pickIncludeDirectory") } else { jni_str!("pickExcludeDirectory") }; log::info!("launch_pick_directory: calling Java CediniaFilePicker method"); env.call_static_method( &picker_class, method, jni_sig!((activity: android.app.Activity) -> void), &[JValue::Object(&native_activity)], )?; log::info!("launch_pick_directory: Java call succeeded"); Ok(()) }) .unwrap_or_else(|e| log::error!("launch_pick_directory: JNI failed: {:?}", e)); } pub fn setup_nav_bar() { let Some(app) = APP_HANDLE.get() else { log::error!("setup_nav_bar: AndroidApp not initialised"); return; }; let Some(loader_ref) = DEX_LOADER_REF.get() else { log::error!("setup_nav_bar: DEX loader not initialised"); return; }; let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }; vm.attach_current_thread(|env| -> jni::errors::Result<()> { let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) }; let class_name = env.new_string("CediniaFilePicker")?; let picker_class_obj = env .call_method( loader_ref.as_obj(), jni_str!("findClass"), jni_sig!((name: java.lang.String) -> java.lang.Class), &[JValue::Object(&class_name)], )? .l()?; let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) }; env.call_static_method( &picker_class, jni_str!("setupNavBar"), jni_sig!((activity: android.app.Activity) -> void), &[JValue::Object(&native_activity)], )?; log::info!("setup_nav_bar: Java call succeeded"); Ok(()) }) .unwrap_or_else(|e| log::error!("setup_nav_bar: JNI failed: {:?}", e)); } pub fn check_storage_permission() -> bool { let Some(app) = APP_HANDLE.get() else { return false }; let Some(loader_ref) = DEX_LOADER_REF.get() else { return false }; let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }; let mut result = false; vm.attach_current_thread(|env| -> jni::errors::Result<()> { let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) }; let class_name = env.new_string("CediniaFilePicker")?; let picker_class_obj = env .call_method( loader_ref.as_obj(), jni_str!("findClass"), jni_sig!((name: java.lang.String) -> java.lang.Class), &[JValue::Object(&class_name)], )? .l()?; let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) }; let val = env .call_static_method( &picker_class, jni_str!("hasStoragePermission"), jni_sig!((activity: android.app.Activity) -> boolean), &[JValue::Object(&native_activity)], )? .z()?; result = val; Ok(()) }) .unwrap_or_else(|e| log::error!("check_storage_permission: JNI failed: {:?}", e)); result } pub fn request_storage_permission() { let Some(app) = APP_HANDLE.get() else { return }; let Some(loader_ref) = DEX_LOADER_REF.get() else { return }; let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }; vm.attach_current_thread(|env| -> jni::errors::Result<()> { let native_activity = unsafe { JObject::from_raw(env, app.activity_as_ptr() as *mut _) }; let class_name = env.new_string("CediniaFilePicker")?; let picker_class_obj = env .call_method( loader_ref.as_obj(), jni_str!("findClass"), jni_sig!((name: java.lang.String) -> java.lang.Class), &[JValue::Object(&class_name)], )? .l()?; let picker_class: JClass = unsafe { JClass::from_raw(env, picker_class_obj.as_raw()) }; env.call_static_method( &picker_class, jni_str!("requestStoragePermission"), jni_sig!((activity: android.app.Activity) -> void), &[JValue::Object(&native_activity)], )?; Ok(()) }) .unwrap_or_else(|e| log::error!("request_storage_permission: JNI failed: {:?}", e)); } ================================================ FILE: cedinia/src/lib.rs ================================================ #![allow(clippy::unwrap_used)] #![allow(clippy::indexing_slicing)] #![allow(clippy::todo)] mod app; mod callbacks; pub mod common; #[cfg(target_os = "android")] mod file_picker_android; pub mod localizer_cedinia; mod model; mod scan_runner; mod scanners; mod set_initial_gui_infos; pub mod settings; mod thumbnail_loader; pub mod translations; mod volumes; slint::include_modules!(); pub use app::run_app; static ANDROID_FILES_PATH: std::sync::OnceLock = std::sync::OnceLock::new(); static ANDROID_CACHE_PATH: std::sync::OnceLock = std::sync::OnceLock::new(); pub fn android_files_path() -> Option<&'static str> { ANDROID_FILES_PATH.get().map(String::as_str) } pub fn android_cache_path() -> Option<&'static str> { ANDROID_CACHE_PATH.get().map(String::as_str) } #[cfg(target_os = "android")] fn setup_android_paths(android_app: &slint::android::AndroidApp) { use jni::objects::{JObject, JString}; use jni::{jni_sig, jni_str}; let vm = unsafe { jni::JavaVM::from_raw(android_app.vm_as_ptr() as *mut _) }; let _ = vm.attach_current_thread(|env| -> jni::errors::Result<()> { let activity_raw = unsafe { JObject::from_raw(env, android_app.activity_as_ptr() as *mut _) }; let files_dir: JObject = env.call_method(&activity_raw, jni_str!("getFilesDir"), jni_sig!(() -> java.io.File), &[])?.l()?; let files_path_obj = env.call_method(&files_dir, jni_str!("getAbsolutePath"), jni_sig!(() -> java.lang.String), &[])?.l()?; let files_path: JString = env.cast_local::(files_path_obj)?; let files_str: String = files_path.try_to_string(env)?; let cache_dir: JObject = env.call_method(&activity_raw, jni_str!("getCacheDir"), jni_sig!(() -> java.io.File), &[])?.l()?; let cache_path_obj = env.call_method(&cache_dir, jni_str!("getAbsolutePath"), jni_sig!(() -> java.lang.String), &[])?.l()?; let cache_path: JString = env.cast_local::(cache_path_obj)?; let cache_str: String = cache_path.try_to_string(env)?; let _ = ANDROID_FILES_PATH.set(files_str.clone()); let _ = ANDROID_CACHE_PATH.set(cache_str.clone()); unsafe { std::env::set_var("DATA_DIR", &files_str) }; eprintln!("setup_android_paths: config='{}' cache='{}'", files_str, cache_str); Ok(()) }); } #[cfg(target_os = "android")] #[unsafe(no_mangle)] fn android_main(android_app: slint::android::AndroidApp) { // Init logcat logging FIRST so every log::* call and every panic message // appears under `adb logcat -s cedinia`. Without this all output goes to // stdout/stderr which Android silently discards for native code. android_logger::init_once(android_logger::Config::default().with_max_level(log::LevelFilter::Debug).with_tag("cedinia")); setup_android_paths(&android_app); crate::app::setup_logger_cache(); log::info!("android_main: started"); let scale = android_app.config().density().unwrap_or(160) as f32 / 160.0; log::info!("android_main: display scale={:.2}", scale); log::info!("android_main: initialising file picker (JNI + DEX)"); file_picker_android::init(&android_app); log::info!("android_main: file picker initialised"); slint::android::init(android_app.clone()).expect("Failed to initialise Slint Android backend"); log::info!("android_main: Slint backend initialised"); file_picker_android::setup_nav_bar(); log::info!("android_main: nav bar pinned"); let rect = android_app.content_rect(); let inset_bottom_px = if rect.bottom > 0 { 0.0f32 } else { 48.0 * scale }; log::info!("android_main: content_rect={:?} inset_bottom={}", rect, inset_bottom_px); log::info!("android_main: launching app UI"); app::run_app_with_insets(inset_bottom_px, scale, android_app); log::info!("android_main: app UI returned (exiting)"); } ================================================ FILE: cedinia/src/localizer_cedinia.rs ================================================ use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader}; use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer}; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; pub static LANGUAGE_LOADER_CEDINIA: std::sync::LazyLock = std::sync::LazyLock::new(|| { let loader: FluentLanguageLoader = fluent_language_loader!(); loader.load_fallback_language(&Localizations).expect("Error while loading fallback language for cedinia"); loader }); #[macro_export] macro_rules! flc { ( $($tt:tt)* ) => {{ i18n_embed_fl::fl!($crate::localizer_cedinia::LANGUAGE_LOADER_CEDINIA, $($tt)*) }}; } pub(crate) fn localizer_cedinia() -> Box { Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_CEDINIA, &Localizations)) } /// Returns 1 for Polish, 0 for English – determined by the OS locale. /// Used to pick the default UI index when no explicit language has been saved. pub(crate) fn detect_os_language_idx() -> i32 { #[cfg(not(target_os = "android"))] { let requested = i18n_embed::DesktopLanguageRequester::requested_languages(); if requested.iter().any(|l| l.language.as_str() == "pl") { return 1; } } 0 } /// Load the given language preference. "auto" uses the OS locale; "pl"/"en" forces a specific /// language. Call this before `translate_items`. pub(crate) fn apply_language_preference(lang: &str) { let localizer = localizer_cedinia(); match lang { "pl" | "en" => { if let Ok(lang_id) = lang.parse::() { let _ = localizer.select(&[lang_id]); } } _ => { // "auto" – use the OS-requested languages on desktop #[cfg(not(target_os = "android"))] { let requested = i18n_embed::DesktopLanguageRequester::requested_languages(); let _ = localizer.select(&requested); } } } } ================================================ FILE: cedinia/src/model.rs ================================================ use slint::{Model, ModelRc, SharedString, VecModel}; use crate::FileEntry; use crate::scan_runner::FileItem; pub fn make_file_model(items: Vec) -> ModelRc { let entries: Vec = items .into_iter() .map(|item| { let val_str: Vec = item.val_str.into_iter().map(SharedString::from).collect(); let val_int: Vec = item.val_int; FileEntry { checked: false, is_header: item.is_header, val_str: ModelRc::new(VecModel::from(val_str)), val_int: ModelRc::new(VecModel::from(val_int)), } }) .collect(); ModelRc::new(VecModel::from(entries)) } pub fn toggle_row(model: &ModelRc, index: usize) { if let Some(vm) = model.as_any().downcast_ref::>() { let mut items: Vec = vm.iter().collect::>(); if let Some(entry) = items.get_mut(index) && !entry.is_header { entry.checked = !entry.checked; } vm.set_vec(items); } } pub fn count_checked(model: &ModelRc) -> i32 { model .as_any() .downcast_ref::>() .map_or(0, |vm| vm.iter().filter(|e: &FileEntry| e.checked).count() as i32) } ================================================ FILE: cedinia/src/scan_runner.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::thread; use crossbeam_channel::{Receiver, Sender, unbounded}; use czkawka_core::common::model::{CheckingMethod, HashType}; use czkawka_core::common::progress_data::{CurrentStage, ProgressData as CoreProgress}; use czkawka_core::common::tool_data::CommonData; use czkawka_core::re_exported::{FilterType, HashAlg}; use czkawka_core::tools::big_file::SearchMode; use czkawka_core::tools::similar_images::SimilarityPreset; use crate::scanners::{ 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, scan_similar_images, scan_temporary_files, }; #[derive(Debug, Clone)] pub struct FileItem { pub is_header: bool, pub val_str: Vec, pub val_int: Vec, } #[derive(Debug, Clone, Default)] pub struct CommonFilters { pub excluded_items: Vec, pub allowed_extensions: Vec, pub excluded_extensions: Vec, pub min_file_size_bytes: u64, pub max_file_size_bytes: Option, } #[derive(Debug)] pub enum ScanRequest { DuplicateFiles { dirs: Vec, check_method: CheckingMethod, hash_type: HashType, use_cache: bool, filters: CommonFilters, }, EmptyFolders { dirs: Vec, filters: CommonFilters, }, SimilarImages { dirs: Vec, similarity_preset: SimilarityPreset, hash_size: u8, hash_alg: HashAlg, image_filter: FilterType, ignore_same_size: bool, filters: CommonFilters, }, EmptyFiles { dirs: Vec, filters: CommonFilters, }, TemporaryFiles { dirs: Vec, filters: CommonFilters, }, BigFiles { dirs: Vec, search_mode: SearchMode, count: usize, filters: CommonFilters, }, BrokenFiles { dirs: Vec, checked_types: u32, filters: CommonFilters, }, BadExtensions { dirs: Vec, filters: CommonFilters, }, SameMusic { dirs: Vec, music_similarity: u32, approximate: bool, check_method: CheckingMethod, filters: CommonFilters, }, BadNames { dirs: Vec, filters: CommonFilters, uppercase_extension: bool, emoji_used: bool, space_at_start_or_end: bool, non_ascii_graphical: bool, remove_duplicated_non_alpha: bool, }, ExifRemover { dirs: Vec, filters: CommonFilters, }, Stop, } #[derive(Debug, Clone)] pub struct ProgressUpdate { pub step_name: String, pub current: i32, pub all: i32, pub is_indeterminate: bool, pub scan_id: u32, } #[derive(Debug)] pub enum ScanResult { Progress(ProgressUpdate), DuplicateFiles(Vec), EmptyFolders(Vec), SimilarImages(Vec), EmptyFiles(Vec), TemporaryFiles(Vec), BigFiles(Vec), BrokenFiles(Vec), BadExtensions(Vec), SameMusic(Vec), BadNames(Vec), ExifRemover(Vec), Finished(u32), } pub trait ScanResultHandler: Send + Sync + 'static { fn on_result(&self, result: ScanResult); } pub fn start_worker(handler: H) -> (Sender, Arc) { let stop_flag = Arc::new(AtomicBool::new(false)); let (req_tx, req_rx) = unbounded::(); thread::Builder::new() .name("cedinia-scanner".into()) .spawn({ let stop_flag = Arc::clone(&stop_flag); move || worker_loop(&req_rx, handler, &stop_flag) }) .expect("Failed to spawn scanner thread"); (req_tx, stop_flag) } fn worker_loop(req_rx: &Receiver, handler: H, stop_flag: &Arc) { use std::sync::atomic::Ordering; let mut scan_id: u32 = 0; let handler = Arc::new(handler); while let Ok(req) = req_rx.recv() { match req { ScanRequest::Stop => { stop_flag.store(true, Ordering::Relaxed); } ScanRequest::DuplicateFiles { dirs, check_method, hash_type, use_cache, filters, } => { scan_id += 1; let items = scan_duplicate_files(dirs, check_method, hash_type, use_cache, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::DuplicateFiles(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::EmptyFolders { dirs, filters } => { scan_id += 1; let items = scan_empty_folders(dirs, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::EmptyFolders(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::SimilarImages { dirs, similarity_preset, hash_size, hash_alg, image_filter, ignore_same_size, filters, } => { scan_id += 1; let items = scan_similar_images( dirs, similarity_preset, hash_size, hash_alg, image_filter, ignore_same_size, &filters, stop_flag, &handler, scan_id, ); handler.on_result(ScanResult::SimilarImages(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::EmptyFiles { dirs, filters } => { scan_id += 1; let items = scan_empty_files(dirs, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::EmptyFiles(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::TemporaryFiles { dirs, filters } => { scan_id += 1; let items = scan_temporary_files(dirs, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::TemporaryFiles(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::BigFiles { dirs, search_mode, count, filters, } => { scan_id += 1; let items = scan_big_files(dirs, search_mode, count, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::BigFiles(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::BrokenFiles { dirs, checked_types, filters } => { scan_id += 1; let items = scan_broken_files(dirs, checked_types, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::BrokenFiles(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::BadExtensions { dirs, filters } => { scan_id += 1; let items = scan_bad_extensions(dirs, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::BadExtensions(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::SameMusic { dirs, music_similarity, approximate, check_method, filters, } => { scan_id += 1; let items = scan_same_music(dirs, music_similarity, approximate, check_method, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::SameMusic(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::BadNames { dirs, filters, uppercase_extension, emoji_used, space_at_start_or_end, non_ascii_graphical, remove_duplicated_non_alpha, } => { scan_id += 1; let items = scan_bad_names( dirs, &filters, stop_flag, &handler, scan_id, uppercase_extension, emoji_used, space_at_start_or_end, non_ascii_graphical, remove_duplicated_non_alpha, ); handler.on_result(ScanResult::BadNames(items)); handler.on_result(ScanResult::Finished(scan_id)); } ScanRequest::ExifRemover { dirs, filters } => { scan_id += 1; let items = scan_exif_remover(dirs, &filters, stop_flag, &handler, scan_id); handler.on_result(ScanResult::ExifRemover(items)); handler.on_result(ScanResult::Finished(scan_id)); } } } } fn stage_uses_bytes(stage: CurrentStage) -> bool { matches!( stage, CurrentStage::DuplicatePreHashing | CurrentStage::DuplicateFullHashing | CurrentStage::SimilarImagesCalculatingHashes | CurrentStage::SameMusicCalculatingFingerprints ) } fn stage_label(stage: CurrentStage) -> &'static str { match stage { CurrentStage::CollectingFiles => "Zbieranie plików", CurrentStage::DuplicateScanningName => "Skanowanie po nazwie", CurrentStage::DuplicateScanningSizeName => "Skanowanie po nazwie i rozmiarze", CurrentStage::DuplicateScanningSize => "Skanowanie po rozmiarze", CurrentStage::DuplicatePreHashing => "Pre-hash", CurrentStage::DuplicateFullHashing => "Haszowanie", CurrentStage::DuplicateCacheLoading | CurrentStage::DuplicatePreHashCacheLoading | CurrentStage::SameMusicCacheLoadingTags | CurrentStage::SameMusicCacheLoadingFingerprints | CurrentStage::ExifRemoverCacheLoading => "Ładowanie cache", CurrentStage::DuplicateCacheSaving | CurrentStage::DuplicatePreHashCacheSaving | CurrentStage::SameMusicCacheSavingTags | CurrentStage::SameMusicCacheSavingFingerprints | CurrentStage::ExifRemoverCacheSaving => "Zapisywanie cache", CurrentStage::SimilarImagesCalculatingHashes => "Obliczanie hashy obrazów", CurrentStage::SimilarImagesComparingHashes => "Porównywanie obrazów", CurrentStage::SimilarVideosCalculatingHashes => "Obliczanie hashy wideo", CurrentStage::BrokenFilesChecking => "Sprawdzanie plików", CurrentStage::BadExtensionsChecking => "Sprawdzanie rozszerzeń", CurrentStage::BadNamesChecking => "Sprawdzanie nazw", CurrentStage::SameMusicReadingTags => "Odczyt tagów muzycznych", CurrentStage::SameMusicComparingTags => "Porównywanie tagów", CurrentStage::SameMusicCalculatingFingerprints => "Obliczanie odcisków muzycznych", CurrentStage::SameMusicComparingFingerprints => "Porównywanie odcisków muzycznych", CurrentStage::ExifRemoverExtractingTags => "Odczyt tagów EXIF", CurrentStage::VideoOptimizerCreatingThumbnails | CurrentStage::SimilarVideosCreatingThumbnails => "Tworzenie miniatur wideo", CurrentStage::VideoOptimizerProcessingVideos => "Przetwarzanie wideo", CurrentStage::DeletingFiles => "Usuwanie plików", CurrentStage::RenamingFiles => "Zmiana nazw plików", CurrentStage::MovingFiles => "Przenoszenie plików", CurrentStage::HardlinkingFiles => "Tworzenie hardlinków", CurrentStage::SymlinkingFiles => "Tworzenie dowiązań", CurrentStage::OptimizingVideos => "Optymalizacja wideo", CurrentStage::CleaningExif => "Czyszczenie EXIF", } } fn stage_label_full(pd: &CoreProgress) -> String { let base = stage_label(pd.sstage); let label = if stage_uses_bytes(pd.sstage) && pd.bytes_to_check > 0 { format!("{base} ({} / {})", fmt_size(pd.bytes_checked), fmt_size(pd.bytes_to_check)) } else { base.to_string() }; if pd.max_stage_idx > 0 { format!("{}/{}\u{2002}{label}", pd.current_stage_idx + 1, pd.max_stage_idx + 1) } else { label } } pub(crate) fn apply_filters(tool: &mut T, filters: &CommonFilters) { if !filters.excluded_items.is_empty() { tool.set_excluded_items(filters.excluded_items.clone()); } if !filters.allowed_extensions.is_empty() { tool.set_allowed_extensions(filters.allowed_extensions.clone()); } if !filters.excluded_extensions.is_empty() { tool.set_excluded_extensions(filters.excluded_extensions.clone()); } if filters.min_file_size_bytes > 0 { tool.set_minimal_file_size(filters.min_file_size_bytes); } if let Some(max) = filters.max_file_size_bytes { tool.set_maximal_file_size(max); } } pub(crate) fn spawn_progress_forwarder(handler: Arc, scan_id: u32) -> (Sender, thread::JoinHandle<()>) { let (ptx, prx) = unbounded::(); let handle = thread::spawn(move || { while let Ok(pd) = prx.recv() { let is_indeterminate = pd.sstage.check_if_loading_saving_cache(); let update = ProgressUpdate { step_name: stage_label_full(&pd), current: pd.entries_checked as i32, all: pd.entries_to_check as i32, is_indeterminate, scan_id, }; handler.on_result(ScanResult::Progress(update)); } }); (ptx, handle) } pub(crate) fn fmt_size(bytes: u64) -> String { humansize::format_size(bytes, humansize::BINARY) } pub(crate) fn fmt_date(unix_secs: u64) -> String { let secs = unix_secs; let mins = secs / 60; let hours = mins / 60; let days = hours / 24; let min = mins % 60; let hour = hours % 24; let mut remaining_days = days; let mut year = 1970u64; loop { let days_in_year = if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) { 366 } else { 365 }; if remaining_days < days_in_year { break; } remaining_days -= days_in_year; year += 1; } let leap = year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)); let months_days: [u64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let mut month = 1u64; for &md in &months_days { if remaining_days < md { break; } remaining_days -= md; month += 1; } let day = remaining_days + 1; format!("{year}-{month:02}-{day:02} {hour:02}:{min:02}") } pub(crate) fn size_to_hi_lo(size: u64) -> (i32, i32) { let hi = (size >> 32) as i32; let lo = (size & 0xFFFF_FFFF) as i32; (hi, lo) } pub(crate) fn file_name(p: &std::path::Path) -> String { p.file_name().unwrap_or_default().to_string_lossy().to_string() } pub(crate) fn parent_str(p: &std::path::Path) -> String { p.parent().map(|x| x.to_string_lossy().to_string()).unwrap_or_default() } ================================================ FILE: cedinia/src/scanners.rs ================================================ use std::cmp::Reverse; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use czkawka_core::common::tool_data::CommonData; use czkawka_core::common::traits::Search; use crate::common::{ 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, MAX_STR_DATA_BROKEN_FILES, MAX_STR_DATA_SAME_MUSIC, MAX_STR_DATA_SIMILAR_IMAGES, STR_BASE_COUNT, STR_IDX_NAME, STR_IDX_PATH, }; use crate::scan_runner::{CommonFilters, FileItem, ScanResultHandler, apply_filters, file_name, fmt_date, fmt_size, parent_str, size_to_hi_lo, spawn_progress_forwarder}; fn base_item(is_header: bool, name: String, path: String, size_str: String, modified_str: String, mod_secs: u64, size_bytes: u64) -> FileItem { let (mod_hi, mod_lo) = size_to_hi_lo(mod_secs); let (size_hi, size_lo) = size_to_hi_lo(size_bytes); let val_str: [String; STR_BASE_COUNT] = [name, path, size_str, modified_str]; let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, size_hi, size_lo]; FileItem { is_header, val_str: val_str.into(), val_int: val_int.into(), } } fn header_item(label: String) -> FileItem { let val_str: [String; STR_BASE_COUNT] = [label, String::new(), String::new(), String::new()]; let val_int: [i32; INT_BASE_COUNT] = [0, 0, 0, 0]; FileItem { is_header: true, val_str: val_str.into(), val_int: val_int.into(), } } fn item_name(item: &FileItem) -> &str { &item.val_str[STR_IDX_NAME] } fn item_path(item: &FileItem) -> &str { &item.val_str[STR_IDX_PATH] } fn item_size_u64(item: &FileItem) -> u64 { let hi = item.val_int[INT_IDX_SIZE_HI] as u64; let lo = item.val_int[INT_IDX_SIZE_LO] as u64; (hi << 32) | (lo & 0xFFFF_FFFF) } pub(crate) fn scan_duplicate_files( dirs: Vec, check_method: czkawka_core::common::model::CheckingMethod, hash_type: czkawka_core::common::model::HashType, use_cache: bool, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32, ) -> Vec { use czkawka_core::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = DuplicateFinderParameters::new(check_method, hash_type, use_cache, 8 * 1024, 0, false); let mut tool = DuplicateFinder::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = Vec::new(); for groups in tool.get_files_sorted_by_hash().values().rev() { let mut sorted_groups: Vec<&Vec<_>> = groups.iter().collect(); sorted_groups.sort_by_key(|g| Reverse(g.len())); for group in sorted_groups { if group.len() < 2 { continue; } let file_size = group[0].size; let total = fmt_size(file_size * group.len() as u64); let per = fmt_size(file_size); items.push(header_item(format!("{} pliki \u{00d7} {} / plik = {} \u{0142}\u{0105}cznie", group.len(), per, total))); for fe in group { items.push(base_item( false, file_name(&fe.path), parent_str(&fe.path), fmt_size(fe.size), fmt_date(fe.modified_date), fe.modified_date, fe.size, )); } } } items } pub(crate) fn scan_empty_folders(dirs: Vec, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32) -> Vec { use czkawka_core::tools::empty_folder::EmptyFolder; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let mut tool = EmptyFolder::new(); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = tool .get_empty_folder_list() .values() .map(|fe| { base_item( false, file_name(&fe.path), parent_str(&fe.path), String::new(), fmt_date(fe.modified_date), fe.modified_date, 0, ) }) .collect(); items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b)))); items } pub(crate) fn scan_similar_images( dirs: Vec, similarity_preset: czkawka_core::tools::similar_images::SimilarityPreset, hash_size: u8, hash_alg: czkawka_core::re_exported::HashAlg, image_filter: czkawka_core::re_exported::FilterType, ignore_same_size: bool, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32, ) -> Vec { use czkawka_core::tools::similar_images::{SimilarImages, SimilarImagesParameters, return_similarity_from_similarity_preset}; let max_diff = return_similarity_from_similarity_preset(similarity_preset, hash_size); let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = SimilarImagesParameters::new(max_diff, hash_size, hash_alg, image_filter, ignore_same_size); let mut tool = SimilarImages::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let raw_groups: &Vec> = tool.get_similar_images(); let mut groups_with_size: Vec<(&Vec<_>, u64)> = raw_groups .iter() .filter(|g| g.len() >= 2) .map(|g| { let total: u64 = g.iter().map(|img| img.size).sum(); (g, total) }) .collect(); groups_with_size.sort_by_key(|&(_, total)| Reverse(total)); let mut items: Vec = Vec::new(); for (group, _) in groups_with_size { items.push(header_item(format!("{} podobnych obraz\u{00f3}w", group.len()))); for img in group { let dims = format!("{}\u{00d7}{} \u{0394}{}", img.width, img.height, img.difference); let (mod_hi, mod_lo) = size_to_hi_lo(img.modified_date); let (size_hi, size_lo) = size_to_hi_lo(img.size); 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]; 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]; items.push(FileItem { is_header: false, val_str: val_str.into(), val_int: val_int.into(), }); } } items } pub(crate) fn scan_empty_files(dirs: Vec, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32) -> Vec { use czkawka_core::tools::empty_files::EmptyFiles; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let mut tool = EmptyFiles::new(); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = tool .get_empty_files() .iter() .map(|fe| { base_item( false, file_name(&fe.path), parent_str(&fe.path), fmt_size(fe.size), fmt_date(fe.modified_date), fe.modified_date, fe.size, ) }) .collect(); items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b)))); items } pub(crate) fn scan_temporary_files(dirs: Vec, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32) -> Vec { use czkawka_core::tools::temporary::Temporary; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let mut tool = Temporary::new(); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = tool .get_temporary_files() .iter() .map(|fe| { base_item( false, file_name(&fe.path), parent_str(&fe.path), fmt_size(fe.size), fmt_date(fe.modified_date), fe.modified_date, fe.size, ) }) .collect(); items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b)))); items } pub(crate) fn scan_big_files( dirs: Vec, search_mode: czkawka_core::tools::big_file::SearchMode, count: usize, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32, ) -> Vec { use czkawka_core::tools::big_file::{BigFile, BigFileParameters}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = BigFileParameters::new(count, search_mode); let mut tool = BigFile::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); tool.get_big_files() .iter() .map(|fe| { base_item( false, file_name(&fe.path), parent_str(&fe.path), fmt_size(fe.size), fmt_date(fe.modified_date), fe.modified_date, fe.size, ) }) .collect() } pub(crate) fn scan_broken_files( dirs: Vec, checked_types: u32, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32, ) -> Vec { use czkawka_core::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = BrokenFilesParameters::new(CheckedTypes::from_bits_truncate(checked_types)); let mut tool = BrokenFiles::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = tool .get_broken_files() .iter() .map(|be| { let (mod_hi, mod_lo) = size_to_hi_lo(be.modified_date); let val_str: [String; MAX_STR_DATA_BROKEN_FILES] = [ file_name(&be.path), parent_str(&be.path), fmt_size(be.size), fmt_date(be.modified_date), be.error_string.clone(), ]; let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, 0, 0]; FileItem { is_header: false, val_str: val_str.into(), val_int: val_int.into(), } }) .collect(); items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b)))); items } pub(crate) fn scan_bad_extensions(dirs: Vec, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32) -> Vec { use czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsParameters}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = BadExtensionsParameters::new(); let mut tool = BadExtensions::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = tool .get_bad_extensions_files() .iter() .map(|be| { let (mod_hi, mod_lo) = size_to_hi_lo(be.modified_date); let (size_hi, size_lo) = size_to_hi_lo(be.size); let val_str: [String; MAX_STR_DATA_BAD_EXTENSIONS] = [ file_name(&be.path), parent_str(&be.path), fmt_size(be.size), fmt_date(be.modified_date), format!(".{} \u{2192} .{}", be.current_extension, be.proper_extension), be.proper_extension.clone(), ]; let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, size_hi, size_lo]; FileItem { is_header: false, val_str: val_str.into(), val_int: val_int.into(), } }) .collect(); items.sort_by(|a, b| { item_size_u64(b) .cmp(&item_size_u64(a)) .then(item_path(a).cmp(item_path(b))) .then(item_name(a).cmp(item_name(b))) }); items } pub(crate) fn scan_bad_names( dirs: Vec, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32, uppercase_extension: bool, emoji_used: bool, space_at_start_or_end: bool, non_ascii_graphical: bool, remove_duplicated_non_alpha: bool, ) -> Vec { use czkawka_core::tools::bad_names::{BadNames, BadNamesParameters, NameIssues}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = BadNamesParameters::new(NameIssues { uppercase_extension, emoji_used, space_at_start_or_end, non_ascii_graphical, restricted_charset_allowed: if non_ascii_graphical { Some(vec!['_', '-', ' ', '.']) } else { None }, remove_duplicated_non_alphanumeric: remove_duplicated_non_alpha, }); let mut tool = BadNames::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec = tool .get_bad_names_files() .iter() .map(|bn| { let (mod_hi, mod_lo) = size_to_hi_lo(bn.modified_date); let val_str: [String; MAX_STR_DATA_BAD_NAMES] = [ file_name(&bn.path), parent_str(&bn.path), fmt_size(bn.size), fmt_date(bn.modified_date), bn.new_name.clone(), ]; let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, 0, 0]; FileItem { is_header: false, val_str: val_str.into(), val_int: val_int.into(), } }) .collect(); items.sort_by(|a, b| item_path(a).cmp(item_path(b)).then(item_name(a).cmp(item_name(b)))); items } pub(crate) fn scan_exif_remover(dirs: Vec, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32) -> Vec { use czkawka_core::tools::exif_remover::{ExifRemover, ExifRemoverParameters}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = ExifRemoverParameters::new(vec![]); let mut tool = ExifRemover::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let mut items: Vec<(u64, FileItem)> = tool .get_exif_files() .iter() .map(|ee| { let (mod_hi, mod_lo) = size_to_hi_lo(ee.modified_date); let (size_hi, size_lo) = size_to_hi_lo(ee.size); let val_str: [String; STR_BASE_COUNT] = [file_name(&ee.path), parent_str(&ee.path), fmt_size(ee.size), fmt_date(ee.modified_date)]; let val_int: [i32; MAX_INT_DATA_EXIF_REMOVER] = [mod_hi, mod_lo, size_hi, size_lo, ee.exif_tags.len() as i32]; ( ee.size, FileItem { is_header: false, val_str: val_str.into(), val_int: val_int.into(), }, ) }) .collect(); items.sort_by_key(|(size, _)| std::cmp::Reverse(*size)); items.into_iter().map(|(_, item)| item).collect() } pub(crate) fn scan_same_music( dirs: Vec, music_similarity: u32, approximate: bool, check_method: czkawka_core::common::model::CheckingMethod, filters: &CommonFilters, stop: &Arc, handler: &Arc, scan_id: u32, ) -> Vec { use czkawka_core::tools::same_music::{MusicSimilarity, SameMusic, SameMusicParameters}; let (ptx, fwd) = spawn_progress_forwarder(Arc::clone(handler), scan_id); let params = SameMusicParameters::new(MusicSimilarity::from_bits_truncate(music_similarity), approximate, check_method, 0.0, 0.0, false); let mut tool = SameMusic::new(params); tool.set_included_paths(dirs); apply_filters(&mut tool, filters); tool.set_recursive_search(true); tool.search(stop, Some(&ptx)); drop(ptx); fwd.join().expect("Failed to join progress forwarder thread"); let raw_groups = tool.get_duplicated_music_entries(); let mut groups_with_size: Vec<(&Vec<_>, u64)> = raw_groups .iter() .filter(|g| g.len() >= 2) .map(|g| { let total: u64 = g.iter().map(|me| me.size).sum(); (g, total) }) .collect(); groups_with_size.sort_by_key(|&(_, total)| Reverse(total)); let mut items: Vec = Vec::new(); for (group, _) in groups_with_size { items.push(header_item(format!("{} podobnych utw\u{00f3}r\u{00f3}w", group.len()))); for me in group { let artist = if me.track_artist.is_empty() { "?" } else { &me.track_artist }; let title = if me.track_title.is_empty() { "?" } else { &me.track_title }; let (mod_hi, mod_lo) = size_to_hi_lo(me.modified_date); let (size_hi, size_lo) = size_to_hi_lo(me.size); let val_str: [String; MAX_STR_DATA_SAME_MUSIC] = [ file_name(&me.path), parent_str(&me.path), fmt_size(me.size), fmt_date(me.modified_date), format!("{artist} \u{2013} {title}"), title.to_string(), ]; let val_int: [i32; INT_BASE_COUNT] = [mod_hi, mod_lo, size_hi, size_lo]; items.push(FileItem { is_header: false, val_str: val_str.into(), val_int: val_int.into(), }); } } items } ================================================ FILE: cedinia/src/set_initial_gui_infos.rs ================================================ use slint::{ComponentHandle, Model, SharedString}; use crate::settings::gui_settings_values::StringComboBoxItems; use crate::{BigFilesSettings, DuplicateSettings, GeneralSettings, MainWindow, SameMusicSettings, SimilarImagesSettings}; pub(crate) fn set_initial_gui_infos(app: &MainWindow) { let items = StringComboBoxItems::new(); fn display_names(items: &[crate::settings::gui_settings_values::StringComboBoxItem]) -> Vec { items.iter().map(|e| SharedString::from(e.display_name.as_str())).collect() } let general = app.global::(); let dup = app.global::(); let si = app.global::(); let bf = app.global::(); let sm = app.global::(); let slint_vec = |model: slint::ModelRc| model.iter().collect::>(); assert_eq!( slint_vec(general.get_min_file_size_options()), display_names(&items.min_file_size), "GeneralSettings.min_file_size_options out of sync with Rust" ); assert_eq!( slint_vec(dup.get_check_method_options()), display_names(&items.duplicates_check_method), "DuplicateSettings.check_method_options out of sync with Rust" ); assert_eq!( slint_vec(dup.get_hash_type_options()), display_names(&items.duplicates_hash_type), "DuplicateSettings.hash_type_options out of sync with Rust" ); assert_eq!( slint_vec(si.get_similarity_preset_options()), display_names(&items.similarity_preset), "SimilarImagesSettings.similarity_preset_options out of sync with Rust" ); assert_eq!( slint_vec(si.get_hash_size_options()), display_names(&items.hash_size), "SimilarImagesSettings.hash_size_options out of sync with Rust" ); assert_eq!( slint_vec(si.get_hash_alg_options()), display_names(&items.hash_alg), "SimilarImagesSettings.hash_alg_options out of sync with Rust" ); assert_eq!( slint_vec(si.get_image_filter_options()), display_names(&items.image_filter), "SimilarImagesSettings.image_filter_options out of sync with Rust" ); assert_eq!( slint_vec(bf.get_search_mode_options()), display_names(&items.biggest_files_method), "BigFilesSettings.search_mode_options out of sync with Rust" ); assert_eq!( slint_vec(bf.get_count_options()), display_names(&items.big_files_count), "BigFilesSettings.count_options out of sync with Rust" ); assert_eq!( slint_vec(sm.get_check_method_options()), display_names(&items.same_music_check_method), "SameMusicSettings.check_method_options out of sync with Rust" ); } ================================================ FILE: cedinia/src/settings/gui_settings_values.rs ================================================ use std::fmt::Debug; use czkawka_core::common::model::{CheckingMethod, HashType}; use czkawka_core::re_exported::{FilterType, HashAlg}; use czkawka_core::tools::big_file::SearchMode; use czkawka_core::tools::similar_images::SimilarityPreset; use log::warn; #[derive(Debug, Clone)] pub struct StringComboBoxItem where T: Clone + Debug, { pub config_name: String, pub display_name: String, pub value: T, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MinFileSize { None, OneKb, EightKb, SixtyFourKb, OneMb, } impl MinFileSize { pub fn to_bytes(self) -> u64 { match self { Self::None => 0, Self::OneKb => 1_024, Self::EightKb => 8 * 1_024, Self::SixtyFourKb => 64 * 1_024, Self::OneMb => 1_024 * 1_024, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MaxFileSize { SixteenKb, OneMb, TenMb, HundredMb, Unlimited, } impl MaxFileSize { /// Returns `None` for Unlimited (no limit imposed). pub fn to_bytes(self) -> Option { match self { Self::SixteenKb => Some(16 * 1_024), Self::OneMb => Some(1_024 * 1_024), Self::TenMb => Some(10 * 1_024 * 1_024), Self::HundredMb => Some(100 * 1_024 * 1_024), Self::Unlimited => None, } } } pub struct StringComboBoxItems { pub min_file_size: Vec>, pub max_file_size: Vec>, pub duplicates_check_method: Vec>, pub duplicates_hash_type: Vec>, pub hash_size: Vec>, pub biggest_files_method: Vec>, pub big_files_count: Vec>, pub similarity_preset: Vec>, pub hash_alg: Vec>, pub image_filter: Vec>, pub same_music_check_method: Vec>, } impl Default for StringComboBoxItems { fn default() -> Self { Self::new() } } impl StringComboBoxItems { pub fn new() -> Self { let min_file_size = Self::convert(&[ ("none", "Brak", MinFileSize::None), ("1kb", "1 KB", MinFileSize::OneKb), ("8kb", "8 KB", MinFileSize::EightKb), ("64kb", "64 KB", MinFileSize::SixtyFourKb), ("1mb", "1 MB", MinFileSize::OneMb), ]); let max_file_size = Self::convert(&[ ("16kb", "16 KB", MaxFileSize::SixteenKb), ("1mb", "1 MB", MaxFileSize::OneMb), ("10mb", "10 MB", MaxFileSize::TenMb), ("100mb", "100 MB", MaxFileSize::HundredMb), ("unlimited", "Bez limitu", MaxFileSize::Unlimited), ]); let duplicates_check_method = Self::convert(&[ ("hash", "Hash", CheckingMethod::Hash), ("name", "Nazwa", CheckingMethod::Name), ("size_and_name", "Rozm+Naz", CheckingMethod::SizeName), ("size", "Rozmiar", CheckingMethod::Size), ]); let duplicates_hash_type = Self::convert(&[ ("blake3", "Blake3", HashType::Blake3), ("crc32", "CRC32", HashType::Crc32), ("xxh3", "XXH3", HashType::Xxh3), ]); let hash_size = Self::convert(&[("8", "8", 8u8), ("16", "16", 16), ("32", "32", 32), ("64", "64", 64)]); let biggest_files_method = Self::convert(&[("biggest", "Największe", SearchMode::BiggestFiles), ("smallest", "Najmniejsze", SearchMode::SmallestFiles)]); let big_files_count = Self::convert(&[("5", "5", 5usize), ("50", "50", 50), ("500", "500", 500), ("5000", "5000", 5000)]); let similarity_preset = Self::convert(&[ ("very_high", "B.Wys.", SimilarityPreset::VeryHigh), ("high", "Wysoki", SimilarityPreset::High), ("medium", "Średni", SimilarityPreset::Medium), ("small", "Niski", SimilarityPreset::Small), ("very_small", "B.Niski", SimilarityPreset::VerySmall), ("minimal", "Min.", SimilarityPreset::Minimal), ]); let hash_alg = Self::convert(&[ ("mean", "Mean", HashAlg::Mean), ("gradient", "Gradient", HashAlg::Gradient), ("double_gradient", "D.Grad.", HashAlg::DoubleGradient), ("vert_gradient", "V.Grad.", HashAlg::VertGradient), ("median", "Median", HashAlg::Median), ("blockhash", "Blockhash", HashAlg::Blockhash), ]); let image_filter = Self::convert(&[ ("nearest", "Nearest", FilterType::Nearest), ("triangle", "Triangle", FilterType::Triangle), ("catmull_rom", "CatmullRom", FilterType::CatmullRom), ("gaussian", "Gaussian", FilterType::Gaussian), ("lanczos3", "Lanczos3", FilterType::Lanczos3), ]); let same_music_check_method = Self::convert(&[("tags", "Tagi", CheckingMethod::AudioTags), ("audio", "Audio", CheckingMethod::AudioContent)]); Self { min_file_size, max_file_size, duplicates_check_method, duplicates_hash_type, hash_size, biggest_files_method, big_files_count, similarity_preset, hash_alg, image_filter, same_music_check_method, } } fn convert(input: &[(&str, &str, T)]) -> Vec> where T: Clone + Debug, { input .iter() .map(|(config_name, display_name, value)| StringComboBoxItem { config_name: config_name.to_string(), display_name: display_name.to_string(), value: value.clone(), }) .collect() } pub fn idx_from_config_name(config_name: &str, items: &[StringComboBoxItem]) -> usize { items.iter().position(|e| e.config_name == config_name).unwrap_or_else(|| { warn!("Unknown config_name \"{config_name}\" in {items:?}, falling back to index 0"); 0 }) } /// Look up enum value by UI index. Use instead of `value_from_config_name` when only the /// SegmentRow idx is available (the `_value` string property may be stale). pub fn value_from_idx(items: &[StringComboBoxItem], idx: i32, default: T) -> T { items.get(idx as usize).map_or_else( || { warn!("idx {idx} out of range in {items:?}, using default"); default }, |e| e.value.clone(), ) } /// Look up the config_name string by UI index. Use in `collect_settings_from_gui`. pub fn config_name_from_idx(items: &[StringComboBoxItem], idx: i32, default: &str) -> String { items.get(idx as usize).map_or_else( || { warn!("idx {idx} out of range in {items:?}, defaulting to \"{default}\""); default.to_string() }, |e| e.config_name.clone(), ) } pub fn value_from_config_name(config_name: &str, items: &[StringComboBoxItem], default: T) -> T { items.iter().find(|e| e.config_name == config_name).map_or_else( || { warn!("Unknown config_name \"{config_name}\" in {items:?}, using default"); default }, |e| e.value.clone(), ) } } ================================================ FILE: cedinia/src/settings/mod.rs ================================================ pub mod gui_settings_values; use std::path::PathBuf; use czkawka_core::common::config_cache_path::get_config_cache_path; use log::{error, info}; use serde::{Deserialize, Serialize}; use crate::settings::gui_settings_values::StringComboBoxItems; fn default_check_method() -> String { "hash".to_string() } fn default_hash_type() -> String { "blake3".to_string() } fn default_hash_size() -> String { "16".to_string() } fn ttrue() -> bool { true } fn default_similarity_preset() -> String { "medium".to_string() } fn default_search_mode() -> String { "biggest".to_string() } fn default_big_files_count() -> String { "50".to_string() } fn default_min_file_size_idx() -> i32 { 0 } fn default_max_file_size_idx() -> i32 { 4 // Unlimited } fn default_language() -> String { "auto".to_string() } fn default_hash_alg() -> String { "mean".to_string() } fn default_image_filter() -> String { "triangle".to_string() } fn default_same_music_check_method() -> String { "tags".to_string() } fn default_excluded_items() -> String { #[cfg(not(target_os = "android"))] { "*/.*".to_string() } #[cfg(target_os = "android")] { String::new() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CediniaSettings { #[serde(default = "ttrue")] pub use_cache: bool, #[serde(default = "ttrue")] pub ignore_hidden: bool, #[serde(default = "default_min_file_size_idx")] pub min_file_size_idx: i32, #[serde(default = "default_max_file_size_idx")] pub max_file_size_idx: i32, #[serde(default = "default_language")] pub language: String, #[serde(default = "default_excluded_items")] pub excluded_items: String, #[serde(default)] pub allowed_extensions: String, #[serde(default)] pub excluded_extensions: String, #[serde(default = "default_check_method")] pub duplicates_check_method: String, #[serde(default = "default_hash_type")] pub duplicates_hash_type: String, #[serde(default = "default_similarity_preset")] pub similar_images_similarity_preset: String, #[serde(default = "default_hash_size")] pub similar_images_hash_size: String, #[serde(default = "default_hash_alg")] pub similar_images_hash_alg: String, #[serde(default = "default_image_filter")] pub similar_images_image_filter: String, #[serde(default)] pub similar_images_ignore_same_size: bool, #[serde(default = "default_search_mode")] pub big_files_search_mode: String, #[serde(default = "default_big_files_count")] pub big_files_count: String, #[serde(default = "ttrue")] pub same_music_title: bool, #[serde(default = "ttrue")] pub same_music_artist: bool, #[serde(default)] pub same_music_year: bool, #[serde(default)] pub same_music_length: bool, #[serde(default)] pub same_music_genre: bool, #[serde(default)] pub same_music_bitrate: bool, #[serde(default)] pub same_music_approximate: bool, #[serde(default = "default_same_music_check_method")] pub same_music_check_method: String, #[serde(default = "ttrue")] pub broken_files_audio: bool, #[serde(default = "ttrue")] pub broken_files_pdf: bool, #[serde(default = "ttrue")] pub broken_files_archive: bool, #[serde(default = "ttrue")] pub broken_files_image: bool, #[serde(default = "ttrue")] pub bad_names_uppercase_extension: bool, #[serde(default = "ttrue")] pub bad_names_emoji_used: bool, #[serde(default = "ttrue")] pub bad_names_space_at_start_or_end: bool, #[serde(default = "ttrue")] pub bad_names_non_ascii_graphical: bool, #[serde(default = "ttrue")] pub bad_names_remove_duplicated_non_alpha: bool, } impl Default for CediniaSettings { fn default() -> Self { Self { use_cache: true, ignore_hidden: true, min_file_size_idx: default_min_file_size_idx(), max_file_size_idx: default_max_file_size_idx(), language: default_language(), excluded_items: default_excluded_items(), allowed_extensions: String::new(), excluded_extensions: String::new(), duplicates_check_method: default_check_method(), duplicates_hash_type: default_hash_type(), similar_images_similarity_preset: default_similarity_preset(), similar_images_hash_size: default_hash_size(), similar_images_hash_alg: default_hash_alg(), similar_images_image_filter: default_image_filter(), similar_images_ignore_same_size: false, big_files_search_mode: default_search_mode(), big_files_count: default_big_files_count(), same_music_title: true, same_music_artist: true, same_music_year: false, same_music_length: false, same_music_genre: false, same_music_bitrate: false, same_music_approximate: false, same_music_check_method: default_same_music_check_method(), broken_files_audio: true, broken_files_pdf: true, broken_files_archive: true, broken_files_image: true, bad_names_uppercase_extension: true, bad_names_emoji_used: true, bad_names_space_at_start_or_end: true, bad_names_non_ascii_graphical: true, bad_names_remove_duplicated_non_alpha: true, } } } fn get_dirs_file() -> Option { let config_folder = get_config_cache_path()?.config_folder; Some(config_folder.join("cedinia_dirs.json")) } #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct DirConfig { included: Vec, excluded: Vec, } pub fn save_dirs(included: &[PathBuf], excluded: &[PathBuf]) { let Some(path) = get_dirs_file() else { error!("Cannot determine dirs config path – dirs not saved"); return; }; if let Some(parent) = path.parent() && let Err(e) = std::fs::create_dir_all(parent) { error!("Cannot create config dir {}: {e}", parent.display()); return; } let config = DirConfig { included: included.iter().map(|p| p.to_string_lossy().to_string()).collect(), excluded: excluded.iter().map(|p| p.to_string_lossy().to_string()).collect(), }; match serde_json::to_string_pretty(&config) { Ok(json) => { if let Err(e) = std::fs::write(&path, json) { error!("Cannot write dirs to {}: {e}", path.display()); } else { info!("Dirs saved to {}", path.display()); } } Err(e) => error!("Cannot serialize dirs: {e}"), } } pub fn load_dirs() -> (Vec, Vec) { let Some(path) = get_dirs_file() else { return (vec![], vec![]); }; if !path.is_file() { return (vec![], vec![]); } match std::fs::read_to_string(&path) { Ok(json) => match serde_json::from_str::(&json) { Ok(c) => { let inc = c.included.iter().map(PathBuf::from).collect(); let exc = c.excluded.iter().map(PathBuf::from).collect(); (inc, exc) } Err(e) => { error!("Cannot parse dirs config: {e}"); (vec![], vec![]) } }, Err(e) => { error!("Cannot read dirs config {}: {e}", path.display()); (vec![], vec![]) } } } fn get_config_file() -> Option { let config_folder = get_config_cache_path()?.config_folder; Some(config_folder.join("cedinia_settings.json")) } pub fn load_settings() -> CediniaSettings { let Some(path) = get_config_file() else { info!("Cannot determine config path – using defaults"); return CediniaSettings::default(); }; if !path.is_file() { info!("Settings file does not exist yet – using defaults"); return CediniaSettings::default(); } match std::fs::read_to_string(&path) { Ok(json) => match serde_json::from_str::(&json) { Ok(s) => { info!("Settings loaded from {}", path.display()); s } Err(e) => { error!("Cannot parse settings from {}: {e} – using defaults", path.display()); CediniaSettings::default() } }, Err(e) => { error!("Cannot read settings file {}: {e} – using defaults", path.display()); CediniaSettings::default() } } } pub fn save_settings(settings: &CediniaSettings) { let Some(path) = get_config_file() else { error!("Cannot determine config path – settings not saved"); return; }; if let Some(parent) = path.parent() && let Err(e) = std::fs::create_dir_all(parent) { error!("Cannot create config dir {}: {e}", parent.display()); return; } match serde_json::to_string_pretty(settings) { Ok(json) => { if let Err(e) = std::fs::write(&path, json) { error!("Cannot write settings to {}: {e}", path.display()); } else { info!("Settings saved to {}", path.display()); } } Err(e) => error!("Cannot serialize settings: {e}"), } } use slint::ComponentHandle; use crate::{BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, GeneralSettings, MainWindow, SameMusicSettings, SimilarImagesSettings}; pub fn apply_settings_to_gui(win: &MainWindow, s: &CediniaSettings) { let items = StringComboBoxItems::new(); win.global::().set_use_cache(s.use_cache); win.global::().set_ignore_hidden(s.ignore_hidden); win.global::().set_min_file_size_idx(s.min_file_size_idx); win.global::().set_max_file_size_idx(s.max_file_size_idx); let lang_idx = match s.language.as_str() { "pl" => 1, "en" => 0, _ => crate::localizer_cedinia::detect_os_language_idx(), }; win.global::().set_language_idx(lang_idx); win.global::().set_excluded_items(s.excluded_items.clone().into()); win.global::().set_allowed_extensions(s.allowed_extensions.clone().into()); win.global::().set_excluded_extensions(s.excluded_extensions.clone().into()); let cm_idx = StringComboBoxItems::idx_from_config_name(&s.duplicates_check_method, &items.duplicates_check_method); win.global::().set_check_method(cm_idx as i32); win.global::().set_check_method_value(s.duplicates_check_method.clone().into()); let ht_idx = StringComboBoxItems::idx_from_config_name(&s.duplicates_hash_type, &items.duplicates_hash_type); win.global::().set_hash_type(ht_idx as i32); win.global::().set_hash_type_value(s.duplicates_hash_type.clone().into()); let sp_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_similarity_preset, &items.similarity_preset); win.global::().set_similarity_preset(sp_idx as i32); win.global::() .set_similarity_preset_value(s.similar_images_similarity_preset.clone().into()); let hs_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_hash_size, &items.hash_size); win.global::().set_hash_size_idx(hs_idx as i32); win.global::().set_hash_size_value(s.similar_images_hash_size.clone().into()); let ha_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_hash_alg, &items.hash_alg); win.global::().set_hash_alg_idx(ha_idx as i32); win.global::().set_hash_alg_value(s.similar_images_hash_alg.clone().into()); let if_idx = StringComboBoxItems::idx_from_config_name(&s.similar_images_image_filter, &items.image_filter); win.global::().set_image_filter_idx(if_idx as i32); win.global::().set_image_filter_value(s.similar_images_image_filter.clone().into()); win.global::().set_ignore_same_size(s.similar_images_ignore_same_size); let sm_idx = StringComboBoxItems::idx_from_config_name(&s.big_files_search_mode, &items.biggest_files_method); win.global::().set_search_mode_idx(sm_idx as i32); win.global::().set_search_mode_value(s.big_files_search_mode.clone().into()); let cnt_idx = StringComboBoxItems::idx_from_config_name(&s.big_files_count, &items.big_files_count); win.global::().set_count_idx(cnt_idx as i32); win.global::().set_count_value(s.big_files_count.clone().into()); let sm = win.global::(); sm.set_title(s.same_music_title); sm.set_artist(s.same_music_artist); sm.set_year(s.same_music_year); sm.set_length(s.same_music_length); sm.set_genre(s.same_music_genre); sm.set_bitrate(s.same_music_bitrate); sm.set_approximate(s.same_music_approximate); let smc_idx = StringComboBoxItems::idx_from_config_name(&s.same_music_check_method, &items.same_music_check_method); sm.set_check_method_idx(smc_idx as i32); sm.set_check_method_value(s.same_music_check_method.clone().into()); let bf = win.global::(); bf.set_check_audio(s.broken_files_audio); bf.set_check_pdf(s.broken_files_pdf); bf.set_check_archive(s.broken_files_archive); bf.set_check_image(s.broken_files_image); let bn = win.global::(); bn.set_uppercase_extension(s.bad_names_uppercase_extension); bn.set_emoji_used(s.bad_names_emoji_used); bn.set_space_at_start_or_end(s.bad_names_space_at_start_or_end); bn.set_non_ascii_graphical(s.bad_names_non_ascii_graphical); bn.set_remove_duplicated_non_alpha(s.bad_names_remove_duplicated_non_alpha); } pub fn collect_settings_from_gui(win: &MainWindow) -> CediniaSettings { let items = StringComboBoxItems::new(); let g = win.global::(); let d = win.global::(); let si = win.global::(); let bfiles = win.global::(); let sm = win.global::(); let bf = win.global::(); let bn = win.global::(); CediniaSettings { use_cache: g.get_use_cache(), ignore_hidden: g.get_ignore_hidden(), min_file_size_idx: g.get_min_file_size_idx(), max_file_size_idx: g.get_max_file_size_idx(), language: match g.get_language_idx() { 1 => "pl".to_string(), _ => "en".to_string(), }, excluded_items: g.get_excluded_items().to_string(), allowed_extensions: g.get_allowed_extensions().to_string(), excluded_extensions: g.get_excluded_extensions().to_string(), duplicates_check_method: items .duplicates_check_method .get(d.get_check_method() as usize) .map_or_else(|| panic!("Invalid check_method idx {} in GUI", d.get_check_method()), |e| e.config_name.clone()), duplicates_hash_type: items .duplicates_hash_type .get(d.get_hash_type() as usize) .map_or_else(|| panic!("Invalid hash_type idx {} in GUI", d.get_hash_type()), |e| e.config_name.clone()), similar_images_similarity_preset: items .similarity_preset .get(si.get_similarity_preset() as usize) .map_or_else(|| panic!("Invalid similarity_preset idx {} in GUI", si.get_similarity_preset()), |e| e.config_name.clone()), similar_images_hash_size: items .hash_size .get(si.get_hash_size_idx() as usize) .map_or_else(|| panic!("Invalid hash_size_idx {} in GUI", si.get_hash_size_idx()), |e| e.config_name.clone()), similar_images_hash_alg: items .hash_alg .get(si.get_hash_alg_idx() as usize) .map_or_else(|| panic!("Invalid hash_alg_idx {} in GUI", si.get_hash_alg_idx()), |e| e.config_name.clone()), similar_images_image_filter: items .image_filter .get(si.get_image_filter_idx() as usize) .map_or_else(|| panic!("Invalid image_filter_idx {} in GUI", si.get_image_filter_idx()), |e| e.config_name.clone()), similar_images_ignore_same_size: si.get_ignore_same_size(), big_files_search_mode: items .biggest_files_method .get(bfiles.get_search_mode_idx() as usize) .map_or_else(|| panic!("Invalid search_mode_idx {} in GUI", bfiles.get_search_mode_idx()), |e| e.config_name.clone()), big_files_count: items .big_files_count .get(bfiles.get_count_idx() as usize) .map_or_else(|| panic!("Invalid count_idx {} in GUI", bfiles.get_count_idx()), |e| e.config_name.clone()), same_music_title: sm.get_title(), same_music_artist: sm.get_artist(), same_music_year: sm.get_year(), same_music_length: sm.get_length(), same_music_genre: sm.get_genre(), same_music_bitrate: sm.get_bitrate(), same_music_approximate: sm.get_approximate(), same_music_check_method: items.same_music_check_method.get(sm.get_check_method_idx() as usize).map_or_else( || panic!("Invalid same_music_check_method_idx {} in GUI", sm.get_check_method_idx()), |e| e.config_name.clone(), ), broken_files_audio: bf.get_check_audio(), broken_files_pdf: bf.get_check_pdf(), broken_files_archive: bf.get_check_archive(), broken_files_image: bf.get_check_image(), bad_names_uppercase_extension: bn.get_uppercase_extension(), bad_names_emoji_used: bn.get_emoji_used(), bad_names_space_at_start_or_end: bn.get_space_at_start_or_end(), bad_names_non_ascii_graphical: bn.get_non_ascii_graphical(), bad_names_remove_duplicated_non_alpha: bn.get_remove_duplicated_non_alpha(), } } ================================================ FILE: cedinia/src/thumbnail_loader.rs ================================================ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use std::time::{Duration, SystemTime}; use czkawka_core::common::image::{ImgResizeOptions, LoadedImage}; use log::trace; use crate::scan_runner::FileItem; pub enum ThumbnailData { Loaded(Vec, u32, u32), Placeholder, } pub struct ThumbnailResult { pub scan_id: u32, pub group_idx: usize, pub item_idx: usize, pub data: ThumbnailData, } fn get_total_ram_mb() -> u64 { if let Ok(content) = std::fs::read_to_string("/proc/meminfo") { for line in content.lines() { if line.starts_with("MemTotal:") && let Some(kb_str) = line.split_whitespace().nth(1) && let Ok(kb) = kb_str.parse::() { return kb / 1024; } } } 4096 } pub fn cache_limit_bytes() -> u64 { let ram_mb = get_total_ram_mb(); let limit_mb: u64 = if ram_mb <= 2048 { 256 } else if ram_mb <= 4096 { 1024 } else if ram_mb <= 8192 { 2048 } else { 4096 }; limit_mb * 1024 * 1024 } pub fn thumbnail_cache_dir() -> PathBuf { #[cfg(target_os = "android")] { let base = crate::android_cache_path().unwrap_or("/data/data/io.github.qarmin.cedinia/cache"); PathBuf::from(base).join("img_thumbnails") } #[cfg(not(target_os = "android"))] { let base = std::env::var("XDG_CACHE_HOME").map_or_else(|_| PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".cache"), PathBuf::from); base.join("cedinia").join("img_thumbnails") } } fn cache_key(path: &str, mtime_secs: u64, file_size: u64) -> String { let mut h = DefaultHasher::new(); path.hash(&mut h); mtime_secs.hash(&mut h); file_size.hash(&mut h); format!("{:016x}.png", h.finish()) } fn try_read_png_cache(cache_path: &Path) -> Option<(Vec, u32, u32)> { let data = std::fs::read(cache_path).ok()?; let img = image::load_from_memory_with_format(&data, image::ImageFormat::Png).ok()?; let rgba = img.into_rgba8(); let w = rgba.width(); let h = rgba.height(); Some((rgba.into_raw(), w, h)) } fn try_write_png_cache(cache_path: &Path, rgba: &[u8], w: u32, h: u32) { let tmp = cache_path.with_extension("tmp"); let write = || -> image::ImageResult<()> { use image::ImageEncoder; let f = std::fs::File::create(&tmp).map_err(image::ImageError::IoError)?; image::codecs::png::PngEncoder::new(f).write_image(rgba, w, h, image::ExtendedColorType::Rgba8) }; if write().is_ok() { let _ = std::fs::rename(&tmp, cache_path); } else { let _ = std::fs::remove_file(&tmp); } } pub fn make_placeholder_image() -> slint::Image { const W: u32 = 32; const H: u32 = 32; const CELL: u32 = 16; let mut rgba = vec![0u8; (W * H * 4) as usize]; for y in 0..H { for x in 0..W { let off = ((y * W + x) * 4) as usize; let v = if ((x / CELL) + (y / CELL)).is_multiple_of(2) { 160u8 } else { 80u8 }; rgba[off] = v; rgba[off + 1] = v; rgba[off + 2] = v; rgba[off + 3] = 255; } } rgba_to_slint_image(&rgba, W, H) } pub fn rgba_to_slint_image(rgba: &[u8], width: u32, height: u32) -> slint::Image { let buffer = slint::SharedPixelBuffer::::clone_from_slice(rgba, width, height); slint::Image::from_rgba8(buffer) } pub fn load_and_resize_thumbnail(path: &str, cache_dir: &Path) -> Option<(Vec, u32, u32)> { use czkawka_core::common::image::get_dynamic_image_from_path; use fast_image_resize::FilterType; let meta = std::fs::metadata(path).ok(); let (mtime_secs, file_size) = meta.as_ref().map_or((0, 0), |m| { let mtime = m.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map_or(0, |d| d.as_secs()); (mtime, m.len()) }); let cache_file = cache_dir.join(cache_key(path, mtime_secs, file_size)); if let Some(cached) = try_read_png_cache(&cache_file) { let now = filetime::FileTime::now(); let _ = filetime::set_file_mtime(&cache_file, now); trace!("Loaded thumbnail from cache for {path} ({file_size} bytes)"); return Some(cached); } trace!("Generating thumbnail for {path} ({file_size} bytes)"); let loaded_data = get_dynamic_image_from_path( path, Some(ImgResizeOptions { max_width: 256, max_height: 256, filter: FilterType::Lanczos3, }), ) .ok()?; let LoadedImage { image, original_width, original_height, } = loaded_data; let should_cache = original_width >= 256 || original_height >= 256; let rgba = image.into_rgba8(); let w = rgba.width(); let h = rgba.height(); let raw = rgba.into_raw(); if should_cache { trace!("Caching thumbnail for {path} at {w}x{h}"); try_write_png_cache(&cache_file, &raw, w, h); } else { trace!("Not caching thumbnail for {path} since it's smaller than 256x256 ({original_width}x{original_height})"); } Some((raw, w, h)) } pub fn collect_thumb_tasks(items: &[FileItem]) -> Vec<(usize, usize, String)> { use crate::common::{STR_IDX_NAME, STR_IDX_PATH}; let mut tasks = Vec::new(); let mut group_idx: i32 = -1; let mut item_idx = 0usize; for item in items { if item.is_header { group_idx += 1; item_idx = 0; } else if group_idx >= 0 { let name = &item.val_str[STR_IDX_NAME]; let path = &item.val_str[STR_IDX_PATH]; let full = if path.is_empty() { name.clone() } else { format!("{path}/{name}") }; tasks.push((group_idx as usize, item_idx, full)); item_idx += 1; } } tasks } pub fn cleanup_old_thumbnails() { let cache_dir = thumbnail_cache_dir(); let cutoff = SystemTime::now().checked_sub(Duration::from_secs(30 * 24 * 3600)).unwrap_or(SystemTime::UNIX_EPOCH); if let Ok(entries) = std::fs::read_dir(&cache_dir) { for entry in entries.flatten() { if let Ok(meta) = entry.metadata() && meta.modified().map(|t| t < cutoff).unwrap_or(false) { let _ = std::fs::remove_file(entry.path()); } } } } pub fn spawn_thumbnail_loader(tasks: Vec<(usize, usize, String)>, tx: std::sync::mpsc::Sender, cancel: Arc, scan_id: u32) { let cache_dir = thumbnail_cache_dir(); let _ = std::fs::create_dir_all(&cache_dir); let cache_dir = Arc::new(cache_dir); std::thread::spawn(move || { if tasks.is_empty() { return; } let num_workers = std::thread::available_parallelism().map(|n| n.get().min(4)).unwrap_or(2); let limit = cache_limit_bytes(); let used_bytes = Arc::new(AtomicU64::new(0)); let next_idx = Arc::new(AtomicUsize::new(0)); let tasks = Arc::new(tasks); let mut handles = Vec::with_capacity(num_workers); for _ in 0..num_workers { let tasks = tasks.clone(); let tx = tx.clone(); let cancel = cancel.clone(); let used_bytes = used_bytes.clone(); let next_idx = next_idx.clone(); let cache_dir = cache_dir.clone(); handles.push(std::thread::spawn(move || { loop { let idx = next_idx.fetch_add(1, Ordering::Relaxed); if idx >= tasks.len() || cancel.load(Ordering::Relaxed) { break; } let (group_idx, item_idx, ref path) = tasks[idx]; let cur = used_bytes.load(Ordering::Relaxed); let data = if cur >= limit { ThumbnailData::Placeholder } else { match load_and_resize_thumbnail(path, &cache_dir) { Some((rgba, w, h)) => { let size = rgba.len() as u64; let prev = used_bytes.fetch_add(size, Ordering::SeqCst); if prev + size <= limit { ThumbnailData::Loaded(rgba, w, h) } else { used_bytes.fetch_sub(size, Ordering::SeqCst); ThumbnailData::Placeholder } } None => ThumbnailData::Placeholder, } }; if tx .send(ThumbnailResult { scan_id, group_idx, item_idx, data, }) .is_err() { break; } } })); } for h in handles { h.join().expect("Thumbnail loader panicked"); } }); } ================================================ FILE: cedinia/src/translations.rs ================================================ use slint::ComponentHandle; use crate::{MainWindow, Translations, flc}; pub(crate) fn translate_items(app: &MainWindow) { let t = app.global::(); t.set_app_name_text(flc!("app_name").into()); t.set_tool_duplicate_files_text(flc!("tool_duplicate_files").into()); t.set_tool_empty_folders_text(flc!("tool_empty_folders").into()); t.set_tool_similar_images_text(flc!("tool_similar_images").into()); t.set_tool_empty_files_text(flc!("tool_empty_files").into()); t.set_tool_temporary_files_text(flc!("tool_temporary_files").into()); t.set_tool_big_files_text(flc!("tool_big_files").into()); t.set_tool_broken_files_text(flc!("tool_broken_files").into()); t.set_tool_bad_extensions_text(flc!("tool_bad_extensions").into()); t.set_tool_same_music_text(flc!("tool_same_music").into()); t.set_tool_bad_names_text(flc!("tool_bad_names").into()); t.set_tool_exif_remover_text(flc!("tool_exif_remover").into()); t.set_tool_directories_text(flc!("tool_directories").into()); t.set_tool_settings_text(flc!("tool_settings").into()); t.set_home_dup_description_text(flc!("home_dup_description").into()); t.set_home_empty_folders_description_text(flc!("home_empty_folders_description").into()); t.set_home_similar_images_description_text(flc!("home_similar_images_description").into()); t.set_home_empty_files_description_text(flc!("home_empty_files_description").into()); t.set_home_temp_files_description_text(flc!("home_temp_files_description").into()); t.set_home_big_files_description_text(flc!("home_big_files_description").into()); t.set_home_broken_files_description_text(flc!("home_broken_files_description").into()); t.set_home_bad_extensions_description_text(flc!("home_bad_extensions_description").into()); t.set_home_same_music_description_text(flc!("home_same_music_description").into()); t.set_home_bad_names_description_text(flc!("home_bad_names_description").into()); t.set_home_exif_description_text(flc!("home_exif_description").into()); t.set_scanning_text(flc!("scanning").into()); t.set_stopping_text(flc!("stopping").into()); t.set_no_results_text(flc!("no_results").into()); t.set_press_start_text(flc!("press_start").into()); t.set_select_label_text(flc!("select_label").into()); t.set_deselect_label_text(flc!("deselect_label").into()); t.set_list_label_text(flc!("list_label").into()); t.set_gallery_label_text(flc!("gallery_label").into()); t.set_selection_popup_title_text(flc!("selection_popup_title").into()); t.set_select_all_text(flc!("select_all").into()); t.set_select_except_one_text(flc!("select_except_one").into()); t.set_select_except_largest_text(flc!("select_except_largest").into()); t.set_select_except_smallest_text(flc!("select_except_smallest").into()); t.set_select_largest_text(flc!("select_largest").into()); t.set_select_smallest_text(flc!("select_smallest").into()); t.set_select_except_highest_res_text(flc!("select_except_highest_res").into()); t.set_select_except_lowest_res_text(flc!("select_except_lowest_res").into()); t.set_select_highest_res_text(flc!("select_highest_res").into()); t.set_select_lowest_res_text(flc!("select_lowest_res").into()); t.set_invert_selection_text(flc!("invert_selection").into()); t.set_close_text(flc!("close").into()); t.set_deselection_popup_title_text(flc!("deselection_popup_title").into()); t.set_deselect_all_text(flc!("deselect_all").into()); t.set_deselect_except_one_text(flc!("deselect_except_one").into()); t.set_cancel_text(flc!("cancel").into()); t.set_delete_text(flc!("delete").into()); t.set_rename_text(flc!("rename").into()); t.set_delete_errors_title_text(flc!("delete_errors_title").into()); t.set_ok_text(flc!("ok").into()); t.set_stopping_overlay_title_text(flc!("stopping_overlay_title").into()); t.set_stopping_overlay_body_text(flc!("stopping_overlay_body").into()); t.set_permission_title_text(flc!("permission_title").into()); t.set_permission_body_text(flc!("permission_body").into()); t.set_grant_text(flc!("grant").into()); t.set_no_permission_scan_warning_text(flc!("no_permission_scan_warning").into()); t.set_settings_tab_general_text(flc!("settings_tab_general").into()); t.set_settings_tab_tools_text(flc!("settings_tab_tools").into()); t.set_settings_tab_diagnostics_text(flc!("settings_tab_diagnostics").into()); t.set_settings_use_cache_text(flc!("settings_use_cache").into()); t.set_settings_use_cache_desc_text(flc!("settings_use_cache_desc").into()); t.set_settings_ignore_hidden_text(flc!("settings_ignore_hidden").into()); t.set_settings_ignore_hidden_desc_text(flc!("settings_ignore_hidden_desc").into()); t.set_settings_scan_label_text(flc!("settings_scan_label").into()); t.set_settings_filters_label_text(flc!("settings_filters_label").into()); t.set_settings_min_file_size_text(flc!("settings_min_file_size").into()); t.set_settings_max_file_size_text(flc!("settings_max_file_size").into()); t.set_settings_language_text(flc!("settings_language").into()); t.set_settings_language_restart_text(flc!("settings_language_restart").into()); t.set_settings_common_label_text(flc!("settings_common_label").into()); t.set_settings_hash_type_desc_text(flc!("settings_hash_type_desc").into()); t.set_settings_similarity_desc_text(flc!("settings_similarity_desc").into()); t.set_settings_hash_size_desc_text(flc!("settings_hash_size_desc").into()); t.set_settings_excluded_items_text(flc!("settings_excluded_items").into()); t.set_settings_excluded_items_placeholder_text(flc!("settings_excluded_items_placeholder").into()); t.set_settings_allowed_extensions_text(flc!("settings_allowed_extensions").into()); t.set_settings_allowed_extensions_placeholder_text(flc!("settings_allowed_extensions_placeholder").into()); t.set_settings_excluded_extensions_text(flc!("settings_excluded_extensions").into()); t.set_settings_excluded_extensions_placeholder_text(flc!("settings_excluded_extensions_placeholder").into()); t.set_settings_duplicates_header_text(flc!("settings_duplicates_header").into()); t.set_settings_check_method_label_text(flc!("settings_check_method_label").into()); t.set_settings_check_method_text(flc!("settings_check_method").into()); t.set_settings_hash_type_label_text(flc!("settings_hash_type_label").into()); t.set_settings_hash_type_text(flc!("settings_hash_type").into()); t.set_settings_similar_images_header_text(flc!("settings_similar_images_header").into()); t.set_settings_similarity_preset_text(flc!("settings_similarity_preset").into()); t.set_settings_hash_size_text(flc!("settings_hash_size").into()); t.set_settings_hash_alg_text(flc!("settings_hash_alg").into()); t.set_settings_image_filter_text(flc!("settings_image_filter").into()); t.set_settings_ignore_same_size_text(flc!("settings_ignore_same_size").into()); t.set_settings_big_files_header_text(flc!("settings_big_files_header").into()); t.set_settings_search_mode_text(flc!("settings_search_mode").into()); t.set_settings_file_count_text(flc!("settings_file_count").into()); t.set_settings_same_music_header_text(flc!("settings_same_music_header").into()); t.set_settings_music_check_method_text(flc!("settings_music_check_method").into()); t.set_settings_music_compare_tags_label_text(flc!("settings_music_compare_tags_label").into()); t.set_settings_music_title_text(flc!("settings_music_title").into()); t.set_settings_music_artist_text(flc!("settings_music_artist").into()); t.set_settings_music_year_text(flc!("settings_music_year").into()); t.set_settings_music_length_text(flc!("settings_music_length").into()); t.set_settings_music_genre_text(flc!("settings_music_genre").into()); t.set_settings_music_bitrate_text(flc!("settings_music_bitrate").into()); t.set_settings_music_approx_text(flc!("settings_music_approx").into()); t.set_settings_broken_files_header_text(flc!("settings_broken_files_header").into()); t.set_settings_broken_files_types_label_text(flc!("settings_broken_files_types_label").into()); t.set_settings_broken_audio_text(flc!("settings_broken_audio").into()); t.set_settings_broken_pdf_text(flc!("settings_broken_pdf").into()); t.set_settings_broken_archive_text(flc!("settings_broken_archive").into()); t.set_settings_broken_image_text(flc!("settings_broken_image").into()); t.set_settings_bad_names_header_text(flc!("settings_bad_names_header").into()); t.set_settings_bad_names_checks_label_text(flc!("settings_bad_names_checks_label").into()); t.set_settings_bad_names_uppercase_ext_text(flc!("settings_bad_names_uppercase_ext").into()); t.set_settings_bad_names_emoji_text(flc!("settings_bad_names_emoji").into()); t.set_settings_bad_names_space_text(flc!("settings_bad_names_space").into()); t.set_settings_bad_names_non_ascii_text(flc!("settings_bad_names_non_ascii").into()); t.set_settings_bad_names_duplicated_text(flc!("settings_bad_names_duplicated").into()); t.set_diagnostics_header_text(flc!("diagnostics_header").into()); t.set_diagnostics_thumbnails_text(flc!("diagnostics_thumbnails").into()); t.set_diagnostics_app_cache_text(flc!("diagnostics_app_cache").into()); t.set_diagnostics_refresh_text(flc!("diagnostics_refresh").into()); t.set_diagnostics_clear_thumbnails_text(flc!("diagnostics_clear_thumbnails").into()); t.set_diagnostics_clear_cache_text(flc!("diagnostics_clear_cache").into()); t.set_diagnostics_collect_test_text(flc!("diagnostics_collect_test").into()); t.set_diagnostics_collect_test_desc_text(flc!("diagnostics_collect_test_desc").into()); t.set_diagnostics_collect_test_run_text(flc!("diagnostics_collect_test_run").into()); t.set_diagnostics_collect_test_stop_text(flc!("diagnostics_collect_test_stop").into()); t.set_collect_test_title_text(flc!("collect_test_title").into()); t.set_collect_test_volumes_text(flc!("collect_test_volumes").into()); t.set_collect_test_folders_text(flc!("collect_test_folders").into()); t.set_collect_test_files_text(flc!("collect_test_files").into()); t.set_collect_test_time_text(flc!("collect_test_time").into()); t.set_collect_test_ms_text(flc!("collect_test_ms").into()); t.set_directories_include_header_text(flc!("directories_include_header").into()); t.set_directories_exclude_header_text(flc!("directories_exclude_header").into()); t.set_directories_add_text(flc!("directories_add").into()); t.set_directories_volume_header_text(flc!("directories_volume_header").into()); t.set_directories_volume_refresh_text(flc!("directories_volume_refresh").into()); t.set_directories_volume_add_text(flc!("directories_volume_add").into()); t.set_no_paths_text(flc!("no_paths").into()); t.set_gallery_delete_button_text(flc!("gallery_delete_button").into()); t.set_gallery_back_text(flc!("gallery_back").into()); t.set_gallery_confirm_delete_text(flc!("gallery_confirm_delete").into()); t.set_deleting_files_text(flc!("deleting_files").into()); t.set_stop_text(flc!("stop").into()); t.set_files_suffix_text(flc!("files_suffix").into()); t.set_scanning_fallback_text(flc!("scanning_fallback").into()); t.set_app_subtitle_text(flc!("app_subtitle").into()); t.set_app_license_text(flc!("app_license").into()); t.set_about_app_label_text(flc!("about_app_label").into()); t.set_cache_label_text(flc!("cache_label").into()); t.set_nav_home_text(flc!("nav_home").into()); t.set_nav_dirs_text(flc!("nav_dirs").into()); t.set_nav_settings_text(flc!("nav_settings").into()); t.set_status_ready_text(flc!("status_ready").into()); t.set_status_stopped_text(flc!("status_stopped").into()); t.set_status_no_results_text(flc!("status_no_results").into()); t.set_status_deleted_selected_text(flc!("status_deleted_selected").into()); t.set_status_deleted_with_errors_text(flc!("status_deleted_with_errors").into()); t.set_scan_not_started_text(flc!("scan_not_started").into()); t.set_found_items_prefix_text(flc!("found_items_prefix").into()); t.set_found_items_suffix_text(flc!("found_items_suffix").into()); t.set_deleted_items_prefix_text(flc!("deleted_items_prefix").into()); t.set_deleted_items_suffix_text(flc!("deleted_items_suffix").into()); t.set_deleted_errors_suffix_text(flc!("deleted_errors_suffix").into()); t.set_renamed_prefix_text(flc!("renamed_prefix").into()); t.set_renamed_files_suffix_text(flc!("renamed_files_suffix").into()); t.set_renamed_errors_suffix_text(flc!("renamed_errors_suffix").into()); t.set_cleaned_exif_prefix_text(flc!("cleaned_exif_prefix").into()); t.set_cleaned_exif_suffix_text(flc!("cleaned_exif_suffix").into()); t.set_cleaned_exif_errors_suffix_text(flc!("cleaned_exif_errors_suffix").into()); t.set_and_more_prefix_text(flc!("and_more_prefix").into()); t.set_and_more_suffix_text(flc!("and_more_suffix").into()); t.set_about_repo_text(flc!("about_repo").into()); t.set_about_translate_text(flc!("about_translate").into()); t.set_about_donate_text(flc!("about_donate").into()); } ================================================ FILE: cedinia/src/volumes.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use slint::{ComponentHandle, Model, SharedString}; use crate::{AppState, MainWindow, VolumeEntry}; pub(crate) fn home_dir() -> PathBuf { #[cfg(target_os = "android")] { PathBuf::from("/sdcard") } #[cfg(not(target_os = "android"))] { std::env::var("HOME").map_or_else(|_| PathBuf::from("/"), PathBuf::from) } } pub(crate) fn detect_storage_volumes() -> Vec { let mut result: Vec = Vec::new(); #[cfg(target_os = "android")] let candidates = vec![ "/sdcard", "/storage/emulated/0", "/storage/emulated/1", "/storage/self/primary", "/mnt/sdcard", "/mnt/extSdCard", "/mnt/external_sd", "/mnt/media_rw", ]; #[cfg(not(target_os = "android"))] let candidates: Vec<&str> = vec![]; let mut mounts: Vec<(String, String)> = Vec::new(); if let Ok(content) = std::fs::read_to_string("/proc/mounts") { for line in content.lines() { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 3 { continue; } let device = parts[0]; let mountpoint = parts[1]; let fstype = parts[2]; if fstype == "vfat" || fstype == "exfat" || fstype == "ntfs" || fstype == "sdcardfs" || fstype == "fuse" || mountpoint.starts_with("/storage/") || mountpoint.starts_with("/sdcard") || mountpoint.starts_with("/mnt/") { if device == "none" && !mountpoint.starts_with("/storage/") && !mountpoint.starts_with("/sdcard") { continue; } mounts.push((mountpoint.to_string(), fstype.to_string())); } } } mounts.sort_by(|a, b| a.0.cmp(&b.0)); mounts.dedup_by(|a, b| a.0 == b.0); for (mountpoint, _fstype) in &mounts { if std::fs::read_dir(mountpoint).is_ok() { let label = classify_mountpoint(mountpoint); result.push(VolumeEntry { path: SharedString::from(mountpoint.as_str()), label: SharedString::from(label), is_included: false, is_excluded: false, }); } } #[cfg(target_os = "android")] for path in &candidates { let already_listed = result.iter().any(|v| v.path.as_str() == *path); if !already_listed && std::fs::read_dir(path).is_ok() { let label = classify_mountpoint(path); result.push(VolumeEntry { path: SharedString::from(*path), label: SharedString::from(label), is_included: false, is_excluded: false, }); } } #[cfg(not(target_os = "android"))] let _ = candidates; // Deduplicate by canonical path: /sdcard, /storage/emulated/0, /storage/self/primary // are often symlinks pointing to the same directory. let mut seen: std::collections::HashSet = std::collections::HashSet::new(); result.retain(|v| { let canonical = std::fs::canonicalize(v.path.as_str()).unwrap_or_else(|_| PathBuf::from(v.path.as_str())); seen.insert(canonical) }); result } pub(crate) fn classify_mountpoint(path: &str) -> &'static str { if path.contains("emulated/0") || path == "/sdcard" || path == "/storage/self/primary" || path == "/mnt/sdcard" { "💾 Pamięć wbudowana (internal)" } else if path.contains("emulated/1") || path.contains("extSdCard") || path.contains("external_sd") || path.contains("sdcard1") || path.contains("sdcard2") || path.starts_with("/storage/") || path.starts_with("/mnt/media_rw/") { "💳 Karta pamięci (SD card)" } else { "📦 Wolumin pamięci" } } pub(crate) fn refresh_volumes_flags(win: &MainWindow, included: &[PathBuf], excluded: &[PathBuf]) { let inc_set: Vec = included.iter().map(|p| p.to_string_lossy().to_string()).collect(); let exc_set: Vec = excluded.iter().map(|p| p.to_string_lossy().to_string()).collect(); let model = win.global::().get_storage_volumes(); for i in 0..model.row_count() { if let Some(mut vol) = model.row_data(i) { let path = vol.path.to_string(); vol.is_included = inc_set.contains(&path); vol.is_excluded = exc_set.contains(&path); model.set_row_data(i, vol); } } } pub(crate) fn count_files_and_dirs_stoppable(root: &std::path::Path, stop: &Arc, stopped: &mut bool) -> (i32, i32) { if stop.load(Ordering::Relaxed) { *stopped = true; return (0, 0); } let mut files: i32 = 0; let mut dirs: i32 = 0; let Ok(rd) = std::fs::read_dir(root) else { return (0, 0); }; for entry in rd.flatten() { if stop.load(Ordering::Relaxed) { *stopped = true; return (files, dirs); } let Ok(ft) = entry.file_type() else { continue; }; if ft.is_dir() { dirs = dirs.saturating_add(1); let (f, d) = count_files_and_dirs_stoppable(&entry.path(), stop, stopped); files = files.saturating_add(f); dirs = dirs.saturating_add(d); if *stopped { return (files, dirs); } } else { files = files.saturating_add(1); } } (files, dirs) } ================================================ FILE: cedinia/ui/app_state.slint ================================================ import { ActiveTool, CollectTestResult, ProgressData, ScanState, VolumeEntry } from "common.slint"; export global GeneralSettings { in-out property use_cache: true; in-out property ignore_hidden: true; in-out property min_file_size_idx: 0; in-out property <[string]> min_file_size_options: ["Brak", "1 KB", "8 KB", "64 KB", "1 MB"]; in-out property max_file_size_idx: 4; in-out property <[string]> max_file_size_options: ["16 KB", "1 MB", "10 MB", "100 MB", "Bez limitu"]; in-out property language_idx: 0; in-out property <[string]> language_options: ["English", "Polski (Polish)"]; in-out property excluded_items: ""; in-out property allowed_extensions: ""; in-out property excluded_extensions: ""; } export global DuplicateSettings { in-out property check_method: 0; in-out property check_method_value: "hash"; in-out property <[string]> check_method_options: ["Hash", "Nazwa", "Rozm+Naz", "Rozmiar"]; in-out property hash_type: 0; in-out property hash_type_value: "blake3"; in-out property <[string]> hash_type_options: ["Blake3", "CRC32", "XXH3"]; } export global SimilarImagesSettings { in-out property similarity_preset: 2; in-out property similarity_preset_value: "medium"; in-out property <[string]> similarity_preset_options: ["B.Wys.", "Wysoki", "Średni", "Niski", "B.Niski", "Min."]; in-out property hash_size_idx: 1; in-out property hash_size_value: "16"; in-out property <[string]> hash_size_options: ["8", "16", "32", "64"]; in-out property hash_alg_idx: 0; in-out property hash_alg_value: "mean"; in-out property <[string]> hash_alg_options: ["Mean", "Gradient", "D.Grad.", "V.Grad.", "Median", "Blockhash"]; in-out property image_filter_idx: 1; in-out property image_filter_value: "triangle"; in-out property <[string]> image_filter_options: ["Nearest", "Triangle", "CatmullRom", "Gaussian", "Lanczos3"]; in-out property ignore_same_size: false; } export global SameMusicSettings { in-out property title: true; in-out property artist: true; in-out property year: false; in-out property length: false; in-out property genre: false; in-out property bitrate: false; in-out property approximate: false; in-out property check_method_idx: 0; in-out property check_method_value: "tags"; in-out property <[string]> check_method_options: ["Tagi", "Audio"]; } export global BrokenFilesSettings { in-out property check_audio: true; in-out property check_pdf: true; in-out property check_archive: true; in-out property check_image: true; } export global BadNamesSettings { in-out property uppercase_extension: true; in-out property emoji_used: true; in-out property space_at_start_or_end: true; in-out property non_ascii_graphical: true; in-out property remove_duplicated_non_alpha: true; } export global BigFilesSettings { in-out property search_mode_idx: 0; in-out property search_mode_value: "biggest"; in-out property <[string]> search_mode_options: ["Największe", "Najmniejsze"]; in-out property count_idx: 1; in-out property count_value: "50"; in-out property <[string]> count_options: ["5", "50", "500", "5000"]; } export global AppState { in-out property active_tool: ActiveTool.Home; in-out property last_scan_tool: ActiveTool.DuplicateFiles; in-out property scan_state: ScanState.Idle; in-out property progress: { step_name: "", current_progress: 0, all_progress: 0, is_indeterminate: false }; in-out property status_message: "Gotowy / Ready"; in-out property show_included_dirs: false; in-out property show_excluded_dirs: false; in-out property bottom_sheet_open: false; in-out property bottom_sheet_title: ""; in-out property selected_count: 0; in-out property inset_top: 0px; in-out property inset_bottom: 0px; callback tool_changed(ActiveTool); callback scan_requested(); callback stop_requested(); callback delete_selected(); callback rename_selected(); callback clean_exif_selected(); callback select_all(); callback deselect_all(); callback select_all_except_one(); callback deselect_all_except_one(); callback invert_selection(); callback select_largest_per_group(); callback select_all_except_largest(); callback select_smallest_per_group(); callback select_all_except_smallest(); callback select_highest_resolution_per_group(); callback select_all_except_highest_resolution(); callback select_lowest_resolution_per_group(); callback select_all_except_lowest_resolution(); callback toggle_file_checked(int); callback pick_include_dir(); callback pick_exclude_dir(); callback add_include_dir(string); callback remove_include_dir(string); callback add_exclude_dir(string); callback remove_exclude_dir(string); in-out property <[VolumeEntry]> storage_volumes: []; callback list_storage_volumes(); callback save_settings_now(); callback open_path(string); callback open_parent_folder(string); callback request_storage_permission(); in-out property storage_permission_granted: true; in-out property show_permission_popup: false; callback run_collect_test(); callback stop_collect_test(); in-out property collect_test_result: { volumes: 0, files: 0, folders: 0, elapsed_ms: 0 }; in-out property collect_test_running: false; in-out property collect_test_done: false; in-out property similar_images_gallery_mode: true; in-out property diag_thumbnails_size: "–"; in-out property diag_app_cache_size: "–"; in-out property diag_refresh_running: false; callback refresh_diag_cache_info(); callback clear_thumbnails_cache(); callback apply_language_change(); callback clear_app_cache(); callback open_url(string); in-out property gallery_delete_popup_visible: false; in-out property gallery_delete_message: ""; in-out property gallery_delete_warning: ""; callback request_gallery_delete(); callback confirm_gallery_delete(); in-out property delete_running: false; in-out property delete_progress_text: ""; callback delete_stop_requested(); in-out property delete_errors_visible: false; in-out property delete_errors_text: ""; in-out property confirm_popup_visible: false; in-out property confirm_popup_message: ""; in-out property confirm_popup_action: ""; callback confirm_popup_ok(); callback confirm_popup_cancel(); } ================================================ FILE: cedinia/ui/bottom_nav.slint ================================================ import { CediniaColors } from "colors.slint"; import { ActiveTool } from "common.slint"; import { AppState } from "app_state.slint"; import { Translations } from "translations.slint"; component NavItem { in property label; in property tool; in property icon; height: 64px; property is_active: AppState.active_tool == root.tool; Rectangle { width: root.width; height: root.height; background: ta.has-hover ? CediniaColors.ripple : transparent; animate background { duration: 100ms; } border-radius: 8px; ta := TouchArea { clicked => { if AppState.active_tool != root.tool { AppState.active_tool = root.tool; AppState.tool_changed(root.tool); } } } VerticalLayout { alignment: LayoutAlignment.center; spacing: 3px; padding-top: 6px; padding-bottom: 6px; Rectangle { height: 3px; horizontal-stretch: 0.0; background: root.is_active ? CediniaColors.accent : transparent; border-radius: 2px; animate background { duration: 150ms; } } Image { source: root.icon; colorize: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive; horizontal-alignment: ImageHorizontalAlignment.center; vertical-alignment: ImageVerticalAlignment.center; animate colorize { duration: 150ms; } } Text { text: root.label; color: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive; font-size: 10px; font-weight: root.is_active ? 700 : 400; horizontal-alignment: TextHorizontalAlignment.center; overflow: elide; animate color { duration: 150ms; } } } } } component DynamicToolItem { height: 64px; property t: AppState.last_scan_tool; property t_label: t == ActiveTool.DuplicateFiles ? Translations.tool_duplicate_files_text : t == ActiveTool.EmptyFolders ? Translations.tool_empty_folders_text : t == ActiveTool.SimilarImages ? Translations.tool_similar_images_text : t == ActiveTool.EmptyFiles ? Translations.tool_empty_files_text : t == ActiveTool.TemporaryFiles ? Translations.tool_temporary_files_text : t == ActiveTool.BigFiles ? Translations.tool_big_files_text : t == ActiveTool.BrokenFiles ? Translations.tool_broken_files_text : t == ActiveTool.BadExtensions ? Translations.tool_bad_extensions_text : t == ActiveTool.SameMusic ? Translations.tool_same_music_text : t == ActiveTool.BadNames ? Translations.tool_bad_names_text : t == ActiveTool.ExifRemover ? Translations.tool_exif_remover_text : Translations.app_name_text; property t_icon: t == ActiveTool.SimilarImages ? @image-url("icons/image.svg") : t == ActiveTool.EmptyFolders ? @image-url("icons/folder_empty.svg") : @image-url("icons/duplicate.svg"); property is_active: AppState.active_tool == root.t; Rectangle { width: root.width; height: root.height; background: ta.has-hover ? CediniaColors.ripple : transparent; animate background { duration: 100ms; } border-radius: 8px; ta := TouchArea { clicked => { if AppState.active_tool != root.t { AppState.active_tool = root.t; AppState.tool_changed(root.t); } } } VerticalLayout { alignment: LayoutAlignment.center; spacing: 3px; padding-top: 6px; padding-bottom: 6px; Rectangle { height: 3px; horizontal-stretch: 0.0; background: root.is_active ? CediniaColors.accent : transparent; border-radius: 2px; animate background { duration: 150ms; } } Image { source: root.t_icon; colorize: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive; horizontal-alignment: ImageHorizontalAlignment.center; vertical-alignment: ImageVerticalAlignment.center; animate colorize { duration: 150ms; } } Text { text: root.t_label; color: root.is_active ? CediniaColors.nav_active : CediniaColors.nav_inactive; font-size: 10px; font-weight: root.is_active ? 700 : 400; horizontal-alignment: TextHorizontalAlignment.center; overflow: elide; animate color { duration: 150ms; } } } } } export component BottomNavBar { height: 64px; horizontal-stretch: 1.0; Rectangle { width: parent.width; height: parent.height; background: CediniaColors.nav_bg; border-width: 1px; border-color: CediniaColors.divider; HorizontalLayout { alignment: LayoutAlignment.stretch; padding-left: 0px; padding-right: 0px; NavItem { horizontal-stretch: 1.0; label: Translations.nav_home_text; tool: ActiveTool.Home; icon: @image-url("icons/home.svg"); } DynamicToolItem { horizontal-stretch: 1.0; } NavItem { horizontal-stretch: 1.0; label: Translations.nav_dirs_text; tool: ActiveTool.Directories; icon: @image-url("icons/folder.svg"); } NavItem { horizontal-stretch: 1.0; label: Translations.nav_settings_text; tool: ActiveTool.Settings; icon: @image-url("icons/more.svg"); } } } } ================================================ FILE: cedinia/ui/colors.slint ================================================ export global CediniaColors { out property bg_primary: #0d0d0d; out property bg_surface: #1a1a1a; out property bg_elevated: #252525; out property bg_card: #1e1e1e; out property accent: #c8960c; out property accent_light: #f0b429; out property accent_muted: #7a5900; out property text_primary: #f0ece0; out property text_secondary:#a89878; out property text_disabled: #505050; out property success: #4caf50; out property warning: #ff9800; out property danger: #f44336; out property divider: #333333; out property border: #3a3a3a; out property ripple: #c8960c40; out property row_normal: #1a1a1a; out property row_alt: #202020; out property row_selected: #3a2e00; out property row_header: #111111; out property nav_bg: #111111; out property nav_active: #c8960c; out property nav_inactive: #606060; } ================================================ FILE: cedinia/ui/common.slint ================================================ export enum SettingsTab { General, Tools, Diagnostics, } export enum ActiveTool { Home, DuplicateFiles, EmptyFolders, SimilarImages, EmptyFiles, TemporaryFiles, BigFiles, BrokenFiles, BadExtensions, SameMusic, BadNames, ExifRemover, Directories, Settings, } export enum ScanState { Idle, Scanning, Stopping, Stopped, Done, } export struct SimilarImageItem { full_path: string, name: string, size: string, val_str: [string], flat_idx: int, thumbnail: image, checked: bool, } export struct SimilarGroupCard { label: string, items: [SimilarImageItem], } export struct ProgressData { step_name: string, current_progress: int, all_progress: int, is_indeterminate: bool, } export struct FileEntry { checked: bool, is_header: bool, val_str: [string], val_int: [int], } export global FileEntryIdx { out property name_idx: 0; out property path_idx: 1; out property size_idx: 2; out property extra_idx: 4; } export struct DirectoryEntry { path: string, is_included: bool, } export struct VolumeEntry { path: string, label: string, is_included: bool, is_excluded: bool, } export struct CollectTestResult { volumes: int, files: int, folders: int, elapsed_ms: int, } ================================================ FILE: cedinia/ui/components.slint ================================================ import { CediniaColors } from "colors.slint"; import { FileEntryIdx } from "common.slint"; export component TouchButton { in property label; in property bg: CediniaColors.accent; in property fg: #000000; in property enabled: true; in property min_h: 52px; callback clicked; height: max(min_h, txt.preferred-height + 24px); min-width: txt.preferred-width + 32px; Rectangle { border-radius: 8px; background: ta.has-hover && root.enabled ? root.bg.brighter(0.15) : root.enabled ? root.bg : CediniaColors.bg_elevated; animate background { duration: 120ms; } ta := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } } txt := Text { text: root.label; color: root.enabled ? root.fg : CediniaColors.text_disabled; font-size: 16px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; } } } export component IconButton { in property icon; in property tooltip; in property enabled: true; in property tint: CediniaColors.text_primary; callback clicked; width: 48px; height: 48px; Rectangle { border-radius: 24px; background: ta.has-hover && root.enabled ? CediniaColors.ripple : transparent; animate background { duration: 120ms; } ta := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } } Image { source: root.icon; colorize: root.enabled ? root.tint : CediniaColors.text_disabled; width: 24px; height: 24px; horizontal-alignment: ImageHorizontalAlignment.center; vertical-alignment: ImageVerticalAlignment.center; } } } export component SectionHeader { in property title; in property subtitle: ""; height: subtitle == "" ? 44px : 60px; Rectangle { width: parent.width; height: parent.height; background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; VerticalLayout { padding-left: 16px; padding-right: 16px; alignment: LayoutAlignment.center; spacing: 2px; Text { text: root.title; color: CediniaColors.accent_light; font-size: 13px; font-weight: 700; letter-spacing: 0.8px; } if root.subtitle != "" : Text { text: root.subtitle; color: CediniaColors.text_secondary; font-size: 12px; } } } } export component FileRow { in property name; in property path; in property size; in property <[string]> val_str: []; in property checked: false; in property is_header: false; in property is_even: false; in property extra_as_line2: false; callback toggle_checked(); callback long_pressed(); callback open_item(); callback open_parent(); height: root.is_header ? 36px : 80px; Rectangle { background: root.is_header ? CediniaColors.row_header : root.checked ? CediniaColors.row_selected : root.is_even ? CediniaColors.row_alt : CediniaColors.row_normal; animate background { duration: 80ms; } if !root.is_header : Rectangle { x: 0; y: 0; width: 4px; height: parent.height; background: root.checked ? CediniaColors.accent : transparent; } ta := TouchArea { clicked => { if (!root.is_header) { root.toggle_checked(); } } pointer-event(e) => { if (!root.is_header) { if (e.button == PointerEventButton.middle && e.kind == PointerEventKind.up) { root.open_item(); } if (e.button == PointerEventButton.right && e.kind == PointerEventKind.up) { root.open_parent(); } } } } if root.is_header : HorizontalLayout { padding-left: 12px; padding-right: 12px; alignment: LayoutAlignment.start; spacing: 4px; Text { text: root.name; color: CediniaColors.text_secondary; font-size: 12px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; overflow: elide; } } if !root.is_header : HorizontalLayout { padding-left: 12px; padding-right: 8px; spacing: 8px; VerticalLayout { alignment: LayoutAlignment.center; Rectangle { width: 24px; height: 24px; border-radius: 12px; border-width: 2px; border-color: root.checked ? CediniaColors.accent : CediniaColors.border; background: root.checked ? CediniaColors.accent : transparent; vertical-stretch: 0.0; animate background { duration: 80ms; } } } VerticalLayout { alignment: LayoutAlignment.center; spacing: 2px; horizontal-stretch: 1.0; Text { text: root.name; color: CediniaColors.text_primary; font-size: 14px; font-weight: 600; overflow: elide; wrap: no-wrap; } if !root.extra_as_line2 : Text { text: root.path; color: CediniaColors.text_secondary; font-size: 11px; wrap: word-wrap; } if root.extra_as_line2 && root.path != "" : Text { text: root.path; color: CediniaColors.text_secondary; font-size: 11px; overflow: elide; wrap: no-wrap; } if !root.extra_as_line2 && root.val_str.length > FileEntryIdx.extra_idx && root.val_str[FileEntryIdx.extra_idx] != "" : Text { text: root.val_str[FileEntryIdx.extra_idx]; color: CediniaColors.text_disabled; font-size: 11px; overflow: elide; wrap: no-wrap; } if root.extra_as_line2 && root.val_str[FileEntryIdx.extra_idx] != "" : Text { text: root.val_str[FileEntryIdx.extra_idx]; color: CediniaColors.text_disabled; font-size: 11px; wrap: word-wrap; } } Text { text: root.size; color: CediniaColors.accent_light; font-size: 12px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; horizontal-alignment: TextHorizontalAlignment.right; min-width: 60px; } } } } export component Divider { height: 1px; Rectangle { background: CediniaColors.divider; } } export component StatusChip { in property label; in property chip_color: CediniaColors.accent_muted; in property text_color: CediniaColors.accent_light; height: 24px; min-width: txt.preferred-width + 16px; Rectangle { border-radius: 12px; background: root.chip_color; txt := Text { text: root.label; color: root.text_color; font-size: 11px; font-weight: 700; horizontal-alignment: center; vertical-alignment: center; } } } ================================================ FILE: cedinia/ui/directories_screen.slint ================================================ import { CediniaColors } from "colors.slint"; import { DirectoryEntry } from "common.slint"; import { AppState } from "app_state.slint"; import { Translations } from "translations.slint"; import { Divider, TouchButton } from "components.slint"; component DirRow { in property path; in property is_included; callback remove(); height: 56px; Rectangle { background: ta.has-hover ? CediniaColors.bg_elevated : CediniaColors.bg_surface; animate background { duration: 100ms; } HorizontalLayout { padding-left: 12px; padding-right: 8px; spacing: 8px; alignment: LayoutAlignment.start; Rectangle { width: 4px; height: 36px; border-radius: 2px; background: root.is_included ? CediniaColors.success : CediniaColors.danger; vertical-stretch: 0.0; } ta := TouchArea { horizontal-stretch: 1.0; } VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; Text { text: root.path; color: CediniaColors.text_primary; font-size: 13px; overflow: elide; } Text { text: root.is_included ? Translations.directories_include_header_text : Translations.directories_exclude_header_text; color: root.is_included ? CediniaColors.success : CediniaColors.danger; font-size: 11px; } } Rectangle { width: 40px; height: 40px; border-radius: 20px; background: rm_ta.has-hover ? CediniaColors.danger.with-alpha(0.2) : transparent; vertical-stretch: 0.0; rm_ta := TouchArea { clicked => { root.remove(); } } Text { text: "X"; color: CediniaColors.danger; font-size: 18px; horizontal-alignment: center; vertical-alignment: center; } } } Divider {} } } export component DirectoriesScreen { in property <[DirectoryEntry]> directories: []; VerticalLayout { height: parent.height; spacing: 0px; if directories.length == 0 : Rectangle {} Flickable { vertical-stretch: 1.0; viewport-height: max(dir_layout.preferred-height, self.height); dir_layout := VerticalLayout { spacing: 0px; if directories.length == 0 : Rectangle { height: 120px; VerticalLayout { alignment: LayoutAlignment.center; spacing: 8px; Text { text: "📂"; font-size: 36px; horizontal-alignment: TextHorizontalAlignment.center; color: CediniaColors.text_disabled; } Text { text: Translations.no_paths_text; color: CediniaColors.text_disabled; font-size: 13px; horizontal-alignment: TextHorizontalAlignment.center; } } } for entry in directories : DirRow { path: entry.path; is_included: entry.is_included; remove => { if (entry.is_included) { AppState.remove_include_dir(entry.path); } else { AppState.remove_exclude_dir(entry.path); } AppState.save_settings_now(); } } } } Rectangle {} Rectangle { vertical-stretch: 0.0; height: 64px; background: CediniaColors.bg_surface; border-width: 1px; border-color: CediniaColors.divider; HorizontalLayout { padding: 10px; spacing: 10px; alignment: LayoutAlignment.stretch; TouchButton { label: Translations.directories_add_text; horizontal-stretch: 1.0; bg: CediniaColors.success.with-alpha(0.8); fg: #000000; clicked => { AppState.pick_include_dir(); AppState.save_settings_now(); } } TouchButton { label: "- " + Translations.directories_exclude_header_text; horizontal-stretch: 1.0; bg: CediniaColors.danger.with-alpha(0.8); fg: #ffffff; clicked => { AppState.pick_exclude_dir(); AppState.save_settings_now(); } } TouchButton { label: Translations.directories_volume_header_text; horizontal-stretch: 1.0; bg: AppState.storage_volumes.length > 0 ? CediniaColors.accent_muted : CediniaColors.bg_elevated; fg: AppState.storage_volumes.length > 0 ? CediniaColors.accent_light : CediniaColors.text_primary; clicked => { if (AppState.storage_volumes.length > 0) { AppState.storage_volumes = []; } else { AppState.list_storage_volumes(); } } } } } } if AppState.storage_volumes.length > 0 : Rectangle { x: 0; y: root.height - self.height - 64px; width: root.width; height: min(vol_popup_layout.preferred-height, root.height - 64px - 8px); background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; drop-shadow-blur: 16px; drop-shadow-color: #00000066; vol_popup_layout := VerticalLayout { spacing: 0px; Rectangle { height: 44px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 16px; padding-right: 8px; spacing: 8px; Text { text: "💾 " + Translations.directories_volume_header_text; color: CediniaColors.accent_light; font-size: 15px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; horizontal-stretch: 1.0; } Rectangle { width: 36px; height: 36px; border-radius: 18px; vertical-stretch: 0.0; background: close_ta.has-hover ? CediniaColors.ripple : transparent; close_ta := TouchArea { clicked => { AppState.storage_volumes = []; } } Text { text: "X"; color: CediniaColors.text_secondary; font-size: 16px; horizontal-alignment: center; vertical-alignment: center; } } } } Rectangle { height: 1px; background: CediniaColors.divider; } Flickable { vertical-stretch: 1.0; viewport-height: max(vol_list.preferred-height, self.height); vol_list := VerticalLayout { spacing: 0px; for vol in AppState.storage_volumes : VerticalLayout { spacing: 0px; Rectangle { height: 64px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 12px; padding-right: 8px; padding-top: 8px; padding-bottom: 8px; spacing: 8px; alignment: LayoutAlignment.start; Rectangle { width: 4px; height: 40px; border-radius: 2px; vertical-stretch: 0.0; background: vol.is_included ? CediniaColors.success : vol.is_excluded ? CediniaColors.danger : CediniaColors.text_disabled; } VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; Text { text: vol.label != "" ? vol.label : vol.path; color: CediniaColors.text_primary; font-size: 13px; font-weight: 600; overflow: elide; } Text { text: vol.path; color: CediniaColors.text_secondary; font-size: 11px; overflow: elide; } } TouchButton { label: vol.is_included ? "X " + Translations.directories_include_header_text : "+ " + Translations.directories_volume_add_text; min_h: 32px; bg: vol.is_included ? CediniaColors.success.with-alpha(0.2) : CediniaColors.success.with-alpha(0.8); fg: vol.is_included ? CediniaColors.success : #000000; clicked => { if (vol.is_included) { AppState.remove_include_dir(vol.path); } else { AppState.add_include_dir(vol.path); } AppState.save_settings_now(); } } TouchButton { label: vol.is_excluded ? "X " + Translations.directories_exclude_header_text : "- " + Translations.directories_exclude_header_text; min_h: 32px; bg: vol.is_excluded ? CediniaColors.danger.with-alpha(0.2) : CediniaColors.danger.with-alpha(0.8); fg: vol.is_excluded ? CediniaColors.danger : #ffffff; clicked => { if (vol.is_excluded) { AppState.remove_exclude_dir(vol.path); } else { AppState.add_exclude_dir(vol.path); } AppState.save_settings_now(); } } } } Divider {} } } } } } } ================================================ FILE: cedinia/ui/home_screen.slint ================================================ import { CediniaColors } from "colors.slint"; import { ActiveTool } from "common.slint"; import { AppState } from "app_state.slint"; import { Translations } from "translations.slint"; component ToolCard { in property emoji; in property title; in property description; in property tool; in property accent: CediniaColors.accent; height: 88px; horizontal-stretch: 1.0; Rectangle { width: parent.width; height: parent.height; border-radius: 10px; background: CediniaColors.bg_card; border-width: 1px; border-color: ta.has-hover ? root.accent : CediniaColors.border; animate border-color { duration: 120ms; } ta := TouchArea { clicked => { AppState.active_tool = root.tool; AppState.last_scan_tool = root.tool; AppState.tool_changed(root.tool); } } HorizontalLayout { padding: 12px; spacing: 12px; alignment: LayoutAlignment.start; Rectangle { width: 48px; height: 48px; border-radius: 24px; background: root.accent.with-alpha(0.15); vertical-stretch: 0.0; Text { text: root.emoji; font-size: 24px; horizontal-alignment: center; vertical-alignment: center; } } VerticalLayout { alignment: LayoutAlignment.center; spacing: 4px; horizontal-stretch: 1.0; Text { text: root.title; color: CediniaColors.text_primary; font-size: 15px; font-weight: 700; } Text { text: root.description; color: CediniaColors.text_secondary; font-size: 12px; overflow: elide; } } Text { text: "›"; color: root.accent; font-size: 22px; vertical-alignment: TextVerticalAlignment.center; } } } } export component HomeScreen { VerticalLayout { horizontal-stretch: 1.0; Flickable { horizontal-stretch: 1.0; vertical-stretch: 1.0; cards := VerticalLayout { width: parent.width; padding: 12px; spacing: 10px; ToolCard { emoji: "📂"; title: Translations.tool_duplicate_files_text; description: Translations.home_dup_description_text; tool: ActiveTool.DuplicateFiles; } ToolCard { emoji: "📁"; title: Translations.tool_empty_folders_text; description: Translations.home_empty_folders_description_text; tool: ActiveTool.EmptyFolders; accent: #ff9800; } ToolCard { emoji: "🖼"; title: Translations.tool_similar_images_text; description: Translations.home_similar_images_description_text; tool: ActiveTool.SimilarImages; accent: #9c27b0; } ToolCard { emoji: "📄"; title: Translations.tool_empty_files_text; description: Translations.home_empty_files_description_text; tool: ActiveTool.EmptyFiles; accent: #607d8b; } ToolCard { emoji: "🗑"; title: Translations.tool_temporary_files_text; description: Translations.home_temp_files_description_text; tool: ActiveTool.TemporaryFiles; accent: #795548; } ToolCard { emoji: "📦"; title: Translations.tool_big_files_text; description: Translations.home_big_files_description_text; tool: ActiveTool.BigFiles; accent: #f44336; } ToolCard { emoji: "⚠"; title: Translations.tool_broken_files_text; description: Translations.home_broken_files_description_text; tool: ActiveTool.BrokenFiles; accent: #e91e63; } ToolCard { emoji: "🏷"; title: Translations.tool_bad_extensions_text; description: Translations.home_bad_extensions_description_text; tool: ActiveTool.BadExtensions; accent: #009688; } ToolCard { emoji: "🎵"; title: Translations.tool_same_music_text; description: Translations.home_same_music_description_text; tool: ActiveTool.SameMusic; accent: #3f51b5; } ToolCard { emoji: "✏"; title: Translations.tool_bad_names_text; description: Translations.home_bad_names_description_text; tool: ActiveTool.BadNames; accent: #ff5722; } ToolCard { emoji: "📷"; title: Translations.tool_exif_remover_text; description: Translations.home_exif_description_text; tool: ActiveTool.ExifRemover; accent: #00bcd4; } } } } } ================================================ FILE: cedinia/ui/main_window.slint ================================================ import { CediniaColors } from "colors.slint"; import { ActiveTool, DirectoryEntry, FileEntry, ScanState, SimilarGroupCard } from "common.slint"; import { AppState, BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, GeneralSettings, SameMusicSettings, SimilarImagesSettings } from "app_state.slint"; import { TopAppBar } from "top_bar.slint"; import { BottomNavBar } from "bottom_nav.slint"; import { ScanProgressBar } from "scan_progress.slint"; import { ResultsList } from "results_list.slint"; import { SimilarImagesGallery } from "similar_images_gallery.slint"; import { SettingsScreen } from "settings_screen.slint"; import { HomeScreen } from "home_screen.slint"; import { DirectoriesScreen } from "directories_screen.slint"; import { TouchButton } from "components.slint"; import { Translations } from "translations.slint"; export { AppState, GeneralSettings, DuplicateSettings, SimilarImagesSettings, BigFilesSettings, SameMusicSettings, BrokenFilesSettings, BadNamesSettings, Translations } export component MainWindow inherits Window { title: "Cedinia"; background: CediniaColors.bg_primary; min-width: 320px; min-height: 480px; preferred-width: 390px; preferred-height: 844px; in-out property <[FileEntry]> duplicate_files_model: []; in-out property <[FileEntry]> empty_folder_model: []; in-out property <[FileEntry]> similar_images_model: []; in-out property <[SimilarGroupCard]> similar_images_groups: []; in-out property <[FileEntry]> empty_files_model: []; in-out property <[FileEntry]> temporary_files_model: []; in-out property <[FileEntry]> big_files_model: []; in-out property <[FileEntry]> broken_files_model: []; in-out property <[FileEntry]> bad_extensions_model: []; in-out property <[FileEntry]> same_music_model: []; in-out property <[FileEntry]> bad_names_model: []; in-out property <[FileEntry]> exif_remover_model: []; in-out property <[DirectoryEntry]> directories_model: []; VerticalLayout { spacing: 0px; width: parent.width; padding-top: root.safe-area-insets.top; padding-bottom: max(AppState.inset_bottom, root.safe-area-insets.bottom); TopAppBar { title: AppState.active_tool == ActiveTool.Home ? Translations.app_name_text : AppState.active_tool == ActiveTool.DuplicateFiles ? Translations.tool_duplicate_files_text : AppState.active_tool == ActiveTool.EmptyFolders ? Translations.tool_empty_folders_text : AppState.active_tool == ActiveTool.SimilarImages ? Translations.tool_similar_images_text : AppState.active_tool == ActiveTool.EmptyFiles ? Translations.tool_empty_files_text : AppState.active_tool == ActiveTool.TemporaryFiles ? Translations.tool_temporary_files_text : AppState.active_tool == ActiveTool.BigFiles ? Translations.tool_big_files_text : AppState.active_tool == ActiveTool.BrokenFiles ? Translations.tool_broken_files_text : AppState.active_tool == ActiveTool.BadExtensions ? Translations.tool_bad_extensions_text : AppState.active_tool == ActiveTool.SameMusic ? Translations.tool_same_music_text : AppState.active_tool == ActiveTool.BadNames ? Translations.tool_bad_names_text : AppState.active_tool == ActiveTool.ExifRemover ? Translations.tool_exif_remover_text : AppState.active_tool == ActiveTool.Directories ? Translations.tool_directories_text : AppState.active_tool == ActiveTool.Settings ? Translations.tool_settings_text : Translations.app_name_text; } ScanProgressBar {} Rectangle { vertical-stretch: 1.0; background: CediniaColors.bg_primary; if AppState.active_tool == ActiveTool.Home : HomeScreen { height: parent.height; width: parent.width; } if AppState.active_tool == ActiveTool.Settings : SettingsScreen { height: parent.height; width: parent.width; } if AppState.active_tool == ActiveTool.Directories : DirectoriesScreen { height: parent.height; width: parent.width; directories: directories_model; } if AppState.active_tool == ActiveTool.DuplicateFiles : ResultsList { height: parent.height; width: parent.width; entries: duplicate_files_model; tool_title: Translations.tool_duplicate_files_text; tool_emoji: "📂"; is_grouped: true; } if AppState.active_tool == ActiveTool.EmptyFolders : ResultsList { height: parent.height; width: parent.width; entries: empty_folder_model; tool_title: Translations.tool_empty_folders_text; tool_emoji: "📁"; } if AppState.active_tool == ActiveTool.SimilarImages && !AppState.similar_images_gallery_mode : ResultsList { height: parent.height; width: parent.width; entries: similar_images_model; tool_title: Translations.tool_similar_images_text; tool_emoji: "🖼"; is_grouped: true; has_gallery_mode: true; has_size_select: true; has_resolution_select: true; } if AppState.active_tool == ActiveTool.SimilarImages && AppState.similar_images_gallery_mode : SimilarImagesGallery { height: parent.height; width: parent.width; groups: similar_images_groups; } if AppState.active_tool == ActiveTool.EmptyFiles : ResultsList { height: parent.height; width: parent.width; entries: empty_files_model; tool_title: Translations.tool_empty_files_text; tool_emoji: "📄"; } if AppState.active_tool == ActiveTool.TemporaryFiles : ResultsList { height: parent.height; width: parent.width; entries: temporary_files_model; tool_title: Translations.tool_temporary_files_text; tool_emoji: "🗑"; } if AppState.active_tool == ActiveTool.BigFiles : ResultsList { height: parent.height; width: parent.width; entries: big_files_model; tool_title: Translations.tool_big_files_text; tool_emoji: "📦"; } if AppState.active_tool == ActiveTool.BrokenFiles : ResultsList { height: parent.height; width: parent.width; entries: broken_files_model; tool_title: Translations.tool_broken_files_text; tool_emoji: "⚠"; extra_as_line2: true; } if AppState.active_tool == ActiveTool.BadExtensions : ResultsList { height: parent.height; width: parent.width; entries: bad_extensions_model; tool_title: Translations.tool_bad_extensions_text; tool_emoji: "🏷"; can_delete: false; can_rename: true; extra_as_line2: true; } if AppState.active_tool == ActiveTool.SameMusic : ResultsList { height: parent.height; width: parent.width; entries: same_music_model; tool_title: Translations.tool_same_music_text; tool_emoji: "🎵"; is_grouped: true; has_size_select: true; } if AppState.active_tool == ActiveTool.BadNames : ResultsList { height: parent.height; width: parent.width; entries: bad_names_model; tool_title: Translations.tool_bad_names_text; tool_emoji: "✏"; can_rename: true; extra_as_line2: true; } if AppState.active_tool == ActiveTool.ExifRemover : ResultsList { height: parent.height; width: parent.width; entries: exif_remover_model; tool_title: Translations.tool_exif_remover_text; tool_emoji: "📷"; can_delete: false; can_clean_exif: true; } } BottomNavBar {} } if AppState.scan_state == ScanState.Stopping : Rectangle { z: 150; background: #000000bb; Rectangle { width: min(parent.width - 48px, 300px); height: stopping_content.preferred-height + 48px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; border-radius: 16px; background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; drop-shadow-blur: 24px; drop-shadow-color: #00000088; stopping_content := VerticalLayout { padding: 24px; spacing: 12px; alignment: LayoutAlignment.center; Text { text: Translations.stopping_overlay_title_text; font-size: 17px; font-weight: 700; color: CediniaColors.warning; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: Translations.stopping_overlay_body_text; font-size: 13px; color: CediniaColors.text_secondary; horizontal-alignment: TextHorizontalAlignment.center; wrap: word-wrap; } } } } if AppState.show_permission_popup : Rectangle { z: 200; background: #00000099; TouchArea { clicked => { AppState.show_permission_popup = false; } } Rectangle { width: min(parent.width - 48px, 340px); height: perm_popup.preferred-height + 32px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; border-radius: 16px; background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; drop-shadow-blur: 24px; drop-shadow-color: #00000088; perm_popup := VerticalLayout { padding: 24px; spacing: 16px; Text { text: Translations.permission_title_text; color: CediniaColors.warning; font-size: 18px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: Translations.permission_body_text; color: CediniaColors.text_secondary; font-size: 13px; wrap: word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } HorizontalLayout { spacing: 12px; TouchButton { label: Translations.cancel_text; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; min_h: 48px; horizontal-stretch: 1.0; clicked => { AppState.show_permission_popup = false; } } TouchButton { label: Translations.grant_text; bg: CediniaColors.accent; fg: #000000; min_h: 48px; horizontal-stretch: 1.0; clicked => { AppState.request_storage_permission(); AppState.show_permission_popup = false; } } } } } } } ================================================ FILE: cedinia/ui/results_list.slint ================================================ import { CediniaColors } from "colors.slint"; import { FileEntry, FileEntryIdx, ScanState } from "common.slint"; import { AppState } from "app_state.slint"; import { Translations } from "translations.slint"; import { FileRow, StatusChip, TouchButton } from "components.slint"; import { ListView } from "std-widgets.slint"; export component ResultsList { in property <[FileEntry]> entries: []; in property tool_title: "Wyniki"; in property tool_emoji: "?"; in property can_delete: true; in property can_rename: false; in property can_clean_exif: false; in property is_grouped: false; in property has_gallery_mode: false; in property has_size_select: false; in property has_resolution_select: false; in property extra_as_line2: false; property menu_state: 0; VerticalLayout { spacing: 0px; if entries.length > 0 : Rectangle { height: 52px; background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; HorizontalLayout { padding-left: 12px; padding-right: 12px; padding-top: 8px; padding-bottom: 8px; spacing: 8px; if root.can_delete && AppState.selected_count > 0 : TouchButton { label: "X"; min_h: 36px; bg: CediniaColors.danger; fg: #ffffff; clicked => { AppState.delete_selected(); } } if root.can_clean_exif && AppState.selected_count > 0 : TouchButton { label: "Clean"; min_h: 36px; bg: CediniaColors.accent_muted; fg: CediniaColors.accent_light; clicked => { AppState.clean_exif_selected(); } } if root.can_rename && AppState.selected_count > 0 : TouchButton { label: "🏷"; min_h: 36px; bg: CediniaColors.accent_muted; fg: CediniaColors.accent_light; clicked => { AppState.rename_selected(); } } if AppState.selected_count > 0 : StatusChip { label: AppState.selected_count; chip_color: CediniaColors.accent_muted; text_color: CediniaColors.accent_light; vertical-stretch: 0.0; } Rectangle { horizontal-stretch: 1.0; } TouchButton { label: Translations.select_label_text; min_h: 36px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_secondary; clicked => { root.menu_state = 1; } } TouchButton { label: Translations.deselect_label_text; min_h: 36px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_secondary; clicked => { root.menu_state = 2; } } if root.has_gallery_mode : TouchButton { label: AppState.similar_images_gallery_mode ? Translations.list_label_text : Translations.gallery_label_text; min_h: 36px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_secondary; clicked => { AppState.similar_images_gallery_mode = !AppState.similar_images_gallery_mode; } } } } if entries.length > 0 : ListView { vertical-stretch: 1.0; for entry[idx] in entries : FileRow { name: entry.val_str[FileEntryIdx.name_idx]; path: entry.val_str[FileEntryIdx.path_idx]; size: entry.val_str[FileEntryIdx.size_idx]; val_str: entry.val_str; checked: entry.checked; is_header: entry.is_header; is_even: mod(idx, 2) == 0; extra_as_line2: root.extra_as_line2 && !entry.is_header; toggle_checked => { AppState.toggle_file_checked(idx); } 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]); } open_parent => { AppState.open_parent_folder(entry.val_str[FileEntryIdx.path_idx]); } } } if entries.length == 0 : Rectangle { vertical-stretch: 1.0; background: CediniaColors.bg_primary; VerticalLayout { alignment: LayoutAlignment.center; spacing: 12px; Text { text: root.tool_emoji; font-size: 56px; horizontal-alignment: TextHorizontalAlignment.center; color: CediniaColors.text_disabled; } Text { text: AppState.scan_state == ScanState.Scanning ? Translations.scanning_text : AppState.scan_state == ScanState.Stopping ? Translations.stopping_text : AppState.scan_state == ScanState.Done ? Translations.no_results_text : Translations.press_start_text; color: CediniaColors.text_disabled; font-size: 15px; horizontal-alignment: TextHorizontalAlignment.center; } if !AppState.storage_permission_granted : Rectangle { height: perm_vl.preferred-height + 24px; width: min(parent.width - 48px, 320px); x: (parent.width - self.width) / 2; border-radius: 10px; background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.warning.with-alpha(0.5); perm_vl := VerticalLayout { padding: 12px; spacing: 10px; Text { text: Translations.no_permission_scan_warning_text; color: CediniaColors.warning; font-size: 13px; wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } TouchArea { height: 40px; clicked => { AppState.request_storage_permission(); } Rectangle { border-radius: 8px; background: CediniaColors.accent_muted; Text { text: Translations.grant_text; color: CediniaColors.accent_light; font-size: 14px; font-weight: 600; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; } } } } } } } } if root.menu_state == 1 : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 50; background: #00000099; TouchArea {} Rectangle { x: (parent.width - self.width) / 2; y: (parent.height - popup_sel_vl.preferred-height - 24px) / 2; width: min(parent.width - 48px, 300px); height: popup_sel_vl.preferred-height + 24px; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000aa; clip: true; popup_sel_vl := VerticalLayout { padding: 12px; spacing: 6px; Text { text: Translations.selection_popup_title_text; color: CediniaColors.text_secondary; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; horizontal-alignment: TextHorizontalAlignment.center; } TouchButton { label: Translations.select_all_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_primary; horizontal-stretch: 1.0; clicked => { AppState.select_all(); root.menu_state = 0; } } if root.is_grouped : TouchButton { label: Translations.select_except_one_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_one(); root.menu_state = 0; } } if root.has_size_select : TouchButton { label: Translations.select_except_largest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_largest(); root.menu_state = 0; } } if root.has_size_select : TouchButton { label: Translations.select_except_smallest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_smallest(); root.menu_state = 0; } } if root.has_size_select : TouchButton { label: Translations.select_largest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_largest_per_group(); root.menu_state = 0; } } if root.has_size_select : TouchButton { label: Translations.select_smallest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_smallest_per_group(); root.menu_state = 0; } } if root.has_resolution_select : TouchButton { label: Translations.select_except_highest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_highest_resolution(); root.menu_state = 0; } } if root.has_resolution_select : TouchButton { label: Translations.select_except_lowest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_lowest_resolution(); root.menu_state = 0; } } if root.has_resolution_select : TouchButton { label: Translations.select_highest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_highest_resolution_per_group(); root.menu_state = 0; } } if root.has_resolution_select : TouchButton { label: Translations.select_lowest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_lowest_resolution_per_group(); root.menu_state = 0; } } TouchButton { label: Translations.invert_selection_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; horizontal-stretch: 1.0; clicked => { AppState.invert_selection(); root.menu_state = 0; } } TouchButton { label: Translations.close_text; min_h: 40px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_disabled; horizontal-stretch: 1.0; clicked => { root.menu_state = 0; } } } } } if root.menu_state == 2 : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 50; background: #00000099; TouchArea {} Rectangle { x: (parent.width - self.width) / 2; y: (parent.height - popup_desel_vl.preferred-height - 24px) / 2; width: min(parent.width - 48px, 300px); height: popup_desel_vl.preferred-height + 24px; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000aa; clip: true; popup_desel_vl := VerticalLayout { padding: 12px; spacing: 6px; Text { text: Translations.deselection_popup_title_text; color: CediniaColors.text_secondary; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; horizontal-alignment: TextHorizontalAlignment.center; } TouchButton { label: Translations.deselect_all_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_primary; horizontal-stretch: 1.0; clicked => { AppState.deselect_all(); root.menu_state = 0; } } if root.is_grouped : TouchButton { label: Translations.deselect_except_one_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.deselect_all_except_one(); root.menu_state = 0; } } TouchButton { label: Translations.close_text; min_h: 40px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_disabled; horizontal-stretch: 1.0; clicked => { root.menu_state = 0; } } } } } if AppState.confirm_popup_visible : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 40; background: #000000bb; TouchArea {} Rectangle { x: parent.width / 2 - self.width / 2; y: parent.height / 2 - self.height / 2; width: min(parent.width - 32px, 400px); height: confirm-card.preferred-height; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000cc; confirm-card := VerticalLayout { padding: 20px; spacing: 12px; Text { text: AppState.confirm_popup_message; color: CediniaColors.text_primary; font-size: 15px; font-weight: 600; wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } HorizontalLayout { spacing: 8px; TouchButton { label: Translations.cancel_text; horizontal-stretch: 1.0; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { AppState.confirm_popup_cancel(); } } TouchButton { label: (AppState.confirm_popup_action == "rename" || AppState.confirm_popup_action == "rename_bad_names") ? Translations.rename_text : Translations.delete_text; horizontal-stretch: 1.0; min_h: 44px; bg: (AppState.confirm_popup_action == "rename" || AppState.confirm_popup_action == "rename_bad_names") ? CediniaColors.accent_muted : CediniaColors.danger; fg: (AppState.confirm_popup_action == "rename" || AppState.confirm_popup_action == "rename_bad_names") ? CediniaColors.accent_light : #ffffff; clicked => { AppState.confirm_popup_ok(); } } } } } } if AppState.delete_errors_visible : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 30; background: #000000bb; TouchArea {} Rectangle { x: parent.width / 2 - self.width / 2; y: parent.height / 2 - self.height / 2; width: min(parent.width - 32px, 400px); height: err-card.preferred-height; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000cc; err-card := VerticalLayout { padding: 20px; spacing: 12px; Text { text: Translations.delete_errors_title_text; color: CediniaColors.warning; font-size: 15px; font-weight: 600; wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: AppState.delete_errors_text; color: CediniaColors.text_secondary; font-size: 11px; wrap: TextWrap.word-wrap; } TouchButton { label: Translations.ok_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { AppState.delete_errors_visible = false; } } } } } Rectangle { x: root.width - self.width - 16px; y: root.height - self.height - 16px; z: 20; width: 56px; height: 56px; border-radius: 28px; background: !AppState.storage_permission_granted ? CediniaColors.text_disabled : AppState.scan_state == ScanState.Scanning ? CediniaColors.warning : AppState.scan_state == ScanState.Stopping ? CediniaColors.warning.with-alpha(0.5) : CediniaColors.accent; drop-shadow-blur: 12px; drop-shadow-color: #00000088; animate background { duration: 200ms; } TouchArea { enabled: AppState.storage_permission_granted && AppState.scan_state != ScanState.Stopping; clicked => { if (AppState.scan_state == ScanState.Scanning) { AppState.stop_requested(); } else { AppState.scan_requested(); } } } Text { text: AppState.scan_state == ScanState.Scanning || AppState.scan_state == ScanState.Stopping ? "⏹" : "▶"; font-size: 22px; color: (!AppState.storage_permission_granted || AppState.scan_state == ScanState.Stopping) ? #00000066 : #000000; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; } } } ================================================ FILE: cedinia/ui/scan_progress.slint ================================================ import { CediniaColors } from "colors.slint"; import { AppState } from "app_state.slint"; import { ScanState } from "common.slint"; import { Translations } from "translations.slint"; export component ScanProgressBar { visible: AppState.scan_state == ScanState.Scanning; height: self.visible ? 52px : 0px; animate height { duration: 200ms; easing: ease; } Rectangle { background: CediniaColors.bg_surface; border-width: 1px; border-color: CediniaColors.divider; VerticalLayout { padding-left: 16px; padding-right: 16px; padding-top: 6px; padding-bottom: 6px; spacing: 4px; HorizontalLayout { Text { text: AppState.progress.step_name != "" ? AppState.progress.step_name : Translations.scanning_fallback_text; color: CediniaColors.text_secondary; font-size: 12px; horizontal-stretch: 1.0; overflow: elide; } if !AppState.progress.is_indeterminate : Text { text: AppState.progress.all_progress > 0 ? (AppState.progress.current_progress + " / " + AppState.progress.all_progress) : (AppState.progress.current_progress + " " + Translations.files_suffix_text); color: CediniaColors.accent_light; font-size: 12px; font-weight: 700; } if AppState.progress.is_indeterminate : Text { text: "…"; color: CediniaColors.text_disabled; font-size: 12px; font-weight: 700; } } if !AppState.progress.is_indeterminate && AppState.progress.all_progress > 0 : Rectangle { horizontal-stretch: 1.0; height: 4px; border-radius: 2px; background: CediniaColors.bg_elevated; Rectangle { x: 0; height: parent.height; width: parent.width * AppState.progress.current_progress / AppState.progress.all_progress; border-radius: 2px; background: CediniaColors.accent; animate width { duration: 300ms; easing: ease-out; } } } if AppState.progress.is_indeterminate : Rectangle { horizontal-stretch: 1.0; height: 4px; border-radius: 2px; background: CediniaColors.accent.with-alpha(0.35); } } } } ================================================ FILE: cedinia/ui/settings_components.slint ================================================ import { CediniaColors } from "colors.slint"; import { AppState } from "app_state.slint"; export component ToggleRow { in property label; in property description: ""; in-out property value; height: root.description == "" ? 56px : 72px; Rectangle { background: ta.has-hover ? CediniaColors.bg_elevated : transparent; animate background { duration: 100ms; } ta := TouchArea { clicked => { root.value = !root.value; AppState.save_settings_now(); } } HorizontalLayout { padding-left: 16px; padding-right: 16px; spacing: 12px; VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; Text { text: root.label; color: CediniaColors.text_primary; font-size: 15px; } if root.description != "" : Text { text: root.description; color: CediniaColors.text_secondary; font-size: 12px; wrap: word-wrap; } } VerticalLayout { alignment: LayoutAlignment.center; Rectangle { width: 50px; height: 28px; vertical-stretch: 0.0; border-radius: 14px; background: root.value ? CediniaColors.accent : CediniaColors.bg_elevated; animate background { duration: 150ms; } Rectangle { x: root.value ? parent.width - self.height - 2px : 2px; y: 2px; width: 24px; height: 24px; border-radius: 12px; background: #ffffff; animate x { duration: 150ms; easing: ease-out; } } } } } } } export component SegmentRow { in property label; in property description: ""; in property <[string]> options; in-out property selected; height: root.description == "" ? 72px : 88px; VerticalLayout { padding-left: 16px; padding-right: 16px; padding-top: 8px; padding-bottom: 8px; spacing: 6px; HorizontalLayout { VerticalLayout { horizontal-stretch: 1.0; alignment: LayoutAlignment.center; Text { text: root.label; color: CediniaColors.text_primary; font-size: 14px; } if root.description != "" : Text { text: root.description; color: CediniaColors.text_secondary; font-size: 11px; } } } HorizontalLayout { spacing: 4px; for opt[i] in root.options : Rectangle { horizontal-stretch: 1.0; height: 30px; border-radius: 6px; background: root.selected == i ? CediniaColors.accent_muted : CediniaColors.bg_elevated; border-width: 1px; border-color: root.selected == i ? CediniaColors.accent_light : CediniaColors.divider; animate background { duration: 100ms; } TouchArea { clicked => { root.selected = i; AppState.save_settings_now(); } } Text { text: opt; color: root.selected == i ? CediniaColors.accent_light : CediniaColors.text_secondary; font-size: 11px; font-weight: root.selected == i ? 700 : 400; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; overflow: elide; } } } } } export component ToolGroupHeader inherits Rectangle { in property label; in property emoji: ""; height: 40px; background: CediniaColors.accent_muted; HorizontalLayout { padding-left: 16px; padding-right: 16px; Text { text: (root.emoji != "" ? root.emoji + " " : "") + root.label; color: CediniaColors.accent_light; font-size: 12px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; letter-spacing: 0.8px; } } } export component CategoryLabel inherits Rectangle { in property label; height: 28px; background: CediniaColors.bg_primary; HorizontalLayout { padding-left: 16px; padding-right: 16px; Text { text: label; color: CediniaColors.text_disabled; font-size: 10px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; letter-spacing: 1px; } } } export component DropdownRow { // in property label; in property description: ""; in property <[string]> options; in-out property selected; callback changed_selection(int); height: description == "" ? 56px : 72px; Rectangle { background: row_ta.has-hover ? CediniaColors.bg_elevated : CediniaColors.bg_surface; animate background { duration: 100ms; } row_ta := TouchArea { clicked => { dropdown.show(); } } HorizontalLayout { padding-left: 16px; padding-right: 16px; spacing: 12px; alignment: LayoutAlignment.center; VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; // Text { // text: root.label; // color: CediniaColors.text_primary; // font-size: 15px; // } // if root.description != "" : Text { text: root.description; color: CediniaColors.text_disabled; font-size: 11px; wrap: word-wrap; } } Text { text: root.options[root.selected]; color: CediniaColors.accent_light; font-size: 13px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; overflow: TextOverflow.elide; max-width: 180px; } } } dropdown := PopupWindow { x: 16px; y: root.height; width: root.width - 32px; Rectangle { background: CediniaColors.bg_elevated; border-radius: 8px; border-width: 1px; border-color: CediniaColors.divider; drop-shadow-blur: 16px; drop-shadow-color: #000000aa; clip: true; VerticalLayout { for opt[i] in root.options : Rectangle { height: 48px; background: item_ta.has-hover ? CediniaColors.accent_muted : (i == root.selected ? CediniaColors.accent_muted.with-alpha(0.5) : transparent); animate background { duration: 80ms; } item_ta := TouchArea { clicked => { root.selected = i; root.changed_selection(i); AppState.save_settings_now(); dropdown.close(); } } HorizontalLayout { padding-left: 20px; padding-right: 16px; spacing: 8px; Text { text: opt; color: i == root.selected ? CediniaColors.accent_light : CediniaColors.text_primary; font-size: 15px; font-weight: i == root.selected ? 700 : 400; vertical-alignment: TextVerticalAlignment.center; horizontal-stretch: 1; } if i == root.selected : Text { text: "✓"; color: CediniaColors.accent_light; font-size: 14px; vertical-alignment: TextVerticalAlignment.center; } } } } } } } export component TextInputRow { in property label; in property placeholder: ""; in-out property value; height: 68px; Rectangle { background: CediniaColors.bg_surface; VerticalLayout { padding-left: 16px; padding-right: 16px; padding-top: 8px; padding-bottom: 8px; spacing: 4px; Text { text: root.label; color: CediniaColors.text_secondary; font-size: 11px; font-weight: 600; } Rectangle { height: 36px; border-radius: 6px; background: CediniaColors.bg_elevated; border-width: 1px; border-color: inp.has-focus ? CediniaColors.accent : CediniaColors.divider; animate border-color { duration: 120ms; } HorizontalLayout { padding-left: 10px; padding-right: 10px; inp := TextInput { text <=> root.value; color: CediniaColors.text_primary; font-size: 13px; vertical-alignment: TextVerticalAlignment.center; single-line: true; accepted => { AppState.save_settings_now(); } } } if inp.text == "" && !inp.has-focus : Text { x: 10px; y: 0px; height: parent.height; width: parent.width - 20px; text: root.placeholder; color: CediniaColors.text_disabled; font-size: 13px; vertical-alignment: TextVerticalAlignment.center; } } } } } ================================================ FILE: cedinia/ui/settings_screen.slint ================================================ import { CediniaColors } from "colors.slint"; import { Divider, TouchButton } from "components.slint"; import { AppState, BadNamesSettings, BigFilesSettings, BrokenFilesSettings, DuplicateSettings, GeneralSettings, SameMusicSettings, SimilarImagesSettings } from "app_state.slint"; import { CategoryLabel, DropdownRow, SegmentRow, TextInputRow, ToggleRow, ToolGroupHeader } from "settings_components.slint"; import { SettingsTab } from "common.slint"; import { Translations } from "translations.slint"; export component SettingsScreen { if AppState.collect_test_done : Rectangle { z: 100; background: #00000099; TouchArea { clicked => { AppState.collect_test_done = false; } } Rectangle { width: min(parent.width - 48px, 360px); height: popup_content.preferred-height + 32px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; border-radius: 16px; background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; drop-shadow-blur: 24px; drop-shadow-color: #00000088; popup_content := VerticalLayout { padding: 24px; spacing: 16px; Text { text: Translations.collect_test_title_text; color: CediniaColors.accent_light; font-size: 18px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.center; } Rectangle { height: 1px; background: CediniaColors.divider; } GridLayout { spacing: 8px; Row { Text { text: Translations.collect_test_volumes_text; color: CediniaColors.text_secondary; font-size: 14px; } Text { text: AppState.collect_test_result.volumes; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; } } Row { Text { text: Translations.collect_test_folders_text; color: CediniaColors.text_secondary; font-size: 14px; } Text { text: AppState.collect_test_result.folders; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; } } Row { Text { text: Translations.collect_test_files_text; color: CediniaColors.text_secondary; font-size: 14px; } Text { text: AppState.collect_test_result.files; color: CediniaColors.text_primary; font-size: 14px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.right; } } Row { Text { text: Translations.collect_test_time_text; color: CediniaColors.text_secondary; font-size: 14px; } 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; } } } TouchButton { label: Translations.ok_text; bg: CediniaColors.accent; fg: #000000; clicked => { AppState.collect_test_done = false; } } } } } property active_tab: SettingsTab.General; changed active_tab => { if (active_tab == SettingsTab.Diagnostics && !AppState.diag_refresh_running) { AppState.refresh_diag_cache_info(); } } VerticalLayout { spacing: 0px; Rectangle { height: 44px; background: CediniaColors.bg_surface; border-width: 1px; border-color: CediniaColors.divider; HorizontalLayout { spacing: 0px; alignment: LayoutAlignment.stretch; for tab_info in [ { label: Translations.settings_tab_general_text, tab: SettingsTab.General }, { label: Translations.settings_tab_tools_text, tab: SettingsTab.Tools }, { label: Translations.settings_tab_diagnostics_text, tab: SettingsTab.Diagnostics }, ] : Rectangle { horizontal-stretch: 1.0; background: transparent; TouchArea { clicked => { active_tab = tab_info.tab; } } VerticalLayout { Rectangle { vertical-stretch: 1.0; } Rectangle { height: 3px; background: active_tab == tab_info.tab ? CediniaColors.accent : transparent; animate background { duration: 150ms; } } } Text { text: tab_info.label; color: active_tab == tab_info.tab ? CediniaColors.accent_light : CediniaColors.text_secondary; font-size: 13px; font-weight: active_tab == tab_info.tab ? 700 : 400; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; animate color { duration: 150ms; } } } } } Flickable { vertical-stretch: 1.0; viewport-height: max(tab_content.preferred-height, self.height); tab_content := VerticalLayout { spacing: 0px; if active_tab == SettingsTab.General : VerticalLayout { spacing: 0px; CategoryLabel { label: Translations.settings_scan_label_text; } ToggleRow { label: Translations.settings_use_cache_text; description: Translations.settings_use_cache_desc_text; value <=> GeneralSettings.use_cache; } Divider {} ToggleRow { label: Translations.settings_ignore_hidden_text; description: Translations.settings_ignore_hidden_desc_text; value <=> GeneralSettings.ignore_hidden; } Divider {} CategoryLabel { label: Translations.settings_filters_label_text; } SegmentRow { label: Translations.settings_min_file_size_text; options: GeneralSettings.min_file_size_options; selected <=> GeneralSettings.min_file_size_idx; } Divider {} SegmentRow { label: Translations.settings_max_file_size_text; options: GeneralSettings.max_file_size_options; selected <=> GeneralSettings.max_file_size_idx; } Divider {} CategoryLabel { label: "LANGUAGE / JĘZYK"; } // This should not be translated DropdownRow { options: GeneralSettings.language_options; selected <=> GeneralSettings.language_idx; changed_selection(_) => { AppState.apply_language_change(); } } Divider {} CategoryLabel { label: Translations.settings_common_label_text; } TextInputRow { label: Translations.settings_excluded_items_text; placeholder: Translations.settings_excluded_items_placeholder_text; value <=> GeneralSettings.excluded_items; } Divider {} TextInputRow { label: Translations.settings_allowed_extensions_text; placeholder: Translations.settings_allowed_extensions_placeholder_text; value <=> GeneralSettings.allowed_extensions; } Divider {} TextInputRow { label: Translations.settings_excluded_extensions_text; placeholder: Translations.settings_excluded_extensions_placeholder_text; value <=> GeneralSettings.excluded_extensions; } Divider {} } if active_tab == SettingsTab.Tools : VerticalLayout { spacing: 0px; ToolGroupHeader { label: Translations.settings_duplicates_header_text; emoji: "📂"; } SegmentRow { label: Translations.settings_check_method_text; options: DuplicateSettings.check_method_options; selected <=> DuplicateSettings.check_method; } Divider {} SegmentRow { label: Translations.settings_hash_type_text; description: Translations.settings_hash_type_desc_text; options: DuplicateSettings.hash_type_options; selected <=> DuplicateSettings.hash_type; } Divider {} ToolGroupHeader { label: Translations.settings_similar_images_header_text; emoji: "🖼"; } SegmentRow { label: Translations.settings_similarity_preset_text; description: Translations.settings_similarity_desc_text; options: SimilarImagesSettings.similarity_preset_options; selected <=> SimilarImagesSettings.similarity_preset; } Divider {} SegmentRow { label: Translations.settings_hash_size_text; description: Translations.settings_hash_size_desc_text; options: SimilarImagesSettings.hash_size_options; selected <=> SimilarImagesSettings.hash_size_idx; } Divider {} SegmentRow { label: Translations.settings_hash_alg_text; options: SimilarImagesSettings.hash_alg_options; selected <=> SimilarImagesSettings.hash_alg_idx; } Divider {} SegmentRow { label: Translations.settings_image_filter_text; options: SimilarImagesSettings.image_filter_options; selected <=> SimilarImagesSettings.image_filter_idx; } Divider {} ToggleRow { label: Translations.settings_ignore_same_size_text; value <=> SimilarImagesSettings.ignore_same_size; } Divider {} ToolGroupHeader { label: Translations.settings_big_files_header_text; emoji: "📦"; } SegmentRow { label: Translations.settings_search_mode_text; options: BigFilesSettings.search_mode_options; selected <=> BigFilesSettings.search_mode_idx; } Divider {} SegmentRow { label: Translations.settings_file_count_text; options: BigFilesSettings.count_options; selected <=> BigFilesSettings.count_idx; } Divider {} ToolGroupHeader { label: Translations.settings_same_music_header_text; emoji: "🎵"; } CategoryLabel { label: Translations.settings_music_compare_tags_label_text; } ToggleRow { label: Translations.settings_music_title_text; value <=> SameMusicSettings.title; } Divider {} ToggleRow { label: Translations.settings_music_artist_text; value <=> SameMusicSettings.artist; } Divider {} ToggleRow { label: Translations.settings_music_year_text; value <=> SameMusicSettings.year; } Divider {} ToggleRow { label: Translations.settings_music_length_text; value <=> SameMusicSettings.length; } Divider {} ToggleRow { label: Translations.settings_music_genre_text; value <=> SameMusicSettings.genre; } Divider {} ToggleRow { label: Translations.settings_music_bitrate_text; value <=> SameMusicSettings.bitrate; } Divider {} ToggleRow { label: Translations.settings_music_approx_text; value <=> SameMusicSettings.approximate; } Divider {} CategoryLabel { label: Translations.settings_music_check_method_text; } SegmentRow { label: Translations.settings_music_check_method_text; options: SameMusicSettings.check_method_options; selected <=> SameMusicSettings.check_method_idx; } Divider {} ToolGroupHeader { label: Translations.settings_broken_files_header_text; emoji: "⚠"; } CategoryLabel { label: Translations.settings_broken_files_types_label_text; } ToggleRow { label: Translations.settings_broken_image_text; value <=> BrokenFilesSettings.check_image; } Divider {} ToggleRow { label: Translations.settings_broken_audio_text; value <=> BrokenFilesSettings.check_audio; } Divider {} ToggleRow { label: Translations.settings_broken_pdf_text; value <=> BrokenFilesSettings.check_pdf; } Divider {} ToggleRow { label: Translations.settings_broken_archive_text; value <=> BrokenFilesSettings.check_archive; } Divider {} ToolGroupHeader { label: Translations.settings_bad_names_header_text; emoji: "✏"; } CategoryLabel { label: Translations.settings_bad_names_checks_label_text; } ToggleRow { label: Translations.settings_bad_names_uppercase_ext_text; value <=> BadNamesSettings.uppercase_extension; } Divider {} ToggleRow { label: Translations.settings_bad_names_emoji_text; value <=> BadNamesSettings.emoji_used; } Divider {} ToggleRow { label: Translations.settings_bad_names_space_text; value <=> BadNamesSettings.space_at_start_or_end; } Divider {} ToggleRow { label: Translations.settings_bad_names_non_ascii_text; value <=> BadNamesSettings.non_ascii_graphical; } Divider {} ToggleRow { label: Translations.settings_bad_names_duplicated_text; value <=> BadNamesSettings.remove_duplicated_non_alpha; } Divider {} } if active_tab == SettingsTab.Diagnostics : VerticalLayout { spacing: 0px; CategoryLabel { label: Translations.diagnostics_header_text; } // Storage permission row Rectangle { height: 72px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; spacing: 12px; VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; Text { text: Translations.permission_title_text; color: CediniaColors.text_primary; font-size: 15px; } Text { text: AppState.storage_permission_granted ? "✅ " + Translations.grant_text : "❌ " + Translations.no_permission_scan_warning_text; color: AppState.storage_permission_granted ? CediniaColors.success : CediniaColors.danger; font-size: 12px; } } if !AppState.storage_permission_granted : TouchButton { label: Translations.grant_text; bg: CediniaColors.warning; fg: #000000; min_h: 44px; clicked => { AppState.request_storage_permission(); } } } } Divider {} // Collect test row Rectangle { height: 72px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; spacing: 12px; VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; Text { text: Translations.diagnostics_collect_test_text; color: CediniaColors.text_primary; font-size: 15px; } Text { text: Translations.diagnostics_collect_test_desc_text; color: CediniaColors.text_secondary; font-size: 12px; } } if !AppState.collect_test_running : TouchButton { label: Translations.diagnostics_collect_test_run_text; bg: CediniaColors.accent; fg: #000000; min_h: 44px; clicked => { AppState.run_collect_test(); } } if AppState.collect_test_running : TouchButton { label: Translations.diagnostics_collect_test_stop_text; bg: CediniaColors.warning; fg: #000000; min_h: 44px; clicked => { AppState.stop_collect_test(); } } } } Divider {} CategoryLabel { label: Translations.cache_label_text; } // Cache sizes + refresh row Rectangle { height: 60px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; spacing: 8px; VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; Text { text: Translations.diagnostics_thumbnails_text + ": " + AppState.diag_thumbnails_size; color: CediniaColors.text_primary; font-size: 14px; } Text { text: Translations.diagnostics_app_cache_text + ": " + AppState.diag_app_cache_size; color: CediniaColors.text_secondary; font-size: 12px; } } TouchButton { label: AppState.diag_refresh_running ? "..." : Translations.diagnostics_refresh_text; min_h: 36px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_secondary; enabled: !AppState.diag_refresh_running; clicked => { AppState.refresh_diag_cache_info(); } } } } Divider {} // Clear cache buttons row Rectangle { height: 56px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 16px; padding-right: 16px; padding-top: 8px; padding-bottom: 8px; spacing: 8px; TouchButton { label: Translations.diagnostics_clear_thumbnails_text; horizontal-stretch: 1.0; min_h: 40px; bg: CediniaColors.danger.with-alpha(0.7); fg: #ffffff; clicked => { AppState.clear_thumbnails_cache(); } } TouchButton { label: Translations.diagnostics_clear_cache_text; horizontal-stretch: 1.0; min_h: 40px; bg: CediniaColors.danger.with-alpha(0.5); fg: #ffffff; clicked => { AppState.clear_app_cache(); } } } } Divider {} CategoryLabel { label: Translations.about_app_label_text; } // Logo – full width, nothing beside it Rectangle { height: 140px; background: CediniaColors.bg_surface; Image { source: @image-url("../icons/logo.svg"); width: min(parent.width - 32px, 260px); height: 120px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; image-fit: ImageFit.contain; } } Divider {} // App name and description Rectangle { background: CediniaColors.bg_surface; height: info_vl.preferred-height + 24px; info_vl := VerticalLayout { padding-left: 16px; padding-right: 16px; padding-top: 12px; padding-bottom: 12px; spacing: 4px; alignment: LayoutAlignment.center; Text { text: "Cedinia 11.0.1"; color: CediniaColors.text_primary; font-size: 18px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: Translations.app_subtitle_text; color: CediniaColors.text_secondary; font-size: 13px; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: Translations.app_license_text; color: CediniaColors.text_disabled; font-size: 11px; horizontal-alignment: TextHorizontalAlignment.center; } } } Divider {} // Links row Rectangle { height: 56px; background: CediniaColors.bg_surface; HorizontalLayout { padding-left: 12px; padding-right: 12px; padding-top: 8px; padding-bottom: 8px; spacing: 8px; TouchButton { label: Translations.about_repo_text; horizontal-stretch: 1.0; min_h: 40px; bg: CediniaColors.accent_muted; fg: CediniaColors.accent_light; clicked => { AppState.open_url("https://github.com/qarmin/czkawka"); } } TouchButton { label: Translations.about_translate_text; horizontal-stretch: 1.0; min_h: 40px; bg: CediniaColors.accent_muted; fg: CediniaColors.accent_light; clicked => { AppState.open_url("https://crowdin.com/project/czkawka"); } } TouchButton { label: Translations.about_donate_text; horizontal-stretch: 1.0; min_h: 40px; bg: CediniaColors.accent_muted; fg: CediniaColors.accent_light; clicked => { AppState.open_url("https://github.com/sponsors/qarmin"); } } } } Divider {} } } } } } ================================================ FILE: cedinia/ui/similar_images_gallery.slint ================================================ import { CediniaColors } from "colors.slint"; import { ScanState, SimilarGroupCard } from "common.slint"; import { AppState } from "app_state.slint"; import { TouchButton } from "components.slint"; import { ListView } from "std-widgets.slint"; import { Translations } from "translations.slint"; component GalleryImageCell { in property thumbnail; in property img_name; in property img_size; in property <[string]> img_val_str: []; in property flat_idx; in property checked: false; width: 170px; ta := TouchArea { clicked => { AppState.toggle_file_checked(root.flat_idx); } } VerticalLayout { padding: 4px; spacing: 4px; Rectangle { height: 140px; border-radius: 8px; clip: true; border-width: root.checked ? 3px : 0px; border-color: CediniaColors.accent; background: CediniaColors.bg_elevated; animate border-width { duration: 100ms; } Image { width: parent.width; height: parent.height; source: root.thumbnail; image-fit: ImageFit.cover; } Rectangle { border-radius: 8px; background: ta.pressed ? #ffffff22 : transparent; } if root.checked : Rectangle { x: parent.width - 30px; y: 6px; width: 24px; height: 24px; border-radius: 12px; background: CediniaColors.accent; z: 5; Text { text: Translations.ok_text; color: #000000; font-size: 11px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; } } } Text { text: root.img_name; font-size: 11px; color: root.checked ? CediniaColors.accent_light : CediniaColors.text_primary; overflow: TextOverflow.elide; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: root.img_size; font-size: 10px; color: CediniaColors.text_secondary; horizontal-alignment: TextHorizontalAlignment.center; } if root.img_val_str.length > 0 && root.img_val_str[0] != "" : Text { text: root.img_val_str[0]; font-size: 10px; color: CediniaColors.text_secondary; horizontal-alignment: TextHorizontalAlignment.center; overflow: TextOverflow.elide; } } } component GalleryToggleBar { height: 52px; in-out property menu_state: 0; Rectangle { background: CediniaColors.bg_elevated; border-width: 1px; border-color: CediniaColors.divider; HorizontalLayout { padding-left: 12px; padding-right: 12px; padding-top: 8px; padding-bottom: 8px; spacing: 8px; if AppState.selected_count > 0 : TouchButton { label: Translations.gallery_delete_button_text; min_h: 36px; bg: CediniaColors.danger; fg: #ffffff; clicked => { AppState.request_gallery_delete(); } } if AppState.selected_count > 0 : Rectangle { width: selected_chip.preferred-width + 16px; height: 28px; border-radius: 14px; background: CediniaColors.accent_muted; vertical-stretch: 0.0; y: (parent.height - self.height) / 2; selected_chip := Text { text: "[" + AppState.selected_count + "]"; color: CediniaColors.accent_light; font-size: 11px; font-weight: 700; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; } } Rectangle { horizontal-stretch: 1.0; } TouchButton { label: Translations.select_label_text; min_h: 36px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { root.menu_state = 1; } } TouchButton { label: Translations.deselect_label_text; min_h: 36px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { root.menu_state = 2; } } TouchButton { label: Translations.list_label_text; min_h: 36px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { AppState.similar_images_gallery_mode = false; } } } } } export component SimilarImagesGallery { in property <[SimilarGroupCard]> groups: []; VerticalLayout { spacing: 0px; toggle_bar := GalleryToggleBar {} if groups.length == 0 : Rectangle { vertical-stretch: 1.0; background: CediniaColors.bg_primary; VerticalLayout { alignment: LayoutAlignment.center; spacing: 12px; Text { text: AppState.scan_state == ScanState.Scanning ? Translations.scanning_fallback_text : Translations.no_results_text; color: CediniaColors.text_disabled; font-size: 15px; horizontal-alignment: TextHorizontalAlignment.center; } } } if groups.length > 0 : ListView { vertical-stretch: 1.0; for group in groups : VerticalLayout { padding-left: 8px; padding-right: 8px; padding-top: 8px; padding-bottom: 4px; spacing: 6px; Rectangle { height: 34px; border-radius: 8px; background: CediniaColors.accent.with-alpha(0.85); Text { x: 12px; y: 0px; height: parent.height; width: parent.width - 24px; text: group.label; color: #000000; font-size: 13px; font-weight: 700; vertical-alignment: TextVerticalAlignment.center; overflow: TextOverflow.elide; } } Flickable { height: 210px; interactive: true; viewport-width: max(root.width - 16px, img-row.min-width); img-row := HorizontalLayout { alignment: LayoutAlignment.start; spacing: 6px; for img in group.items : GalleryImageCell { thumbnail: img.thumbnail; img_name: img.name; img_size: img.size; img_val_str: img.val_str; flat_idx: img.flat_idx; checked: img.checked; } } } Rectangle { height: 1px; background: CediniaColors.divider; } } } } if toggle_bar.menu_state == 1 : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 50; background: #00000099; TouchArea {} Rectangle { x: (parent.width - self.width) / 2; y: (parent.height - popup_sel_vl.preferred-height - 24px) / 2; width: min(parent.width - 48px, 300px); height: popup_sel_vl.preferred-height + 24px; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000aa; clip: true; popup_sel_vl := VerticalLayout { padding: 12px; spacing: 6px; Text { text: Translations.selection_popup_title_text; color: CediniaColors.text_secondary; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; horizontal-alignment: TextHorizontalAlignment.center; } TouchButton { label: Translations.select_all_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_primary; horizontal-stretch: 1.0; clicked => { AppState.select_all(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_except_one_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_one(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_except_largest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_largest(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_except_smallest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_smallest(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_largest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_largest_per_group(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_smallest_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_smallest_per_group(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_except_highest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_highest_resolution(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_except_lowest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_all_except_lowest_resolution(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_highest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_highest_resolution_per_group(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.select_lowest_res_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.select_lowest_resolution_per_group(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.invert_selection_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; horizontal-stretch: 1.0; clicked => { AppState.invert_selection(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.close_text; min_h: 40px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_disabled; horizontal-stretch: 1.0; clicked => { toggle_bar.menu_state = 0; } } } } } if toggle_bar.menu_state == 2 : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 50; background: #00000099; TouchArea {} Rectangle { x: (parent.width - self.width) / 2; y: (parent.height - popup_desel_vl.preferred-height - 24px) / 2; width: min(parent.width - 48px, 300px); height: popup_desel_vl.preferred-height + 24px; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000aa; clip: true; popup_desel_vl := VerticalLayout { padding: 12px; spacing: 6px; Text { text: Translations.deselection_popup_title_text; color: CediniaColors.text_secondary; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; horizontal-alignment: TextHorizontalAlignment.center; } TouchButton { label: Translations.deselect_all_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_primary; horizontal-stretch: 1.0; clicked => { AppState.deselect_all(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.deselect_except_one_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.accent_light; horizontal-stretch: 1.0; clicked => { AppState.deselect_all_except_one(); toggle_bar.menu_state = 0; } } TouchButton { label: Translations.close_text; min_h: 40px; bg: CediniaColors.bg_elevated; fg: CediniaColors.text_disabled; horizontal-stretch: 1.0; clicked => { toggle_bar.menu_state = 0; } } } } } if AppState.gallery_delete_popup_visible : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 25; background: #000000bb; TouchArea {} Rectangle { x: parent.width / 2 - self.width / 2; y: parent.height / 2 - self.height / 2; width: min(parent.width - 32px, 380px); height: card.preferred-height; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 28px; drop-shadow-color: #000000cc; card := VerticalLayout { padding: 24px; spacing: 14px; Text { text: AppState.gallery_delete_message; color: CediniaColors.text_primary; font-size: 16px; font-weight: 600; wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } if AppState.gallery_delete_warning != "" : Text { text: AppState.gallery_delete_warning; color: CediniaColors.warning; font-size: 13px; wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } HorizontalLayout { spacing: 10px; TouchButton { label: Translations.gallery_back_text; horizontal-stretch: 1.0; min_h: 48px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { AppState.gallery_delete_popup_visible = false; } } TouchButton { label: Translations.gallery_confirm_delete_text; horizontal-stretch: 1.0; min_h: 48px; bg: CediniaColors.danger; fg: #ffffff; clicked => { AppState.confirm_gallery_delete(); } } } } } } if AppState.delete_running : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 26; background: #000000bb; TouchArea {} Rectangle { x: parent.width / 2 - self.width / 2; y: parent.height / 2 - self.height / 2; width: min(parent.width - 32px, 340px); height: prog-card.preferred-height; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000cc; prog-card := VerticalLayout { padding: 24px; spacing: 14px; Text { text: Translations.deleting_files_text; color: CediniaColors.text_primary; font-size: 16px; font-weight: 600; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: AppState.delete_progress_text; color: CediniaColors.text_secondary; font-size: 13px; horizontal-alignment: TextHorizontalAlignment.center; } TouchButton { label: Translations.stop_text; min_h: 44px; bg: CediniaColors.warning; fg: #000000; clicked => { AppState.delete_stop_requested(); } } } } } if AppState.delete_errors_visible : Rectangle { x: 0; y: 0; width: root.width; height: root.height; z: 27; background: #000000bb; TouchArea {} Rectangle { x: parent.width / 2 - self.width / 2; y: parent.height / 2 - self.height / 2; width: min(parent.width - 32px, 400px); height: err-card.preferred-height; border-radius: 14px; background: CediniaColors.bg_elevated; drop-shadow-blur: 24px; drop-shadow-color: #000000cc; err-card := VerticalLayout { padding: 20px; spacing: 12px; Text { text: Translations.delete_errors_title_text; color: CediniaColors.warning; font-size: 15px; font-weight: 600; wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; } Text { text: AppState.delete_errors_text; color: CediniaColors.text_secondary; font-size: 11px; wrap: TextWrap.word-wrap; } TouchButton { label: Translations.ok_text; min_h: 44px; bg: CediniaColors.bg_surface; fg: CediniaColors.text_secondary; clicked => { AppState.delete_errors_visible = false; } } } } } Rectangle { x: root.width - self.width - 16px; y: root.height - self.height - 16px; z: 20; width: 56px; height: 56px; border-radius: 28px; background: AppState.scan_state == ScanState.Scanning ? CediniaColors.warning : AppState.scan_state == ScanState.Stopping ? CediniaColors.warning.with-alpha(0.5) : CediniaColors.accent; drop-shadow-blur: 12px; drop-shadow-color: #00000088; animate background { duration: 200ms; } TouchArea { enabled: AppState.scan_state != ScanState.Stopping; clicked => { if (AppState.scan_state == ScanState.Scanning) { AppState.stop_requested(); } else { AppState.scan_requested(); } } } Text { text: AppState.scan_state == ScanState.Scanning || AppState.scan_state == ScanState.Stopping ? "■" : "▶"; font-size: 22px; color: AppState.scan_state == ScanState.Stopping ? #00000066 : #000000; horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; } } } ================================================ FILE: cedinia/ui/top_bar.slint ================================================ import { CediniaColors } from "colors.slint"; import { AppState } from "app_state.slint"; import { ScanState } from "common.slint"; export component TopAppBar { in property title: "Cedinia"; height: 56px; Rectangle { background: CediniaColors.bg_elevated; drop-shadow-color: #00000066; drop-shadow-blur: 6px; drop-shadow-offset-y: 2px; HorizontalLayout { padding-left: 16px; padding-right: 12px; padding-top: 8px; padding-bottom: 8px; spacing: 10px; alignment: LayoutAlignment.start; Image { source: @image-url("../icons/logo.svg"); width: 48px; height: 48px; horizontal-alignment: ImageHorizontalAlignment.center; vertical-alignment: ImageVerticalAlignment.center; } VerticalLayout { alignment: LayoutAlignment.center; horizontal-stretch: 1.0; spacing: 2px; Text { text: root.title; color: CediniaColors.accent_light; font-size: 18px; font-weight: 700; } Text { text: AppState.status_message; color: CediniaColors.text_secondary; font-size: 11px; overflow: elide; } } if AppState.scan_state == ScanState.Scanning : Rectangle { width: 10px; height: 10px; border-radius: 5px; background: CediniaColors.accent; vertical-stretch: 0.0; animate opacity { duration: 800ms; iteration-count: -1; easing: ease-in-out; } } } } } ================================================ FILE: cedinia/ui/translations.slint ================================================ export global Translations { // App / top bar titles in-out property app_name_text: "Cedinia"; in-out property tool_duplicate_files_text: "Duplicates"; in-out property tool_empty_folders_text: "Empty Folders"; in-out property tool_similar_images_text: "Similar Images"; in-out property tool_empty_files_text: "Empty Files"; in-out property tool_temporary_files_text: "Temporary Files"; in-out property tool_big_files_text: "Biggest Files"; in-out property tool_broken_files_text: "Broken Files"; in-out property tool_bad_extensions_text: "Bad Extensions"; in-out property tool_same_music_text: "Duplicate Music"; in-out property tool_bad_names_text: "Bad Names"; in-out property tool_exif_remover_text: "EXIF Data"; in-out property tool_directories_text: "Directories"; in-out property tool_settings_text: "Settings"; // Home screen tool cards in-out property home_dup_description_text: "Find files with identical content"; in-out property home_empty_folders_description_text: "Directories without content"; in-out property home_similar_images_description_text: "Find visually similar photos"; in-out property home_empty_files_description_text: "Files with zero size"; in-out property home_temp_files_description_text: "Temporary and cached files"; in-out property home_big_files_description_text: "Biggest/Smallest files on disk"; in-out property home_broken_files_description_text: "PDF, audio, images, archives"; in-out property home_bad_extensions_description_text: "Files with incorrect extension"; in-out property home_same_music_description_text: "Similar audio files by tags"; in-out property home_bad_names_description_text: "Files with problematic characters in name"; in-out property home_exif_description_text: "Images with EXIF metadata"; // Results list in-out property scanning_text: "Scanning…"; in-out property stopping_text: "Stopping…"; in-out property no_results_text: "No results"; in-out property press_start_text: "Press START to scan"; in-out property select_label_text: "Sel."; in-out property deselect_label_text: "Des."; in-out property list_label_text: "List"; in-out property gallery_label_text: "Gal."; // Selection popup in-out property selection_popup_title_text: "Select"; in-out property select_all_text: "Select all"; in-out property select_except_one_text: "Select except one"; in-out property select_except_largest_text: "Select except largest"; in-out property select_except_smallest_text: "Select except smallest"; in-out property select_largest_text: "Select largest"; in-out property select_smallest_text: "Select smallest"; in-out property select_except_highest_res_text: "Select except highest resolution"; in-out property select_except_lowest_res_text: "Select except lowest resolution"; in-out property select_highest_res_text: "Select highest resolution"; in-out property select_lowest_res_text: "Select lowest resolution"; in-out property invert_selection_text: "Invert selection"; in-out property close_text: "Close"; // Deselection popup in-out property deselection_popup_title_text: "Deselect"; in-out property deselect_all_text: "Deselect all"; in-out property deselect_except_one_text: "Deselect except one"; // Confirm popup in-out property cancel_text: "Cancel"; in-out property delete_text: "Delete"; in-out property rename_text: "Rename"; // Delete errors popup in-out property delete_errors_title_text: "Failed to delete some files:"; in-out property ok_text: "OK"; // Stopping overlay in-out property stopping_overlay_title_text: "■ Stopping"; in-out property stopping_overlay_body_text: "Finishing current scan…\nPlease wait."; // Permission popup in-out property permission_title_text: "🔒 File Access"; in-out property permission_body_text: "To scan files, the app needs access to device storage. Without this permission, scanning will not be possible."; in-out property grant_text: "Grant"; in-out property no_permission_scan_warning_text: "No file permission – grant access to scan"; // Settings screen in-out property settings_tab_general_text: "General"; in-out property settings_tab_tools_text: "Tools"; in-out property settings_tab_diagnostics_text: "Info"; in-out property settings_use_cache_text: "Use cache"; in-out property settings_use_cache_desc_text: "Speeds up subsequent scans (hash/images)"; in-out property settings_ignore_hidden_text: "Ignore hidden files"; in-out property settings_ignore_hidden_desc_text: "Files and folders starting with '.'"; in-out property settings_scan_label_text: "SCANNING"; in-out property settings_filters_label_text: "FILTERS (all tools)"; in-out property settings_min_file_size_text: "Min. file size"; in-out property settings_excluded_items_text: "EXCLUDED ITEMS (glob patterns, comma-separated)"; in-out property settings_excluded_items_placeholder_text: "e.g. *.tmp, */.git/*, */node_modules/*"; in-out property settings_allowed_extensions_text: "ALLOWED EXTENSIONS (empty = all)"; in-out property settings_allowed_extensions_placeholder_text: "e.g. jpg, png, mp4"; in-out property settings_excluded_extensions_text: "EXCLUDED EXTENSIONS"; in-out property settings_excluded_extensions_placeholder_text: "e.g. bak, tmp, log"; // Settings — Tools section labels in-out property settings_duplicates_header_text: "DUPLICATES"; in-out property settings_check_method_label_text: "COMPARISON METHOD"; in-out property settings_check_method_text: "Method"; in-out property settings_hash_type_label_text: "HASH TYPE"; in-out property settings_hash_type_text: "Hash type"; in-out property settings_similar_images_header_text: "SIMILAR IMAGES"; in-out property settings_similarity_preset_text: "Similarity threshold"; in-out property settings_hash_size_text: "Hash size"; in-out property settings_hash_alg_text: "Hash algorithm"; in-out property settings_image_filter_text: "Resize filter"; in-out property settings_ignore_same_size_text: "Ignore images with the same dimensions"; in-out property settings_big_files_header_text: "BIGGEST FILES"; in-out property settings_search_mode_text: "Search mode"; in-out property settings_file_count_text: "File count"; in-out property settings_same_music_header_text: "DUPLICATE MUSIC"; in-out property settings_music_check_method_text: "Comparison mode"; in-out property settings_music_compare_tags_label_text: "COMPARED TAGS"; in-out property settings_music_title_text: "Title"; in-out property settings_music_artist_text: "Artist"; in-out property settings_music_year_text: "Year"; in-out property settings_music_length_text: "Length"; in-out property settings_music_genre_text: "Genre"; in-out property settings_music_bitrate_text: "Bitrate"; in-out property settings_music_approx_text: "Approximate tag comparison"; in-out property settings_broken_files_header_text: "BROKEN FILES"; in-out property settings_broken_files_types_label_text: "CHECKED TYPES"; in-out property settings_broken_audio_text: "Audio"; in-out property settings_broken_pdf_text: "PDF"; in-out property settings_broken_archive_text: "Archive"; in-out property settings_broken_image_text: "Image"; in-out property settings_bad_names_header_text: "BAD NAMES"; in-out property settings_bad_names_checks_label_text: "CHECKS"; in-out property settings_bad_names_uppercase_ext_text: "Uppercase extension"; in-out property settings_bad_names_emoji_text: "Emoji in name"; in-out property settings_bad_names_space_text: "Spaces at start/end"; in-out property settings_bad_names_non_ascii_text: "Non-ASCII characters"; in-out property settings_bad_names_duplicated_text: "Duplicated characters"; // Settings — General: new filter strings in-out property settings_max_file_size_text: "Max. file size"; in-out property settings_language_text: "Language"; in-out property settings_language_restart_text: "Requires app restart"; in-out property settings_common_label_text: "COMMON SETTINGS"; // Settings — Tools descriptions in-out property settings_hash_type_desc_text: "Blake3 – fastest; CRC32/xxH3 – alternatives"; in-out property settings_similarity_desc_text: "Very High = only near-identical"; in-out property settings_hash_size_desc_text: "Larger = more accurate, slower"; // Settings — Info/Diagnostics tab in-out property diagnostics_header_text: "DIAGNOSTICS"; in-out property diagnostics_thumbnails_text: "Thumbnails"; in-out property diagnostics_app_cache_text: "App cache"; in-out property diagnostics_refresh_text: "Refresh"; in-out property diagnostics_clear_thumbnails_text: "Clear thumbnails"; in-out property diagnostics_clear_cache_text: "Clear cache"; in-out property diagnostics_collect_test_text: "Scan test"; in-out property diagnostics_collect_test_desc_text: "Scans each volume recursively"; in-out property diagnostics_collect_test_run_text: "Run"; in-out property diagnostics_collect_test_stop_text: "Stop"; // About section links in-out property about_repo_text: "Repository"; in-out property about_donate_text: "Donate"; in-out property about_translate_text: "Translations"; // Diagnostics collect-test popup in-out property collect_test_title_text: "📊 Test results"; in-out property collect_test_volumes_text: "💾 Volumes:"; in-out property collect_test_folders_text: "📁 Folders:"; in-out property collect_test_files_text: "📄 Files:"; in-out property collect_test_time_text: "⏱ Time:"; in-out property collect_test_ms_text: " ms"; // Directories screen in-out property directories_include_header_text: "Directories to scan"; in-out property directories_exclude_header_text: "Excluded directories"; in-out property directories_add_text: "+ Add"; in-out property directories_volume_header_text: "Volumes"; in-out property directories_volume_refresh_text: "Refresh"; in-out property directories_volume_add_text: "Add"; in-out property no_paths_text: "No paths – add below"; // Gallery / delete popups in-out property gallery_delete_button_text: "Delete"; in-out property gallery_back_text: "Back"; in-out property gallery_confirm_delete_text: "Yes, delete"; in-out property deleting_files_text: "Deleting files…"; in-out property stop_text: "Stop"; in-out property files_suffix_text: "files"; in-out property scanning_fallback_text: "Scanning…"; // About section in diagnostics tab in-out property about_app_label_text: "ABOUT"; in-out property cache_label_text: "CACHE"; in-out property app_subtitle_text: "In honour of the Battle of Cedynia (972 CE)"; in-out property app_license_text: "Frontend for Czkawka Core • GPL-3.0"; // Bottom nav in-out property nav_home_text: "Home"; in-out property nav_dirs_text: "Directories"; in-out property nav_settings_text: "Settings"; // Status messages set from Rust in-out property status_ready_text: "Ready"; in-out property status_stopped_text: "Stopped"; in-out property status_no_results_text: "No results"; in-out property status_deleted_selected_text: "Deleted selected"; in-out property status_deleted_with_errors_text: "Deleted with errors"; in-out property scan_not_started_text: "Scan not started"; in-out property found_items_prefix_text: "Found"; in-out property found_items_suffix_text: "items"; in-out property deleted_items_prefix_text: "Deleted"; in-out property deleted_items_suffix_text: "items"; in-out property deleted_errors_suffix_text: "errors"; in-out property renamed_prefix_text: "Renamed"; in-out property renamed_files_suffix_text: "files"; in-out property renamed_errors_suffix_text: "errors"; in-out property cleaned_exif_prefix_text: "Cleaned EXIF from"; in-out property cleaned_exif_suffix_text: "files"; in-out property cleaned_exif_errors_suffix_text: "errors"; // Delete errors popup (more items) in-out property and_more_prefix_text: "…and"; in-out property and_more_suffix_text: "more"; } ================================================ FILE: ci_tester/Cargo.toml ================================================ [package] name = "ci_tester" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release] debug-assertions = true overflow-checks = true debug = true [dependencies] state = "0.6.0" handsome_logger = "0.9.1" log = "0.4.20" ================================================ FILE: ci_tester/src/main.rs ================================================ use std::collections::BTreeSet; use std::fs; use std::process::{Command, Stdio}; use std::env; use log::info; use std::path::Path; use std::process::Output; #[derive(Default, Clone, Debug)] struct CollectedFiles { files: BTreeSet, folders: BTreeSet, symlinks: BTreeSet, } static CZKAWKA_PATH: state::InitCell = state::InitCell::new(); static COLLECTED_FILES: state::InitCell = state::InitCell::new(); const ATTEMPTS: u32 = 10; const PRINT_MESSAGES_TO_TERMINAL_INSTEAD_OUTPUT: bool = true; pub(crate) fn collect_output(output: &Output) -> String { let stdout = &output.stdout; let stderr = &output.stderr; let stdout_str = String::from_utf8_lossy(stdout); let stderr_str = String::from_utf8_lossy(stderr); format!("{stdout_str}\n{stderr_str}") } fn test_args() { let modes = ["dup", "big", "empty-folders", "empty-files", "temp", "image", "symlinks", "broken", "ext", "video", "music"]; for mode in modes { println!("Testing mode {}", mode); let _ = fs::remove_dir_all("RandomDirWithoutContent"); fs::create_dir_all("RandomDirWithoutContent").expect("Should not fail in tests"); run_with_good_status(&[CZKAWKA_PATH.get().as_str(), mode, "-d", "RandomDirWithoutContent", "-W"], true); } } // App runs - ./ci_tester PATH_TO_CZKAWKA fn main() { handsome_logger::init().expect("Should not fail in tests"); let args: Vec = std::env::args().collect(); let path_to_czkawka = args[1].clone(); CZKAWKA_PATH.set(path_to_czkawka); test_args(); remove_test_dir(); run_with_good_status(&["ls"], false); unzip_files(); let all_files = collect_all_files_and_dirs("TestFiles").expect("Should not fail in tests"); COLLECTED_FILES.set(all_files); remove_test_dir(); println!("Starting checking"); for _ in 0..ATTEMPTS { test_empty_files(); test_big_files(); test_smallest_files(); test_biggest_files(); test_empty_folders(); test_temporary_files(); test_symlinks_files(); test_remove_duplicates_one_oldest(); test_remove_duplicates_one_newest(); test_remove_duplicates_all_expect_newest(); test_remove_duplicates_all_expect_oldest(); test_remove_duplicates_one_smallest(); test_remove_duplicates_one_biggest(); test_remove_duplicates_all_expect_biggest(); test_remove_duplicates_all_expect_smallest(); test_remove_same_music_tags_one_oldest(); test_remove_same_music_tags_one_newest(); test_remove_same_music_tags_all_expect_oldest(); test_remove_same_music_tags_all_expect_newest(); test_remove_same_music_tags_one_smallest(); test_remove_same_music_tags_one_biggest(); test_remove_same_music_tags_all_expect_biggest(); test_remove_same_music_tags_all_expect_smallest(); test_remove_same_music_content_one_oldest(); test_remove_same_music_content_all_expect_oldest(); test_remove_same_music_content_one_newest(); test_remove_same_music_content_all_expect_newest(); test_remove_same_music_content_one_smallest(); test_remove_same_music_content_one_biggest(); test_remove_same_music_content_all_expect_biggest(); test_remove_same_music_content_all_expect_smallest(); test_remove_videos_one_oldest(); test_remove_videos_one_newest(); test_remove_videos_all_expect_oldest(); test_remove_videos_all_expect_newest(); test_remove_videos_one_smallest(); test_remove_videos_one_biggest(); test_remove_videos_all_expect_biggest(); test_remove_videos_all_expect_smallest(); } println!("Completed checking"); } fn test_remove_videos_one_oldest() { info!("test_remove_videos_one_oldest"); run_test(&["video", "-d", "TestFiles", "-D", "OO", "-W"], vec!["Videos/V3.webm"], Vec::new(), Vec::new()); } fn test_remove_videos_one_newest() { info!("test_remove_videos_one_newest"); run_test(&["video", "-d", "TestFiles", "-D", "ON", "-W"], vec!["Videos/V5.mp4"], Vec::new(), Vec::new()); } fn test_remove_videos_all_expect_oldest() { info!("test_remove_videos_all_expect_oldest"); run_test( &["video", "-d", "TestFiles", "-D", "AEO", "-W"], vec!["Videos/V1.mp4", "Videos/V2.mp4", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_videos_all_expect_newest() { info!("test_remove_videos_all_expect_newest"); run_test( &["video", "-d", "TestFiles", "-D", "AEN", "-W"], vec!["Videos/V1.mp4", "Videos/V2.mp4", "Videos/V3.webm"], Vec::new(), Vec::new(), ); } fn test_remove_videos_one_smallest() { info!("test_remove_videos_one_smallest"); run_test(&["video", "-d", "TestFiles", "-D", "OS", "-W"], vec!["Videos/V2.mp4"], Vec::new(), Vec::new()); } fn test_remove_videos_one_biggest() { info!("test_remove_videos_one_biggest"); run_test(&["video", "-d", "TestFiles", "-D", "OB", "-W"], vec!["Videos/V3.webm"], Vec::new(), Vec::new()); } fn test_remove_videos_all_expect_smallest() { info!("test_remove_videos_all_expect_smallest"); run_test( &["video", "-d", "TestFiles", "-D", "AES", "-W"], vec!["Videos/V1.mp4", "Videos/V3.webm", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_videos_all_expect_biggest() { info!("test_remove_videos_all_expect_biggest"); run_test( &["video", "-d", "TestFiles", "-D", "AEB", "-W"], vec!["Videos/V1.mp4", "Videos/V2.mp4", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_one_newest() { info!("test_remove_same_music_content_one_newest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "ON", "-W"], vec!["Music/M2.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_all_expect_newest() { info!("test_remove_same_music_content_all_expect_newest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "AEN", "-W"], vec!["Music/M1.mp3", "Music/M3.flac", "Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_all_expect_oldest() { info!("test_remove_same_music_content_all_expect_oldest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "AEO", "-W"], vec!["Music/M1.mp3", "Music/M2.mp3", "Music/M3.flac"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_one_oldest() { info!("test_remove_same_music_content_one_oldest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "OO", "-W"], vec!["Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_one_biggest() { info!("test_remove_same_music_content_one_biggest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "OB", "-W"], vec!["Music/M3.flac"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_all_expect_biggest() { info!("test_remove_same_music_content_all_expect_biggest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "AEB", "-W"], vec!["Music/M1.mp3", "Music/M2.mp3", "Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_all_expect_smallest() { info!("test_remove_same_music_content_all_expect_smallest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "AES", "-W"], vec!["Music/M1.mp3", "Music/M3.flac", "Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_content_one_smallest() { info!("test_remove_same_music_content_one_smallest"); run_test( &["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "OS", "-W"], vec!["Music/M2.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_tags_one_oldest() { info!("test_remove_same_music_one_oldest"); run_test(&["music", "-d", "TestFiles", "-D", "OO", "-W"], vec!["Music/M5.mp3"], Vec::new(), Vec::new()); } fn test_remove_same_music_tags_one_newest() { info!("test_remove_same_music_one_newest"); run_test(&["music", "-d", "TestFiles", "-D", "ON", "-W"], vec!["Music/M2.mp3"], Vec::new(), Vec::new()); } fn test_remove_same_music_tags_all_expect_oldest() { info!("test_remove_same_music_all_expect_oldest"); run_test( &["music", "-d", "TestFiles", "-D", "AEO", "-W"], vec!["Music/M1.mp3", "Music/M2.mp3", "Music/M3.flac"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_tags_all_expect_newest() { info!("test_remove_same_music_all_expect_newest"); run_test( &["music", "-d", "TestFiles", "-D", "AEN", "-W"], vec!["Music/M1.mp3", "Music/M3.flac", "Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_tags_one_smallest() { info!("test_remove_same_music_one_smallest"); run_test(&["music", "-d", "TestFiles", "-D", "OS", "-W"], vec!["Music/M1.mp3"], Vec::new(), Vec::new()); } fn test_remove_same_music_tags_one_biggest() { info!("test_remove_same_music_one_biggest"); run_test(&["music", "-d", "TestFiles", "-D", "OB", "-W"], vec!["Music/M3.flac"], Vec::new(), Vec::new()); } fn test_remove_same_music_tags_all_expect_smallest() { info!("test_remove_same_music_all_expect_smallest"); run_test( &["music", "-d", "TestFiles", "-D", "AES", "-W"], vec!["Music/M2.mp3", "Music/M3.flac", "Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_same_music_tags_all_expect_biggest() { info!("test_remove_same_music_all_expect_biggest"); run_test( &["music", "-d", "TestFiles", "-D", "AEB", "-W"], vec!["Music/M1.mp3", "Music/M2.mp3", "Music/M5.mp3"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_all_expect_oldest() { info!("test_remove_duplicates_all_expect_oldest"); run_test( &["dup", "-d", "TestFiles", "-D", "AEO", "-W"], vec!["Images/A1.jpg", "Images/A5.jpg", "Music/M1.mp3", "Music/M2.mp3", "Videos/V1.mp4", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_all_expect_newest() { info!("test_remove_duplicates_all_expect_newest"); run_test( &["dup", "-d", "TestFiles", "-D", "AEN", "-W"], vec!["Images/A2.jpg", "Images/A5.jpg", "Music/M1.mp3", "Music/M5.mp3", "Videos/V1.mp4", "Videos/V2.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_one_newest() { info!("test_remove_duplicates_one_newest"); run_test( &["dup", "-d", "TestFiles", "-D", "ON", "-W"], vec!["Images/A1.jpg", "Music/M2.mp3", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_one_oldest() { info!("test_remove_duplicates_one_oldest"); run_test( &["dup", "-d", "TestFiles", "-D", "OO", "-W"], vec!["Images/A2.jpg", "Music/M5.mp3", "Videos/V2.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_all_expect_smallest() { info!("test_remove_duplicates_all_expect_smallest"); run_test( &["dup", "-d", "TestFiles", "-D", "AES", "-W"], vec!["Images/A2.jpg", "Images/A5.jpg", "Music/M2.mp3", "Music/M5.mp3", "Videos/V2.mp4", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_all_expect_biggest() { info!("test_remove_duplicates_all_expect_biggest"); run_test( &["dup", "-d", "TestFiles", "-D", "AEN", "-W"], vec!["Images/A2.jpg", "Images/A5.jpg", "Music/M1.mp3", "Music/M5.mp3", "Videos/V1.mp4", "Videos/V2.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_one_biggest() { info!("test_remove_duplicates_one_biggest"); run_test( &["dup", "-d", "TestFiles", "-D", "ON", "-W"], vec!["Images/A1.jpg", "Music/M2.mp3", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_remove_duplicates_one_smallest() { info!("test_remove_duplicates_one_smallest"); run_test( &["dup", "-d", "TestFiles", "-D", "OS", "-W"], vec!["Images/A1.jpg", "Music/M1.mp3", "Videos/V1.mp4"], Vec::new(), Vec::new(), ); } fn test_symlinks_files() { info!("test_symlinks_files"); run_test(&["symlinks", "-d", "TestFiles", "-D", "-W"], Vec::new(), Vec::new(), vec!["Symlinks/EmptyFiles"]); } fn test_temporary_files() { info!("test_temporary_files"); run_test(&["temp", "-d", "TestFiles", "-D", "-W"], vec!["Temporary/Boczze.cache"], Vec::new(), Vec::new()); } fn test_empty_folders() { info!("test_empty_folders"); run_test( &["empty-folders", "-d", "TestFiles", "-D", "-W"], Vec::new(), vec!["EmptyFolders/One", "EmptyFolders/Two", "EmptyFolders/Two/TwoInside"], Vec::new(), ); } fn test_biggest_files() { info!("test_biggest_files"); run_test( &["big", "-d", "TestFiles", "-n", "6", "-D", "-W"], vec!["Music/M3.flac", "Music/M4.mp3", "Videos/V2.mp4", "Videos/V3.webm", "Videos/V1.mp4", "Videos/V5.mp4"], Vec::new(), Vec::new(), ); } fn test_smallest_files() { info!("test_smallest_files"); run_test( &["big", "-d", "TestFiles", "-J", "-n", "5", "-D", "-W"], vec!["Broken/Br.jpg", "Broken/Br.mp3", "Broken/Br.pdf", "Broken/Br.zip", "EmptyFolders/ThreeButNot/KEKEKE"], Vec::new(), Vec::new(), ); } fn test_empty_files() { info!("test_empty_files"); run_test(&["empty-files", "-d", "TestFiles", "-D", "-W"], vec!["EmptyFile"], Vec::new(), Vec::new()); } fn test_big_files() { info!("test_big_files"); run_test(&["big", "-d", "TestFiles", "-n", "2", "-D", "-W"], vec!["Music/M4.mp3", "Videos/V3.webm"], Vec::new(), Vec::new()); } //////////////////////////////////// //////////////////////////////////// /////////HELPER FUNCTIONS/////////// //////////////////////////////////// //////////////////////////////////// fn run_test(arguments: &[&str], expected_files_differences: Vec<&'static str>, expected_folders_differences: Vec<&'static str>, expected_symlinks_differences: Vec<&'static str>) { println!("====================================================="); unzip_files(); assert!(Path::new("TestFiles").exists()); // Add path_to_czkawka to arguments let mut all_arguments = Vec::new(); all_arguments.push(CZKAWKA_PATH.get().as_str()); all_arguments.extend_from_slice(arguments); run_with_good_status(&all_arguments, PRINT_MESSAGES_TO_TERMINAL_INSTEAD_OUTPUT); file_folder_diffs( COLLECTED_FILES.get(), expected_files_differences, expected_folders_differences, expected_symlinks_differences, ); remove_test_dir(); } fn unzip_files() { run_with_good_status(&["unzip", "-qq", "-X", "TestFiles.zip", "-d", "TestFiles"], false); } fn remove_test_dir() { let _ = fs::remove_dir_all("TestFiles"); } fn run_with_good_status(str_command: &[&str], print_messages: bool) { let mut command = Command::new(str_command[0]); let mut com = command.args(&str_command[1..]); com.env("ENABLE_TERMINAL_LOGS_IN_CLI", "1"); com.env("RUST_BACKTRACE", "1"); if !print_messages { com = com.stderr(Stdio::piped()).stdout(Stdio::piped()); } let output = com.spawn().unwrap().wait_with_output().unwrap(); let all_output = collect_output(&output); let command_copyable = str_command.join(" "); assert!(output.status.success(), "Command \"{command_copyable}\" failed with status: {:?}, from folder {:?}\n\n and output: {all_output}",env::current_dir() ,output.status.code()); } fn file_folder_diffs( all_files: &CollectedFiles, mut expected_files_differences: Vec<&'static str>, mut expected_folders_differences: Vec<&'static str>, mut expected_symlinks_differences: Vec<&'static str>, ) { let current_files = collect_all_files_and_dirs("TestFiles").expect("Should not fail in tests"); let mut diff_files = all_files .files .difference(¤t_files.files) .map(|e| e.strip_prefix("TestFiles/").expect("Should not fail in tests").to_string()) .collect::>(); let mut diff_folders = all_files .folders .difference(¤t_files.folders) .map(|e| e.strip_prefix("TestFiles/").expect("Should not fail in tests").to_string()) .collect::>(); let mut diff_symlinks = all_files .symlinks .difference(¤t_files.symlinks) .map(|e| e.strip_prefix("TestFiles/").expect("Should not fail in tests").to_string()) .collect::>(); expected_symlinks_differences.sort(); expected_folders_differences.sort(); expected_files_differences.sort(); diff_files.sort(); diff_folders.sort(); diff_symlinks.sort(); assert_eq!(diff_files, expected_files_differences); assert_eq!(diff_folders, expected_folders_differences); assert_eq!(diff_symlinks, expected_symlinks_differences); } fn collect_all_files_and_dirs(dir: &str) -> std::io::Result { let mut files = BTreeSet::new(); let mut folders = BTreeSet::new(); let mut symlinks = BTreeSet::new(); let mut folders_to_check = vec![dir.to_string()]; while let Some(folder) = folders_to_check.pop() { let rd = fs::read_dir(folder)?; for entry in rd { let entry = entry?; let file_type = entry.file_type()?; let path_str = entry.path().to_string_lossy().to_string(); if file_type.is_dir() { folders.insert(path_str.clone()); folders_to_check.push(path_str); } else if file_type.is_symlink() { symlinks.insert(path_str); } else if file_type.is_file() { files.insert(path_str); } else { panic!("Unknown type of file {path_str}"); } } } // for dir in &folders_to_check { // println!("Folder \"{}\"", dir) // } // for symlink in &symlinks { // println!("Symlink \"{}\"", symlink) // } // for file in &files { // let metadata = fs::metadata(file)?; // println!("File \"{}\" with size {} bytes", file, metadata.len()); // } folders.remove(dir); // println!("Found {} files, {} folders and {} symlinks", files.len(), folders.len(), symlinks.len()); Ok(CollectedFiles { files, folders, symlinks }) } ================================================ FILE: clippy.toml ================================================ allow-indexing-slicing-in-tests = true allow-unwrap-in-tests = true avoid-breaking-exported-api = false ================================================ FILE: czkawka_cli/Cargo.toml ================================================ [package] name = "czkawka_cli" version = "11.0.1" authors = ["Rafał Mikrut "] edition = "2024" rust-version = "1.92.0" description = "CLI frontend of Czkawka" license = "MIT" homepage = "https://github.com/qarmin/czkawka" repository = "https://github.com/qarmin/czkawka" [dependencies] clap = { version = "4.5", features = ["derive", "color"] } log = "0.4.22" czkawka_core = { path = "../czkawka_core", version = "11.0.1", features = [] } indicatif = "0.18" crossbeam-channel = { version = "0.5", features = [] } ctrlc = { version = "3.4", features = ["termination"] } humansize = "2.1" [features] default = [] heif = ["czkawka_core/heif"] libraw = ["czkawka_core/libraw"] libavif = ["czkawka_core/libavif"] # Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails # No-op on other OSes, it is slower and provides less helpful error messages xdg_portal_trash = ["czkawka_core/xdg_portal_trash"] no_colors = [] [lints] workspace = true ================================================ FILE: czkawka_cli/LICENSE_MIT ================================================ MIT License Copyright (c) 2020-2026 Rafał Mikrut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: czkawka_cli/README.md ================================================ # Czkawka CLI CLI frontend that allows you to use Czkawka from the terminal. ## Requirements Precompiled binaries should work without any additional dependencies on Linux (Ubuntu 22.04+), Windows (10+), and macOS (10.15+). On 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. If you want to use the similar videos tool, you need to install ffmpeg (runtime dependency). If you want to use heif/libraw/libavif (build/runtime dependency), you need to install the required packages. - macOS: `brew install ffmpeg libraw libheif libavif dav1d` – [ffmpeg formula](https://formulae.brew.sh/formula/ffmpeg) - Linux: `sudo apt install ffmpeg libraw-dev libheif-dev libavif-dev libdav1d-dev` - Windows: `choco install ffmpeg` – or, if not working, download from [ffmpeg.org](https://ffmpeg.org/download.html#build-windows) and unpack to the location with `czkawka_cli.exe`. Heif and libraw are not supported on Windows. ## Compilation To compile, you need to have Rust installed via [rustup](https://rustup.rs/). Then, build with: ```shell cargo run --release --bin czkawka_cli ``` You can enable additional features with: ```shell cargo run --release --bin czkawka_cli --features "heif,libraw,libavif" ``` ## How to use The application includes concise help for each tool, which you can display by running: ``` czkawka_cli --help ``` You can also get detailed information about the parameters of a specific tool by running, for example: ``` czkawka_cli dup --help ``` Example usage: ```shell czkawka dup -d /home/rafal -e /home/rafal/Obrazy -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo czkawka empty-folders -d /home/rafal/rr /home/gateway -f results.txt czkawka big -d /home/rafal/ /home/piszczal -e /home/rafal/Roman -n 25 -x VIDEO -f results.txt czkawka empty-files -d /home/rafal /home/szczekacz -e /home/rafal/Pulpit -R -f results.txt czkawka temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D czkawka music -d /home/rafal -e /home/rafal/Pulpit -z "artist,year, ARTISTALBUM, ALBUM___tiTlE" -f results.txt czkawka symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt ``` ## LICENSE MIT ================================================ FILE: czkawka_cli/src/commands.rs ================================================ use std::path::PathBuf; #[cfg(not(feature = "no_colors"))] use clap::builder::Styles; #[cfg(not(feature = "no_colors"))] use clap::builder::styling::AnsiColor; use czkawka_core::CZKAWKA_VERSION; use czkawka_core::common::model::{CheckingMethod, HashType}; use czkawka_core::common::tool_data::DeleteMethod; use czkawka_core::re_exported::{Cropdetect, FilterType, HashAlg}; use czkawka_core::tools::broken_files::CheckedTypes; use czkawka_core::tools::same_music::MusicSimilarity; use czkawka_core::tools::similar_videos::{ALLOWED_SKIP_FORWARD_AMOUNT, ALLOWED_VID_HASH_DURATION, DEFAULT_SKIP_FORWARD_AMOUNT, crop_detect_from_str_opt}; use czkawka_core::tools::video_optimizer::VideoCodec; #[cfg(not(feature = "no_colors"))] pub const CLAP_STYLING: Styles = Styles::styled() .header(AnsiColor::Green.on_default().bold()) .usage(AnsiColor::Green.on_default().bold()) .literal(AnsiColor::Cyan.on_default().bold()) .placeholder(AnsiColor::Cyan.on_default().bold()) .error(AnsiColor::Red.on_default().bold()) .valid(AnsiColor::Green.on_default().bold()) .invalid(AnsiColor::Yellow.on_default().bold()); #[derive(clap::Parser)] #[clap( name = "czkawka", help_template = HELP_TEMPLATE, version = CZKAWKA_VERSION, )] #[cfg_attr(not(feature = "no_colors"), clap(styles = CLAP_STYLING))] pub struct Args { #[command(subcommand)] pub command: Commands, } #[derive(Debug, clap::Subcommand)] pub enum Commands { #[clap( name = "dup", about = "Finds duplicate files", 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" )] Duplicates(DuplicatesArgs), #[clap( name = "empty-folders", about = "Finds empty folders", after_help = "EXAMPLE:\n czkawka empty-folders -d /home/rafal/rr /home/gateway -f results.txt" )] EmptyFolders(EmptyFoldersArgs), #[clap( name = "big", about = "Finds big files", after_help = "EXAMPLE:\n czkawka big -d /home/rafal/ /home/piszczal -e /home/rafal/Roman -n 25 -J -x VIDEO -f results.txt" )] BiggestFiles(BiggestFilesArgs), #[clap( name = "empty-files", about = "Finds empty files", after_help = "EXAMPLE:\n czkawka empty-files -d /home/rafal /home/szczekacz -e /home/rafal/Pulpit -R -f results.txt" )] EmptyFiles(EmptyFilesArgs), #[clap( name = "temp", about = "Finds temporary files", after_help = "EXAMPLE:\n czkawka temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D" )] Temporary(TemporaryArgs), #[clap( name = "image", about = "Finds similar images", after_help = "EXAMPLE:\n czkawka image -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt" )] SimilarImages(SimilarImagesArgs), #[clap(name = "music", about = "Finds same music by tags", after_help = "EXAMPLE:\n czkawka music -d /home/rafal -f results.txt")] SameMusic(SameMusicArgs), #[clap( name = "symlinks", about = "Finds invalid symlinks", after_help = "EXAMPLE:\n czkawka symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt" )] InvalidSymlinks(InvalidSymlinksArgs), #[clap( name = "broken", about = "Finds broken files", after_help = "EXAMPLE:\n czkawka broken -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt" )] BrokenFiles(BrokenFilesArgs), #[clap(name = "video", about = "Finds similar video files", after_help = "EXAMPLE:\n czkawka video -d /home/rafal -f results.txt")] SimilarVideos(SimilarVideosArgs), #[clap( name = "ext", about = "Finds files with invalid extensions", after_help = "EXAMPLE:\n czkawka ext -d /home/czokolada/ -f results.txt" )] BadExtensions(BadExtensionsArgs), #[clap( name = "bad-names", about = "Finds files with bad names", after_help = "EXAMPLE:\n czkawka bad-names -d /home/rafal -f results.txt" )] BadNames(BadNamesArgs), #[clap( name = "video-optimizer", about = "Optimizes video files (transcode or crop)", after_help = "EXAMPLE:\n czkawka video-optimizer -d /home/rafal -f results.txt" )] VideoOptimizer(VideoOptimizerArgs), #[clap( name = "exif-remover", about = "Finds and removes EXIF tags from images", after_help = "EXAMPLE:\n czkawka exif-remover -d /home/rafal -f results.txt" )] ExifRemover(ExifRemoverArgs), } #[derive(Debug, clap::Args)] pub struct DuplicatesArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub reference_directories: ReferenceDirectories, #[clap( short = 'Z', long, value_parser = parse_minimal_file_size, default_value = "257144", help = "Minimum prehash cache file size in bytes", long_help = "Minimum size of prehash cached files in bytes" )] pub minimal_prehash_cache_file_size: u64, #[clap( short = 'u', long, help = "Use prehash cache", long_help = "Use prehash cache to speed up the scanning process by avoiding rehashing files that have already been hashed" )] pub use_prehash_cache: bool, #[clap( short, long, value_parser = parse_minimal_file_size, default_value = "8192", help = "Minimum size in bytes", long_help = "Minimum size of checked files in bytes, assigning bigger value may speed up searching" )] pub minimal_file_size: u64, #[clap( short = 'i', long, value_parser = parse_maximal_file_size, default_value = "18446744073709551615", help = "Maximum size in bytes", long_help = "Maximum size of checked files in bytes, assigning lower value may speed up searching" )] pub maximal_file_size: u64, #[clap( short = 'c', long, value_parser = parse_minimal_file_size, default_value = "257144", help = "Minimum cached file size in bytes", 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" )] pub minimal_cached_file_size: u64, #[clap( short, long, default_value = "HASH", value_parser = parse_checking_method_duplicate, help = "Search method (NAME, SIZE, HASH)", 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" )] pub search_method: CheckingMethod, #[clap(flatten)] pub delete_method: DMethod, #[clap( short = 't', long, default_value = "BLAKE3", value_parser = parse_hash_type, help = "Hash type (BLAKE3, CRC32, XXH3)", 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." )] pub hash_type: HashType, #[clap(flatten)] pub case_sensitive_name_comparison: CaseSensitiveNameComparison, #[clap(flatten)] pub allow_hard_links: AllowHardLinks, } #[derive(Debug, clap::Args)] pub struct EmptyFoldersArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub delete_method: SDMethod, } #[derive(Debug, clap::Args)] pub struct BiggestFilesArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap( short, long, default_value = "50", help = "Number of files to be shown", long_help = "Number of biggest (or smallest with -J flag) files to display in results" )] pub number_of_files: usize, #[clap(flatten)] pub delete_method: SDMethod, #[clap( short = 'J', long, help = "Finds the smallest files instead the biggest", long_help = "Switch mode to find smallest files instead of biggest ones" )] pub smallest_mode: bool, } #[derive(Debug, clap::Args)] pub struct EmptyFilesArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub delete_method: SDMethod, } #[derive(Debug, clap::Args)] pub struct TemporaryArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub delete_method: SDMethod, } #[derive(Debug, clap::Args)] pub struct SimilarImagesArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub reference_directories: ReferenceDirectories, #[clap( short, long, value_parser = parse_minimal_file_size, default_value = "16384", help = "Minimum size in bytes", long_help = "Minimum size of checked files in bytes, assigning bigger value may speed up searching" )] pub minimal_file_size: u64, #[clap( short = 'i', long, value_parser = parse_minimal_file_size, default_value = "18446744073709551615", help = "Maximum size in bytes", long_help = "Maximum size of checked files in bytes, assigning lower value may speed up searching" )] pub maximal_file_size: u64, #[clap( short = 's', long, default_value = "5", value_parser = clap::value_parser!(u32).range(0..=40), help = "Maximum difference between images (0-40)", 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." )] pub max_difference: u32, #[clap(flatten)] pub delete_method: DMethod, #[clap(flatten)] pub allow_hard_links: AllowHardLinks, #[clap(flatten)] pub ignore_same_size: IgnoreSameSize, #[clap( short = 'g', long, default_value = "Gradient", value_parser = parse_similar_hash_algorithm, help = "Hash algorithm (Mean, Gradient, Blockhash, VertGradient, DoubleGradient, Median)", 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." )] pub hash_alg: HashAlg, #[clap( short = 'z', long, default_value = "Nearest", value_parser = parse_similar_image_filter, help = "Image resize filter (Lanczos3, Nearest, Triangle, Gaussian, CatmullRom)", 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." )] pub image_filter: FilterType, #[clap( short = 'c', long, default_value = "16", value_parser = parse_image_hash_size, help = "Hash size (8, 16, 32, 64)", 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." )] pub hash_size: u8, } #[derive(Debug, clap::Args)] pub struct SameMusicArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub reference_directories: ReferenceDirectories, #[clap(flatten)] pub delete_method: DMethod, #[clap( short, long, help = "Approximate comparison of music tags", long_help = "Use approximate comparison when comparing music tags (allows small differences in tag values)" )] pub approximate_comparison: bool, #[clap( short, long, help = "Compare fingerprints only with similar titles", long_help = "When using audio content comparison, only compare files that have similar titles to reduce false positives and speed up the process" )] pub compare_fingerprints_only_with_similar_titles: bool, #[clap( short = 'z', long, default_value = "track_title,track_artist", value_parser = parse_music_duplicate_type, help = "Search method (track_title,track_artist,year,bitrate,genre,length)", long_help = "Sets which rows must be equal to set these files as duplicates (may be mixed, but must be divided by commas)." )] pub music_similarity: MusicSimilarity, #[clap( short, long, default_value = "TAGS", value_parser = parse_checking_method_same_music, help = "Search method (CONTENT, TAGS)", long_help = "Methods to search files.\nCONTENT - finds similar audio files by content, TAGS - finds similar music by tags." )] pub search_method: CheckingMethod, #[clap( short, long, value_parser = parse_minimal_file_size, default_value = "8192", help = "Minimum size in bytes", long_help = "Minimum size of checked files in bytes, assigning bigger value may speed up searching" )] pub minimal_file_size: u64, #[clap( short = 'i', long, value_parser = parse_maximal_file_size, default_value = "18446744073709551615", help = "Maximum size in bytes", long_help = "Maximum size of checked files in bytes, assigning lower value may speed up searching" )] pub maximal_file_size: u64, #[clap( short = 'l', long, value_parser = parse_minimum_segment_duration, default_value = "10.0", help = "Minimum segment duration in seconds", 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" )] pub minimum_segment_duration: f32, #[clap( short = 'Y', long, value_parser = parse_maximum_difference, default_value = "2.0", help = "Maximum difference between audio segments", 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." )] pub maximum_difference: f64, } fn parse_maximum_difference(src: &str) -> Result { match src.parse::() { Ok(maximum_difference) => { if maximum_difference <= 0.0 { Err("Maximum difference must be bigger than 0".to_string()) } else if maximum_difference >= 10.0 { Err("Maximum difference must be smaller than 10.0".to_string()) } else { Ok(maximum_difference) } } Err(e) => Err(e.to_string()), } } fn parse_minimum_segment_duration(src: &str) -> Result { match src.parse::() { Ok(minimum_segment_duration) => { if minimum_segment_duration <= 0.0 { Err("Minimum segment duration must be bigger than 0".to_string()) } else if minimum_segment_duration >= 3600.0 { Err("Minimum segment duration must be smaller than 3600(greater values not have much sense)".to_string()) } else { Ok(minimum_segment_duration) } } Err(e) => Err(e.to_string()), } } #[derive(Debug, clap::Args)] pub struct InvalidSymlinksArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub delete_method: SDMethod, } #[derive(Debug, clap::Args)] pub struct BrokenFilesArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub delete_method: SDMethod, #[clap( short, long, default_value = "PDF", value_parser = parse_broken_files, help = "Checking file types (PDF, AUDIO, IMAGE, ARCHIVE, VIDEO)", 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" )] pub checked_types: Vec, } #[derive(Debug, clap::Args)] pub struct SimilarVideosArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub reference_directories: ReferenceDirectories, #[clap(flatten)] pub delete_method: DMethod, #[clap(flatten)] pub allow_hard_links: AllowHardLinks, #[clap(flatten)] pub ignore_same_size: IgnoreSameSize, #[clap( short, long, value_parser = parse_minimal_file_size, default_value = "8192", help = "Minimum size in bytes", long_help = "Minimum size of checked files in bytes, assigning bigger value may speed up searching" )] pub minimal_file_size: u64, #[clap( short = 'i', long, value_parser = parse_maximal_file_size, default_value = "18446744073709551615", help = "Maximum size in bytes", long_help = "Maximum size of checked files in bytes, assigning lower value may speed up searching" )] pub maximal_file_size: u64, #[clap( short = 't', long, value_parser = parse_tolerance, default_value = "10", help = "Video maximum difference (allowed values <0,20>)", long_help = "Maximum difference between video frames, bigger value means that videos can looks more and more different (allowed values <0,20>)" )] pub tolerance: i32, #[clap( short = 'U', long, default_value_t = DEFAULT_SKIP_FORWARD_AMOUNT, value_parser = parse_skip_forward_amount, help = "Skip forward amount in seconds (allowed values: 0-300, default: 15)", 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." )] pub skip_forward_amount: u32, #[clap( short = 'B', long, default_value = "letterbox", value_parser = parse_crop_detect, help = "Crop detect method (none, letterbox, motion)", 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." )] pub crop_detect: Cropdetect, #[clap( short = 'A', long, default_value = "10", value_parser = parse_scan_duration, help = "Scan duration in seconds", 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." )] pub scan_duration: u32, } #[derive(Debug, clap::Args)] pub struct BadExtensionsArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap( short = 'F', long, help = "Fix bad extensions", long_help = "Automatically rename files to use proper extensions based on their detected file type" )] pub fix_extensions: bool, } #[derive(Debug, clap::Args)] pub struct BadNamesArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(flatten)] pub delete_method: SDMethod, #[clap( short = 'u', long, help = "Check for uppercase extensions", long_help = "Detects files with uppercase extensions (e.g., .JPG instead of .jpg)" )] pub uppercase_extension: bool, #[clap(short = 'j', long, help = "Check for emoji in filenames", long_help = "Detects files with emoji characters in their names")] pub emoji_used: bool, #[clap( short = 'w', long, help = "Check for spaces at start or end", long_help = "Detects files with spaces at the beginning or end of their names" )] pub space_at_start_or_end: bool, #[clap( short = 'n', long, help = "Check for non-ASCII characters", long_help = "Detects files with non-ASCII graphical characters in their names" )] pub non_ascii_graphical: bool, #[clap( short = 'r', long, help = "Restricted charset (comma-separated)", long_help = "List of allowed special characters. Any other characters will be flagged as problematic. Example: '_- .' for underscore, dash, space, and dot" )] pub restricted_charset: Option, #[clap( short = 'a', long, help = "Check for duplicated non-alphanumeric characters", long_help = "Detects files with duplicated non-alphanumeric characters (e.g., 'file__name' or 'file..txt')" )] pub remove_duplicated_non_alphanumeric: bool, #[clap( short = 'F', long, help = "Fix bad names automatically", long_help = "Automatically rename files to fix detected naming issues" )] pub fix_names: bool, } #[derive(Debug, clap::Args)] pub struct VideoOptimizerArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap(subcommand)] pub mode: VideoOptimizerMode, } #[derive(Debug, clap::Subcommand)] pub enum VideoOptimizerMode { #[clap(name = "transcode", about = "Transcode videos to different codec")] Transcode(TranscodeArgs), #[clap(name = "crop", about = "Crop black bars from videos")] Crop(CropArgs), } #[derive(Debug, clap::Args)] pub struct TranscodeArgs { #[clap( short = 'c', long, help = "Excluded video codecs (comma-separated)", long_help = "Comma-separated list of video codecs to exclude from transcoding (e.g., 'h265,av1,vp9')" )] pub excluded_codecs: Option, #[clap(short = 't', long, help = "Generate thumbnails", long_help = "Generate video thumbnails for preview")] pub generate_thumbnails: bool, #[clap( short = 'V', long, default_value = "10", value_parser = clap::value_parser!(u8).range(1..=99), help = "Thumbnail position percentage (1-99)", long_help = "Percentage from start of video where thumbnail should be taken (1-99%)" )] pub thumbnail_percentage: u8, #[clap(short = 'g', long, help = "Generate thumbnail grid", long_help = "Generate a grid of thumbnails instead of single thumbnail")] pub thumbnail_grid: bool, #[clap( short = 'Z', long, default_value = "3", value_parser = clap::value_parser!(u8).range(2..=6), help = "Thumbnail grid tiles per side (2-6)", long_help = "Number of tiles per side for thumbnail grid (2-6). Only used if -g is enabled." )] pub thumbnail_grid_tiles_per_side: u8, #[clap(short = 'F', long, help = "Fix/optimize videos", long_help = "Actually perform the transcoding on found videos")] pub fix_videos: bool, #[clap( long, default_value = "h265", value_parser = parse_video_codec, help = "Target codec (h264, h265, av1, vp9)", long_help = "Target video codec for transcoding (h264, h265, av1, vp9). Only used with -F flag." )] pub target_codec: VideoCodec, #[clap( long, default_value = "23", value_parser = clap::value_parser!(u32).range(0..=51), help = "Encoding quality (0-51)", long_help = "Video encoding quality (0-51). Lower values mean better quality. 23 is default for h264/h265, 30 for av1/vp9." )] pub quality: u32, #[clap(long, help = "Fail if result not smaller", long_help = "Fail the optimization if resulting file is not smaller than original")] pub fail_if_not_smaller: bool, #[clap(long, help = "Overwrite original files", long_help = "Overwrite original video files with optimized versions")] pub overwrite_original: bool, #[clap(long, help = "Limit video size", long_help = "Limit maximum video dimensions")] pub limit_video_size: bool, #[clap( long, default_value = "1920", value_parser = clap::value_parser!(u32), help = "Maximum video width", long_help = "Maximum video width in pixels when limit_video_size is enabled" )] pub max_width: u32, #[clap( long, default_value = "1080", value_parser = clap::value_parser!(u32), help = "Maximum video height", long_help = "Maximum video height in pixels when limit_video_size is enabled" )] pub max_height: u32, } #[derive(Debug, clap::Args)] pub struct CropArgs { #[clap( short = 'm', long, default_value = "blackbars", value_parser = parse_crop_mechanism, help = "Crop detection mechanism (blackbars, staticcontent)", long_help = "Mechanism for detecting areas to crop: 'blackbars' for removing black bars, 'staticcontent' for detecting static content areas" )] pub crop_mechanism: String, #[clap( short = 'k', long, default_value = "32", value_parser = clap::value_parser!(u8).range(0..=128), help = "Black pixel threshold (0-128)", long_help = "Threshold for considering a pixel as black when detecting black bars (0-128). Lower values are stricter." )] pub black_pixel_threshold: u8, #[clap( short = 'b', long, default_value = "90", value_parser = clap::value_parser!(u8).range(50..=100), help = "Black bar minimum percentage (50-100)", long_help = "Minimum percentage of black pixels in a line to consider it a black bar (50-100%)" )] pub black_bar_percentage: u8, #[clap( short = 's', long, default_value = "20", value_parser = parse_max_samples, help = "Maximum samples (5-1000)", long_help = "Maximum number of video frames to sample when detecting black bars (5-1000)" )] pub max_samples: usize, #[clap( short = 'z', long, default_value = "10", value_parser = parse_min_crop_size, help = "Minimum crop size (1-1000)", long_help = "Minimum size in pixels for crop area to be considered (1-1000)" )] pub min_crop_size: u32, #[clap(short = 't', long, help = "Generate thumbnails", long_help = "Generate video thumbnails for preview")] pub generate_thumbnails: bool, #[clap( short = 'V', long, default_value = "10", value_parser = clap::value_parser!(u8).range(1..=99), help = "Thumbnail position percentage (1-99)", long_help = "Percentage from start of video where thumbnail should be taken (1-99%)" )] pub thumbnail_percentage: u8, #[clap(short = 'g', long, help = "Generate thumbnail grid", long_help = "Generate a grid of thumbnails instead of single thumbnail")] pub thumbnail_grid: bool, #[clap( short = 'Z', long, default_value = "3", value_parser = clap::value_parser!(u8).range(2..=6), help = "Thumbnail grid tiles per side (2-6)", long_help = "Number of tiles per side for thumbnail grid (2-6). Only used if -g is enabled." )] pub thumbnail_grid_tiles_per_side: u8, #[clap(short = 'F', long, help = "Fix/crop videos", long_help = "Actually perform the cropping on found videos")] pub fix_videos: bool, #[clap(long, help = "Overwrite original files", long_help = "Overwrite original video files with cropped versions")] pub overwrite_original: bool, #[clap( long, value_parser = parse_video_codec, help = "Target codec (h264, h265, av1, vp9)", long_help = "Optional: Also transcode to different codec while cropping. Only used with -F flag." )] pub target_codec: Option, #[clap( long, value_parser = clap::value_parser!(u32).range(0..=51), help = "Encoding quality (0-51)", long_help = "Video encoding quality when transcoding (0-51). Only used when target_codec is specified." )] pub quality: Option, } #[derive(Debug, clap::Args)] pub struct ExifRemoverArgs { #[clap(flatten)] pub common_cli_items: CommonCliItems, #[clap( short = 'i', long, help = "Ignored EXIF tags (comma-separated)", long_help = "Comma-separated list of EXIF tag names to ignore (not remove). Example: 'Orientation,DateTime,Software'" )] pub ignored_tags: Option, #[clap(short = 'F', long, help = "Remove EXIF tags", long_help = "Actually remove EXIF tags from files")] pub fix_exif: bool, #[clap( short = 'o', long, help = "Override original files", long_help = "Override original files instead of creating backup files with '_cleaned' suffix" )] pub override_file: bool, } #[derive(Debug, clap::Args)] pub struct CommonCliItems { #[clap( short = 'T', long, default_value = "0", help = "Number of threads to use (0 = all available)", 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." )] pub thread_number: usize, #[clap( short, long, required = true, help = "Directory(ies) to search", long_help = "List of directory(ies) to search (absolute paths). These directories will be scanned but not set as reference folders." )] pub directories: Vec, #[clap( short, long, help = "Excluded directory(ies)", long_help = "List of directory(ies) to exclude from search (absolute paths). Files in these directories will be completely ignored." )] pub excluded_directories: Vec, #[clap( short = 'E', long, help = "Excluded item(s)", long_help = "List of excluded items using wildcards (e.g., */temp*, *.tmp). May be slower than -e, so use -e for directories when possible." )] pub excluded_items: Vec, #[clap( short = 'x', long, help = "Allowed file extension(s)", 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)" )] pub allowed_extensions: Vec, #[clap(short = 'P', long, help = "Excluded file extension(s)", long_help = "List of file extensions to exclude from search.")] pub excluded_extensions: Vec, #[clap(flatten)] pub file_to_save: FileToSave, #[clap(flatten)] pub json_compact_file_to_save: JsonCompactFileToSave, #[clap(flatten)] pub json_pretty_file_to_save: JsonPrettyFileToSave, #[clap( short = 'R', long, help = "Prevents recursive check of folders", long_help = "Disables recursive directory traversal. Only files in the top-level directories will be scanned." )] pub not_recursive: bool, #[cfg(target_family = "unix")] #[clap( short = 'X', long, help = "Exclude files on other filesystems", long_help = "Prevents scanning files on different filesystems (useful to avoid scanning mounted drives, network shares, etc.)" )] pub exclude_other_filesystems: bool, #[clap(flatten)] pub do_not_print: DoNotPrint, #[clap( short = 'W', long, help = "Ignore error code when files are found", long_help = "Suppresses error exit code when duplicate/similar files are found. Useful for scripts that should continue regardless of findings." )] pub ignore_error_code_on_found: bool, #[clap( short = 'H', long, help = "Disable cache", long_help = "Disables the cache system. This will make scanning slower but ensures fresh results without cached data." )] pub disable_cache: bool, } #[derive(Debug, clap::Args, Clone, Copy)] pub struct DoNotPrint { #[clap( short = 'N', long, help = "Do not print results to console", long_help = "Suppresses printing of search results to the console. Useful when only saving results to files." )] pub do_not_print_results: bool, #[clap( short = 'M', long, help = "Do not print messages to console", long_help = "Suppresses all informational messages, warnings, and errors from being printed to console." )] pub do_not_print_messages: bool, } #[derive(Debug, clap::Args, Clone, Copy)] pub struct DMethod { #[clap( short = 'D', long, default_value = "NONE", value_parser = parse_delete_method, help = "Delete method (AEN, AEO, ON, OO, AEB, AES, OB, OS, HARD)", 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)" )] pub delete_method: DeleteMethod, #[clap( short = 'Q', long, help = "Dry run - preview operations", long_help = "Performs a dry run showing what operations would be performed without actually executing them." )] pub dry_run: bool, #[clap( short = 'y', long, help = "Move items to trash", long_help = "Instead of permanently deleting files, move them to the system trash/recycle bin where they can be recovered." )] pub move_to_trash: bool, } // Simple delete method - delete files or not #[derive(Debug, clap::Args, Clone, Copy)] pub struct SDMethod { #[clap(short = 'D', long, help = "Delete found items", long_help = "Automatically delete all found items matching the criteria.")] pub delete_files: bool, #[clap( short = 'Q', long, help = "Dry run - preview operations", long_help = "Performs a dry run showing what operations would be performed without actually executing them." )] pub dry_run: bool, #[clap( short = 'y', long, help = "Move items to trash", long_help = "Instead of permanently deleting files, move them to the system trash/recycle bin where they can be recovered." )] pub move_to_trash: bool, } #[derive(Debug, clap::Args)] pub struct FileToSave { #[clap( short, long, value_name = "file-name", help = "Save results to formatted text file", long_help = "Saves the search results into a human-readable formatted text file." )] pub file_to_save: Option, } #[derive(Debug, clap::Args)] pub struct ReferenceDirectories { #[clap( short, long, help = "Reference directory(ies)", 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)." )] pub reference_directories: Vec, } #[derive(Debug, clap::Args)] pub struct JsonCompactFileToSave { #[clap( short = 'C', long, value_name = "json-file-name", help = "Save results to compact JSON file", long_help = "Saves the search results into a compact (minified) JSON file without extra whitespace." )] pub compact_file_to_save: Option, } #[derive(Debug, clap::Args)] pub struct JsonPrettyFileToSave { #[clap( short, long, value_name = "pretty-json-file-name", help = "Save results to pretty JSON file", long_help = "Saves the search results into a pretty-printed (indented) JSON file for better readability." )] pub pretty_file_to_save: Option, } #[derive(Debug, clap::Args)] pub struct AllowHardLinks { #[clap( short = 'L', long, help = "Do not ignore hard links", long_help = "Treats hard links as separate files rather than ignoring them. By default, hard links are detected and only counted once." )] pub allow_hard_links: bool, } #[derive(Debug, clap::Args)] pub struct CaseSensitiveNameComparison { #[clap( short = 'l', long, help = "Use case-sensitive name comparison", long_help = "Enables case-sensitive file name comparison. By default, comparisons are case-insensitive (e.g., 'File.txt' equals 'file.txt')." )] pub case_sensitive_name_comparison: bool, } #[derive(Debug, clap::Args)] pub struct IgnoreSameSize { #[clap( short = 'J', long, help = "Ignore files with same size", 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)." )] pub ignore_same_size: bool, } impl FileToSave { pub(crate) fn file_name(&self) -> Option<&str> { if let Some(file_name) = &self.file_to_save { return file_name.to_str(); } None } } impl JsonCompactFileToSave { pub(crate) fn file_name(&self) -> Option<&str> { if let Some(file_name) = &self.compact_file_to_save { return file_name.to_str(); } None } } impl JsonPrettyFileToSave { pub(crate) fn file_name(&self) -> Option<&str> { if let Some(file_name) = &self.pretty_file_to_save { return file_name.to_str(); } None } } fn parse_scan_duration(s: &str) -> Result { match s.parse::() { Ok(scan_duration) => { if ALLOWED_VID_HASH_DURATION.contains(&scan_duration) { Ok(scan_duration) } else { Err(format!("Scan duration must be one of: {ALLOWED_VID_HASH_DURATION:?}")) } } Err(e) => Err(e.to_string()), } } fn parse_crop_detect(src: &str) -> Result { match crop_detect_from_str_opt(src) { Some(crop_detect) => Ok(crop_detect), None => Err(format!("Crop detect \"{src}\" is not valid")), } } fn parse_skip_forward_amount(src: &str) -> Result { match src.parse::() { Ok(skip_forward_amount) => { if !ALLOWED_SKIP_FORWARD_AMOUNT.contains(&skip_forward_amount) { Err(format!("Skip forward amount must be one of: {ALLOWED_SKIP_FORWARD_AMOUNT:?}")) } else { Ok(skip_forward_amount) } } Err(e) => Err(e.to_string()), } } fn parse_hash_type(src: &str) -> Result { match src.to_ascii_lowercase().as_str() { "blake3" => Ok(HashType::Blake3), "crc32" => Ok(HashType::Crc32), "xxh3" => Ok(HashType::Xxh3), _ => Err("Couldn't parse the hash type (allowed: BLAKE3, CRC32, XXH3)"), } } fn parse_tolerance(src: &str) -> Result { match src.parse::() { Ok(t) => { if (0..=20).contains(&t) { Ok(t) } else { Err("Tolerance should be in range <0,20>(Higher and lower similarity )") } } _ => Err("Failed to parse tolerance as i32 value."), } } fn parse_checking_method_duplicate(src: &str) -> Result { match src.to_ascii_lowercase().as_str() { "name" => Ok(CheckingMethod::Name), "size" => Ok(CheckingMethod::Size), "size_name" => Ok(CheckingMethod::SizeName), "hash" => Ok(CheckingMethod::Hash), _ => Err("Couldn't parse the search method (allowed: NAME, SIZE, HASH)"), } } fn parse_broken_files(src: &str) -> Result { match src.to_ascii_lowercase().as_str() { "pdf" => Ok(CheckedTypes::PDF), "audio" => Ok(CheckedTypes::AUDIO), "image" => Ok(CheckedTypes::IMAGE), "archive" => Ok(CheckedTypes::ARCHIVE), "video" => Ok(CheckedTypes::VIDEO), _ => Err("Couldn't parse the broken files type (allowed: PDF, AUDIO, IMAGE, ARCHIVE, VIDEO)"), } } fn parse_checking_method_same_music(src: &str) -> Result { match src.to_ascii_lowercase().as_str() { "tags" => Ok(CheckingMethod::AudioTags), "content" => Ok(CheckingMethod::AudioContent), _ => Err("Couldn't parse the search method (allowed: TAGS, CONTENT)"), } } fn parse_video_codec(src: &str) -> Result { match src.to_ascii_lowercase().as_str() { "h264" => Ok(VideoCodec::H264), "h265" | "hevc" => Ok(VideoCodec::H265), "av1" => Ok(VideoCodec::Av1), "vp9" => Ok(VideoCodec::Vp9), _ => Err("Couldn't parse the video codec (allowed: h264, h265, av1, vp9)"), } } fn parse_max_samples(src: &str) -> Result { match src.parse::() { Ok(val) if (5..=1000).contains(&val) => Ok(val), Ok(_) => Err("Maximum samples must be between 5 and 1000".to_string()), Err(e) => Err(e.to_string()), } } fn parse_min_crop_size(src: &str) -> Result { match src.parse::() { Ok(val) if (1..=1000).contains(&val) => Ok(val), Ok(_) => Err("Minimum crop size must be between 1 and 1000".to_string()), Err(e) => Err(e.to_string()), } } fn parse_delete_method(src: &str) -> Result { match src.to_ascii_lowercase().as_str() { "none" => Ok(DeleteMethod::None), "aen" => Ok(DeleteMethod::AllExceptNewest), "aeo" => Ok(DeleteMethod::AllExceptOldest), "hard" => Ok(DeleteMethod::HardLink), "on" => Ok(DeleteMethod::OneNewest), "oo" => Ok(DeleteMethod::OneOldest), "aeb" => Ok(DeleteMethod::AllExceptBiggest), "aes" => Ok(DeleteMethod::AllExceptSmallest), "ob" => Ok(DeleteMethod::OneBiggest), "os" => Ok(DeleteMethod::OneSmallest), _ => Err("Couldn't parse the delete method (allowed: AEN, AEO, ON, OO, HARD, AEB, AES, OB, OS)"), } } fn parse_minimal_file_size(src: &str) -> Result { match src.parse::() { Ok(minimal_file_size) => { if minimal_file_size > 0 { Ok(minimal_file_size) } else { Err("Minimum file size must be at least 1 byte".to_string()) } } Err(e) => Err(e.to_string()), } } fn parse_maximal_file_size(src: &str) -> Result { match src.parse::() { Ok(maximal_file_size) => Ok(maximal_file_size), Err(e) => Err(e.to_string()), } } fn parse_similar_image_filter(src: &str) -> Result { let filter_type = match src.to_lowercase().as_str() { "lanczos3" => FilterType::Lanczos3, "nearest" => FilterType::Nearest, "triangle" => FilterType::Triangle, "gaussian" => FilterType::Gaussian, "catmullrom" => FilterType::CatmullRom, _ => return Err("Couldn't parse the image resize filter (allowed: Lanczos3, Nearest, Triangle, Gaussian, Catmullrom)".to_string()), }; Ok(filter_type) } fn parse_similar_hash_algorithm(src: &str) -> Result { let algorithm = match src.to_lowercase().as_str() { "mean" => HashAlg::Mean, "gradient" => HashAlg::Gradient, "blockhash" => HashAlg::Blockhash, "vertgradient" => HashAlg::VertGradient, "doublegradient" => HashAlg::DoubleGradient, "median" => HashAlg::Median, _ => return Err("Couldn't parse the hash algorithm (allowed: Mean, Gradient, Blockhash, VertGradient, DoubleGradient, Median)".to_string()), }; Ok(algorithm) } fn parse_image_hash_size(src: &str) -> Result { let hash_size = match src.to_lowercase().as_str() { "8" => 8, "16" => 16, "32" => 32, "64" => 64, _ => return Err("Couldn't parse the image hash size (allowed: 8, 16, 32, 64)".to_string()), }; Ok(hash_size) } fn parse_music_duplicate_type(src: &str) -> Result { if src.trim().is_empty() { return Ok(MusicSimilarity::NONE); } let mut similarity: MusicSimilarity = MusicSimilarity::NONE; let parts: Vec = src.split(',').map(|e| e.to_lowercase().replace('_', "")).collect(); if parts.contains(&"tracktitle".into()) { similarity |= MusicSimilarity::TRACK_TITLE; } if parts.contains(&"trackartist".into()) { similarity |= MusicSimilarity::TRACK_ARTIST; } if parts.contains(&"year".into()) { similarity |= MusicSimilarity::YEAR; } if parts.contains(&"bitrate".into()) { similarity |= MusicSimilarity::BITRATE; } if parts.contains(&"genre".into()) { similarity |= MusicSimilarity::GENRE; } if parts.contains(&"length".into()) { similarity |= MusicSimilarity::LENGTH; } if similarity == MusicSimilarity::NONE { return Err("Couldn't parse the music search method (allowed: track_title,track_artist,year,bitrate,genre,length)".to_string()); } Ok(similarity) } fn parse_crop_mechanism(src: &str) -> Result { match src.to_lowercase().as_str() { "blackbars" | "staticcontent" => Ok(src.to_lowercase()), _ => Err("Invalid crop mechanism. Allowed values: blackbars, staticcontent".to_string()), } } const HELP_TEMPLATE: &str = r#" {bin} {version} USAGE: {usage} [FLAGS] [OPTIONS] OPTIONS: {options} COMMANDS: {subcommands} try "{usage} -h" to get more info about a specific tool EXAMPLES: {bin} dup -d /home/rafal -e /home/rafal/Obrazy -m 25 -x 7z rar IMAGE -s hash -f results.txt -D aeo {bin} empty-folders -d /home/rafal/rr /home/gateway -f results.txt {bin} big -d /home/rafal/ /home/piszczal -e /home/rafal/Roman -n 25 -x VIDEO -f results.txt {bin} empty-files -d /home/rafal /home/szczekacz -e /home/rafal/Pulpit -R -f results.txt {bin} temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D {bin} image -d /home/rafal -e /home/rafal/Pulpit -f results.txt {bin} music -d /home/rafal -e /home/rafal/Pulpit -z \"artist,year,ARTISTALBUM,ALBUM___tiTlE\" -f results.txt {bin} symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt {bin} broken -d /home/mikrut/ -e /home/mikrut/trakt -f results.txt {bin} ext -d /home/mikrut/ -e /home/mikrut/trakt -f results.txt {bin} bad-names -d /home/rafal -u -j -w -n -f results.txt {bin} video-optimizer -d /home/rafal transcode -c h264 -f results.txt {bin} video-optimizer -d /home/rafal crop -m blackbars -f results.txt {bin} exif-remover -d /home/rafal -x IMAGE -f results.txt"#; ================================================ FILE: czkawka_cli/src/main.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::thread; use clap::Parser; use commands::Commands; use crossbeam_channel::{Receiver, Sender, unbounded}; use czkawka_core::common::config_cache_path::{print_infos_and_warnings, set_config_cache_path}; use czkawka_core::common::consts::DEFAULT_THREAD_SIZE; use czkawka_core::common::image::register_image_decoding_hooks; use czkawka_core::common::logger::{filtering_messages, print_version_mode, setup_logger}; use czkawka_core::common::progress_data::ProgressData; use czkawka_core::common::set_number_of_threads; use czkawka_core::common::tool_data::{CommonData, DeleteMethod}; use czkawka_core::common::traits::{AllTraits, FixingItems, PrintResults, Search}; use czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsFixParams, BadExtensionsParameters}; use czkawka_core::tools::bad_names::{BadNames, BadNamesParameters, NameFixerParams, NameIssues}; use czkawka_core::tools::big_file::{BigFile, BigFileParameters, SearchMode}; use czkawka_core::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes}; use czkawka_core::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters}; use czkawka_core::tools::empty_files::EmptyFiles; use czkawka_core::tools::empty_folder::EmptyFolder; use czkawka_core::tools::exif_remover::{ExifRemover, ExifRemoverParameters, ExifTagsFixerParams}; use czkawka_core::tools::invalid_symlinks::InvalidSymlinks; use czkawka_core::tools::same_music::{SameMusic, SameMusicParameters}; use czkawka_core::tools::similar_images::{SimilarImages, SimilarImagesParameters}; use czkawka_core::tools::similar_videos::{SimilarVideos, SimilarVideosParameters}; use czkawka_core::tools::temporary::Temporary; use czkawka_core::tools::video_optimizer::{ VideoCropFixParams, VideoCropParams, VideoCroppingMechanism, VideoOptimizer, VideoOptimizerFixParams, VideoOptimizerParameters, VideoTranscodeFixParams, VideoTranscodeParams, }; use log::{debug, error, info}; use crate::commands::{ Args, BadExtensionsArgs, BadNamesArgs, BiggestFilesArgs, BrokenFilesArgs, CommonCliItems, DMethod, DuplicatesArgs, EmptyFilesArgs, EmptyFoldersArgs, ExifRemoverArgs, InvalidSymlinksArgs, SDMethod, SameMusicArgs, SimilarImagesArgs, SimilarVideosArgs, TemporaryArgs, VideoOptimizerArgs, }; use crate::progress::connect_progress; mod commands; mod progress; #[derive(Debug)] pub struct CliOutput { pub found_any_files: bool, pub ignored_error_code_on_found: bool, pub output: String, } fn main() { register_image_decoding_hooks(); if cfg!(debug_assertions) { use clap::CommandFactory; Args::command().debug_assert(); } let command = Args::parse().command; let config_cache_path_set_result = set_config_cache_path("Czkawka", "Czkawka"); setup_logger(true, "czkawka_cli", filtering_messages); print_version_mode("Czkawka cli"); print_infos_and_warnings(config_cache_path_set_result.infos, config_cache_path_set_result.warnings); if cfg!(debug_assertions) { debug!("Running command - {command:?}"); } let (progress_sender, progress_receiver): (Sender, Receiver) = unbounded(); let stop_flag = Arc::new(AtomicBool::new(false)); let store_flag_cloned = stop_flag.clone(); let calculate_thread = thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || match command { Commands::Duplicates(duplicates_args) => duplicates(duplicates_args, &stop_flag, &progress_sender), Commands::EmptyFolders(empty_folders_args) => empty_folders(empty_folders_args, &stop_flag, &progress_sender), Commands::BiggestFiles(biggest_files_args) => biggest_files(biggest_files_args, &stop_flag, &progress_sender), Commands::EmptyFiles(empty_files_args) => empty_files(empty_files_args, &stop_flag, &progress_sender), Commands::Temporary(temporary_args) => temporary(temporary_args, &stop_flag, &progress_sender), Commands::SimilarImages(similar_images_args) => similar_images(similar_images_args, &stop_flag, &progress_sender), Commands::SameMusic(same_music_args) => same_music(same_music_args, &stop_flag, &progress_sender), Commands::InvalidSymlinks(invalid_symlinks_args) => invalid_symlinks(invalid_symlinks_args, &stop_flag, &progress_sender), Commands::BrokenFiles(broken_files_args) => broken_files(broken_files_args, &stop_flag, &progress_sender), Commands::SimilarVideos(similar_videos_args) => similar_videos(similar_videos_args, &stop_flag, &progress_sender), Commands::BadExtensions(bad_extensions_args) => bad_extensions(bad_extensions_args, &stop_flag, &progress_sender), Commands::BadNames(bad_names_args) => bad_names(bad_names_args, &stop_flag, &progress_sender), Commands::VideoOptimizer(video_optimizer_args) => video_optimizer(video_optimizer_args, &stop_flag, &progress_sender), Commands::ExifRemover(exif_remover_args) => exif_remover(exif_remover_args, &stop_flag, &progress_sender), }) .expect("Failed to spawn calculation thread"); ctrlc::set_handler(move || { if store_flag_cloned.load(std::sync::atomic::Ordering::SeqCst) { return; } info!("Got Ctrl+C signal, stopping..."); store_flag_cloned.store(true, std::sync::atomic::Ordering::SeqCst); }) .expect("Error setting Ctrl-C handler"); connect_progress(&progress_receiver); let cli_output = calculate_thread.join().expect("Failed to join calculation thread"); #[expect(clippy::print_stdout)] if !cli_output.output.is_empty() { println!("{}", cli_output.output); } if cli_output.found_any_files && !cli_output.ignored_error_code_on_found { std::process::exit(11); } else { std::process::exit(0); } } fn duplicates(duplicates: DuplicatesArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let DuplicatesArgs { common_cli_items, reference_directories, minimal_file_size, maximal_file_size, minimal_cached_file_size, search_method, delete_method, hash_type, allow_hard_links, case_sensitive_name_comparison, minimal_prehash_cache_file_size, use_prehash_cache, } = duplicates; let params = DuplicateFinderParameters::new( search_method, hash_type, use_prehash_cache, minimal_cached_file_size, minimal_prehash_cache_file_size, case_sensitive_name_comparison.case_sensitive_name_comparison, ); let mut tool = DuplicateFinder::new(params); set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref())); tool.set_minimal_file_size(minimal_file_size); tool.set_maximal_file_size(maximal_file_size); tool.set_hide_hard_links(!allow_hard_links.allow_hard_links); set_advanced_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn empty_folders(empty_folders: EmptyFoldersArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let EmptyFoldersArgs { common_cli_items, delete_method } = empty_folders; let mut tool = EmptyFolder::new(); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn biggest_files(biggest_files: BiggestFilesArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let BiggestFilesArgs { common_cli_items, number_of_files, delete_method, smallest_mode, } = biggest_files; let big_files_mode = if smallest_mode { SearchMode::SmallestFiles } else { SearchMode::BiggestFiles }; let params = BigFileParameters::new(number_of_files, big_files_mode); let mut tool = BigFile::new(params); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn empty_files(empty_files: EmptyFilesArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let EmptyFilesArgs { common_cli_items, delete_method } = empty_files; let mut tool = EmptyFiles::new(); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn temporary(temporary: TemporaryArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let TemporaryArgs { common_cli_items, delete_method } = temporary; let mut tool = Temporary::new(); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn similar_images(similar_images: SimilarImagesArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let SimilarImagesArgs { common_cli_items, reference_directories, minimal_file_size, maximal_file_size, max_difference, hash_alg, image_filter, hash_size, delete_method, allow_hard_links, ignore_same_size, } = similar_images; let params = SimilarImagesParameters::new(max_difference, hash_size, hash_alg, image_filter, ignore_same_size.ignore_same_size); let mut tool = SimilarImages::new(params); set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref())); tool.set_minimal_file_size(minimal_file_size); tool.set_maximal_file_size(maximal_file_size); tool.set_hide_hard_links(!allow_hard_links.allow_hard_links); set_advanced_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn same_music(same_music: SameMusicArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let SameMusicArgs { common_cli_items, reference_directories, delete_method, minimal_file_size, maximal_file_size, music_similarity, minimum_segment_duration, maximum_difference, search_method, approximate_comparison, compare_fingerprints_only_with_similar_titles, } = same_music; let params = SameMusicParameters::new( music_similarity, approximate_comparison, search_method, minimum_segment_duration, maximum_difference, compare_fingerprints_only_with_similar_titles, ); let mut tool = SameMusic::new(params); set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref())); tool.set_minimal_file_size(minimal_file_size); tool.set_maximal_file_size(maximal_file_size); set_advanced_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn invalid_symlinks(invalid_symlinks: InvalidSymlinksArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let InvalidSymlinksArgs { common_cli_items, delete_method } = invalid_symlinks; let mut tool = InvalidSymlinks::new(); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn broken_files(broken_files: BrokenFilesArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let BrokenFilesArgs { common_cli_items, delete_method, checked_types, } = broken_files; let mut checked_type = CheckedTypes::NONE; for check_type in checked_types { checked_type |= check_type; } let params = BrokenFilesParameters::new(checked_type); let mut tool = BrokenFiles::new(params); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn similar_videos(similar_videos: SimilarVideosArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let SimilarVideosArgs { reference_directories, common_cli_items, tolerance, minimal_file_size, maximal_file_size, delete_method, allow_hard_links, ignore_same_size, skip_forward_amount, crop_detect, scan_duration, } = similar_videos; let params = SimilarVideosParameters::new( tolerance, ignore_same_size.ignore_same_size, skip_forward_amount, scan_duration, crop_detect, false, // creating thumbnails in CLI, makes almost no sense 10, // creating thumbnails in CLI, makes almost no sense false, // creating thumbnails in CLI, makes almost no sense 2, // creating thumbnails in CLI, makes almost no sense ); let mut tool = SimilarVideos::new(params); set_common_settings(&mut tool, &common_cli_items, Some(reference_directories.reference_directories.as_ref())); tool.set_minimal_file_size(minimal_file_size); tool.set_maximal_file_size(maximal_file_size); tool.set_hide_hard_links(!allow_hard_links.allow_hard_links); set_advanced_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); save_and_write_results_to_writer(&tool, &common_cli_items) } fn bad_extensions(bad_extensions: BadExtensionsArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let BadExtensionsArgs { common_cli_items, fix_extensions } = bad_extensions; let params = BadExtensionsParameters::new(); let mut tool = BadExtensions::new(params); set_common_settings(&mut tool, &common_cli_items, None); tool.search(stop_flag, Some(progress_sender)); if fix_extensions { let fix_params = BadExtensionsFixParams {}; tool.fix_items(stop_flag, Some(progress_sender), fix_params); } save_and_write_results_to_writer(&tool, &common_cli_items) } fn bad_names(bad_names: BadNamesArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let BadNamesArgs { common_cli_items, delete_method, uppercase_extension, emoji_used, space_at_start_or_end, non_ascii_graphical, restricted_charset, remove_duplicated_non_alphanumeric, fix_names, } = bad_names; let restricted_charset_allowed = restricted_charset.and_then(|s| { let mut items: Vec<_> = s.chars().collect(); items.sort_unstable(); items.dedup(); if items.is_empty() { None } else { Some(items) } }); let name_issues = NameIssues { uppercase_extension, emoji_used, space_at_start_or_end, non_ascii_graphical, restricted_charset_allowed, remove_duplicated_non_alphanumeric, }; let params = BadNamesParameters::new(name_issues); let mut tool = BadNames::new(params); set_common_settings(&mut tool, &common_cli_items, None); set_simple_delete(&mut tool, delete_method); tool.search(stop_flag, Some(progress_sender)); if fix_names { let fix_params = NameFixerParams::default(); tool.fix_items(stop_flag, Some(progress_sender), fix_params); } save_and_write_results_to_writer(&tool, &common_cli_items) } fn video_optimizer(video_optimizer: VideoOptimizerArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { use crate::commands::{CropArgs, TranscodeArgs, VideoOptimizerMode as CliVideoOptimizerMode}; let VideoOptimizerArgs { common_cli_items, mode } = video_optimizer; match mode { CliVideoOptimizerMode::Transcode(transcode_args) => { let TranscodeArgs { excluded_codecs, generate_thumbnails, thumbnail_percentage, thumbnail_grid, fix_videos, target_codec, quality, fail_if_not_smaller, overwrite_original, limit_video_size, max_width, max_height, thumbnail_grid_tiles_per_side, } = transcode_args; let excluded_codecs_vec = excluded_codecs.map_or_else( || vec!["hevc".to_string(), "h265".to_string(), "av1".to_string(), "vp9".to_string()], |s| s.split(',').map(|c| c.trim().to_string()).collect(), ); let params = VideoOptimizerParameters::VideoTranscode(VideoTranscodeParams::new( excluded_codecs_vec, generate_thumbnails, thumbnail_percentage, thumbnail_grid, thumbnail_grid_tiles_per_side, )); let mut tool = VideoOptimizer::new(params); set_common_settings(&mut tool, &common_cli_items, None); tool.search(stop_flag, Some(progress_sender)); if fix_videos { let fix_params = VideoOptimizerFixParams::VideoTranscode(VideoTranscodeFixParams { codec: target_codec, quality, fail_if_not_smaller, overwrite_original, limit_video_size, max_width, max_height, }); tool.fix_items(stop_flag, Some(progress_sender), fix_params); } save_and_write_results_to_writer(&tool, &common_cli_items) } CliVideoOptimizerMode::Crop(crop_args) => { let CropArgs { crop_mechanism, black_pixel_threshold, black_bar_percentage, max_samples, min_crop_size, generate_thumbnails, thumbnail_percentage, thumbnail_grid, thumbnail_grid_tiles_per_side, fix_videos, overwrite_original, target_codec, quality, } = crop_args; #[expect(clippy::match_same_arms)] let crop_mech = match crop_mechanism.as_str() { "blackbars" => VideoCroppingMechanism::BlackBars, "staticcontent" => VideoCroppingMechanism::StaticContent, _ => VideoCroppingMechanism::BlackBars, }; let params = VideoOptimizerParameters::VideoCrop(VideoCropParams::with_custom_params( crop_mech, black_pixel_threshold, black_bar_percentage, max_samples, min_crop_size, generate_thumbnails, thumbnail_percentage, thumbnail_grid, thumbnail_grid_tiles_per_side, )); let mut tool = VideoOptimizer::new(params); set_common_settings(&mut tool, &common_cli_items, None); tool.search(stop_flag, Some(progress_sender)); if fix_videos { let fix_params = VideoOptimizerFixParams::VideoCrop(VideoCropFixParams { overwrite_original, target_codec, quality, crop_mechanism: crop_mech, }); tool.fix_items(stop_flag, Some(progress_sender), fix_params); } save_and_write_results_to_writer(&tool, &common_cli_items) } } } fn exif_remover(exif_remover: ExifRemoverArgs, stop_flag: &Arc, progress_sender: &Sender) -> CliOutput { let ExifRemoverArgs { common_cli_items, ignored_tags, fix_exif, override_file, } = exif_remover; let ignored_tags_vec = ignored_tags.map(|s| s.split(',').map(|tag| tag.trim().to_string()).collect()).unwrap_or_default(); let params = ExifRemoverParameters::new(ignored_tags_vec); let mut tool = ExifRemover::new(params); set_common_settings(&mut tool, &common_cli_items, None); tool.search(stop_flag, Some(progress_sender)); if fix_exif { let fix_params = ExifTagsFixerParams { override_file }; tool.fix_items(stop_flag, Some(progress_sender), fix_params); } save_and_write_results_to_writer(&tool, &common_cli_items) } fn save_and_write_results_to_writer(component: &T, common_cli_items: &CommonCliItems) -> CliOutput { if let Some(file_name) = common_cli_items.file_to_save.file_name() && let Err(e) = component.print_results_to_file(file_name) { error!("Failed to save results to file {e}"); } if let Some(file_name) = common_cli_items.json_compact_file_to_save.file_name() && let Err(e) = component.save_results_to_file_as_json(file_name, false) { error!("Failed to save compact json results to file {e}"); } if let Some(file_name) = common_cli_items.json_pretty_file_to_save.file_name() && let Err(e) = component.save_results_to_file_as_json(file_name, true) { error!("Failed to save pretty json results to file {e}"); } let mut buf_writer = std::io::BufWriter::new(Vec::new()); if !common_cli_items.do_not_print.do_not_print_results { let _ = component.print_results_to_writer(&mut buf_writer).map_err(|e| { error!("Failed to print results to output: {e}"); }); } if !common_cli_items.do_not_print.do_not_print_messages { let _ = component.get_text_messages().print_messages_to_writer(&mut buf_writer).map_err(|e| { error!("Failed to print results to output: {e}"); }); } let mut cli_output = CliOutput { found_any_files: component.found_any_items(), ignored_error_code_on_found: common_cli_items.ignore_error_code_on_found, output: String::new(), }; if let Ok(file_vec) = buf_writer.into_inner() && let Ok(output) = String::from_utf8(file_vec) { cli_output.output = output; } cli_output } fn set_simple_delete(component: &mut T, s_delete: SDMethod) where T: AllTraits, { if s_delete.delete_files { component.set_delete_method(DeleteMethod::Delete); } component.set_dry_run(s_delete.dry_run); component.set_move_to_trash(s_delete.move_to_trash); } fn set_advanced_delete(component: &mut T, a_delete: DMethod) where T: AllTraits, { component.set_delete_method(a_delete.delete_method); component.set_dry_run(a_delete.dry_run); component.set_move_to_trash(a_delete.move_to_trash); } fn set_common_settings(component: &mut T, common_cli_items: &CommonCliItems, reference_directories: Option<&Vec>) where T: AllTraits, { set_number_of_threads(common_cli_items.thread_number); let mut included_directories = common_cli_items.directories.clone(); if let Some(reference_directories) = reference_directories { included_directories.extend_from_slice(reference_directories); component.set_reference_paths(reference_directories.clone()); } component.set_included_paths(included_directories); component.set_excluded_paths(common_cli_items.excluded_directories.clone()); component.set_excluded_items(common_cli_items.excluded_items.clone()); component.set_recursive_search(!common_cli_items.not_recursive); #[cfg(target_family = "unix")] component.set_exclude_other_filesystems(common_cli_items.exclude_other_filesystems); component.set_allowed_extensions(common_cli_items.allowed_extensions.clone()); component.set_excluded_extensions(common_cli_items.excluded_extensions.clone()); component.set_use_cache(!common_cli_items.disable_cache); } ================================================ FILE: czkawka_cli/src/progress.rs ================================================ use std::time::Duration; use crossbeam_channel::Receiver; use czkawka_core::common::model::ToolType; use czkawka_core::common::progress_data::{CurrentStage, ProgressData}; use humansize::{BINARY, format_size}; use indicatif::{ProgressBar, ProgressStyle}; pub(crate) fn connect_progress(progress_receiver: &Receiver) { let mut pb = ProgressBar::new(1); let mut latest_id = None; while let Ok(progress_data) = progress_receiver.recv() { // We only need to recreate progress bar if stage changed if latest_id != Some(progress_data.current_stage_idx) { pb.finish_and_clear(); if progress_data.current_stage_idx == 0 { pb = get_progress_bar_for_collect_files(); } else if progress_data.sstage.check_if_loading_saving_cache() { pb = get_progress_loading_saving_cache(progress_data.sstage.check_if_loading_cache()); } else if progress_data.bytes_to_check != 0 { pb = get_progress_known_values(progress_data.bytes_to_check); } else { pb = get_progress_known_values(progress_data.entries_to_check as u64); } latest_id = Some(progress_data.current_stage_idx); } if progress_data.sstage == CurrentStage::CollectingFiles && progress_data.tool_type != ToolType::EmptyFolders { pb.set_message(format!("Collecting files: {}", progress_data.entries_checked)); } else if progress_data.sstage == CurrentStage::CollectingFiles { pb.set_message(format!("Collecting folders: {}", progress_data.entries_checked)); } else if !progress_data.sstage.check_if_loading_saving_cache() { if progress_data.bytes_to_check != 0 { pb.set_position(progress_data.bytes_checked); pb.set_message(format!( "{}: {}/{} ({}/{})", get_progress_message(&progress_data), progress_data.entries_checked, progress_data.entries_to_check, format_size(progress_data.bytes_checked, BINARY), format_size(progress_data.bytes_to_check, BINARY) )); } else { pb.set_position(progress_data.entries_checked as u64); pb.set_message(format!( "{}: {}/{}", get_progress_message(&progress_data), progress_data.entries_checked, progress_data.entries_to_check )); } } } pb.finish_and_clear(); } pub(crate) fn get_progress_message(progress_data: &ProgressData) -> String { match progress_data.sstage { CurrentStage::SameMusicReadingTags => "Reading tags", CurrentStage::SameMusicCalculatingFingerprints => "Calculating fingerprints", CurrentStage::SameMusicComparingTags => "Comparing tags", CurrentStage::SameMusicComparingFingerprints => "Comparing fingerprints", CurrentStage::DuplicatePreHashing => "Calculating prehashes", CurrentStage::DuplicateFullHashing => "Calculating hashes", CurrentStage::SimilarImagesCalculatingHashes => "Calculating image hashes", CurrentStage::SimilarImagesComparingHashes => "Comparing image hashes", CurrentStage::SimilarVideosCalculatingHashes => "Reading similar values", CurrentStage::SimilarVideosCreatingThumbnails | CurrentStage::VideoOptimizerCreatingThumbnails => "Creating video thumbnails", CurrentStage::BrokenFilesChecking => "Checking broken files", CurrentStage::BadExtensionsChecking => "Checking extensions of files", CurrentStage::DeletingFiles => "Deleting files/folders", CurrentStage::RenamingFiles => "Renaming files", CurrentStage::MovingFiles => "Moving files", CurrentStage::HardlinkingFiles => "Creating hardlinks", CurrentStage::SymlinkingFiles => "Creating symlinks", CurrentStage::OptimizingVideos => "Optimizing videos", CurrentStage::CleaningExif => "Cleaning EXIF data", CurrentStage::ExifRemoverExtractingTags => "Extracting EXIF tags", CurrentStage::VideoOptimizerProcessingVideos => "Processing videos", CurrentStage::BadNamesChecking => "Checking names of files", CurrentStage::CollectingFiles | CurrentStage::DuplicateCacheSaving | CurrentStage::DuplicateCacheLoading | CurrentStage::DuplicatePreHashCacheSaving | CurrentStage::DuplicatePreHashCacheLoading | CurrentStage::DuplicateScanningName | CurrentStage::DuplicateScanningSizeName | CurrentStage::DuplicateScanningSize | CurrentStage::SameMusicCacheSavingTags | CurrentStage::SameMusicCacheLoadingTags | CurrentStage::SameMusicCacheSavingFingerprints | CurrentStage::SameMusicCacheLoadingFingerprints | CurrentStage::ExifRemoverCacheLoading | CurrentStage::ExifRemoverCacheSaving => unreachable!("This stages(caches, initial files scanning) should be handled somewhere else"), } .to_string() } pub(crate) fn get_progress_bar_for_collect_files() -> ProgressBar { let pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(120)); #[expect(clippy::literal_string_with_formatting_args)] pb.set_style( ProgressStyle::with_template("{msg} {spinner:.blue}") .expect("Failed to create progress bar style") .tick_strings(&["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸", "▪▪▪▪▪"]), ); pb } pub(crate) fn get_progress_known_values(max_value: u64) -> ProgressBar { let pb = ProgressBar::new(max_value); pb.set_style( ProgressStyle::with_template("{msg} [{bar}]") .expect("Failed to create progress bar style") .progress_chars("=> "), ); pb } pub(crate) fn get_progress_loading_saving_cache(loading: bool) -> ProgressBar { let msg = if loading { "Loading cache" } else { "Saving cache" }; let pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(120)); pb.set_style( ProgressStyle::with_template(&format!("{msg} {{spinner:.blue}}")) .expect("Failed to create progress bar style") .tick_strings(&["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸", "▪▪▪▪▪"]), ); pb } ================================================ FILE: czkawka_core/Cargo.toml ================================================ [package] name = "czkawka_core" version = "11.0.1" authors = ["Rafał Mikrut "] edition = "2024" rust-version = "1.92.0" description = "Core of Czkawka app" license = "MIT" homepage = "https://github.com/qarmin/czkawka" repository = "https://github.com/qarmin/czkawka" build = "build.rs" [dependencies] humansize = "2.1" rayon = "1.10" crossbeam-channel = "0.5" # For stable iteration over hashmaps. indexmap = "2.11" # For saving/loading config files to specific directories directories-next = "2.0" # Needed by similar images image_hasher = { version = "3.0", features = ["fast_resize_unstable"] } bk-tree = "0.5" image = { version = "0.25", default-features = false, features = ["bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp", "rayon"] } hamming-bitwise-fast = "1.0" fast_image_resize = { version = "6.0.0", features = ["image"] } # Needed by same music bitflags = "2.6" lofty = "0.23" # Needed by broken files zip = { version = "8.1", features = ["aes-crypto", "bzip2", "deflate", "time"], default-features = false } lopdf = "0.39.0" # Needed by audio similarity feature rusty-chromaprint = "0.3" symphonia = { version = "0.5", features = ["all"] } # Hashes for duplicate files blake3 = "1.5" crc32fast = "1.4" xxhash-rust = { version = "0.8", features = ["xxh3"] } tempfile = "3.13" # Video Duplicates vid_dup_finder_lib = "0.4" filetime = "0.2.26" # For extracting video properties using ffprobe CLI # https://github.com/theduke/ffprobe-rs/issues/33 #ffprobe = "0.4.0" # Saving/Loading Cache serde = "1.0" bincode = "<2.0" serde_json = "1.0" # Language i18n-embed = { version = "0.16", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.10" rust-embed = { version = "8.5", features = ["debug-embed"] } once_cell = "1.20" # Raw image files rawler = "0.7.0" libraw-rs = { version = "0.0.4", optional = true } jxl-oxide = { version = "0.12.0", features = ["image"] } # Checking for invalid extensions mime_guess = "2.0" infer = "0.19" # Newer version of libheif-rs, which is not compatible with Ubuntu 22.04, and works only o libheif-rs = { version = "2", optional = true, default-features = false, features = ["v1_17", "image"] } nom-exif = "2.1.0" # EXIF data cleaning little_exif = "0.6.20" dunce = "1.0.5" os_info = { version = "3", default-features = false } log = "0.4.22" handsome_logger = "0.9" fun_time = { version = "0.3", features = ["log"] } itertools = "0.14" static_assertions = "1.1.0" file-rotate = "0.8.0" open = "5.3" log-panics = { version = "2.1.0", features = ["with-backtrace"] } deunicode = "1.6.2" glibc_musl_version = "0.1.0" rand = "0.10.0" ashpd = { version = "0.13.2", optional = true, features = ["trash"] } tokio = { version = "1.49.0", optional = true } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] trash = "5.1" [target.'cfg(windows)'.dependencies] file-id = "0.2.2" [build-dependencies] rustc_version = "0.4" glibc_musl_version = "0.1.0" [dev-dependencies] criterion = { version = "0.8", default-features = false, features = [] } [[bench]] name = "hash_calculation_benchmark" harness = false [features] default = [] heif = ["dep:libheif-rs"] libraw = ["dep:libraw-rs"] libavif = ["image/avif-native", "image/avif"] blake_pure = ["blake3/pure"] # Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails # No-op on other OSes, it is slower and provides less helpful error messages xdg_portal_trash = ["ashpd", "tokio"] [lints] workspace = true ================================================ FILE: czkawka_core/LICENSE_CC_BY_4_TEST_FILES ================================================ All icons and audio files, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). Copyright (c) 2020-2026 Rafał Mikrut - test_resources/*/*.png - test_resources/*/*.mp3 (generated by AI) License: CC-BY-4.0 Creative Commons Attribution 4.0 International Public License . By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. . Section 1 -- Definitions. . a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. . b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. . c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. . d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. . e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. . f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. . g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. . h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. . i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. . j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. . k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. . Section 2 -- Scope. . a. License grant. . 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: . a. reproduce and Share the Licensed Material, in whole or in part; and . b. produce, reproduce, and Share Adapted Material. . 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. . 3. Term. The term of this Public License is specified in Section 6(a). . 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. . 5. Downstream recipients. . a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. . b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. . 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). . b. Other rights. . 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. . 2. Patent and trademark rights are not licensed under this Public License. . 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. . Section 3 -- License Conditions. . Your exercise of the Licensed Rights is expressly made subject to the following conditions. . a. Attribution. . 1. If You Share the Licensed Material (including in modified form), You must: . a. retain the following if it is supplied by the Licensor with the Licensed Material: . i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); . ii. a copyright notice; . iii. a notice that refers to this Public License; . iv. a notice that refers to the disclaimer of warranties; . v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; . b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and . c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. . 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. . 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. . 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. . Section 4 -- Sui Generis Database Rights. . Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: . a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; . b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and . c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. . For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. . Section 5 -- Disclaimer of Warranties and Limitation of Liability. . a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. . b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. . c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. . Section 6 -- Term and Termination. . a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. . b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: . 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or . 2. upon express reinstatement by the Licensor. . For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. . c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. . d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. . Section 7 -- Other Terms and Conditions. . a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. . b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. . Section 8 -- Interpretation. . a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. . b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. . c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. . d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ================================================ FILE: czkawka_core/LICENSE_MIT ================================================ MIT License Copyright (c) 2020-2026 Rafał Mikrut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: czkawka_core/README.md ================================================ # Czkawka Core Core of Czkawka GUI/CLI and Krokiet projects. ================================================ FILE: czkawka_core/benches/hash_calculation_benchmark.rs ================================================ use std::env::temp_dir; use std::fs::File; use std::hint::black_box; use std::io::Write; use std::path::PathBuf; use std::sync::Arc; use criterion::{Criterion, criterion_group, criterion_main}; use czkawka_core::common::model::HashType; use czkawka_core::tools::duplicate::{DuplicateEntry, hash_calculation}; fn setup_test_file(size: u64) -> PathBuf { let path = temp_dir().join("test_file"); let mut file = File::create(&path).expect("Failed to create test file"); file.write_all(&vec![0u8; size as usize]).expect("Failed to write to test file"); path } fn get_file_entry(size: u64) -> DuplicateEntry { let path = setup_test_file(size); DuplicateEntry { path, modified_date: 0, size, hash: String::new(), } } fn benchmark_hash_calculation_vec(c: &mut Criterion) { let file_entry = get_file_entry(FILE_SIZE); let function_name = format!("hash_calculation_vec_file_{FILE_SIZE}_buffer_{BUFFER_SIZE}"); c.bench_function(&function_name, |b| { b.iter(|| { let mut buffer = vec![0u8; BUFFER_SIZE]; hash_calculation( black_box(&mut buffer), black_box(&file_entry), black_box(HashType::Blake3), &Arc::default(), &Arc::default(), ) .expect("Failed to calculate hash"); }); }); } fn benchmark_hash_calculation_arr(c: &mut Criterion) { let file_entry = get_file_entry(FILE_SIZE); let function_name = format!("hash_calculation_arr_file_{FILE_SIZE}_buffer_{BUFFER_SIZE}"); c.bench_function(&function_name, |b| { b.iter(|| { let mut buffer = [0u8; BUFFER_SIZE]; hash_calculation( black_box(&mut buffer), black_box(&file_entry), black_box(HashType::Blake3), &Arc::default(), &Arc::default(), ) .expect("Failed to calculate hash"); }); }); } criterion_group!(benches, benchmark_hash_calculation_vec<{16 * 1024 * 1024}, {16 * 1024}>, benchmark_hash_calculation_vec<{16 * 1024 * 1024}, {1024 * 1024}>, benchmark_hash_calculation_arr<{16 * 1024 * 1024}, {16 * 1024}>, benchmark_hash_calculation_arr<{16 * 1024 * 1024}, {1024 * 1024}>, ); criterion_main!(benches); ================================================ FILE: czkawka_core/build.rs ================================================ fn main() { let rust_version = match rustc_version::version_meta() { Ok(meta) => { let rust_v = meta.semver.to_string(); let rust_date = meta.commit_date.unwrap_or_default(); format!("{rust_v} ({rust_date})") } Err(_) => "".to_string(), }; println!("cargo:rustc-env=RUST_VERSION_INTERNAL={rust_version}"); if let Ok(encoded) = std::env::var("CARGO_ENCODED_RUSTFLAGS") { println!("cargo:rustc-env=UUSED_RUSTFLAGS={encoded}"); } // Get Git commit hash let git_commit = std::process::Command::new("git") .args(["rev-parse", "HEAD"]) .output() .ok() .and_then(|output| if output.status.success() { String::from_utf8(output.stdout).ok() } else { None }) .map_or_else(|| "".to_string(), |s| s.trim().to_string()); println!("cargo:rustc-env=CZKAWKA_GIT_COMMIT={git_commit}"); // Get short Git commit hash let git_commit_short = if git_commit != "" && git_commit.len() >= 10 { git_commit.chars().take(10).collect::() } else { git_commit }; println!("cargo:rustc-env=CZKAWKA_GIT_COMMIT_SHORT={git_commit_short}"); // Commit date let git_commit_date = std::process::Command::new("git") .args(["log", "-1", "--format=%cd", "--date=format:%Y-%m-%d"]) .output() .ok() .and_then(|output| if output.status.success() { String::from_utf8(output.stdout).ok() } else { None }) .map_or_else(|| "".to_string(), |s| s.trim().to_string()); println!("cargo:rustc-env=CZKAWKA_GIT_COMMIT_DATE={git_commit_date}"); // Official build flag if std::env::var("CZKAWKA_OFFICIAL_BUILD") == Ok("1".to_string()) { println!("cargo:rustc-env=CZKAWKA_OFFICIAL_BUILD=1"); } else { println!("cargo:rustc-env=CZKAWKA_OFFICIAL_BUILD=0"); } let using_cranelift = std::env::var("CARGO_PROFILE_RELEASE_CODEGEN_UNITS") == Ok("1".to_string()) || std::env::var("CARGO_PROFILE_DEV_CODEGEN_BACKEND") == Ok("cranelift".to_string()); if using_cranelift { println!("cargo:rustc-env=USING_CRANELIFT=1"); } if cfg!(target_os = "linux") { if let Ok(ver) = glibc_musl_version::get_os_libc_versions() { println!("cargo:rustc-env=CZKAWKA_LIBC_VERSIONS={ver}"); } if cfg!(target_env = "gnu") { println!("cargo:rustc-env=CZKAWKA_LIBC=glibc"); } else if cfg!(target_env = "musl") { println!("cargo:rustc-env=CZKAWKA_LIBC=musl"); } else { println!("cargo:rustc-env=CZKAWKA_LIBC=unknown"); } } } ================================================ FILE: czkawka_core/i18n/ar/czkawka_core.ftl ================================================ # Core core_similarity_original = الأصل core_similarity_very_high = عالية جدا core_similarity_high = مرتفع core_similarity_medium = متوسط core_similarity_small = صغير core_similarity_very_small = صغير جدا core_similarity_minimal = الحد الأدنى core_cannot_open_dir = لا يمكن فتح dir { $dir }، السبب { $reason } core_cannot_read_entry_dir = لا يمكن قراءة الإدخال في dir { $dir }، السبب { $reason } core_cannot_read_metadata_dir = لا يمكن قراءة البيانات الوصفية في dir { $dir }، السبب { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = يبدو أن الملف { $name } قد تم تعديله قبل يونكس Epoch core_folder_modified_before_epoch = يبدو أن المجلد { $name } قد تم تعديله قبل يونكس Epoch core_file_no_modification_date = غير قادر على الحصول على تاريخ التعديل من الملف { $name }، السبب { $reason } core_folder_no_modification_date = غير قادر على الحصول على تاريخ التعديل من المجلد { $name }، السبب { $reason } core_cannot_start_scan_no_included_paths = لا يمكن بدء المسح، لأن لا توجد مسارات مضمنة core_skip_exist_check_all_included_paths_nonexistent = لا يمكن بدء المسح، لأن جميع المسارات المدرجة غير موجودة core_missing_no_chosen_included_path = لم يتم اختيار مسار مضمن صالح (كانت المسارات المضمنة المستبعدة تستبعد جميع المسارات المضمنة) core_reference_included_paths_same = لا يمكن بدء المسح حيث تكون جميع المسارات المدرجة الصالحة أيضًا مسارات مرجعية، حاول التحقق من الصحة أو تعطيل المسارات المرجعية core_path_must_exists = يجب أن يكون المسار المقدم موجودًا، مع تجاهل { $path } core_must_be_directory_or_file = يجب أن يشير المسار المقدم إلى دليل أو ملف صالح، مع تجاهل { $path } core_excluded_paths_pointless_slash = باستثناء / لا معنى له، لأنه يعني عدم فحص الملفات core_paths_unable_to_get_device_id = تعذر الحصول على معرف الجهاز من المجلد { $path } core_needs_allowed_extensions_limited_by_tool = لا يمكن بدء المسح، عندما تم استبعاد جميع الإضافات المتاحة في هذا الأداة ({ $extensions }) من المسح core_needs_allowed_extensions = لا يمكن بدء المسح، عندما تم استبعاد جميع الإضافات من المسح core_needs_to_set_at_least_one_broken_option = لا يمكن بدء المسح، عندما لا يتم تعيين خيار "معطل" للمسح core_needs_to_set_at_least_one_bad_name_option = لا يمكن بدء المسح، عندما لا يتم تعيين خيار الاسم السيئ للبحث عنه core_ffmpeg_not_found = لا يمكن العثور على تثبيت مناسب لـ FFmpeg أو FFprobe. هذه برامج خارجية يجب تثبيتها يدويًا. core_ffmpeg_not_found_windows = تأكد من أن ffmpeg.exe و ffprobe.exe متوفرتان في PATH أو يتم وضعهما مباشرة في نفس المجلد مع التطبيق القابل للتنفيذ core_invalid_symlink_infinite_recursion = التكرار اللامتناهي core_invalid_symlink_non_existent_destination = ملف الوجهة غير موجود core_messages_limit_reached_characters = تجاوز عدد الرسائل الحد الأقصى المحدد ({ $current }/{ $limit } حرفا)، لذا تم اقتطاع الخروج. لقراءة الإخراج الكامل، قم بتعطيل خيار الحد في الإعدادات. core_messages_limit_reached_lines = تجاوز عدد الرسائل الحد الأقصى المحدد ({ $current }/{ $limit } سطر)، لذا تم اقتطاع الخروج. لقراءة الإخراج الكامل، قم بتعطيل خيار الحد في الإعدادات. core_error_moving_to_trash = خطأ أثناء نقل "{ $file }" إلى سلة المحذوفات: { $error } core_error_removing = خطأ أثناء حذف "{ $file }": { $error } core_no_similarity_method_selected = لا يمكن العثور على ملفات موسيقية مماثلة بدون طريقة تشابه محددة core_failed_to_spawn_command = فشل أمر الإطلاق: { $reason } core_failed_to_check_process_status = فشل التحقق من حالة العملية: { $reason } core_failed_to_wait_for_process = فشل الانتظار للعملية: { $reason } core_failed_to_read_video_properties = فشل في قراءة خصائص الفيديو: { $reason } core_failed_to_execute_ffmpeg = فشل تنفيذ ffmpeg: { $reason } core_ffmpeg_failed_with_status = فشل ffmpeg مع الحالة { $status }: { $stderr } (الأمر: { $command }) core_failed_to_load_image_frame = فشل تحميل إطار الصورة: { $reason } core_failed_to_extract_frame = فشل استخراج الإطار في { $time } ثانية من "{ $file }": { $reason } core_failed_to_save_thumbnail = فشل حفظ썸 فين لـ "{ $file }": { $reason } core_failed_get_frame_at_timestamp = فشل في الحصول على الإطار في الطابع الزمني { $timestamp } من "{ $file }": { $reason } core_failed_get_frame_from_file = فشل في الحصول على الإطار من "{ $file }" في الطابع الزمني { $timestamp }: { $reason } core_invalid_crop_rectangle = غير صالح مستطيل المحصول: يسار={ $left }، أعلى={ $top }، يمين={ $right }، أسفل={ $bottom } core_failed_to_crop_video_file = فشل قص الفيديو "{ $file }": { $reason } core_cropped_video_not_created = الملف المرئي المقتطع لم يتم إنشاؤه: { $temp } core_unable_check_hash_of_file = تعذر التحقق من تجزئة الملف "{ $file }"، والسبب { $reason } core_error_checking_hash_of_file = حدث خطأ عند التحقق من تجزئة الملف "{ $file }"، السبب { $reason } core_image_zero_dimensions = الصورة لها عرض أو ارتفاع يساوي صفر "{ $path }" core_image_open_failed = لا يمكن فتح ملف الصورة "{ $path }": { $reason } core_not_directory_remove = محاولة إزالة المجلد "{ $path }" والذي ليس مجلدًا core_cannot_read_directory = لا يمكن قراءة الدليل "{ $path }" core_cannot_read_entry_from_directory = لا يمكن قراءة الإدخال من الدليل "{ $path }" core_folder_contains_file_inside = المجلد يحتوي على الملف "{ $entry }" داخل "{ $folder }" core_unknown_directory_entry = تعذر تحديد نوع الملف لإدخال الدليل "{ $entry }" داخل "{ $path }" core_video_width_exceeds_limit = عرض الفيديو { $width } يتجاوز الحد الأقصى لـ { $limit } core_video_height_exceeds_limit = فيديو الارتفاع { $height } يتجاوز الحد الأقصى لـ { $limit } core_failed_to_process_video = فشل معالجة ملف الفيديو { $file }: { $reason } core_optimized_file_larger = الملف المحسن { $optimized } (الحجم: { $new_size }) ليس أصغر من الأصلي { $original } (الحجم: { $original_size }) core_unknown_codec = ترميز غير معروف: { $codec } core_invalid_video_optimizer_mode = وضع مُحسِّن الفيديو غير صالح: '{ $mode }'. القيم المسموح بها: transcode, crop core_folder_does_not_exist = المجلد غير موجود: { $folder } core_path_not_directory = المسار ليس مجلدًا: { $folder } core_test_error_for_folder = خطأ في الاختبار للمجلد: { $folder } core_unknown_exif_tag_group = مجموعة بيانات EXIF غير المعروفة: { $tag } core_error_comparing_fingerprints = خطأ أثناء مقارنة بصمات الأصابع: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = فشل إنشاء صورة مصغرة لـ "{ $file }": الصور المستخرجة لها أبعاد مختلفة core_failed_to_generate_thumbnail = فشل إنشاء الصورة المصغرة لـ "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = فشل استخراج الإطار في { $time } ثانية من "{ $file }": { $reason } core_video_file_does_not_exist = الملف المرئي غير موجود (يمكن حذفه بين المسح/الخطوات اللاحقة): "{ $path }" core_image_too_large = الصورة كبيرة جداً ({ $width }x{ $height }) - أكثر من المدعوم { $max } بكسل core_failed_to_get_video_metadata = فشل الحصول على بيانات الفيديو للملف "{ $file }": { $reason } core_failed_to_get_video_codec = فشل الحصول على ترميز الفيديو للملف "{ $file }" core_failed_to_get_video_duration = تعذر الحصول على مدة الفيديو للملف "{ $file }" core_failed_to_get_video_dimensions = فشل الحصول على أبعاد الفيديو للملف "{ $file }" core_frame_dimensions_mismatch = أبعاد الإطار لعلامة الوقت { $timestamp } لا تتطابق مع أبعاد الإطار الأول ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = تعذر تحميل البيانات من ملف التخزين المؤقت { $file }، والسبب { $reason } core_failed_to_load_data_from_json_cache = تعذر تحميل البيانات من ملف ذاكرة التخزين المؤقت JSON { $file}، والسبب { $reason} core_failed_to_replace_with_optimized = فشل استبدال الملف "{ $file }" بإصدار مُحسّن: { $reason } core_failed_to_write_data_to_cache = لا يمكن كتابة البيانات إلى ملف التخزين المؤقت "{ $file }"، والسبب { $reason } core_properly_saved_cache_entries = تم حفظها بشكل صحيح في الملف { $count } إدخالات ذاكرة تخزين مؤقت. core_video_processing_stopped_by_user = تم إيقاف معالجة الفيديو بواسطة المستخدم core_thumbnail_generation_stopped_by_user = إنشاء الصور المصغرة توقف بواسطة المستخدم core_failed_to_optimize_video = فشل تحسين الفيديو "{ $file }": { $reason } core_failed_to_crop_video = فشل قص الفيديو "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = فشل الحصول على بيانات التعريف للملف المحسن "{ $file }": { $reason } core_cannot_create_config_folder = لا يمكن إنشاء مجلد الإعدادات "{ $folder }"، والسبب { $reason } core_cannot_create_cache_folder = لا يمكن إنشاء مجلد ذاكرة التخزين المؤقت "{ $folder }"، والسبب { $reason } core_cannot_create_or_open_cache_file = لا يمكن إنشاء أو فتح ملف التخزين المؤقت "{ $file }"، والسبب { $reason } core_cannot_set_config_cache_path = لا يمكن تعيين مسار التكوين/التخزين المؤقت - لن يتم استخدام التكوين والتخزين المؤقت. core_invalid_extension_contains_space = { $extension } ليس امتدادًا صالحًا لأنه يحتوي على مساحة فارغة داخلية core_invalid_extension_contains_dot = { $extension } ليس امتدادًا صالحًا لأنه يحتوي على نقطة داخلية ================================================ FILE: czkawka_core/i18n/bg/czkawka_core.ftl ================================================ # Core core_similarity_original = Оригинален core_similarity_very_high = Много високо core_similarity_high = Високо core_similarity_medium = Средно core_similarity_small = Малко core_similarity_very_small = Много малък core_similarity_minimal = Минимално core_cannot_open_dir = Не може да се отвори папка { $dir }, причината е { $reason } core_cannot_read_entry_dir = Не може да се прочете папка { $dir }, причината е { $reason } core_cannot_read_metadata_dir = Не могат да се прочетат мета-данните в папка { $dir }, причината е { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch core_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch core_file_no_modification_date = Невъзможно е да се извлече дата на промяна от файл { $name }, причината е { $reason } core_folder_no_modification_date = Невъзможно е да се извлече дата на промяна от папка { $name }, причината е { $reason } core_cannot_start_scan_no_included_paths = Не може да се стартира сканирането, защото няма включени пътища core_skip_exist_check_all_included_paths_nonexistent = Не може да се стартира сканирането, защото всички включени пътища не съществуват core_missing_no_chosen_included_path = Няма избран валиден път, включен (изключените пътища биха могли да са изключили всички включени пътища) core_reference_included_paths_same = Не може да се стартира сканиране, където всички валидни включени пътища са също и препратени пътища, опитайте се да валидирате или да деактивирате препратените пътища core_path_must_exists = Предоставеният път трябва да съществува, пренебрегвайки { $path } core_must_be_directory_or_file = Предоставеният път трябва да сочи валиден директория или файл, пренебрегвайки { $path } core_excluded_paths_pointless_slash = Изключването / е безсмислено, защото означава, че няма да бъдат сканирани файлове core_paths_unable_to_get_device_id = Не може да се получи идентификатор на устройството от папката { $path } core_needs_allowed_extensions_limited_by_tool = Не може да се стартира сканирането, когато всички налични разширения в този инструмент ({ $extensions }) бяха изключени от сканирането core_needs_allowed_extensions = Не може да се стартира сканирането, когато всички разширения са били изключени от сканирането core_needs_to_set_at_least_one_broken_option = Не може да се стартира сканиране, когато не е зададена опция за сканиране на повредени core_needs_to_set_at_least_one_bad_name_option = Не може да се стартира сканирането, когато не е зададена опцията за лошо име за сканиране core_ffmpeg_not_found = Не може да се намери подходяща инсталация на FFmpeg или FFprobe. Те са външни програми, които трябва да бъдат инсталирани ръчно. core_ffmpeg_not_found_windows = Бъдете сигурни, че ffmpeg.exe и ffprobe.exe са налични в PATH или са разположени напрямок в същата папка като изпълнимият файл на апplикацията core_invalid_symlink_infinite_recursion = Безкрайна рекурсия core_invalid_symlink_non_existent_destination = Несъществуващ дестинационен файл core_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. core_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. core_error_moving_to_trash = Грешка при преместване на "{ $file }" в кош: { $error } core_error_removing = Огледална грешка при изтриване на "{ $file }": { $error } core_no_similarity_method_selected = Не можете да намерите подобри музикални файлове без избран метод за сличност core_failed_to_spawn_command = Не успя да стартира командата: { $reason } core_failed_to_check_process_status = Не успях да проверя статуса на процеса: { $reason } core_failed_to_wait_for_process = Не успях да изчакам процеса: { $reason } core_failed_to_read_video_properties = Не успях да прочета свойствата на видеото: { $reason } core_failed_to_execute_ffmpeg = Не успях да изпълня ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg се провали с статус { $status }: { $stderr } (команда: { $command }) core_failed_to_load_image_frame = Не успях да заредя кадъра на изображението: { $reason } core_failed_to_extract_frame = Не успях да извлека кадъра на { $time } секунди от "{ $file }": { $reason } core_failed_to_save_thumbnail = Не успях да запазя миниатюра за "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Не успях да получа кадъра в момента { $timestamp } от "{ $file }": { $reason } core_failed_get_frame_from_file = Не успях да получа кадъра от "{ $file }" в момента { $timestamp }: { $reason } core_invalid_crop_rectangle = Невалиден правоъгълник на отрязък: ляво={ $left }, горно={ $top }, дясно={ $right }, долно={ $bottom } core_failed_to_crop_video_file = Не успях да изрежа видео файла "{ $file }": { $reason } core_cropped_video_not_created = Отреденият видео файл не беше създаден: { $temp } core_unable_check_hash_of_file = Не може да се провери хеша на файла "{ $file }", причина { $reason } core_error_checking_hash_of_file = Грешка възникна при проверка на хеша на файла "{ $file }", причина { $reason } core_image_zero_dimensions = Изображението има нулева ширина или височина "{ $path }" core_image_open_failed = Не може да се отвори файла с изображение "{ $path }": { $reason } core_not_directory_remove = Опитвам да премахна папката "{ $path }" която не е директория core_cannot_read_directory = Не може да се прочете директорията "{ $path }" core_cannot_read_entry_from_directory = Не мога да прочета записа от директорията "{ $path }" core_folder_contains_file_inside = Папка съдържа файл "{ $entry }" в "{ $folder }" core_unknown_directory_entry = Не може да се определи типа на файла на входа на директорията "{ $entry }" в "{ $path }" core_video_width_exceeds_limit = Видео ширина { $width } надхвърля лимита на { $limit } core_video_height_exceeds_limit = Видео височина { $height } надхвърля лимита от { $limit } core_failed_to_process_video = Не успях да обработя видео файла { $file }: { $reason } core_optimized_file_larger = Оптимизиран файл { $optimized } (размер: { $new_size }) не е по-малък от оригиналния { $original } (размер: { $original_size }) core_unknown_codec = Неизвестен кодек: { $codec } core_invalid_video_optimizer_mode = Невалиден режим на оптимизация на видео: '{ $mode }'. Разрешени стойности: transcode, crop core_folder_does_not_exist = Папка не съществува: { $folder } core_path_not_directory = Пътят не е директория: { $folder } core_test_error_for_folder = Грешка при тест за папка: { $folder } core_unknown_exif_tag_group = Неизвестна EXIF група таг: { $tag } core_error_comparing_fingerprints = Грешка при сравняване на пръстови отпечатъци: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Не успях да генерирам миниатюра за "{ $file }": извлечените кадри имат различни размери core_failed_to_generate_thumbnail = Не успях да генерирам миниатюра за "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Не успях да извлека кадъра на { $time } секунди от "{ $file }": { $reason } core_video_file_does_not_exist = Видео файлът не съществува (може да бъде премахнат между сканирането/по-късните стъпки): "{ $path }" core_image_too_large = Изображението е твърде голямо ({ $width }x{ $height }) - повече от поддържаните { $max } пиксела core_failed_to_get_video_metadata = Не успях да получа метаданните на видеото за файла "{ $file }": { $reason } core_failed_to_get_video_codec = Не успях да получа видео кодек за файла "{ $file }" core_failed_to_get_video_duration = Не успях да получа продължителността на видеото за файла "{ $file }" core_failed_to_get_video_dimensions = Не успях да получа размерите на видеото за файла "{ $file }" core_frame_dimensions_mismatch = Размерите на кадъра за времето { $timestamp } не съвпадат с размерите на първия кадър ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Не успях да заредя данните от кеш файла { $file }, причина { $reason } core_failed_to_load_data_from_json_cache = Не успях да заредя данните от JSON кеш файла { $file }, причина { $reason } core_failed_to_replace_with_optimized = Не успях да заменя файла "{ $file }" с оптимизираната версия: { $reason } core_failed_to_write_data_to_cache = Не може да се запише данни към кеш файла "{ $file }", причина { $reason } core_properly_saved_cache_entries = Правилно запазени в файл { $count } кеш записи. core_video_processing_stopped_by_user = Видео обработката беше спряна от потребителя core_thumbnail_generation_stopped_by_user = Генерирането на миниатюри беше спряно от потребителя core_failed_to_optimize_video = Не успях да оптимизирам видео "{ $file }": { $reason } core_failed_to_crop_video = Не успя да се изреже видеото "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Не успях да получа метаданните на оптимизирания файл "{ $file }": { $reason } core_cannot_create_config_folder = Не може да се създаде папка с конфигурация "{ $folder }", причина { $reason } core_cannot_create_cache_folder = Не може да се създаде кешираща папка "{ $folder }", причина { $reason } core_cannot_create_or_open_cache_file = Не може да се създаде или отвори кеширания файл "{ $file }", причина { $reason } core_cannot_set_config_cache_path = Не може да се зададе път към config/cache - config и cache няма да бъдат използвани. core_invalid_extension_contains_space = { $extension } не е валиден разширение, защото съдържа празно пространство вътре core_invalid_extension_contains_dot = { $extension } не е валиден разширение, защото съдържа точка вътре ================================================ FILE: czkawka_core/i18n/cs/czkawka_core.ftl ================================================ # Core core_similarity_original = Originál core_similarity_very_high = Velmi vysoká core_similarity_high = Vysoká core_similarity_medium = Střední core_similarity_small = Malá core_similarity_very_small = Velmi malá core_similarity_minimal = Minimální core_cannot_open_dir = Nelze otevřít adresář { $dir }, důvod { $reason } core_cannot_read_entry_dir = Nelze načíst záznam v adresáři { $dir }, důvod { $reason } core_cannot_read_metadata_dir = Nelze načíst metadata v adresáři { $dir }, důvod { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Soubor { $name } se zdá být před Unix Epoch upraven core_folder_modified_before_epoch = Složka { $name } se zdá být upravena před Unix Epoch core_file_no_modification_date = Nelze získat datum úpravy ze souboru { $name }, důvod { $reason } core_folder_no_modification_date = Nelze získat datum úpravy ze složky { $name }, důvod { $reason } core_cannot_start_scan_no_included_paths = Nemožno spustit skenování, protože nejsou zahrnuty žádné cesty core_skip_exist_check_all_included_paths_nonexistent = Nemožno zahájit skenování, protože všechny zahrnuté cesty neexistují core_missing_no_chosen_included_path = Neosáhlý zahrnutý cíl nebyl vybrán (vyloučený cesty mohly vyloučit všechny zahrnuté cesty) core_reference_included_paths_same = Nelze spustit sken, kde jsou všechny platné zahrnuté cesty také odkazované cesty, zkuste ověřit nebo vypnout odkazované cesty core_must_be_directory_or_file = Zadaná cesta musí ukazovat na platný adresář nebo soubor, ignoruje { $path } core_excluded_paths_pointless_slash = Vyloučení / je zbytečné, protože to znamená, že nebude naskenováno žádných souborů core_paths_unable_to_get_device_id = Nemožno získat ID zařízení z adresáře { $path } core_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 core_needs_allowed_extensions = Nedaří se spustit sken, když byly všechny rozšíření vyloučeny ze skenu core_needs_to_set_at_least_one_broken_option = Nemožno spustit sken, ak nie je nastavená možnosť zlomeného skenovania core_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í core_ffmpeg_not_found = Nemohu najít správnou instalaci FFmpeg nebo FFprobe. Jedná se o externí programy, které je třeba nainstalovat ručně. core_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 core_invalid_symlink_infinite_recursion = Nekonečná rekurze core_invalid_symlink_non_existent_destination = Neexistující cílový soubor core_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. core_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. core_error_moving_to_trash = Chyba při přesouvání "{ $file }" do koše: { $error } core_error_removing = Chyba při odstraňování "{ $file }": { $error } core_no_similarity_method_selected = Nemůže najít podobné hudební soubory bez vybrané metody podobnosti core_failed_to_spawn_command = Selhalo spuštění příkazu: { $reason } core_failed_to_wait_for_process = Selhalo čekání na proces: { $reason } core_failed_to_read_video_properties = Selhalo načtení vlastností videa: { $reason } core_failed_to_execute_ffmpeg = Selhalo provedení ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg selhal s stavem { $status }: { $stderr } (příkaz: { $command }) core_failed_to_load_image_frame = Selhalo načtení snímku obrazu: { $reason } core_failed_to_extract_frame = Selhalo načtení snímku v { $time } sekundách z "{ $file }": { $reason } core_failed_to_save_thumbnail = Selhalo uložení miniatury pro "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Selhalo získání snímku v časové značce { $timestamp } z "{ $file }": { $reason } core_failed_get_frame_from_file = Selhalo získání snímku z "{ $file }" v časové značce { $timestamp }: { $reason } core_invalid_crop_rectangle = Neplatná obdélníková oblast pro zrání: levý={ $left }, horní={ $top }, pravý={ $right }, dolní={ $bottom } core_failed_to_crop_video_file = Selhalo oříznutí video souboru "{ $file }": { $reason } core_cropped_video_not_created = Zkrácený video soubor nebyl vytvořen: { $temp } core_unable_check_hash_of_file = Nemožno zkontrolovat soubor "{ $file }", důvod { $reason } core_error_checking_hash_of_file = Chyba nastala při kontrole souhrnu souboru "{ $file }", důvod { $reason } core_image_zero_dimensions = Obraz má nulovou šířku nebo výšku "{ $path }" core_image_open_failed = Nemožno otevřít soubor s obrázkem "{ $path }": { $reason } core_not_directory_remove = Pokouším se odstranit složku "{ $path }" která není adresář core_cannot_read_directory = Nelze číst adresář "{ $path }" core_cannot_read_entry_from_directory = Nelze přečíst záznam z adresáře "{ $path }" core_folder_contains_file_inside = Složka obsahuje soubor "{ $entry }" uvnitř "{ $folder }" core_unknown_directory_entry = Nemožno určit typ súboru záznamu adresára "{ $entry }" v "{ $path }" core_video_width_exceeds_limit = Video šířka { $width } překračuje limit { $limit } core_video_height_exceeds_limit = Video výška { $height } překračuje limit { $limit } core_failed_to_process_video = Selhalo zpracování video souboru { $file }: { $reason } core_optimized_file_larger = Optimalizovaný soubor { $optimized } (velikost: { $new_size }) není menší než originální { $original } (velikost: { $original_size }) core_unknown_codec = Neznámý kodek: { $codec } core_invalid_video_optimizer_mode = Neplatý režim optimalizátoru videa: '{ $mode }'. Umožněné hodnoty: transkodovat, oříznout core_folder_does_not_exist = Složka neexistuje: { $folder } core_path_not_directory = Cesta není adresář: { $folder } core_test_error_for_folder = Test chyba pro složku: { $folder } core_unknown_exif_tag_group = Neznámá EXIF skupina značek: { $tag } core_error_comparing_fingerprints = Chyba při porovnávání otisků prstů: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Selhalo generování miniatury pro "{ $file }": extrahované snímky mají různé rozměry core_failed_to_generate_thumbnail = Selhalo při generování miniatury pro "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Selhalo načtení snímku v { $time } sekundách z "{ $file }": { $reason } core_video_file_does_not_exist = Video soubor neexistuje (může být odstraněn mezi skenem/pozdějšími kroky): "{ $path }" core_image_too_large = Obraz je příliš velký ({ $width }x{ $height }) - více než podporovaných { $max } pixelů core_failed_to_get_video_metadata = Selhalo načtení metadat videa pro soubor "{ $file }": { $reason } core_failed_to_get_video_codec = Selhalo načtení video kódu pro soubor "{ $file }" core_failed_to_get_video_duration = Selhalo načtení délky videa pro soubor "{ $file }" core_failed_to_get_video_dimensions = Selhalo načtení rozměrů souboru "{ $file }" core_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 }) core_failed_to_load_data_from_cache = Nepodařilo se načíst data z mezipaměťového souboru { $file }, důvod { $reason } core_failed_to_load_data_from_json_cache = Nepodařilo se načíst data z JSON mezipaměťového souboru { $file }, důvod { $reason } core_failed_to_replace_with_optimized = Selhalo při nahrazení souboru "{ $file }" optimalizovanou verzí: { $reason } core_failed_to_write_data_to_cache = Nelze zapisovat data do souboru dočasné paměti "{ $file }", důvod { $reason } core_properly_saved_cache_entries = Správně uloženo do souboru { $count } políček mezipaměti. core_video_processing_stopped_by_user = Video zpracování bylo uživatelem zastaveno core_thumbnail_generation_stopped_by_user = Generování miniatur bylo zastaveno uživatelem core_failed_to_optimize_video = Selhalo při optimalizaci videa "{ $file }": { $reason } core_failed_to_crop_video = Selhalo oříznutí videa "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Selhalo načtení metadat optimalizovaného souboru "{ $file }": { $reason } core_cannot_create_config_folder = Nemožno vytvořit složku „{ $folder }“, důvod { $reason } core_cannot_create_cache_folder = Nemůže být vytvořena složka pro ukládání "{ $folder }", důvod { $reason } core_cannot_create_or_open_cache_file = Nemožno vytvořit nebo otevřít mezipaměťový soubor "{ $file }", důvod { $reason } core_cannot_set_config_cache_path = Nelze nastavit cestu k souboru s konfigurací/cache - konfigurace a cache nebude použity. core_invalid_extension_contains_space = { $extension } není platná přípona, protože obsahuje prázdné místo uvnitř core_invalid_extension_contains_dot = { $extension } není platná přípona, protože obsahuje tečku uvnitř core_path_must_exists = Zadaná cesta musí existovat, bez ohledu na { $path } core_failed_to_check_process_status = Nepodařilo se zkontrolovat stav procesu: { $reason } ================================================ FILE: czkawka_core/i18n/de/czkawka_core.ftl ================================================ # Core core_similarity_original = Original core_similarity_very_high = Sehr Hoch core_similarity_high = Hoch core_similarity_medium = Mittel core_similarity_small = Klein core_similarity_very_small = Sehr klein core_similarity_minimal = Minimalistisch core_cannot_open_dir = Verzeichnis { $dir } kann nicht geöffnet werden, Grund { $reason } core_cannot_read_entry_dir = Kann Eintrag in Verzeichnis { $dir } nicht lesen, Grund { $reason } core_cannot_read_metadata_dir = Metadaten können in Verzeichnis { $dir } nicht gelesen werden, Grund { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Datei { $name } scheint vor der Unix-Epoche geändert worden zu sein core_folder_modified_before_epoch = Ordner { $name } scheint vor der Unix-Epoche geändert worden zu sein core_file_no_modification_date = Konnte das Änderungsdatum von Datei { $name } nicht abrufen, Grund { $reason } core_folder_no_modification_date = Konnte das Änderungsdatum aus dem Ordner { $name } nicht abrufen, Grund { $reason } core_cannot_start_scan_no_included_paths = Kann den Scan nicht starten, da keine enthaltenen Pfade vorhanden sind core_skip_exist_check_all_included_paths_nonexistent = Kann den Scan nicht starten, da alle enthaltenen Pfade nicht existieren core_missing_no_chosen_included_path = Kein gültiger einzubander Pfad ausgewählt (ausgeschlossene Pfade hätten alle einzubanderten Pfade ausschließen können) core_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 core_excluded_paths_pointless_slash = Ausgeschlossen / ist zwecklos, weil es bedeutet, dass keine Dateien gescannt werden core_needs_allowed_extensions_limited_by_tool = Kann den Scan nicht starten, wenn alle verfügbaren Erweiterungen in diesem Tool ({ $extensions }) vom Scan ausgeschlossen wurden core_needs_allowed_extensions = Kann den Scan nicht starten, wenn alle Erweiterungen vom Scan ausgeschlossen wurden core_needs_to_set_at_least_one_broken_option = Kann den Scan nicht starten, wenn keine Option „defektes Element“ zum Scannen festgelegt ist core_needs_to_set_at_least_one_bad_name_option = Kann den Scan nicht starten, wenn keine Option „schlechter Name“ zum Scannen festgelegt ist core_ffmpeg_not_found = Kann die korrekte Installation von FFmpeg oder FFprobe nicht finden. Dies sind externe Programme, die manuell installiert werden müssen. core_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 core_invalid_symlink_infinite_recursion = Endlose Rekursion core_invalid_symlink_non_existent_destination = Nicht existierende Zieldatei core_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. core_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. core_error_moving_to_trash = Fehler beim Verschieben von "{ $file }" in den Papierkorb: { $error } core_error_removing = Fehler beim Entfernen von "{ $file }": { $error } core_no_similarity_method_selected = Kann keine ähnlichen Musikdateien ohne eine ausgewählte Similarity-Methode finden core_failed_to_spawn_command = Fehlgeschlagenes Spawnen des Befehls: { $reason } core_failed_to_check_process_status = Fehlgeschlagen bei der Überprüfung des Prozessstatus: { $reason } core_failed_to_wait_for_process = Fehlgeschlagenes Warten auf Prozess: { $reason } core_failed_to_read_video_properties = Fehlgeschlagen beim Lesen der Videoeigenschaften: { $reason } core_failed_to_execute_ffmpeg = Fehlgeschlagenes Ausführen von ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg fehlgeschlagen mit Status { $status }: { $stderr } (Befehl: { $command }) core_failed_to_load_image_frame = Fehlgeschlagenes Laden des Bildrahmens: { $reason } core_failed_to_extract_frame = Fehlgeschlagenes Extrahieren des Frames bei { $time } Sekunden aus "{ $file }": { $reason } core_failed_to_save_thumbnail = Fehlgeschlagen beim Speichern des Miniaturansichts für "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Fehlgeschlagenes Abrufen des Frames bei Zeitstempel { $timestamp } von "{ $file }": { $reason } core_failed_get_frame_from_file = Fehlgeschlagenes Abrufen des Frames von "{ $file }" zu Zeitstempel { $timestamp }: { $reason } core_invalid_crop_rectangle = Ungültiges Zuschneide-Rechteck: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom } core_failed_to_crop_video_file = Fehlgeschlagenes Zuschneiden der Videodatei "{ $file }": { $reason } core_cropped_video_not_created = Das Video-Datei wurde nicht erstellt: { $temp } core_unable_check_hash_of_file = Kann Hash von Datei "{ $file }" nicht überprüft werden, Grund { $reason } core_error_checking_hash_of_file = Fehler beim Prüfen des Hash von Datei "{ $file }", Grund { $reason } core_image_zero_dimensions = Bild hat breite Null oder Höhe "{ $path }" core_image_open_failed = Kann das Bildfile "{ $path }" nicht öffnen: { $reason } core_not_directory_remove = Versuche, Ordner "{ $path }" zu entfernen, der kein Verzeichnis ist core_cannot_read_directory = Kann den Verzeichnis "{ $path }" nicht lesen core_cannot_read_entry_from_directory = Kann den Eintrag nicht aus dem Verzeichnis "{ $path }" lesen core_folder_contains_file_inside = Ordner enthält Datei "{ $entry }" innerhalb "{ $folder }" core_unknown_directory_entry = Kann den Dateityp des Verzeichniseintrags "{ $entry }" innerhalb "{ $path }" nicht bestimmen core_video_width_exceeds_limit = Video Breite { $width } überschreitet die Grenze von { $limit } core_video_height_exceeds_limit = Videohöhe { $height } überschreitet die Grenze von { $limit } core_failed_to_process_video = Fehlgeschlagenes Verarbeiten der Videodatei { $file }: { $reason } core_optimized_file_larger = Optimierter Datei { $optimized } (Größe: { $new_size }) ist nicht kleiner als Original { $original } (Größe: { $original_size }) core_unknown_codec = Unbekannter Codec: { $codec } core_invalid_video_optimizer_mode = Ungültiger Video-Optimierungsmodus: '{ $mode }'. Erlaubte Werte: transkodieren, zuschneiden core_folder_does_not_exist = Ordner existiert nicht: { $folder } core_path_not_directory = Der Pfad ist keine Verzeichnis: { $folder } core_test_error_for_folder = Testfehler für Ordner: { $folder } core_unknown_exif_tag_group = Unbekanntes EXIF-Tag-Gruppen: { $tag } core_error_comparing_fingerprints = Fehler beim Vergleichen von Fingerabdrücken: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Fehlgeschlagen, Miniatur für "{ $file }" zu generieren: extrahierte Frames haben unterschiedliche Dimensionen core_failed_to_generate_thumbnail = Fehlgeschlagenes Generieren des Miniaturansichts für "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Fehlgeschlagenes Extrahieren des Frames bei { $time } Sekunden aus "{ $file }": { $reason } core_video_file_does_not_exist = Video-Datei existiert nicht (kann zwischen Scan/späteren Schritten entfernt werden): "{ $path }" core_image_too_large = Bild ist zu groß ({ $width }x{ $height }) - mehr als unterstützt { $max } Pixel core_failed_to_get_video_metadata = Fehlgeschlagen beim Abrufen der Videodaten für Datei "{ $file }": { $reason } core_failed_to_get_video_codec = Fehlgeschlagenes Abrufen des Videocodecs für die Datei "{ $file }" core_failed_to_get_video_duration = Fehlgeschlagen, die Video-Dauer für die Datei "{ $file }" zu erhalten core_failed_to_get_video_dimensions = Fehlgeschlagen, Video-Abmessungen für Datei "{ $file }" zu erhalten core_frame_dimensions_mismatch = Rahmenmaße für Zeitstempel { $timestamp } stimmen nicht mit den ersten Rahmenmaßen ({ $first_w }x{ $first_h }) überein core_failed_to_load_data_from_cache = Fehler beim Laden von Daten aus der Cache-Datei { $file }, Grund { $reason } core_failed_to_load_data_from_json_cache = Fehler beim Laden von Daten aus der JSON-Cache-Datei { $file }, Grund { $reason } core_failed_to_replace_with_optimized = Fehlgeschlagenes Ersetzen der Datei "{ $file }" mit der optimierten Version: { $reason } core_failed_to_write_data_to_cache = Kann keine Daten in die Cache-Datei "{ $file }" schreiben, Grund { $reason } core_properly_saved_cache_entries = Ordentlich in Datei { $count } Cache-Einträge gespeichert. core_video_processing_stopped_by_user = Video-Verarbeitung wurde durch Benutzer gestoppt core_thumbnail_generation_stopped_by_user = Erstellung von Vorschaubildern wurde durch Benutzer gestoppt core_failed_to_optimize_video = Fehlgeschlagenes Optimieren des Videos "{ $file }": { $reason } core_failed_to_crop_video = Fehlgeschlagenes Zuschneiden des Videos "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Fehlgeschlagenes Abrufen der Metadaten der optimierten Datei "{ $file }": { $reason } core_cannot_create_config_folder = Kann die Konfigurationsordner "{ $folder }" nicht erstellen, Grund { $reason } core_cannot_create_cache_folder = Kann den Cache-Ordner "{ $folder }" nicht erstellen, Grund { $reason } core_cannot_create_or_open_cache_file = Kann die Cache-Datei "{ $file }" nicht erstellen oder öffnen, Grund { $reason } core_cannot_set_config_cache_path = Kann die Konfiguration/Cache-Pfad nicht setzen - Konfiguration und Cache werden nicht verwendet. core_invalid_extension_contains_space = { $extension } ist keine gültige Erweiterung, da sie Leerzeichen enthält core_invalid_extension_contains_dot = { $extension } ist keine gültige Erweiterung, da sie einen Punkt enthält core_path_must_exists = Der angegebene Pfad muss existieren, wobei { $path } ignoriert wird core_must_be_directory_or_file = Der angegebene Pfad muss auf ein gültiges Verzeichnis oder eine Datei verweisen, wobei { $path } ignoriert wird core_paths_unable_to_get_device_id = Gerät-ID konnte nicht aus dem Ordner { $path } abgerufen werden ================================================ FILE: czkawka_core/i18n/el/czkawka_core.ftl ================================================ # Core core_similarity_original = Αρχικό core_similarity_very_high = Πολύ Υψηλή core_similarity_high = Υψηλή core_similarity_medium = Μεσαίο core_similarity_small = Μικρό core_similarity_very_small = Πολύ Μικρό core_similarity_minimal = Ελάχιστα core_cannot_open_dir = Αδυναμία ανοίγματος dir { $dir }, λόγος { $reason } core_cannot_read_entry_dir = Αδυναμία ανάγνωσης καταχώρησης στον κατάλογο { $dir }, λόγος { $reason } core_cannot_read_metadata_dir = Αδύνατη η ανάγνωση μεταδεδομένων στον κατάλογο { $dir }, λόγος { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Το αρχείο { $name } φαίνεται να έχει τροποποιηθεί πριν το Unix Epoch core_folder_modified_before_epoch = Φάκελος { $name } φαίνεται να έχει τροποποιηθεί πριν το Unix Epoch core_file_no_modification_date = Δεν είναι δυνατή η λήψη ημερομηνίας τροποποίησης από το αρχείο { $name }, λόγος { $reason } core_folder_no_modification_date = Δεν είναι δυνατή η λήψη ημερομηνίας τροποποίησης από το φάκελο { $name }, λόγος { $reason } core_cannot_start_scan_no_included_paths = Δεν μπορεί να ξεκινήσει η σάρωση, επειδή δεν περιλαμβάνονται καθόλου οι διαδρομές core_skip_exist_check_all_included_paths_nonexistent = Δεν μπορεί να ξεκινήσει η σάρωση, επειδή οι μισθοί διαδρομές που περιλαμβάνονται δεν υπάρχουν core_missing_no_chosen_included_path = Δεν επιλέχθηκε έγκυρος συμπεριλαμβανόμενος δρόμος (οι αποκλεισμένοι δρόμοι θα μπορούσαν να έχουν αποκλείσει όλους τους συμπεριλαμβανόμενους δρόμους) core_reference_included_paths_same = Δεν μπορεί να ξεκινήσει η σάρωση όπου όλα τα έγκυρα συμπεριλημμένα μονοπάτια είναι επίσης μονοπάτια αναφοράς, προσπαθήστε να επικυρώσετε ή να απενεργοποιήσετε τα μονοπάτια αναφοράς core_path_must_exists = Παρασχόμενος ο δρόμος πρέπει να υπάρχει, αγνοώντας το { $path } core_must_be_directory_or_file = Παρέχεται ο δρόμος πρέπει να δείχνει προς έναν έγκυρο κατάλογο ή αρχείο, αγνοώντας { $path } core_excluded_paths_pointless_slash = Αποκλείοντας / είναι μάταιο, γιατί σημαίνει ότι κανένα αρχείο δεν θα σαρωθεί core_paths_unable_to_get_device_id = Δεν μπορώ να λάβω το ID συσκευής από τον φάκελο { $path } core_needs_allowed_extensions_limited_by_tool = Δεν μπορεί να ξεκινήσει η σάρωση, όταν όλα τα πρόσθετα διαθέσιμα σε αυτό το εργαλείο ({ $extensions }) έχουν αποκλειστεί από την σάρωση core_needs_allowed_extensions = Δεν μπορεί να ξεκινήσει η σάρωση, όταν έχουν αποκλειστεί όλες οι προσθήκες από τη σάρωση core_needs_to_set_at_least_one_broken_option = Δεν μπορεί να ξεκινήσει η σάρωση, όταν δεν έχει οριστεί η επιλογή "κατεστραμμένο" για σάρωση core_needs_to_set_at_least_one_bad_name_option = Δεν μπορεί να ξεκινήσει η σάρωση, όταν δεν έχει ρυθμιστεί η επιλογή για κακό όνομα για σάρωση core_ffmpeg_not_found = Δεν μπορεί να βρεθεί μια σωστή εγκατάσταση του FFmpeg ή FFprobe. Αυτά είναι εξωτερικά προγράμματα που πρέπει να εγκατασταθούν χειροκίνητα. core_ffmpeg_not_found_windows = Να είστε βέβαιος ότι ffmpeg.exe και ffprobe.exe είναι διαθέσιμα σε PATH ή τοποθετούνται απευθείας στον ίδιο φάκελο με το εκτελέσιμο app core_invalid_symlink_infinite_recursion = Άπειρη αναδρομή core_invalid_symlink_non_existent_destination = Αρχείο ανύπαρκτου προορισμού core_messages_limit_reached_characters = Ο αριθμός μηνυμάτων υπερέβη το καθορισμένο όριο ({ $current }/{ $limit } χαρακτήρες), οπότε η έξοδος περικόπηκε. Για να διαβάσετε την πλήρη έξοδο, απενεργοποιήστε την επιλογή περιορισμού στις ρυθμίσεις. core_messages_limit_reached_lines = Ο αριθμός μηνυμάτων υπερέβη το καθορισμένο όριο ({ $current }/{ $limit } γραμμές), οπότε η έξοδος περικόπηκε. Για να διαβάσετε την πλήρη έξοδο, απενεργοποιήστε την επιλογή περιορισμού στις ρυθμίσεις. core_error_moving_to_trash = Σφάλμα κατά μετακίνηση "{ $file }" στον κάλαπο”. { $error } core_error_removing = Σφάλμα κατά την αφαίρεση "{ $file }": { $error } core_no_similarity_method_selected = Δεν μπορεί να βρεθούν παρόμοια μουσικά αρχεία χωρίς μια επιλεγμένη μέθοδο ομοιότητας core_failed_to_spawn_command = Αποτυχία εκκίνησης εντολής: { $reason } core_failed_to_check_process_status = Αποτυχία ελέγχου της κατάστασης της διαδικασίας: { $reason } core_failed_to_wait_for_process = Αποτυχία αναμονής για τη διαδικασία: { $reason } core_failed_to_read_video_properties = Αποτυχία ανάγνωσης ιδιοτήτων βίντεο: { $reason } core_failed_to_execute_ffmpeg = Αποτυχία εκτέλεσης ffmpeg: { $reason } core_ffmpeg_failed_with_status = έπεσε η ffmpeg με την κατάσταση { $status }: { $stderr } (εντολή: { $command }) core_failed_to_load_image_frame = Αποτυχία φόρτωσης πλαισίου εικόνας: { $reason } core_failed_to_extract_frame = Αποτυχία εξαγωγής καρέ στις { $time } δευτερόλεπτα από το "{ $file }": { $reason } core_failed_to_save_thumbnail = Αποτυχία αποθήκευσης μικρογραφίας για "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Αποτυχία ανάκτησης καρέ στην σφραγίδα χρόνου { $timestamp } από το "{ $file }": { $reason } core_failed_get_frame_from_file = Αποτυχία λήψης πλαισίου από "{ $file }" στην σφραγίδα χρόνου { $timestamp }: { $reason } core_invalid_crop_rectangle = Μη έγκυρος ορθογώνιο καλλιέργειας: αριστερά={ $left }, άνω={ $top }, δεξιά={ $right }, κάτω={ $bottom } core_failed_to_crop_video_file = Αποτυχία κοπής του αρχείου βίντεο "{ $file }": { $reason } core_cropped_video_not_created = Το αρχείο βίντεο που κόπηκε δεν δημιουργήθηκε: { $temp } core_unable_check_hash_of_file = Δεν μπορώ να ελέγξω το hash του αρχείου "{ $file }", λόγος { $reason } core_error_checking_hash_of_file = Σφάλμα συνέβη κατά την επαλήθευση του hash του αρχείου "{ $file }", λόγος { $reason } core_image_zero_dimensions = Η εικόνα έχει μηδενικό πλάτος ή ύψος "{ $path }" core_image_open_failed = Δεν μπορεί να ανοίξει το αρχείο εικόνας "{ $path }": { $reason } core_not_directory_remove = Προσπαθώντας να διαγράψω τον φάκελο "{ $path }" που δεν είναι κατάλογος core_cannot_read_directory = Δεν μπορώ να διαβάσω τον κατάλογο "{ $path }" core_cannot_read_entry_from_directory = Δεν μπορώ να διαβάσω την εγγραφή από τον κατάλογο "{ $path }" core_folder_contains_file_inside = Ο φάκελος περιέχει το αρχείο "{ $entry }" μέσα στο "{ $folder }" core_unknown_directory_entry = Δεν μπορεί να προσδιοριστεί ο τύπος αρχείου της καταχώρησης καταλόγου "{ $entry }" μέσα στο "{ $path }" core_video_width_exceeds_limit = Βίντεο πλάτος { $width } υπερβαίνει το όριο του { $limit } core_video_height_exceeds_limit = Βίντεο ύψος { $height } υπερβαίνει το όριο των { $limit } core_failed_to_process_video = Αποτυχία επεξεργασίας αρχείου βίντεο { $file }: { $reason } core_optimized_file_larger = Βελτιστοποιημένο αρχείο { $optimized } (μέγεθος: { $new_size }) δεν είναι μικρότερο από το αρχικό { $original } (μέγεθος: { $original_size }) core_unknown_codec = Άγνωστος κωδικοποιητής: { $codec } core_invalid_video_optimizer_mode = Μη έγκυρος τρόπος βελτιστοποίησης βίντεο: '{ $mode }'. Επιτρεπτές τιμές: transcode, crop core_folder_does_not_exist = Ο φάκελος δεν υπάρχει: { $folder } core_path_not_directory = Το μονοπάτι δεν είναι κατάλογος: { $folder } core_test_error_for_folder = Σφάλμα δοκιμής για φάκελο: { $folder } core_unknown_exif_tag_group = Άγνωστη ομάδα ετικετών EXIF: { $tag } core_error_comparing_fingerprints = Σφάλμα κατά σύγκριση δακτυλικών αποτυπωμάτων: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Αποτυχία δημιουργίας μικρογραφίας για "{ $file }": τα εξωθημένα πλάνα έχουν διαφορετικές διαστάσεις core_failed_to_generate_thumbnail = Αποτυχία δημιουργίας μικρογραφίας για "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Αποτυχία εξαγωγής καρέ στις { $time } δευτερόλεπτα από το "{ $file }": { $reason } core_video_file_does_not_exist = Το αρχείο βίντεο δεν υπάρχει (μπορεί να αφαιρεθεί μεταξύ σάρωσης/μετέπειτα βημάτων): "{ $path }" core_image_too_large = Η εικόνα είναι πολύ μεγάλη ({ $width }x{ $height }) - περισσότερο από το υποστηριζόμενο { $max } pixels core_failed_to_get_video_metadata = Αποτυχία ανάκτησης μεταδεδομένων βίντεο για το αρχείο "{ $file }": { $reason } core_failed_to_get_video_codec = Αποτυχία ανάκτησης του codec βίντεο για το αρχείο "{ $file }" core_failed_to_get_video_duration = Αποτυχία λήψης της διάρκειας βίντεο για το αρχείο "{ $file }" core_failed_to_get_video_dimensions = Αποτυχία λήψης διαστάσεων βίντεο για το αρχείο "{ $file }" core_frame_dimensions_mismatch = Οι διαστάσεις του πλαισίου για την σφραγίδα χρόνου { $timestamp } δεν ταιριάζουν με τις διαστάσεις του πρώτου πλαισίου ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Αποτυχία φόρτωσης δεδομένων από το αρχείο cache { $file }, λόγος { $reason } core_failed_to_load_data_from_json_cache = Αποτυχία φόρτωσης δεδομένων από το αρχείο cache json { $file }, λόγος { $reason } core_failed_to_replace_with_optimized = Αποτυχία αντικατάστασης του αρχείου "{ $file }" με την βελτιστοποιημένη έκδοση: { $reason } core_failed_to_write_data_to_cache = Δεν μπορεί να γραφτεί δεδομένα στο αρχείο cache "{ $file }", λόγος { $reason } core_properly_saved_cache_entries = Αποθηκεύτηκε σωστά στο αρχείο { $count } καταχωρήσεις cache. core_video_processing_stopped_by_user = Η επεξεργασία βίντεο σταμάτησε από τον χρήστη core_thumbnail_generation_stopped_by_user = Δημιουργία μικρογραφιών σταματήθηκε από χρήστη core_failed_to_optimize_video = Αποτυχία βελτιστοποίησης βίντεο "{ $file }": { $reason } core_failed_to_crop_video = Αποτυχία κοπής βίντεο "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Αποτυχία ανάκτησης μεταδεδομένων του βελτιστοποιημένου αρχείου "{ $file }": { $reason } core_cannot_create_config_folder = Δεν μπορεί να δημιουργηθεί ο φάκελος διαμόρφωσης "{ $folder }", λόγος { $reason } core_cannot_create_cache_folder = Δεν μπορεί να δημιουργηθεί ο φάκελος προσωρινής αποθήκευσης "{ $folder }", λόγος { $reason } core_cannot_create_or_open_cache_file = Δεν μπορεί να δημιουργηθεί ή να ανοίξει το αρχείο cache "{ $file }", λόγος { $reason } core_cannot_set_config_cache_path = Δεν μπορεί να ρυθμιστεί ο/η διαδρομή config/cache - η config και η cache δεν θα χρησιμοποιηθούν. core_invalid_extension_contains_space = { $extension } δεν είναι έγκυρος τύπος επέκτασης επειδή περιέχει κενό διάστημα μέσα core_invalid_extension_contains_dot = Το { $extension } δεν είναι έγκυρος τύπος επέκτασης επειδή περιέχει τελεία μέσα ================================================ FILE: czkawka_core/i18n/en/czkawka_core.ftl ================================================ # Core core_similarity_original = Original core_similarity_very_high = Very High core_similarity_high = High core_similarity_medium = Medium core_similarity_small = Small core_similarity_very_small = Very Small core_similarity_minimal = Minimal core_cannot_open_dir = Cannot open dir {$dir}, reason {$reason} core_cannot_read_entry_dir = Cannot read entry in dir {$dir}, reason {$reason} core_cannot_read_metadata_dir = Cannot read metadata in dir {$dir}, reason {$reason} core_cannot_read_metadata_file = Cannot read metadata of file {$file}, reason {$reason} core_file_modified_before_epoch = File {$name} seems to have been modified before the Unix Epoch core_folder_modified_before_epoch = Folder {$name} seems to have been modified before the Unix Epoch core_file_no_modification_date = Unable to get modification date from file {$name}, reason {$reason} core_folder_no_modification_date = Unable to get modification date from folder {$name}, reason {$reason} core_cannot_start_scan_no_included_paths = Cannot start scan, because there are no included paths core_skip_exist_check_all_included_paths_nonexistent = Cannot start scan, because all included paths do not exist core_missing_no_chosen_included_path = No valid included path was chosen(excluded paths could have excluded all included paths) core_reference_included_paths_same = Cannot start scan where all valid included paths are also referenced paths, try to validate or disable referenced paths core_path_must_exists = Provided path must exist, ignoring { $path } core_must_be_directory_or_file = Provided path must point to a vaild directory or file, ignoring { $path } core_excluded_paths_pointless_slash = Excluding / is pointless, because it means no files will be scanned core_paths_unable_to_get_device_id = Unable to get device id from folder { $path } core_needs_allowed_extensions_limited_by_tool = Cannot start scan, when all extensions available in this tool ({ $extensions }) were excluded from scan core_needs_allowed_extensions = Cannot start scan, when all extensions were excluded from scan core_needs_to_set_at_least_one_broken_option = Cannot start scan, when there is no broken option set to scan for core_needs_to_set_at_least_one_bad_name_option = Cannot start scan, when there is no bad name option set to scan for core_ffmpeg_not_found = Cannot find a proper installation of FFmpeg or FFprobe. These are external programs that must be installed manually. core_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 core_invalid_symlink_infinite_recursion = Infinite recursion core_invalid_symlink_non_existent_destination = Non-existent destination file core_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. core_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. core_error_moving_to_trash = Error while moving "{ $file }" to the trash: { $error } core_error_removing = Error while removing "{ $file }": { $error } core_no_similarity_method_selected = Cannot find similar music files without a selected similarity method core_failed_to_spawn_command = Failed to spawn command: { $reason } core_failed_to_check_process_status = Failed to check process status: { $reason } core_failed_to_wait_for_process = Failed to wait for process: { $reason } core_failed_to_read_video_properties = Failed to read video properties: { $reason } core_failed_to_execute_ffmpeg = Failed to execute ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg failed with status { $status }: { $stderr } (command: { $command }) core_failed_to_load_image_frame = Failed to load image frame: { $reason } core_failed_to_extract_frame = Failed to extract frame at { $time } seconds from "{ $file }": { $reason } core_failed_to_save_thumbnail = Failed to save thumbnail for "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Failed to get frame at timestamp { $timestamp } from "{ $file }": { $reason } core_failed_get_frame_from_file = Failed to get frame from "{ $file }" at timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Invalid crop rectangle: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom } core_failed_to_crop_video_file = Failed to crop video file "{ $file }": { $reason } core_cropped_video_not_created = Cropped video file was not created: { $temp } core_unable_check_hash_of_file = Unable to check hash of file "{ $file }", reason { $reason } core_error_checking_hash_of_file = Error happened when checking hash of file "{ $file }", reason { $reason } core_image_zero_dimensions = Image has zero width or height "{ $path }" core_image_open_failed = Cannot open image file "{ $path }": { $reason } core_not_directory_remove = Trying to remove folder "{ $path }" which is not a directory core_cannot_read_directory = Cannot read directory "{ $path }" core_cannot_read_entry_from_directory = Cannot read entry from directory "{ $path }" core_folder_contains_file_inside = Folder contains file "{ $entry }" inside "{ $folder }" core_unknown_directory_entry = Unable to determine file type of directory entry "{ $entry }" inside "{ $path }" core_video_width_exceeds_limit = Video width { $width } exceeds the limit of { $limit } core_video_height_exceeds_limit = Video height { $height } exceeds the limit of { $limit } core_failed_to_process_video = Failed to process video file { $file }: { $reason } core_optimized_file_larger = Optimized file { $optimized } (size: { $new_size }) is not smaller than original { $original } (size: { $original_size }) core_unknown_codec = Unknown codec: { $codec } core_invalid_video_optimizer_mode = Invalid video optimizer mode: '{ $mode }'. Allowed values: transcode, crop core_folder_does_not_exist = Folder does not exist: { $folder } core_path_not_directory = Path is not a directory: { $folder } core_test_error_for_folder = Test error for folder: { $folder } core_unknown_exif_tag_group = Unknown EXIF tag group: { $tag } core_error_comparing_fingerprints = Error while comparing fingerprints: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Failed to generate thumbnail for "{ $file }": extracted frames have different dimensions core_failed_to_generate_thumbnail = Failed to generate thumbnail for "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Failed to extract frame at { $time } seconds from "{ $file }": { $reason } core_video_file_does_not_exist = Video file does not exist (could be removed between scan/later steps): "{ $path }" core_image_too_large = Image is too large ({ $width }x{ $height }) - more than supported { $max } pixels core_failed_to_get_video_metadata = Failed to get video metadata for file "{ $file }": { $reason } core_failed_to_get_video_codec = Failed to get video codec for file "{ $file }" core_failed_to_get_video_duration = Failed to get video duration for file "{ $file }" core_failed_to_get_video_dimensions = Failed to get video dimensions for file "{ $file }" core_frame_dimensions_mismatch = Frame dimensions for timestamp { $timestamp } do not match the first frame dimensions ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Failed to load data from cache file { $file }, reason { $reason } core_failed_to_load_data_from_json_cache = Failed to load data from json cache file { $file }, reason { $reason } core_failed_to_replace_with_optimized = Failed to replace file "{ $file }" with optimized version: { $reason } core_failed_to_write_data_to_cache = Cannot write data to cache file "{ $file }", reason { $reason } core_properly_saved_cache_entries = Properly saved to file { $count } cache entries. core_video_processing_stopped_by_user = Video processing was stopped by user core_thumbnail_generation_stopped_by_user = Thumbnail generation was stopped by user core_failed_to_optimize_video = Failed to optimize video "{ $file }": { $reason } core_failed_to_crop_video = Failed to crop video "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Failed to get metadata of optimized file "{ $file }": { $reason } core_cannot_create_config_folder = Cannot create config folder "{ $folder }", reason { $reason } core_cannot_create_cache_folder = Cannot create cache folder "{ $folder }", reason { $reason } core_cannot_create_or_open_cache_file = Cannot create or open cache file "{ $file }", reason { $reason } core_cannot_set_config_cache_path = Cannot set config/cache path - config and cache will not be used. core_invalid_extension_contains_space = { $extension } is not a valid extension because it contains empty space inside core_invalid_extension_contains_dot = { $extension } is not a valid extension because it contains dot inside core_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. core_ffmpeg_error = FFmpeg error while processing { $file }, status code { $code }, reason { $reason } ================================================ FILE: czkawka_core/i18n/es-ES/czkawka_core.ftl ================================================ # Core core_similarity_original = Original core_similarity_very_high = Muy alta core_similarity_high = Alta core_similarity_medium = Medio core_similarity_small = Pequeño core_similarity_very_small = Muy pequeño core_similarity_minimal = Mínimo core_cannot_open_dir = No se puede abrir el directorio { $dir }, razón { $reason } core_cannot_read_entry_dir = No se puede leer la entrada en directorio { $dir }, razón { $reason } core_cannot_read_metadata_dir = No se pueden leer metadatos en el directorio { $dir }, razón { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = El archivo { $name } parece haber sido modificado antes del Epoch Unix core_folder_modified_before_epoch = La carpeta { $name } parece haber sido modificada antes del Epoch Unix core_file_no_modification_date = No se puede obtener la fecha de modificación del archivo { $name }, razón { $reason } core_folder_no_modification_date = No se puede obtener la fecha de modificación de la carpeta { $name }, razón { $reason } core_cannot_start_scan_no_included_paths = No se puede iniciar el escaneo, porque no hay rutas incluidas core_skip_exist_check_all_included_paths_nonexistent = No se puede iniciar el escaneo, porque todas las rutas incluidas no existen core_missing_no_chosen_included_path = No ruta incluida válida fue elegida (las rutas excluidas podrían haber excluido todas las rutas incluidas) core_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 core_path_must_exists = Se debe proporcionar la ruta especificada, ignorando { $path } core_must_be_directory_or_file = Proporcionado el camino debe apuntar a un directorio o archivo válido, ignorando { $path } core_excluded_paths_pointless_slash = Excluyendo / es inútil, porque significa que no se escanean archivos core_paths_unable_to_get_device_id = Imposible obtener el id del dispositivo del directorio { $path } core_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 core_needs_allowed_extensions = No se puede iniciar el escaneo, cuando todas las extensiones fueron excluidas del escaneo core_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 core_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 core_ffmpeg_not_found = No se puede encontrar una instalación adecuada de FFmpeg o FFprobe. Estos son programas externos que deben instalarse manualmente. core_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 core_invalid_symlink_infinite_recursion = Recursión infinita core_invalid_symlink_non_existent_destination = Archivo de destino inexistente core_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. core_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. core_error_moving_to_trash = Error al mover "{ $file }" a la papelera: { $error } core_error_removing = Error al eliminar "{ $file }": { $error } core_no_similarity_method_selected = No se pueden encontrar archivos de música similares sin un método de similitud seleccionado core_ffmpeg_failed_with_status = ffmpeg falló con estado { $status }: { $stderr } (comando: { $command }) core_failed_to_extract_frame = Falló al extraer el fotograma en { $time } segundos de "{ $file }": { $reason } core_failed_to_save_thumbnail = Falló al guardar el miniagujete para "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Error al obtener el fotograma en el sello de tiempo { $timestamp } de "{ $file }": { $reason } core_failed_get_frame_from_file = No se pudo obtener el fotograma de "{ $file }" en el sello de tiempo { $timestamp }: { $reason } core_invalid_crop_rectangle = ¡Rectángulo de recorte inválido: izquierda={ $left }, arriba={ $top }, derecha={ $right }, abajo={ $bottom } core_failed_to_crop_video_file = El recorte del archivo de video "{ $file }" falló: { $reason } core_cropped_video_not_created = El archivo de video recortado no fue creado: { $temp } core_unable_check_hash_of_file = Imposible verificar el hash del archivo "{ $file }", la razón { $reason } core_error_checking_hash_of_file = Error ocurrió al verificar el hash del archivo "{ $file }", razón { $reason } core_image_zero_dimensions = La imagen tiene un ancho o alto de cero "{ $path }" core_image_open_failed = No se puede abrir el archivo de imagen "{ $path }": { $reason } core_not_directory_remove = Intentando eliminar la carpeta "{ $path }" que no es un directorio core_cannot_read_directory = No se puede leer el directorio "{ $path }" core_cannot_read_entry_from_directory = No se puede leer la entrada del directorio "{ $path }" core_folder_contains_file_inside = La carpeta contiene el archivo "{ $entry }" dentro de "{ $folder }" core_unknown_directory_entry = No se puede determinar el tipo de archivo de la entrada del directorio "{ $entry }" dentro de "{ $path }" core_video_width_exceeds_limit = Video ancho { $width } excede el límite de { $limit } core_video_height_exceeds_limit = Video altura { $height } excede el límite de { $limit } core_failed_to_process_video = No se pudo procesar el archivo de video { $file }: { $reason } core_optimized_file_larger = Archivo optimizado { $optimized } (tamaño: { $new_size }) no es más pequeño que el original { $original } (tamaño: { $original_size }) core_unknown_codec = Códec desconocido: { $codec } core_invalid_video_optimizer_mode = El modo optimizador de video no es válido: '{ $mode }'. Los valores permitidos: transcodificar, recortar core_folder_does_not_exist = La carpeta no existe: { $folder } core_path_not_directory = La ruta no es un directorio: { $folder } core_test_error_for_folder = Error de prueba para la carpeta: { $folder } core_unknown_exif_tag_group = Grupo de etiquetas EXIF desconocido: { $tag } core_error_comparing_fingerprints = Error al comparar huellas dactilares: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Falló al generar miniatura para "{ $file }": los fotogramas extraídos tienen diferentes dimensiones core_failed_to_generate_thumbnail = No se pudo generar miniatura para "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Falló al extraer el fotograma en { $time } segundos de "{ $file }": { $reason } core_video_file_does_not_exist = El archivo de video no existe (puede ser eliminado entre las etapas de escaneo/más tarde): "{ $path }" core_image_too_large = La imagen es demasiado grande ({ $width }x{ $height }) - más que los soportados { $max } píxeles core_failed_to_get_video_metadata = No se pudo obtener los metadatos del video para el archivo "{ $file }": { $reason } core_failed_to_get_video_codec = No se pudo obtener el códec de video para el archivo "{ $file }" core_failed_to_get_video_duration = No se pudo obtener la duración del video para el archivo "{ $file }" core_failed_to_get_video_dimensions = No se pudo obtener las dimensiones del video para el archivo "{ $file }" core_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 }) core_failed_to_load_data_from_cache = No se pudo cargar los datos del archivo de caché { $file }, la razón { $reason } core_failed_to_load_data_from_json_cache = No se pudo cargar los datos del archivo de caché json { $file }, motivo { $reason } core_failed_to_replace_with_optimized = No se pudo reemplazar el archivo "{ $file }" con la versión optimizada: { $reason } core_failed_to_write_data_to_cache = No se puede escribir datos al archivo de caché "{ $file }", la razón { $reason } core_properly_saved_cache_entries = Guardado correctamente a archivo { $count } entradas de caché. core_video_processing_stopped_by_user = El procesamiento de video fue detenido por el usuario core_thumbnail_generation_stopped_by_user = La generación de miniaturas fue detenida por el usuario core_failed_to_optimize_video = Falló optimizar el video "{ $file }": { $reason } core_failed_to_crop_video = Falló al recortar el video "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = No se pudo obtener los metadatos del archivo optimizado "{ $file }": { $reason } core_cannot_create_config_folder = No se puede crear la carpeta de configuración "{ $folder }", la razón { $reason } core_cannot_create_cache_folder = No se puede crear la carpeta de caché "{ $folder }", la razón { $reason } core_cannot_create_or_open_cache_file = No se puede crear o abrir el archivo de caché "{ $file }", la razón { $reason } core_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. core_invalid_extension_contains_space = { $extension } no es una extensión válida porque contiene espacios en blanco dentro core_invalid_extension_contains_dot = { $extension } no es una extensión válida porque contiene un punto dentro core_failed_to_spawn_command = No se pudo ejecutar el comando: { $reason } core_failed_to_check_process_status = No se pudo verificar el estado del proceso: { $reason } core_failed_to_wait_for_process = No se pudo esperar a que finalizara el proceso: { $reason } core_failed_to_read_video_properties = No se pudieron leer las propiedades del video: { $reason } core_failed_to_execute_ffmpeg = No se pudo ejecutar ffmpeg: { $reason } core_failed_to_load_image_frame = No se pudo cargar el fotograma de la imagen: { $reason } ================================================ FILE: czkawka_core/i18n/fa/czkawka_core.ftl ================================================ # Core core_similarity_original = اصولی core_similarity_very_high = بسیار بلند core_similarity_high = ارتفاع core_similarity_medium = میانبر core_similarity_small = کوچک core_similarity_very_small = بسیار کوچک core_similarity_minimal = 最少istantly converted to Persian: مینیمال core_cannot_open_dir = نمی‌توانم مسیر { $dir } را باز کنم، دلیل آن { $reason } core_cannot_read_entry_dir = نمی‌توانید درایه‌ای از پوشه { $dir } را بخوانید، دلیل آن { $reason } core_cannot_read_metadata_dir = می‌توانید مетا داده در پوشه { $dir } را خواند، با دلیل "{ $reason }" core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = فایل { $name } به نظر می‌رسد قبل از زمان‌بندی سیستم عکس آشامیده شده است core_folder_modified_before_epoch = دایره‌نامه { $name } به نظر می‌رسد قبل از زمان‌پا‌شناخت عینک لوبیا برمدرمود شده است core_file_no_modification_date = نemی‌توانم تاریخ تغییرات فایل { $name } را دریافت کنم، دلیل { $reason } core_folder_no_modification_date = نemی‌توانم تاریخ بروزرسanی از پوشه { $name } را دریافته‌ام، دلیل "{ $reason }" core_cannot_start_scan_no_included_paths = امکان شروع اسکن وجود ندارد، زیرا هیچ مسیرهای گنجانده شده‌ای وجود ندارد core_skip_exist_check_all_included_paths_nonexistent = امکان شروع اسکن وجود ندارد، زیرا تمام مسیرهای گنجانیده شده وجود ندارند core_missing_no_chosen_included_path = مسیر گنجانده شده معتبری انتخاب نشد (مسیرهای رد شده می‌توانستند تمام مسیرهای گنجانده شده را رد کنند) core_reference_included_paths_same = امکان شروع اسکن وجود ندارد، جایی که تمام مسیرهای گنجانده شده معتبر نیز مسیرهای ارجاعی هستند، لطفاً اعتبار سنجی را انجام دهید یا مسیرهای ارجاعی را غیرفعال کنید core_path_must_exists = مسیر ارائه شده باید وجود داشته باشد، نادیده گرفتن { $path } core_must_be_directory_or_file = مسیر ارائه شده باید به یک دایرکتوری یا فایل معتبر اشاره کند، با نادیده گرفتن { $path } core_excluded_paths_pointless_slash = исключить / бессмысленно, потому что это означает, что файлы не будут сканироваться core_paths_unable_to_get_device_id = امکان دریافت شناسه دستگاه از پوشه { $path } وجود ندارد core_needs_allowed_extensions_limited_by_tool = امکان شروع اسکن وجود ندارد، زمانی که تمام افزونه‌های موجود در این ابزار ({ $extensions }) از اسکن حذف شده‌اند core_needs_allowed_extensions = امکان شروع اسکن وجود ندارد، زمانی که تمام افزونه‌ها از اسکن حذف شده‌اند core_needs_to_set_at_least_one_broken_option = امکان شروع اسکن وجود ندارد، زمانی که گزینه "شکسته" برای اسکن تنظیم نشده باشد core_needs_to_set_at_least_one_bad_name_option = امکان شروع اسکن وجود ندارد، زمانی که گزینه نام نامناسب تنظیم نشده باشد تا برای اسکن جستجو شود core_ffmpeg_not_found = امکان یافتن یک نصب مناسب از FFmpeg یا FFprobe وجود ندارد. این‌ها برنامه‌های خارجی هستند که باید به صورت دستی نصب شوند. core_ffmpeg_not_found_windows = با توجه به نگارش همان طور که در متن داده شده است، مطمئن شوید که ffmpeg.exe و ffprobe.exe در PATH موجود هستند یا در آن پوشه که شامل اجرایabled exe اپلیکیشن است قرار داده شده‌اند core_invalid_symlink_infinite_recursion = بازگشت نامتناهی core_invalid_symlink_non_existent_destination = فایل مقصد مفقود core_messages_limit_reached_characters = تعداد پیام‌هایی که بیش از حاشیه مقرر ({ $current }/{ $limit } کاراکتر) بودند، باعث قطع شدن خروجی شد. برای مشاهده کامل خروجی، گزینه محدود سازی را در تنظیمات غیرفعال کنید. core_messages_limit_reached_lines = تعداد پیام‌ها حاشیه مقرر ({ $current }/{ $limit } 行) را بیشینه کرد، بنابراین خروجی کوتاه شده است. برای مطالعه خروجی کامل، گزینه محدود کردن را در تنظیمات غیرفعال کنید. core_error_moving_to_trash = خطا در منتقل کردن "{ $file }" به سبد حذف شد: { $error } core_error_removing = خطا در حذف "{ $file }": { $error } core_no_similarity_method_selected = فیلدهای موسیقی مشابه را بدون انتخاب روش مشابهی پیدا نمی‌توانید بیابید core_failed_to_spawn_command = ناموفق بود تا دستورالعمل تولید شود: { $reason } core_failed_to_check_process_status = ناموفق بودن بررسی وضعیت فرآیند: { $reason } core_failed_to_wait_for_process = ناموفق بود برای منتظر ماندن از فرآیند: { $reason } core_failed_to_read_video_properties = ناموفقیت در خواندن ویژگی‌های ویدیو: { $reason } core_failed_to_execute_ffmpeg = ناموفق بود تا ffmpeg اجرا شود: { $reason } core_ffmpeg_failed_with_status = ffmpeg با وضعیت { $status } ناموفق بود: { $stderr } (دستور: { $command }) core_failed_to_load_image_frame = ناموفقیت بارگذاری فریم تصویر: { $reason } core_failed_to_extract_frame = ناموفق بود دریافت فریم در { $time } ثانیه از "{ $file }": { $reason } core_failed_to_save_thumbnail = ناموفق بود برای ذخیره تصویر کوچک برای "{ $file }": { $reason } core_failed_get_frame_at_timestamp = ناموفق برای دریافت فریم در زمان‌بندی { $timestamp } از "{ $file }": { $reason } core_failed_get_frame_from_file = ناموفق برای دریافت فریم از "{ $file }" در زمان‌بندی { $timestamp }: { $reason } core_invalid_crop_rectangle = عدم معتبر بودن مستطیل کشتزار: چپ={ $left }، بالا={ $top }، راست={ $right }، پایین={ $bottom } core_failed_to_crop_video_file = فشل برش فایل ویدیویی "{ $file }": { $reason } core_cropped_video_not_created = فایل ویدیوی برش خورده ایجاد نشد: { $temp } core_unable_check_hash_of_file = امکان بررسی هش فایل "{ $file }" وجود ندارد، دلیل { $reason } core_error_checking_hash_of_file = خطای رخ داده هنگام بررسی هش فایل "{ $file }"، دلیل { $reason } core_image_zero_dimensions = تصویر دارای عرض یا ارتفاع صفر "{ $path }" core_image_open_failed = امکان باز کردن فایل تصویر "{ $path }": { $reason } core_not_directory_remove = در حال حذف پوشه "{ $path }" که یک دایرکتوری نیست core_cannot_read_directory = امکان خواندن دایرکتوری "{ $path }" وجود ندارد core_cannot_read_entry_from_directory = Could not read entry from directory "{ $path }" core_folder_contains_file_inside = فایل "{ $entry }" داخل پوشه "{ $folder }" وجود دارد core_unknown_directory_entry = امکان تعیین نوع فایل ورودی دایرکتوری "{ $entry }" داخل "{ $path }" وجود ندارد core_video_width_exceeds_limit = Video عرض { $width } از حد { $limit } تجاوز می‌کند core_video_height_exceeds_limit = Video ارتفاع { $height } از حد { $limit } تجاوز می‌کند core_failed_to_process_video = فشل پردازش فایل ویدیویی { $file }: { $reason } core_optimized_file_larger = فایل بهینه‌شده { $optimized } (حجم: { $new_size }) کوچکتر از فایل اصلی { $original } (حجم: { $original_size }) نیست core_unknown_codec = کدک ناشناخته: { $codec } core_invalid_video_optimizer_mode = حالت بهینه‌سازی ویدیو نامعتبر: '{ $mode }'. مقادیر مجاز: transcode, crop core_folder_does_not_exist = فোলدر وجود ندارد: { $folder } core_path_not_directory = مسیر معتبر نیست: { $folder } core_test_error_for_folder = خطای آزمایشی برای پوشه: { $folder } core_unknown_exif_tag_group = گروه برچسب EXIF ناشناخته: { $tag } core_error_comparing_fingerprints = خطای مقایسه اثر انگشت: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = ناموفق بود ایجاد پیش‌نمایه‌ی برای "{ $file }": فریم‌های استخراج‌شده ابعاد متفاوتی دارند core_failed_to_generate_thumbnail = ناموفق بود ایجاد پیش‌نمایه‌ی "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = ناموفق بود دریافت فریم در { $time } ثانیه از "{ $file }": { $reason } core_video_file_does_not_exist = فایل ویدیویی وجود ندارد (می‌توان آن را بین اسکن/مراحل بعدی حذف کرد): "{ $path }" core_image_too_large = تصویر خیلی بزرگ است ({ $width }x{ $height }) - بیش از حد مجاز { $max } پیکسل core_failed_to_get_video_metadata = ناموفق بود دریافت اطلاعات ویدئویی برای فایل "{ $file }": { $reason } core_failed_to_get_video_codec = ناموفق بود دریافت کدک ویدیویی برای فایل "{ $file }" core_failed_to_get_video_duration = ناموفق بود دریافت مدت زمان ویدیو برای فایل "{ $file }" core_failed_to_get_video_dimensions = ناموفق بود دریافت ابعاد ویدیو برای فایل "{ $file }" core_frame_dimensions_mismatch = ابعاد فریم برای زمان‌بندی { $timestamp } با ابعاد فریم اول ({ $first_w }x{ $first_h }) مطابقت ندارند core_failed_to_load_data_from_cache = ناموفق بود تا داده‌ها را از فایل کش { $file } بارگیری شود، دلیل { $reason } core_failed_to_load_data_from_json_cache = ناموفق بود تا داده‌ها را از فایل کش JSON { $file} بارگیری شود، دلیل { $reason } core_failed_to_replace_with_optimized = ناموفق بود فایل "{ $file }" با نسخه بهینه جایگزین شود: { $reason } core_failed_to_write_data_to_cache = امکان نوشتن داده‌ها به فایل کش "{ $file }" وجود ندارد، دلیل { $reason } core_properly_saved_cache_entries = ذخیره شده به درستی در فایل { $count } ورودی کش. core_video_processing_stopped_by_user = پردازش ویدیو توسط کاربر متوقف شد core_thumbnail_generation_stopped_by_user = تولید پیش‌نمایی متوقف شد توسط کاربر core_failed_to_optimize_video = ناموفق بود برای بهینه‌سازی ویدیو "{ $file }": { $reason } core_failed_to_crop_video = ناموفق بود برش ویدیو "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = ناموفق بود دریافت اطلاعات متا داده شده از فایل بهینه شده "{ $file }": { $reason } core_cannot_create_config_folder = امکان ایجاد پوشه تنظیمات "{ $folder }" وجود ندارد، دلیل { $reason } core_cannot_create_cache_folder = امکان ایجاد پوشه کش "{ $folder }" وجود ندارد، دلیل { $reason } core_cannot_create_or_open_cache_file = امکان ایجاد یا باز کردن فایل کش "{ $file }" وجود ندارد، دلیل { $reason } core_cannot_set_config_cache_path = امکان تنظیم مسیر config/cache وجود ندارد - config و cache استفاده نخواهند شد. core_invalid_extension_contains_space = { $extension } یک پسوند معتبر نیست زیرا حاوی فاصله خالی در داخل است core_invalid_extension_contains_dot = { $extension } یک پسوند معتبر نیست زیرا شامل نقطه داخل آن است ================================================ FILE: czkawka_core/i18n/fr/czkawka_core.ftl ================================================ # Core core_similarity_original = Originale core_similarity_very_high = Très haute core_similarity_high = Haute core_similarity_medium = Moyenne core_similarity_small = Basse core_similarity_very_small = Très basse core_similarity_minimal = Minimale core_cannot_open_dir = Impossible d’ouvrir le répertoire { $dir }. Raison : { $reason } core_cannot_read_entry_dir = Impossible de lire l'entrée dans le répertoire { $dir }. Raison : { $reason } core_cannot_read_metadata_dir = Impossible de lire les métadonnées dans le répertoire { $dir }. Raison  : { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Le fichier { $name } semble avoir été modifié avant l'époque Unix core_folder_modified_before_epoch = Le dossier { $name } semble avoir été modifié avant l'époque Unix core_file_no_modification_date = Impossible d'obtenir la date de modification du fichier { $name }. Raison  : { $reason } core_folder_no_modification_date = Impossible d'obtenir la date de modification du dossier { $name }. Raison : { $reason } core_cannot_start_scan_no_included_paths = Impossible de démarrer l'analyse, car il n'y a pas de chemins inclus core_skip_exist_check_all_included_paths_nonexistent = Impossible de démarrer l'analyse, car tous les chemins inclus n'existent pas core_missing_no_chosen_included_path = Aucune voie incluse valide n'a été choisie (les voies exclues auraient pu exclure toutes les voies incluses) core_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 core_path_must_exists = Le chemin fourni doit exister, en ignorant { $path } core_must_be_directory_or_file = Le chemin fourni doit pointer vers un répertoire ou un fichier valide, en ignorant { $path } core_excluded_paths_pointless_slash = Exclure / est inutile, car cela signifie que aucun fichier ne sera scanné core_paths_unable_to_get_device_id = Impossible d’obtenir l’identifiant de l’appareil à partir du dossier { $path } core_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 core_needs_allowed_extensions = Impossible de démarrer l'analyse, lorsque toutes les extensions ont été exclues de l'analyse core_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 core_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 core_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. core_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 core_invalid_symlink_infinite_recursion = Récursion infinie core_invalid_symlink_non_existent_destination = Fichier de destination inexistant core_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. core_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. core_error_moving_to_trash = Erreur lors du déplacement de "{ $file }" vers la poubelle : { $error } core_error_removing = Erreur lors de la suppression de "{ $file }": { $error } core_no_similarity_method_selected = Impossible de trouver des fichiers musicaux similaires sans une méthode de similarité sélectionnée core_ffmpeg_failed_with_status = ffmpeg a échoué avec le statut { $status } : { $stderr } (commande : { $command }) core_failed_to_load_image_frame = Erreur de chargement du cadre d'image : { $reason } core_failed_to_extract_frame = Échec de l'extraction du cadre à { $time } secondes depuis "{ $file }": { $reason } core_failed_to_save_thumbnail = Échec de sauvegarde de la miniature pour "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Échec de récupération du cadre au timestamp { $timestamp } depuis "{ $file }": { $reason } core_failed_get_frame_from_file = Échec de récupération du cadre à partir de "{ $file }" à l'horodatage { $timestamp } : { $reason } core_invalid_crop_rectangle = Rectangle de culture non valide : gauche={ $left }, haut={ $top }, droite={ $right }, bas={ $bottom } core_failed_to_crop_video_file = Échec du recadrage du fichier vidéo "{ $file }": { $reason } core_cropped_video_not_created = Fichier vidéo coupé non créé : { $temp } core_unable_check_hash_of_file = Impossible de vérifier le hachage du fichier "{ $file }", la raison est { $reason } core_error_checking_hash_of_file = Erreur survenue lors de la vérification du haché du fichier "{ $file }", la raison { $reason } core_image_zero_dimensions = Image a zéro largeur ou hauteur "{ $path }" core_image_open_failed = Impossible d'ouvrir le fichier image "{ $path }": { $reason } core_not_directory_remove = Essayer de supprimer le dossier "{ $path }" qui n'est pas un répertoire core_cannot_read_directory = Impossible de lire le répertoire "{ $path }" core_cannot_read_entry_from_directory = Impossible de lire l’entrée du répertoire "{ $path }" core_folder_contains_file_inside = Le dossier contient le fichier "{ $entry }" à l'intérieur "{ $folder }" core_unknown_directory_entry = Impossible de déterminer le type de fichier de l'entrée de répertoire "{ $entry }" dans "{ $path }" core_video_width_exceeds_limit = La largeur de la vidéo { $width } dépasse la limite de { $limit } core_video_height_exceeds_limit = Vidéo hauteur { $height } dépasse la limite de { $limit } core_failed_to_process_video = Échec du traitement du fichier vidéo { $file }: { $reason } core_unknown_codec = Codec inconnu : { $codec } core_invalid_video_optimizer_mode = Mode d'optimisation vidéo non valide : '{ $mode }'. Valeurs autorisées : transcode, crop core_folder_does_not_exist = Le dossier n’existe pas : { $folder } core_path_not_directory = Le chemin n'est pas un répertoire : { $folder } core_test_error_for_folder = Erreur de test pour le dossier : { $folder } core_unknown_exif_tag_group = Groupe de balises EXIF inconnu : { $tag } core_failed_to_generate_thumbnail_frames_different_dimensions = Échec de génération de miniature pour "{ $file }": les images extraites ont des dimensions différentes core_failed_to_generate_thumbnail = Échec de la génération de miniature pour "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Échec de l'extraction du cadre à { $time } secondes depuis "{ $file }": { $reason } core_video_file_does_not_exist = Fichier vidéo introuvable (peut être supprimé entre les étapes de numérisation/plus tard) : "{ $path }" core_image_too_large = L'image est trop grande ({ $width }x{ $height }) - plus que le supporté { $max } pixels core_failed_to_get_video_metadata = Échec de récupération des métadonnées vidéo pour le fichier "{ $file }": { $reason } core_failed_to_get_video_codec = Échec de récupération du codec vidéo pour le fichier "{ $file }" core_failed_to_get_video_duration = Impossible d’obtenir la durée de la vidéo pour le fichier "{ $file }" core_failed_to_get_video_dimensions = Impossible d'obtenir les dimensions de la vidéo pour le fichier "{ $file }" core_frame_dimensions_mismatch = Les dimensions du cadre pour le timestamp { $timestamp } ne correspondent pas aux dimensions du premier cadre ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Échec de chargement des données depuis le fichier de cache { $file }, la raison { $reason } core_failed_to_load_data_from_json_cache = Échec de chargement des données à partir du fichier de cache JSON { $file }, la raison { $reason } core_failed_to_replace_with_optimized = Échec du remplacement du fichier "{ $file }" par la version optimisée : { $reason } core_failed_to_write_data_to_cache = Impossible d'écrire des données dans le fichier de cache "{ $file }", la raison { $reason } core_properly_saved_cache_entries = Sauvegardé correctement dans le fichier { $count } entrées de cache. core_video_processing_stopped_by_user = Le traitement vidéo a été arrêté par l'utilisateur core_thumbnail_generation_stopped_by_user = La génération de miniatures a été arrêtée par l'utilisateur core_failed_to_optimize_video = Échec de l'optimisation de la vidéo "{ $file }": { $reason } core_failed_to_crop_video = Échec du recadrage de la vidéo "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Échec de récupération des métadonnées du fichier optimisé "{ $file }": { $reason } core_cannot_create_config_folder = Impossible de créer le dossier de configuration "{ $folder }", la raison est { $reason } core_cannot_create_cache_folder = Impossible de créer le dossier de cache "{ $folder }", la raison { $reason } core_cannot_create_or_open_cache_file = Impossible de créer ou d'ouvrir le fichier de cache "{ $file }", la raison { $reason } core_cannot_set_config_cache_path = Impossible de définir le chemin de config/cache - la config et le cache ne seront pas utilisés. core_invalid_extension_contains_space = { $extension } n'est pas une extension valide car elle contient des espaces vides à l'intérieur core_invalid_extension_contains_dot = { $extension } n'est pas une extension valide car elle contient un point à l'intérieur core_failed_to_spawn_command = Impossible de lancer la commande : { $reason } core_failed_to_check_process_status = Impossible de vérifier l'état du processus : { $reason } core_failed_to_wait_for_process = Impossible d'attendre la fin du processus : { $reason } core_failed_to_read_video_properties = Impossible de lire les propriétés de la vidéo : { $reason } core_failed_to_execute_ffmpeg = Impossible d'exécuter ffmpeg : { $reason } core_optimized_file_larger = Le fichier optimisé { $optimized } (taille : { $new_size }) n'est pas plus petit que le fichier original { $original } (taille : { $original_size }) core_error_comparing_fingerprints = Erreur lors de la comparaison des empreintes : { $reason } ================================================ FILE: czkawka_core/i18n/it/czkawka_core.ftl ================================================ # Core core_similarity_original = Originali core_similarity_very_high = Altissima core_similarity_high = Alta core_similarity_medium = Media core_similarity_small = Piccola core_similarity_very_small = Piccolissima core_similarity_minimal = Minima core_cannot_open_dir = Impossibile aprire cartella { $dir }, motivo { $reason } core_cannot_read_entry_dir = Impossibile leggere elemento nella cartella { $dir }, ragione { $reason } core_cannot_read_metadata_dir = Impossibile leggere metadati nella cartella { $dir }, ragione { $reason } core_cannot_read_metadata_file = Impossibile leggere i metadati del file { $file } , ragione { $reason } core_file_modified_before_epoch = Il file { $name } sembra essere stato modificato prima dell'Epoch Unix core_folder_modified_before_epoch = La cartella { $name } sembra essere stata modificata prima dell'Epoch Unix core_file_no_modification_date = Impossibile recuperare data di modifica dal file { $name }, ragione { $reason } core_folder_no_modification_date = Impossibile recuperare data di modifica dalla cartella { $name }, ragione { $reason } core_cannot_start_scan_no_included_paths = Impossibile avviare la scansione, perché non ci sono percorsi inclusi core_skip_exist_check_all_included_paths_nonexistent = Impossibile avviare la scansione, perché tutti i percorsi inclusi non esistono core_missing_no_chosen_included_path = Non è stato incluso nessun percorso valido (i percorsi esclusi potrebbero aver escluso tutti i percorsi inclusi) core_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 core_path_must_exists = Percorso fornito non esistente, ignoro { $path } core_must_be_directory_or_file = Il percorso fornito deve puntare a una cartella o file validi, ignoro { $path } core_excluded_paths_pointless_slash = Escludendo / è inutile, perché significa che nessun file verrà scansionato core_paths_unable_to_get_device_id = Impossibile ottenere l'id del dispositivo dalla cartella { $path } core_needs_allowed_extensions_limited_by_tool = Impossibile avviare la scansione, quando tutte le estensioni disponibili in questo strumento ({ $extensions }) sono state escluse dalla scansione core_needs_allowed_extensions = Impossibile avviare la scansione, quando tutte le estensioni sono state escluse dalla scansione core_needs_to_set_at_least_one_broken_option = Impossibile avviare la scansione, quando non è impostata l'opzione "broken" per la scansione core_needs_to_set_at_least_one_bad_name_option = Impossibile avviare la scansione, quando non è impostata l'opzione "nome errato" per la scansione core_ffmpeg_not_found = Non riesco a trovare un'installazione appropriata di FFmpeg o FFprobe. Questi sono programmi esterni che devono essere installati manualmente. core_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 core_invalid_symlink_infinite_recursion = Ricorsione infinita core_invalid_symlink_non_existent_destination = File di destinazione inesistente core_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. core_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. core_error_moving_to_trash = Errore durante lo spostamento di "{ $file }" nel cestino: { $error } core_error_removing = Errore durante la rimozione "{ $file }": { $error } core_no_similarity_method_selected = Non riesco a trovare file musicali simili senza un metodo di similarità selezionato core_failed_to_spawn_command = Fallito generare comando: { $reason } core_failed_to_check_process_status = Impossibile controllare lo stato del processo: { $reason } core_failed_to_wait_for_process = Impossibile attendere il processo: { $reason } core_failed_to_read_video_properties = Impossibile leggere le proprietà del video: { $reason } core_failed_to_execute_ffmpeg = Impossibile eseguire ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg fallito con stato { $status }: { $stderr } (comando: { $command }) core_failed_to_load_image_frame = Impossibile caricare il frame dell'immagine: { $reason } core_failed_to_extract_frame = Fallito nell'estrarre il frame a { $time } secondi da "{ $file }": { $reason } core_failed_to_save_thumbnail = Impossibile salvare l'anteprima per "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Impossibile ottenere il frame al timestamp { $timestamp } da "{ $file }": { $reason } core_failed_get_frame_from_file = Impossibile ottenere il frame da "{ $file }" al timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Rettangolo di riempimento non valido: sinistra={ $left }, in alto={ $top }, destra={ $right }, in basso={ $bottom } core_failed_to_crop_video_file = Impossibile ritagliare il file video "{ $file }": { $reason } core_cropped_video_not_created = Il file video ritagliato non è stato creato: { $temp } core_unable_check_hash_of_file = Impossibile controllare l'hash del file "{ $file }", motivo { $reason } core_error_checking_hash_of_file = Errore avvenuto durante il controllo dell'hash del file "{ $file }", motivo { $reason } core_image_zero_dimensions = L'immagine ha zero larghezza o altezza "{ $path }" core_image_open_failed = Impossibile aprire il file immagine "{ $path }": { $reason } core_not_directory_remove = Tentativo di rimuovere la cartella "{ $path }" che non è una directory core_cannot_read_directory = Impossibile leggere la directory "{ $path }" core_cannot_read_entry_from_directory = Impossibile leggere l'entrata dal directory "{ $path }" core_folder_contains_file_inside = La cartella contiene il file "{ $entry }" all'interno di "{ $folder }" core_unknown_directory_entry = Impossibile determinare il tipo di file dell'inserimento della directory "{ $entry }" all'interno di "{ $path }" core_video_width_exceeds_limit = Video larghezza { $width } supera il limite di { $limit } core_video_height_exceeds_limit = Video altezza { $height } supera il limite di { $limit } core_failed_to_process_video = Impossibile elaborare il file video { $file }: { $reason } core_optimized_file_larger = File ottimizzato { $optimized } (dimensione: { $new_size }) non è più piccolo dell'originale { $original } (dimensione: { $original_size }) core_unknown_codec = Codec sconosciuto: { $codec } core_invalid_video_optimizer_mode = Modalità ottimizzatore video non valida: '{ $mode }'. Valori ammessi: transcodifica, ritaglio core_folder_does_not_exist = La cartella non esiste: { $folder } core_path_not_directory = Il percorso non è una directory: { $folder } core_test_error_for_folder = Errore di test per cartella: { $folder } core_unknown_exif_tag_group = Gruppo di tag EXIF sconosciuto: { $tag } core_error_comparing_fingerprints = Errore durante il confronto delle impronte digitali: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Impossibile generare l'anteprima per "{ $file }": i fotogrammi estratti hanno dimensioni diverse core_failed_to_generate_thumbnail = Impossibile generare l'anteprima per "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Fallito nell'estrarre il frame a { $time } secondi da "{ $file }": { $reason } core_video_file_does_not_exist = File video non esistente (può essere rimosso tra le fasi di scansione/successive): "{ $path }" core_image_too_large = L'immagine è troppo grande ({ $width }x{ $height }) - più di { $max } pixel supportati core_failed_to_get_video_metadata = Impossibile ottenere i metadati video per il file "{ $file }": { $reason } core_failed_to_get_video_codec = Impossibile ottenere il codec video per il file "{ $file }" core_failed_to_get_video_duration = Impossibile ottenere la durata del video per il file "{ $file }" core_failed_to_get_video_dimensions = Impossibile ottenere le dimensioni del video per il file "{ $file }" core_frame_dimensions_mismatch = Dimensioni del fotogramma per timestamp { $timestamp } non corrispondono alle dimensioni del primo fotogramma ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Impossibile caricare i dati dal file di cache { $file }, motivo { $reason } core_failed_to_load_data_from_json_cache = Impossibile caricare i dati dal file di cache json { $file }, motivo { $reason } core_failed_to_replace_with_optimized = Impossibile sostituire il file "{ $file }" con la versione ottimizzata: { $reason } core_failed_to_write_data_to_cache = Impossibile scrivere i dati nel file di cache "{ $file }", motivo { $reason } core_properly_saved_cache_entries = Salvatato correttamente nel file { $count } voci di cache. core_video_processing_stopped_by_user = L'elaborazione video è stata interrotta dall'utente core_thumbnail_generation_stopped_by_user = Generazione miniatura interrotta dall'utente core_failed_to_optimize_video = Impossibile ottimizzare il video "{ $file }": { $reason } core_failed_to_crop_video = Impossibile ritagliare il video "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Impossibile ottenere i metadati del file ottimizzato "{ $file }": { $reason } core_cannot_create_config_folder = Impossibile creare la cartella di configurazione "{ $folder }", motivo { $reason } core_cannot_create_cache_folder = Impossibile creare la cartella di cache "{ $folder }", motivo { $reason } core_cannot_create_or_open_cache_file = Impossibile creare o aprire il file di cache "{ $file }", motivo { $reason } core_cannot_set_config_cache_path = Impossibile impostare il percorso config/cache - config e cache non verranno utilizzati. core_invalid_extension_contains_space = { $extension } non è un'estensione valida perché contiene spazi vuoti all'interno core_invalid_extension_contains_dot = { $extension } non è un'estensione valida perché contiene un punto all'interno ================================================ FILE: czkawka_core/i18n/ja/czkawka_core.ftl ================================================ # Core core_similarity_original = 新規に作成 core_similarity_very_high = 非常に高い core_similarity_high = 高い core_similarity_medium = ミディアム core_similarity_small = 小 core_similarity_very_small = 非常に小さい core_similarity_minimal = 最小 core_cannot_open_dir = ディレクトリを開くことができません { $dir }、理由 { $reason } core_cannot_read_entry_dir = Dir { $dir } でエントリを読み込めません、理由 { $reason } core_cannot_read_metadata_dir = Dir { $dir } でメタデータを読み込めません、理由 { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = ファイル { $name } は Unix Epoch より前に変更されているようです core_folder_modified_before_epoch = フォルダ { $name } は、Unix Epoch の前に変更されているようです core_file_no_modification_date = ファイル { $name } から変更日を取得できません、理由 { $reason } core_folder_no_modification_date = フォルダ { $name } から変更日を取得できません、理由 { $reason } core_cannot_start_scan_no_included_paths = スキャンを開始できません、含まれているパスがないため。 core_skip_exist_check_all_included_paths_nonexistent = スキャンを開始できません。指定されたすべてのパスが存在しません。 core_missing_no_chosen_included_path = 有効な含まれるパスが選択されませんでした(除外されたパスがすべての含まれるパスを除外した可能性があります) core_reference_included_paths_same = スキャンを開始できません。すべての有効な含まれるパスが参照パスでもある場合、検証を試みてください、または参照パスを無効化してください。 core_path_must_exists = 提供されたパスが存在しなければなりません、{ $path } を無視して core_must_be_directory_or_file = 提供されたパスは、有効なディレクトリまたはファイルに指し示す必要があり、{ $path } を無視します。 core_excluded_paths_pointless_slash = 除外 / は無意味で、ファイルがスキャンされないことを意味するからです core_paths_unable_to_get_device_id = フォルダ { $path } からデバイスIDを取得できません core_needs_allowed_extensions_limited_by_tool = スキャンを開始できません。このツール ({ $extensions }) に存在するすべての拡張機能を除外しても。 core_needs_allowed_extensions = スキャンを開始できません。すべての拡張機能をスキャンから除外したとき core_needs_to_set_at_least_one_broken_option = スキャンを開始できません。破損オプションが設定されていない場合に発生します。 core_needs_to_set_at_least_one_bad_name_option = スキャンを開始できません。悪い名前オプションが設定されていない場合にのみスキャンします。 core_ffmpeg_not_found = FFmpegまたはFFprobeの適切なインストールを見つけられません。これらは外部プログラムであり、手動でインストールする必要があります。. core_ffmpeg_not_found_windows = ffmpeg.exeとffprobe.exeがPATHで使用できるか、アプリ実行ファイルと同じフォルダに直接配置されていることを確認してください core_invalid_symlink_infinite_recursion = 無限再帰性 core_invalid_symlink_non_existent_destination = 保存先ファイルが存在しません core_messages_limit_reached_characters = メッセージ数が設定された制限({ $current }/{ $limit } 文字)を超えたため、出力は切り捨てられました。 フル出力を読み込むには、設定で制限オプションを無効にします。. core_messages_limit_reached_lines = メッセージ数が設定された制限({ $current }/{ $limit } 行)を超えたため、出力は切り捨てられました。 フル出力を読み込むには、設定で制限オプションを無効にします。. core_error_moving_to_trash = "{ $file }" をゴミ箱に移動中にエラーが発生しました: { $error } core_error_removing = エラーを削除中に "{ $file }" で発生しました: { $error } core_no_similarity_method_selected = 類似の音楽ファイルを見つけることができません。選択された類似性方法がない場合 core_failed_to_spawn_command = コマンドの生成に失敗しました:{ $reason } core_failed_to_check_process_status = プロセス状態の確認に失敗しました:{ $reason } core_failed_to_wait_for_process = プロセスを待機できませんでした:{ $reason } core_failed_to_read_video_properties = ビデオプロパティの読み込みに失敗しました: { $reason } core_failed_to_execute_ffmpeg = ffmpegの実行に失敗しました:{ $reason } core_ffmpeg_failed_with_status = ffmpeg はステータス { $status } で失敗しました: { $stderr } (コマンド: { $command }) core_failed_to_load_image_frame = 画像フレームの読み込みに失敗しました:{ $reason } core_failed_to_extract_frame = { $time }秒でフレームを抽出できませんでした。「{ $file }」から:{ $reason } core_failed_to_save_thumbnail = サムネイルを "{ $file }" のために保存できませんでした:{ $reason } core_failed_get_frame_at_timestamp = タイムスタンプ { $timestamp } から "{ $file }" のフレームを取得できませんでした:{ $reason } core_failed_get_frame_from_file = "{ $file }" からフレームを取得できませんでした。タイムスタンプ { $timestamp }、理由 { $reason }。 core_invalid_crop_rectangle = 無効な作物矩形:左={ $left }、上={ $top }、右={ $right }、下={ $bottom } core_failed_to_crop_video_file = ビデオファイル "{ $file }" のトリミングに失敗しました:{ $reason } core_cropped_video_not_created = 切り抜かれた動画ファイルが作成されませんでした:{ $temp } core_unable_check_hash_of_file = ファイル "{ $file }" のハッシュを確認できません。理由 { $reason } core_error_checking_hash_of_file = ファイル "{ $file }" のハッシュチェック時にエラーが発生しました、理由 { $reason } core_image_zero_dimensions = 画像はゼロの幅または高さ "{ $path }" core_image_open_failed = 画像ファイル "{ $path }" を開けません:{ $reason } core_not_directory_remove = フォルダ "{ $path }" を削除しようとしています。これはディレクトリではありません。 core_cannot_read_directory = "{ $path }" を読み取れません core_cannot_read_entry_from_directory = ディレクトリ "{ $path }" からエントリを読み取ることができません。 core_folder_contains_file_inside = フォルダ内にファイル "{ $entry }" が "{ $folder }" 内に存在します。 core_unknown_directory_entry = ディレクトリエントリ "{ $entry }" のファイルタイプを "{ $path }" 内で判別できません。 core_video_width_exceeds_limit = 動画の幅 { $width } は { $limit } の制限を超えています core_video_height_exceeds_limit = 動画の高さ { $height } は { $limit } の制限を超えています core_failed_to_process_video = ビデオファイル { $file } の処理に失敗しました: { $reason } core_optimized_file_larger = 最適化ファイル { $optimized } (サイズ: { $new_size }) は、元の { $original } (サイズ: { $original_size }) よりも小さくありません。 core_unknown_codec = 不明コーデック:{ $codec } core_invalid_video_optimizer_mode = 無効なビデオ最適化モード:'{ $mode }'。許可される値:transcode, crop core_folder_does_not_exist = フォルダが存在しません: { $folder } core_path_not_directory = パスはディレクトリではありません:{ $folder } core_test_error_for_folder = フォルダのテストエラー:{ $folder } core_unknown_exif_tag_group = 不明EXIFタググループ:{ $tag } core_error_comparing_fingerprints = 指紋の比較中にエラー:{ $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = "{ $file }" のサムネイルを生成できませんでした:抽出されたフレームの寸法が異なります core_failed_to_generate_thumbnail = "{ $file }" のサムネイルの生成に失敗しました:{ $reason } core_failed_to_extract_frame_at_seek_time = { $time }秒でフレームを抽出できませんでした。「{ $file }」から:{ $reason } core_video_file_does_not_exist = ビデオファイルが存在しません(スキャン/後続ステップ間で削除しても構いません):"{ $path }" core_image_too_large = 画像が大きすぎです ({ $width }x{ $height }) - { $max }ピクセルを超えています core_failed_to_get_video_metadata = ファイル "{ $file }" のビデオメタデータを取得できませんでした:{ $reason } core_failed_to_get_video_codec = ファイル "{ $file }" のビデオコーデックを取得できませんでした。 core_failed_to_get_video_duration = ファイル "{ $file }" の動画の期間を取得できませんでした。 core_failed_to_get_video_dimensions = ファイル "{ $file }" のビデオ寸法を取得できませんでした。 core_frame_dimensions_mismatch = タイムスタンプ { $timestamp } のフレーム寸法と、最初のフレーム寸法 ({ $first_w }x{ $first_h }) が一致しません。 core_failed_to_load_data_from_cache = キャッシュファイル { $file } からデータ読み込みに失敗しました、理由 { $reason } core_failed_to_load_data_from_json_cache = JSONキャッシュファイル { $file } からデータ読み込みに失敗しました。理由 { $reason } core_failed_to_replace_with_optimized = ファイル "{ $file }" を最適化バージョンで置き換えに失敗しました: { $reason } core_failed_to_write_data_to_cache = キャッシュファイル "{ $file }" へのデータ書き込みに失敗しました、理由 { $reason } core_properly_saved_cache_entries = ファイルに正しく保存されました { $count } 件のキャッシュエントリ。. core_video_processing_stopped_by_user = ビデオ処理はユーザーによって停止されました core_thumbnail_generation_stopped_by_user = サムネイル生成はユーザーによって停止されました core_failed_to_optimize_video = ビデオの最適化に失敗しました "{ $file }": { $reason } core_failed_to_crop_video = ビデオのトリミングに失敗しました "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = 最適化されたファイル "{ $file }" のメタデータ取得に失敗しました:{ $reason } core_cannot_create_config_folder = 設定ファイル "{ $folder }" を作成できません。理由 { $reason } です。 core_cannot_create_cache_folder = キャッシュフォルダ "{ $folder }" を作成できません。理由 { $reason } core_cannot_create_or_open_cache_file = キャッシュファイル "{ $file }" を作成または開けません。理由 { $reason } core_cannot_set_config_cache_path = 設定/キャッシュのパスを設定できません - 設定とキャッシュは使用されません。. core_invalid_extension_contains_space = { $extension } は有効な拡張子ではありません。なぜなら、中に空白が含まれているからです。 core_invalid_extension_contains_dot = { $extension } は有効な拡張子ではありません。なぜなら、中にドットが含まれているからです。 ================================================ FILE: czkawka_core/i18n/ko/czkawka_core.ftl ================================================ # Core core_similarity_original = 원본 core_similarity_very_high = 매우 높음 core_similarity_high = 높음 core_similarity_medium = 보통 core_similarity_small = 낮음 core_similarity_very_small = 매우 낮음 core_similarity_minimal = 최소 core_cannot_open_dir = { $dir } 디렉터리를 열 수 없습니다. 이유: { $reason } core_cannot_read_entry_dir = { $dir } 디렉터리를 열 수 없습니다. 이유: { $reason } core_cannot_read_metadata_dir = { $dir } 디렉터리의 메타데이터를 열 수 없습니다. 이유: { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch core_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch core_file_no_modification_date = { $name } 파일의 수정된 시각을 읽을 수 없습니다. 이유: { $reason } core_folder_no_modification_date = { $name } 폴더의 수정된 시각을 읽을 수 없습니다. 이유: { $reason } core_cannot_start_scan_no_included_paths = 스캔을 시작할 수 없습니다, 포함된 경로가 없습니다 core_skip_exist_check_all_included_paths_nonexistent = 스캔을 시작할 수 없습니다, 모든 포함된 경로가 존재하지 않기 때문입니다 core_missing_no_chosen_included_path = 유효한 포함된 경로가 선택되지 않았습니다(제외된 경로가 모든 포함된 경로를 배제했을 수 있습니다) core_reference_included_paths_same = 모든 유효한 포함된 경로가 참조 경로로도 참조되는 경우 스캔을 시작할 수 없습니다. 유효성을 검사하거나 참조 경로를 비활성화하십시오 core_path_must_exists = 제공된 경로가 존재해야 하며, { $path } 무시합니다 core_must_be_directory_or_file = 제공된 경로가 유효한 디렉터리 또는 파일에 가리키도록 해야 하며, { $path } 무시합니다 core_excluded_paths_pointless_slash = 제외 / 는 무의미하며, 이는 파일이 스캔되지 않음을 의미하기 때문입니다 core_paths_unable_to_get_device_id = 폴더 { $path } 에서 장치 ID를 가져올 수 없음 core_needs_allowed_extensions_limited_by_tool = 스캔을 시작할 수 없습니다, 이 도구({ $extensions })에 있는 모든 확장 기능이 스캔에서 제외되었기 때문입니다 core_needs_allowed_extensions = 스캔 시작할 수 없습니다, 모든 확장 프로그램이 스캔에서 제외되었을 때 core_needs_to_set_at_least_one_broken_option = 스캔을 시작할 수 없습니다, 손상 옵션이 스캔하도록 설정되지 않았을 때 core_needs_to_set_at_least_one_bad_name_option = 스캔을 시작할 수 없습니다, 잘못된 이름 옵션이 스캔하도록 설정되지 않았을 때 core_ffmpeg_not_found = FFmpeg 또는 FFprobe의 적절한 설치 파일을 찾을 수 없습니다. 이러한 프로그램들은 수동으로 설치해야 합니다. core_ffmpeg_not_found_windows = ffmpeg.exe와 ffprobe.exe가 PATH에 있거나 앱 실행 파일과 같은 폴더에 직접 배치되어 있는지 확인하세요 core_invalid_symlink_infinite_recursion = 무한 재귀 core_invalid_symlink_non_existent_destination = 목표 파일이 없음 core_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. core_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. core_error_moving_to_trash = "{ $file }"를 쓰레기통으로 옮길 때 오류가 발생했습니다: { $error } core_error_removing = "{ $file }" 삭제 중 오류: { $error } core_no_similarity_method_selected = 유형을 선택하지 않았기 때문에 유사한 음악 파일을 찾을 수 없습니다 core_failed_to_spawn_command = 명령어 생성 실패: { $reason } core_failed_to_check_process_status = 프로세스 상태 확인 실패: { $reason } core_failed_to_wait_for_process = 프로세스 대기 실패: { $reason } core_failed_to_read_video_properties = 비디오 속성 읽기 실패: { $reason } core_failed_to_execute_ffmpeg = ffmpeg 실행 실패: { $reason } core_ffmpeg_failed_with_status = ffmpeg 실패했습니다 상태 { $status }: { $stderr } (명령: { $command }) core_failed_to_load_image_frame = 이미지 프레임을 로드하지 못했습니다: { $reason } core_failed_to_extract_frame = 실패했습니다: { $time } 초에서 "{ $file }"에서 프레임을 추출하지 못했습니다: { $reason } core_failed_to_save_thumbnail = 썸네일 { $file } 저장 실패: { $reason } core_failed_get_frame_at_timestamp = 실패했습니다. 타임스탬프 { $timestamp }에서 "{ $file }"에서 프레임을 가져오지 못했습니다: { $reason } core_failed_get_frame_from_file = "{ $file }"에서 프레임을 가져오지 못했습니다. 타임스탬프 { $timestamp }: { $reason } core_invalid_crop_rectangle = 유효하지 않은 래스터 영역: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom } core_failed_to_crop_video_file = 비디오 파일 "{ $file }" 자르기 실패: { $reason } core_cropped_video_not_created = 잘린 비디오 파일이 생성되지 않았습니다: { $temp } core_unable_check_hash_of_file = 파일 해시 확인 불가 { $file }, 이유 { $reason } core_image_zero_dimensions = 이미지의 너비 또는 높이는 0입니다 "{ $path }" core_image_open_failed = 불가능: "{ $path }" 이미지 파일을 열 수 없습니다. { $reason } core_not_directory_remove = 폴더 "{ $path }"을 제거하려고 하는데, 디렉터리가 아닙니다 core_cannot_read_directory = "{ $path }"를 읽을 수 없습니다 core_cannot_read_entry_from_directory = 디렉토리 "{ $path }"에서 항목을 읽을 수 없습니다 core_folder_contains_file_inside = 폴더 안에 파일 "{ $entry }" 가 "{ $folder }" 안에 있습니다 core_unknown_directory_entry = 디렉토리 항목 "{ $entry }"의 파일 유형을 "{ $path }" 내에서 확인할 수 없음 core_video_width_exceeds_limit = 비디오 너비 { $width } 가 { $limit } 의 제한을 초과합니다 core_video_height_exceeds_limit = 비디오 높이 { $height }는 { $limit }의 제한을 초과합니다 core_failed_to_process_video = 비디오 파일 { $file } 처리 실패: { $reason } core_optimized_file_larger = 최적화된 파일 { $optimized } (크기: { $new_size }) 는 원본 { $original } (크기: { $original_size }) 보다 작지 않습니다 core_unknown_codec = 알 수 없는 코덱: { $codec } core_invalid_video_optimizer_mode = 유효하지 않은 비디오 최적화 모드: '{ $mode }'. 허용 값: transcode, crop core_folder_does_not_exist = 폴더가 존재하지 않습니다: { $folder } core_path_not_directory = 경로는 디렉터리가 아닙니다: { $folder } core_test_error_for_folder = 폴더 오류 테스트: { $folder } core_unknown_exif_tag_group = 알 수 없는 EXIF 태그 그룹: { $tag } core_error_comparing_fingerprints = 지문 비교 중 오류: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = "{ $file }"에 대한 썸네일 생성 실패: 추출된 프레임의 차원이 다릅니다 core_failed_to_generate_thumbnail = "{ $file }": { $reason } 생성을 실패했습니다 core_failed_to_extract_frame_at_seek_time = 실패했습니다: { $time } 초에서 "{ $file }"에서 프레임을 추출하지 못했습니다: { $reason } core_video_file_does_not_exist = 비디오 파일이 존재하지 않습니다 (스캔/후속 단계 사이에 제거할 수 있음): "{ $path }" core_image_too_large = 이미지가 너무 큽니다 ({ $width }x{ $height }) - { $max } 픽셀 이상을 지원하지 않습니다 core_failed_to_get_video_metadata = 파일 "{ $file }"의 비디오 메타데이터를 가져오지 못했습니다: { $reason } core_failed_to_get_video_codec = 파일 "{ $file }"의 비디오 코덱을 가져오지 못했습니다 core_failed_to_get_video_duration = 파일 "{ $file }"의 비디오 길이 가져오기 실패 core_failed_to_get_video_dimensions = 파일 "{ $file }"의 비디오 치수를 가져오지 못했습니다 core_frame_dimensions_mismatch = 프레임 치수 타임스탬프 { $timestamp }와 첫 번째 프레임 치수 ({ $first_w }x{ $first_h })가 일치하지 않습니다 core_failed_to_load_data_from_cache = 캐시 파일 { $file } 로부터 데이터를 로드하지 못했습니다. 이유 { $reason } core_failed_to_load_data_from_json_cache = JSON 캐시 파일 { $file } 로부터 데이터 로드 실패, 이유 { $reason } core_failed_to_replace_with_optimized = 파일 "{ $file }"을 최적화된 버전으로 대체하지 못했습니다: { $reason } core_failed_to_write_data_to_cache = 캐시 파일 "{ $file }"에 데이터를 쓸 수 없습니다, 이유 { $reason } core_properly_saved_cache_entries = 파일에 제대로 저장됨 { $count } 캐시 항목. core_video_processing_stopped_by_user = 사용자가 비디오 처리 중단을 중단했습니다 core_thumbnail_generation_stopped_by_user = 썸네일 생성 중단됨 core_failed_to_optimize_video = 비디오 최적화 실패 "{ $file }": { $reason } core_failed_to_crop_video = 비디오 자르기 실패 "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = 최적화된 파일 "{ $file }"의 메타데이터를 가져오지 못했습니다: { $reason } core_cannot_create_config_folder = 설정 폴더 "{ $folder }"를 생성할 수 없습니다, 이유 { $reason } core_cannot_create_cache_folder = 캐시 폴더 "{ $folder }"를 생성할 수 없습니다, 이유 { $reason } core_cannot_create_or_open_cache_file = 캐시 파일 "{ $file }"를 생성하거나 열 수 없습니다, 이유 { $reason } core_cannot_set_config_cache_path = 설정/캐시 경로 설정 불가 - 설정 및 캐시는 사용되지 않습니다. core_invalid_extension_contains_space = { $extension }는 유효하지 않은 확장자입니다. 확장자 안에 빈 공간이 있기 때문입니다 core_invalid_extension_contains_dot = { $extension }는 유효하지 않은 확장자입니다. 확장자 안에 점이 포함되어 있기 때문입니다 core_error_checking_hash_of_file = 파일 "{ $file }"의 해시 값을 확인하는 과정에서 오류가 발생했습니다. 원인: { $reason } ================================================ FILE: czkawka_core/i18n/nl/czkawka_core.ftl ================================================ # Core core_similarity_original = Origineel core_similarity_very_high = Zeer hoog core_similarity_high = hoog core_similarity_medium = Middelgroot core_similarity_small = Klein core_similarity_very_small = Zeer Klein core_similarity_minimal = Minimaal core_cannot_open_dir = Kan dir { $dir }niet openen, reden { $reason } core_cannot_read_entry_dir = Kan invoer niet lezen in map { $dir }, reden { $reason } core_cannot_read_metadata_dir = Kan metadata niet lezen in map { $dir }, reden { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Het bestand { $name } lijkt aangepast te zijn voor Unix Epoch core_folder_modified_before_epoch = Map { $name } lijkt gewijzigd te zijn voor Unix Epoch core_file_no_modification_date = Niet in staat om de datum van bestand { $name }te krijgen, reden { $reason } core_folder_no_modification_date = Niet in staat om wijzigingsdatum van map { $name }te krijgen, reden { $reason } core_cannot_start_scan_no_included_paths = Kan de scan niet starten, omdat er geen opgenomen paden zijn core_skip_exist_check_all_included_paths_nonexistent = Kan de scan niet starten, omdat alle opgenomen paden niet bestaan core_missing_no_chosen_included_path = Geen geldige opgenomen pad werd gekozen (uitgesloten paden konden alle opgenomen paden uitsluiten) core_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 core_excluded_paths_pointless_slash = Uitsluiten / is zinloos, omdat het betekent dat er geen bestanden zullen worden gescand core_needs_allowed_extensions_limited_by_tool = Kan de scan niet starten, wanneer alle extensies beschikbaar in dit hulpmiddel ({ $extensions }) zijn uitgesloten van de scan core_needs_allowed_extensions = Kan de scan niet starten, wanneer alle extensies zijn uitgesloten van de scan core_needs_to_set_at_least_one_broken_option = Kan geen scan starten, wanneer er geen gebroken optie is ingesteld om te scannen core_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 core_ffmpeg_not_found = Kan geen juiste installatie van FFmpeg of FFprobe vinden. Dit zijn externe programma's die handmatig geïnstalleerd moeten worden. core_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 core_invalid_symlink_infinite_recursion = Oneindige recursie core_invalid_symlink_non_existent_destination = Niet-bestaand doelbestand core_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. core_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. core_error_moving_to_trash = Fout bij het verplaatsen van "{ $file }" naar de prullenbak: { $error } core_error_removing = Fout bij het verwijderen van "{ $file }": { $error } core_no_similarity_method_selected = Kan geen soortgelijke muziekbestanden vinden zonder een geselecteerde similariteitsmethode core_failed_to_spawn_command = Faillissement van het opstarten van het commando: { $reason } core_ffmpeg_failed_with_status = ffmpeg faalde met status { $status }: { $stderr } (command: { $command }) core_failed_to_extract_frame = Faalde om frame te extraheren op { $time } seconden van "{ $file }": { $reason } core_failed_to_save_thumbnail = Faillissement van miniature opslaan voor "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Faalde bij het ophalen van frame op timestamp { $timestamp } van "{ $file }": { $reason } core_failed_get_frame_from_file = Faalde bij het ophalen van frame van "{ $file }" op timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Ongeldige oogstrechthoek: links={ $left }, boven={ $top }, rechts={ $right }, onder={ $bottom } core_failed_to_crop_video_file = Faillissement van video bestand "{ $file }": { $reason } core_cropped_video_not_created = Verwijderde videobestand is niet aangemaakt: { $temp } core_unable_check_hash_of_file = Kan hash van bestand "{ $file }" niet controleren, reden { $reason } core_error_checking_hash_of_file = Fout opgetreden bij het controleren van de hash van bestand "{ $file }", reden { $reason } core_image_open_failed = Kan bestand niet openen "{ $path }": { $reason } core_not_directory_remove = Proberen om map "{ $path }" te verwijderen, wat geen directory is core_cannot_read_directory = Kan de directory "{ $path }" niet lezen core_cannot_read_entry_from_directory = Kan geen vermelding lezen uit de map "{ $path }" core_folder_contains_file_inside = Map bevat bestand "{ $entry }" in "{ $folder }" core_unknown_directory_entry = Kan het type bestand van de directory-entry "{ $entry }" niet bepalen binnen "{ $path }" core_video_width_exceeds_limit = Video breedte { $width } overschrijdt de limiet van { $limit } core_video_height_exceeds_limit = Video hoogte { $height } overschrijdt de limiet van { $limit } core_failed_to_process_video = Faalde bij het verwerken van videobestand { $file }: { $reason } core_optimized_file_larger = Geoptimaliseerd bestand { $optimized } (grootte: { $new_size }) is niet kleiner dan origineel { $original } (grootte: { $original_size }) core_unknown_codec = Onbekend codec: { $codec } core_invalid_video_optimizer_mode = Ongeldige videobesturing modus: '{ $mode }'. Toegestane waarden: transcode, crop core_folder_does_not_exist = Map niet bestaan: { $folder } core_path_not_directory = Pad is geen directory: { $folder } core_test_error_for_folder = Test fout voor map: { $folder } core_unknown_exif_tag_group = Onbekende EXIF tag groep: { $tag } core_failed_to_generate_thumbnail_frames_different_dimensions = Faalde bij het genereren van de miniaturen voor "{ $file }": de geëxtraheerde frames hebben verschillende afmetingen core_failed_to_generate_thumbnail = Faalde bij het genereren van miniaturen voor "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Faalde om frame te extraheren op { $time } seconden van "{ $file }": { $reason } core_video_file_does_not_exist = Bestand bestaat niet (kan worden verwijderd tussen scan/latere stappen): "{ $path }" core_image_too_large = Afbeelding is te groot ({ $width }x{ $height }) - meer dan ondersteund { $max } pixels core_failed_to_get_video_metadata = Faalde bij het ophalen van videometadeta voor bestand "{ $file }": { $reason } core_failed_to_get_video_codec = Faalde bij het ophalen van de videocodec voor bestand "{ $file }" core_failed_to_get_video_duration = Kon geen duur van het bestand "{ $file }" ophalen core_failed_to_get_video_dimensions = Faalde bij het ophalen van de videogrootte voor bestand "{ $file }" core_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 }) core_failed_to_load_data_from_cache = Faalde bij het laden van gegevens uit cachebestand { $file }, reden { $reason } core_failed_to_load_data_from_json_cache = Faalde bij het laden van gegevens uit JSON cachebestand { $file }, reden { $reason } core_failed_to_replace_with_optimized = Faalde bij het vervangen van bestand "{ $file }" met de geoptimaliseerde versie: { $reason } core_failed_to_write_data_to_cache = Kan geen gegevens schrijven naar cachebestand "{ $file }", reden { $reason } core_properly_saved_cache_entries = Correct opgeslagen naar bestand { $count } cache-items. core_video_processing_stopped_by_user = Video verwerking is gestopt door gebruiker core_thumbnail_generation_stopped_by_user = Thumbnail generatie is gestopt door gebruiker core_failed_to_optimize_video = Faalde het bij het optimaliseren van video "{ $file }": { $reason } core_failed_to_crop_video = Failliet video snijden "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Faalde bij het ophalen van metadata van de geoptimaliseerde bestand "{ $file }": { $reason } core_cannot_create_config_folder = Kan configuratiefolder "{ $folder }" niet aanmaken, reden { $reason } core_cannot_create_cache_folder = Kan cache map "{ $folder }" niet aanmaken, reden { $reason } core_cannot_create_or_open_cache_file = Kan bestand { $file } niet aanmaken of openen, reden { $reason } core_cannot_set_config_cache_path = Kan de config/cache pad niet instellen - config en cache zullen niet worden gebruikt. core_invalid_extension_contains_space = { $extension } is geen geldige extensie omdat het lege ruimte bevat binnenin core_invalid_extension_contains_dot = { $extension } is geen geldige extensie omdat het punt erin zit core_path_must_exists = Het opgegeven pad moet bestaan, waarbij { $path } genegeerd wordt core_must_be_directory_or_file = Het opgegeven pad moet verwijzen naar een geldige map of bestand, waarbij { $path } genegeerd wordt core_paths_unable_to_get_device_id = Kan het apparaat-ID niet ophalen vanuit de map { $path } core_failed_to_check_process_status = Het controleren van de processtatus is mislukt: { $reason } core_failed_to_wait_for_process = Het wachten op het proces is mislukt: { $reason } core_failed_to_read_video_properties = Het lukte niet om de videoproperties te lezen: { $reason } core_failed_to_execute_ffmpeg = Het uitvoeren van ffmpeg is mislukt: { $reason } core_failed_to_load_image_frame = Het laden van het afbeeldingsframe is mislukt: { $reason } core_image_zero_dimensions = De afbeelding heeft een breedte of hoogte van nul: "{ $path }" core_error_comparing_fingerprints = Fout tijdens het vergelijken van vingerafdrukken: { $reason } ================================================ FILE: czkawka_core/i18n/no/czkawka_core.ftl ================================================ # Core core_similarity_original = Opprinnelig core_similarity_very_high = Veldig høy core_similarity_high = Høy core_similarity_medium = Middels core_similarity_small = Liten core_similarity_very_small = Veldig liten core_similarity_minimal = Minimalt core_cannot_open_dir = Kan ikke åpne dir { $dir }, årsak { $reason } core_cannot_read_entry_dir = Kan ikke lese oppføringen i dir { $dir }, årsak { $reason } core_cannot_read_metadata_dir = Kan ikke lese metadata i dir { $dir }, årsak { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Filen { $name } ser ut til å ha blitt endret før Unix Epoch core_folder_modified_before_epoch = Mappen { $name } ser ut til å ha blitt endret før Unix Epoch core_file_no_modification_date = Klarte ikke å hente endringsdato fra filen { $name }. Årsak { $reason } core_folder_no_modification_date = Klarte ikke å hente endringsdato fra mappen { $name }. Årsak { $reason } core_cannot_start_scan_no_included_paths = Kan ikke starte skanning, fordi det ikke er inkludert stier core_skip_exist_check_all_included_paths_nonexistent = Kan ikke starte skanning, fordi alle inkluderte stiene ikke eksisterer core_missing_no_chosen_included_path = Ingen gyldig inkludert sti ble valgt (utelatelser kunne ha utelukket alle inkluderte stier) core_reference_included_paths_same = Kan ikke starte skanning der alle gyldige inkluderte stier også er refererte stier, prøv å validere eller deaktivere refererte stier core_must_be_directory_or_file = Angitt sti må peke til en gyldig mappe eller fil, og ignorere { $path } core_excluded_paths_pointless_slash = Unntatt / er meningsløst, fordi det betyr at ingen filer vil bli scannet core_paths_unable_to_get_device_id = Kan ikke hente enhets-ID fra mappen { $path } core_needs_allowed_extensions_limited_by_tool = Kan ikke starte skanning, når alle utvidelser tilgjengelige i dette verktøyet ({ $extensions }) ble ekskludert fra skanning core_needs_allowed_extensions = Kan ikke starte skanning, når alle utvidelser ble ekskludert fra skanning core_needs_to_set_at_least_one_broken_option = Kan ikke starte skanning, når det ikke er satt en ødelagt alternativ for å skanne etter core_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 core_ffmpeg_not_found = Kan ikke finne en passende installasjon av FFmpeg eller FFprobe. Disse er eksterne programmer som må installeres manuelt. core_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 core_invalid_symlink_infinite_recursion = Uendelig rekursjon core_invalid_symlink_non_existent_destination = Ikke-eksisterende målfil core_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. core_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. core_error_moving_to_trash = Feil ved flytting av "{ $file }" til papirkurven: { $error } core_error_removing = Feil ved sletting av "{ $file }": { $error } core_no_similarity_method_selected = Kan ikke finne lignende musikkfiler uten en valgt likhet metode core_failed_to_spawn_command = Krevde ikke å starte kommando: { $reason } core_failed_to_check_process_status = Feilet med å sjekke prosessstatus: { $reason } core_failed_to_wait_for_process = Krevde ikke å vente på prosessen: { $reason } core_failed_to_read_video_properties = Kunne ikke lese videoegenskaper: { $reason } core_failed_to_execute_ffmpeg = Krevde ikke utførelse av ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg feilet med status { $status }: { $stderr } (kommando: { $command }) core_failed_to_load_image_frame = Klarte ikke å laste inn bildefragment: { $reason } core_failed_to_extract_frame = Klarte ikke å hente ut ramme ved { $time } sekunder fra "{ $file }": { $reason } core_failed_to_save_thumbnail = Feilet ved å lagre miniatyrbilde for "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Feilet å hente ramme ved timestamp { $timestamp } fra "{ $file }": { $reason } core_failed_get_frame_from_file = Kunne ikke hente ramme fra "{ $file }" ved timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Ugyldig avlingsrektangel: venstre={ $left }, øvre={ $top }, høyre={ $right }, nedre={ $bottom } core_failed_to_crop_video_file = Feilet med å beskjære videofilen "{ $file }": { $reason } core_cropped_video_not_created = Klippet videofil ble ikke opprettet: { $temp } core_unable_check_hash_of_file = Uklart å sjekke hasj for fil "{ $file }", årsak { $reason } core_error_checking_hash_of_file = Feil oppstod ved sjekk av hasj for fil "{ $file }", årsak { $reason } core_image_zero_dimensions = Bildet har null bredde eller høyde "{ $path }" core_image_open_failed = Kan ikke åpne bildefil "{ $path }": { $reason } core_not_directory_remove = Prøver å fjerne mappen "{ $path }" som ikke er en mappe core_cannot_read_directory = Kan ikke lese katalog "{ $path }" core_cannot_read_entry_from_directory = Kan ikke lese oppføring fra katalog "{ $path }" core_folder_contains_file_inside = Mappen inneholder filen "{ $entry }" inne i "{ $folder }" core_unknown_directory_entry = Kan ikke fastslå filtype for directory entry "{ $entry }" inne i "{ $path }" core_video_width_exceeds_limit = Video bredde { $width } overstiger grensen for { $limit } core_video_height_exceeds_limit = Video høyde { $height } overstiger grensen på { $limit } core_failed_to_process_video = Klarte ikke å behandle videofilen { $file }: { $reason } core_unknown_codec = Ukjent kodek: { $codec } core_invalid_video_optimizer_mode = Ugyldig videobestøkningsmodus: '{ $mode }'. Tillatte verdier: transkode, kutt core_path_not_directory = Stien er ikke en mappe: { $folder } core_test_error_for_folder = Test feil for mappe: { $folder } core_unknown_exif_tag_group = Ukjent EXIF-tag gruppe: { $tag } core_error_comparing_fingerprints = Feil ved sammenligning av fingeravtrykk: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Feilet med å generere miniatyrbilde for "{ $file }": uthentede rammer har forskjellige dimensjoner core_failed_to_generate_thumbnail = Feilet med å generere miniatyrbilde for "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Klarte ikke å hente ut ramme ved { $time } sekunder fra "{ $file }": { $reason } core_video_file_does_not_exist = Video fil finnes ikke (kan fjernes mellom skanning/senere steg): "{ $path }" core_image_too_large = Bildet er for stort ({ $width }x{ $height }) - mer enn støttet { $max } piksler core_failed_to_get_video_metadata = Klarte ikke å hente videometadata for fil "{ $file }": { $reason } core_failed_to_get_video_codec = Kunne ikke hente videocodec for fil "{ $file }" core_failed_to_get_video_duration = Kunne ikke hente videolengde for fil "{ $file }" core_failed_to_get_video_dimensions = Kunne ikke hente vide dimensjoner for fil "{ $file }" core_frame_dimensions_mismatch = Ramme dimensjoner for tidsstempel { $timestamp } stemmer ikke overens med de første ramme dimensjonene ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Kunne ikke laste data fra cache-fil { $file }, årsak { $reason } core_failed_to_load_data_from_json_cache = Kunne ikke laste data fra json-cachen { $file}, årsak { $reason } core_failed_to_replace_with_optimized = Klarte ikke å erstatte filen "{ $file }" med den optimaliserte versjonen: { $reason } core_failed_to_write_data_to_cache = Kan ikke skrive data til cache-fil "{ $file }", årsak { $reason } core_properly_saved_cache_entries = Riktig lagret til fil { $count } cache-poster. core_video_processing_stopped_by_user = Videobehandling ble stoppet av bruker core_thumbnail_generation_stopped_by_user = Miniatyrgenerering ble stoppet av bruker core_failed_to_optimize_video = Kjempetilfelte video "{ $file }": { $reason } core_failed_to_crop_video = Feilet å beskjære video "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Klarte ikke å hente metadata for den optimaliserte filen "{ $file }": { $reason } core_cannot_create_config_folder = Kan ikke opprette konfigurasjonsmappe "{ $folder }", årsak { $reason } core_cannot_create_cache_folder = Kan ikke opprette cache-mappe "{ $folder }", årsak { $reason } core_cannot_create_or_open_cache_file = Kan ikke opprette eller åpne cachefil "{ $file }", årsak { $reason } core_cannot_set_config_cache_path = Kan ikke sette config/cache-sti - config og cache vil ikke bli brukt. core_invalid_extension_contains_space = { $extension } er ikke en gyldig filtype fordi den inneholder mellomrom core_path_must_exists = Den angitte stien må eksistere, ignorerer { $path } core_optimized_file_larger = Den optimaliserte filen { $optimized } (størrelse: { $new_size }) er ikke mindre enn den originale filen { $original } (størrelse: { $original_size }) core_folder_does_not_exist = Mappen finnes ikke: { $folder } core_invalid_extension_contains_dot = { $extension } er ikke en gyldig filtype fordi den inneholder et punkt (.) inni seg ================================================ FILE: czkawka_core/i18n/pl/czkawka_core.ftl ================================================ # Core core_similarity_original = Oryginalny core_similarity_very_high = Bardzo Duże core_similarity_high = Duże core_similarity_medium = Średnie core_similarity_small = Małe core_similarity_very_small = Bardzo Małe core_similarity_minimal = Minimalne core_cannot_open_dir = Nie można otworzyć folderu { $dir }, powód { $reason } core_cannot_read_entry_dir = Nie można odczytać danych z folderu { $dir }, powód { $reason } core_cannot_read_metadata_dir = Nie można odczytać metadanych folderu "{ $dir }": { $reason } core_cannot_read_metadata_file = Nie można odczytać metadanych pliku "{ $file }": { $reason } core_file_modified_before_epoch = Plik "{ $name }" wygląda na zmodyfikowany przed epoką Unix core_folder_modified_before_epoch = Folder "{ $name }" wygląda na zmodyfikowany przed epoką Unix core_file_no_modification_date = Nie udało się pobrać daty modyfikacji z pliku { $name }, powód { $reason } core_folder_no_modification_date = Nie udało się pobrać daty modyfikacji z folderu { $name }, powód { $reason } core_cannot_start_scan_no_included_paths = Nie można uruchomić skanowania, ponieważ nie wybrano żadnych folderów wejściowych core_skip_exist_check_all_included_paths_nonexistent = Nie można uruchomić skanowania, ponieważ wszystkie ścieżki do wyszukiwania nie istnieją core_missing_no_chosen_included_path = Nie wybrano prawidłowej ścieżki do wyszukiwania (wykluczone ścieżki mogły wykluczyć wszystkie ścieżki wejściowe) core_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 core_path_must_exists = Podana ścieżka musi istnieć, ignorowanie { $path } core_must_be_directory_or_file = Podany ścieżka musi wskazywać na ważny katalog lub plik, pomijając { $path } core_excluded_paths_pointless_slash = Wykluczenie / jest bezcelowe, ponieważ oznacza to, że żadne pliki nie zostaną przeskanowane core_paths_unable_to_get_device_id = Nie można uzyskać identyfikatora urządzenia z folderu { $path } core_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 core_needs_allowed_extensions = Nie można uruchomić skanu, gdy wszystkie rozszerzenia zostały wykluczone z skanu core_needs_to_set_at_least_one_broken_option = Nie można uruchomić skanu, gdy nie ustawiono opcji "uszkodzony" do skanowania core_needs_to_set_at_least_one_bad_name_option = Nie można uruchomić skanu, jeśli nie ustawiono opcji "złe nazwy" do skanowania core_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. core_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 core_invalid_symlink_infinite_recursion = Nieskończona rekurencja core_invalid_symlink_non_existent_destination = Nieistniejący docelowy plik core_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. core_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. core_error_moving_to_trash = Błąd podczas przenoszenia "{ $file }" do kosza: { $error } core_error_removing = Błąd podczas usuwania "{ $file }": { $error } core_no_similarity_method_selected = Nie można znaleźć podobnych plików muzycznych bez wybranego sposobu podobieństwa core_failed_to_spawn_command = Nie udało się wygenerować polecenia: { $reason } core_failed_to_check_process_status = Błąd podczas sprawdzania statusu procesu: { $reason } core_failed_to_wait_for_process = Nie udało się oczekiwać na proces: { $reason } core_failed_to_read_video_properties = Nie udało się odczytać właściwości wideo: { $reason } core_failed_to_execute_ffmpeg = Nie udało się wykonać ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg nie powiodło się z kodem { $status }: { $stderr } (polecenie: { $command }) core_failed_to_load_image_frame = Błąd ładowania ramy obrazu: { $reason } core_failed_to_extract_frame = Nie udało się wyodrębnić klatki o { $time } sekundach z "{ $file }": { $reason } core_failed_to_save_thumbnail = Nie udało się zapisać miniaturki dla "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Błąd podczas pobierania klatki o znaczniku { $timestamp } z "{ $file }": { $reason } core_failed_get_frame_from_file = Nie udało się uzyskać ramy z "{ $file }" o znaczniku czasu { $timestamp }: { $reason } core_invalid_crop_rectangle = Nieprawidłowy prostokąt uprawnień: lewy={ $left }, górny={ $top }, prawy={ $right }, dolny={ $bottom } core_failed_to_crop_video_file = Nie udało się przyciąć pliku wideo "{ $file }": { $reason } core_cropped_video_not_created = Przekrojony plik wideo nie został utworzony: { $temp } core_unable_check_hash_of_file = Nie można sprawdzić sumy kontrolnej pliku "{ $file }", powód { $reason } core_error_checking_hash_of_file = Błąd wystąpił podczas sprawdzania sumy kontrolnej pliku "{ $file }", powód { $reason } core_image_zero_dimensions = Obraz ma zerową szerokość lub wysokość "{ $path }" core_image_open_failed = Nie można otworzyć pliku obrazu "{ $path }": { $reason } core_not_directory_remove = Próba usunięcia folderu "{ $path }" który nie jest katalogiem core_cannot_read_directory = Nie można odczytać katalogu "{ $path }" core_cannot_read_entry_from_directory = Nie można odczytać wpisu z katalogu "{ $path }" core_folder_contains_file_inside = Folder zawiera plik "{ $entry }" wewnątrz "{ $folder }" core_unknown_directory_entry = Nie można określić typu pliku wpisu katalogowego "{ $entry }" wewnątrz "{ $path }" core_video_width_exceeds_limit = Szerokość wideo { $width } przekracza limit { $limit } core_video_height_exceeds_limit = Wysokość wideo { $height } przekracza limit { $limit } core_failed_to_process_video = Nie udało się przetworzyć pliku wideo { $file }: { $reason } core_optimized_file_larger = Zoptymalizowany plik { $optimized } (rozmiar: { $new_size }) nie jest mniejszy niż oryginalny { $original } (rozmiar: { $original_size }) core_unknown_codec = Nieznany kodek: { $codec } core_invalid_video_optimizer_mode = Nieprawidłowy tryb optymalizacji wideo: '{ $mode }'. Dopuszczalne wartości: transkoduj, przycinaj core_folder_does_not_exist = Folder nie istnieje: { $folder } core_path_not_directory = Ścieżka nie jest katalogiem: { $folder } core_test_error_for_folder = Test błąd dla folderu: { $folder } core_unknown_exif_tag_group = Nieznana grupa tagów EXIF: { $tag } core_error_comparing_fingerprints = Błąd podczas porównywania odcisków palców: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Nie udało się wygenerować miniaturki dla "{ $file }": wyekstrahowane klatki mają różne wymiary core_failed_to_generate_thumbnail = Nie udało się wygenerować miniaturki dla "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Nie udało się wyodrębnić klatki o { $time } sekundach z "{ $file }": { $reason } core_video_file_does_not_exist = Plik wideo nie istnieje (można usunąć między skanowaniem/późniejszymi krokami): "{ $path }" core_image_too_large = Obraz jest zbyt duży ({ $width }x{ $height }) - więcej niż obsługiwane { $max } pikseli core_failed_to_get_video_metadata = Nie udało się uzyskać metadanych wideo dla pliku "{ $file }": { $reason } core_failed_to_get_video_codec = Nie udało się uzyskać kodeka wideo dla pliku "{ $file }" core_failed_to_get_video_duration = Nie udało się uzyskać długości wideo dla pliku "{ $file }" core_failed_to_get_video_dimensions = Nie udało się uzyskać wymiarów wideo dla pliku "{ $file }" core_frame_dimensions_mismatch = Wymiary klatki dla znacznika czasu { $timestamp } nie pasują do wymiarów pierwszej klatki ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Nie udało się załadować danych z pliku pamięci podręcznej "{ $file }": { $reason } core_failed_to_load_data_from_json_cache = Nie udało się załadować danych z pliku pamięci podręcznej JSON "{ $file }": { $reason } core_failed_to_replace_with_optimized = Nie udało się zastąpić pliku "{ $file }" zoptymalizowaną wersją: { $reason } core_failed_to_write_data_to_cache = Nie można zapisać danych do pliku pamięci podręcznej "{ $file }": { $reason } core_properly_saved_cache_entries = Prawidłowo zapisano w pamięci podręcznej { $count } wpisów do pliku. core_video_processing_stopped_by_user = Przetwarzanie wideo zostało przerwane przez użytkownika core_thumbnail_generation_stopped_by_user = Generowanie miniatur zostało przerwane przez użytkownika core_failed_to_optimize_video = Pominięto optymalizację wideo "{ $file }": { $reason } core_failed_to_crop_video = Nie udało się przyciąć wideo "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Nie udało się pobrać metadanych z zoptymalizowanego pliku "{ $file }": { $reason } core_cannot_create_config_folder = Nie można utworzyć folderu konfiguracyjnego "{ $folder }": { $reason } core_cannot_create_cache_folder = Nie można utworzyć folderu pamięci podręcznej "{ $folder }": { $reason } core_cannot_create_or_open_cache_file = Nie można utworzyć ani otworzyć pliku pamięci podręcznej "{ $file }": { $reason } core_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. core_invalid_extension_contains_space = "{ $extension }" nie jest prawidłowym rozszerzeniem, ponieważ zawiera spację core_invalid_extension_contains_dot = "{ $extension }" nie jest prawidłowym rozszerzeniem, ponieważ zawiera kropkę ================================================ FILE: czkawka_core/i18n/pt-BR/czkawka_core.ftl ================================================ # Core core_similarity_original = Please provide the text to translate core_similarity_very_high = Muito grande core_similarity_high = Grande core_similarity_medium = Médio core_similarity_small = Pequeno core_similarity_very_small = Muito pequeno core_similarity_minimal = Mínimo core_cannot_open_dir = Não foi possível abrir o diretório ‘{ $dir }’, por causa de ‘{ $reason }’ core_cannot_read_entry_dir = Não foi possível ler os dados do diretório ‘{ $dir }’, por causa de ‘{ $reason }’ core_cannot_read_metadata_dir = Não foi possível ler os metadados do diretório ‘{ $dir }’, por causa de ‘{ $reason }’ core_cannot_read_metadata_file = Não foi possível ler os metadados no arquivo ‘{ $file }’, por causa de ‘{ $reason }’ core_file_modified_before_epoch = O arquivo { $name } parece ser modificado antes do Epoch Unix core_folder_modified_before_epoch = A pasta ‘{ $name }’ parece ter sido modificada antes do ‘Epoch’ do Unix core_file_no_modification_date = Não foi possível obter a data da modificação do arquivo ‘{ $name }’, por causa de ‘{ $reason }’ core_folder_no_modification_date = Não foi possível obter a data da modificação da pasta ‘{ $name }’, por causa de ‘{ $reason }’ core_cannot_start_scan_no_included_paths = Não é possível iniciar a varredura, porque não há caminhos incluídos core_skip_exist_check_all_included_paths_nonexistent = Não é possível iniciar a varredura, porque todos os caminhos incluídos não existem core_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) core_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 core_path_must_exists = O caminho que foi fornecido tem que apontar para um diretório, por tanto, o caminho ‘{ $path }’ será ignorado core_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 core_excluded_paths_pointless_slash = Se você excluir a barra ‘ / ’, significa que nenhum arquivo será verificado core_paths_unable_to_get_device_id = Não foi possível obter o ID (identificador) do dispositivo da pasta ‘{ $path }’ core_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 core_needs_allowed_extensions = Não foi possível iniciar a verificação porque todas as extensões foram excluídas da verificação core_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 core_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 core_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. core_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 core_invalid_symlink_infinite_recursion = Ocorreu um erro de execução na recursão infinita core_invalid_symlink_non_existent_destination = O arquivo de destino não existe core_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. core_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. core_error_moving_to_trash = Ocorreu o erro ‘{ $error }’ ao tentar mover o arquivo ‘{ $file }’ para a lixeira core_error_removing = Ocorreu o erro ‘{ $error }’ ao tentar remover o arquivo ‘{ $file }’ core_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 core_failed_to_spawn_command = Falha ao gerar comando: { $reason } core_ffmpeg_failed_with_status = ffmpeg falhou com o status { $status }: { $stderr } (comando: { $command }) core_failed_to_extract_frame = Falha ao extrair quadro em { $time } segundos de "{ $file }": { $reason } core_failed_to_save_thumbnail = Falha ao salvar miniatura para "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Falha ao obter frame no timestamp { $timestamp } de "{ $file }": { $reason } core_failed_get_frame_from_file = Falhou ao obter o quadro de "{ $file }" no timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Retângulo de cultivo inválido: esquerda={ $left }, superior={ $top }, direita={ $right }, inferior={ $bottom } core_failed_to_crop_video_file = Falha ao recortar o arquivo de vídeo "{ $file }": { $reason } core_cropped_video_not_created = Arquivo de vídeo cortado não foi criado: { $temp } core_unable_check_hash_of_file = Não foi possível verificar o hash do arquivo "{ $file }", motivo { $reason } core_error_checking_hash_of_file = Erro ocorrido ao verificar o hash do arquivo "{ $file }", motivo { $reason } core_image_zero_dimensions = A imagem tem largura ou altura zero "{ $path }" core_image_open_failed = Não é possível abrir o arquivo de imagem "{ $path }": { $reason } core_not_directory_remove = Tentando remover a pasta "{ $path }" que não é um diretório core_cannot_read_directory = Não é possível ler o diretório "{ $path }" core_cannot_read_entry_from_directory = Não é possível ler entrada do diretório "{ $path }" core_folder_contains_file_inside = A pasta contém o arquivo "{ $entry }" dentro de "{ $folder }" core_unknown_directory_entry = Não foi possível determinar o tipo de arquivo da entrada de diretório "{ $entry }" dentro de "{ $path }" core_video_height_exceeds_limit = Vídeo altura { $height } excede o limite de { $limit } core_failed_to_process_video = Falha ao processar o arquivo de vídeo { $file }: { $reason } core_unknown_codec = Codec desconhecido: { $codec } core_invalid_video_optimizer_mode = Modo otimizador de vídeo inválido: '{ $mode }'. Valores permitidos: transcodar, cortar core_folder_does_not_exist = A pasta não existe: { $folder } core_path_not_directory = O caminho não é um diretório: { $folder } core_test_error_for_folder = Erro de teste para a pasta: { $folder } core_unknown_exif_tag_group = Grupo EXIF desconhecido: { $tag } core_failed_to_generate_thumbnail_frames_different_dimensions = Falha ao gerar miniatura para "{ $file }": os quadros extraídos têm dimensões diferentes core_failed_to_generate_thumbnail = Falha ao gerar miniatura para "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Falha ao extrair quadro em { $time } segundos de "{ $file }": { $reason } core_video_file_does_not_exist = Arquivo de vídeo não existe (pode ser removido entre as etapas de digitalização/mais tarde): "{ $path }" core_failed_to_get_video_metadata = Falha ao obter metadados de vídeo para o arquivo "{ $file }": { $reason } core_failed_to_get_video_codec = Falha ao obter codec de vídeo para o arquivo "{ $file }" core_failed_to_get_video_duration = Falhou ao obter a duração do vídeo para o arquivo "{ $file }" core_failed_to_get_video_dimensions = Falha ao obter as dimensões do vídeo para o arquivo "{ $file }" core_frame_dimensions_mismatch = Dimensões do quadro para timestamp { $timestamp } não correspondem às dimensões do primeiro quadro ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Falha ao carregar dados do arquivo de cache { $file }, motivo { $reason } core_failed_to_load_data_from_json_cache = Falha ao carregar dados do arquivo de cache JSON { $file }, motivo { $reason } core_failed_to_replace_with_optimized = Falha ao substituir o arquivo "{ $file }" pela versão otimizada: { $reason } core_failed_to_write_data_to_cache = Não é possível escrever dados para o arquivo de cache "{ $file }", motivo { $reason } core_properly_saved_cache_entries = Salvado corretamente para o arquivo { $count } entradas de cache. core_video_processing_stopped_by_user = O processamento de vídeo foi interrompido pelo usuário core_thumbnail_generation_stopped_by_user = Geração de miniaturas foi interrompida pelo usuário core_failed_to_optimize_video = Falha ao otimizar o vídeo "{ $file }": { $reason } core_failed_to_crop_video = Falha ao recortar o vídeo "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Falha ao obter metadados do arquivo otimizado "{ $file }": { $reason } core_cannot_create_config_folder = Não é possível criar a pasta de configuração "{ $folder }", a razão é { $reason } core_cannot_create_cache_folder = Não é possível criar a pasta de cache "{ $folder }", motivo { $reason } core_cannot_create_or_open_cache_file = Não é possível criar ou abrir o arquivo de cache "{ $file }", a razão { $reason } core_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. core_invalid_extension_contains_space = { $extension } não é uma extensão válida porque contém espaço em branco dentro core_invalid_extension_contains_dot = { $extension } não é uma extensão válida porque contém ponto dentro core_failed_to_check_process_status = Falha ao verificar o status do processo: { $reason } core_failed_to_wait_for_process = Falha ao aguardar o processo: { $reason } core_failed_to_read_video_properties = Falha ao ler as propriedades do vídeo: { $reason } core_failed_to_execute_ffmpeg = Falha ao executar o ffmpeg: { $reason } core_failed_to_load_image_frame = Falha ao carregar o quadro da imagem: { $reason } core_video_width_exceeds_limit = Largura do vídeo { $width } excede o limite de { $limit } core_optimized_file_larger = O arquivo otimizado { $optimized } (tamanho: { $new_size }) não é menor que o arquivo original { $original } (tamanho: { $original_size }) core_error_comparing_fingerprints = Erro ao comparar impressões digitais: { $reason } core_image_too_large = A imagem é muito grande ({$width}x{$height}) - excede o limite de { $max } pixels suportados ================================================ FILE: czkawka_core/i18n/pt-PT/czkawka_core.ftl ================================================ # Core core_similarity_original = Original core_similarity_very_high = Muito alto core_similarity_high = Alto core_similarity_medium = Média core_similarity_small = Pequeno core_similarity_very_small = Muito Pequeno core_similarity_minimal = Mínimo core_cannot_open_dir = Não é possível abrir o diretório { $dir }, razão { $reason } core_cannot_read_entry_dir = Não é possível ler a entrada no diretório { $dir }, razão { $reason } core_cannot_read_metadata_dir = Não é possível ler os metadados no diretório { $dir }, razão { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = O arquivo { $name } parece ter sido modificado antes do Epoch Unix core_folder_modified_before_epoch = A pasta { $name } parece ter sido modificada antes do Epoch Unix core_file_no_modification_date = Não foi possível obter a data de modificação do arquivo { $name }, motivo { $reason } core_folder_no_modification_date = Não foi possível obter a data de modificação da pasta { $name }, motivo { $reason } core_cannot_start_scan_no_included_paths = Não é possível iniciar a varredura, porque não há caminhos incluídos core_skip_exist_check_all_included_paths_nonexistent = Não é possível iniciar a varredura, porque todos os caminhos incluídos não existem core_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) core_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 core_path_must_exists = O caminho fornecido deve existir, ignorando { $path } core_must_be_directory_or_file = O caminho fornecido deve apontar para um diretório ou arquivo válido, ignorando { $path } core_excluded_paths_pointless_slash = Excluindo / é inútil, porque significa que nenhum arquivo será escaneado core_paths_unable_to_get_device_id = Impossível obter o ID do dispositivo da pasta { $path } core_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 core_needs_allowed_extensions = Não é possível iniciar a varredura, quando todas as extensões foram excluídas da varredura core_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 core_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 core_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. core_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 core_invalid_symlink_infinite_recursion = Recursão infinita core_invalid_symlink_non_existent_destination = Arquivo de destino não existe core_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. core_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. core_error_moving_to_trash = Erro ao mover "{ $file }" para a lixeira: { $error } core_error_removing = Erro ao remover "{ $file }": { $error } core_no_similarity_method_selected = Não é possível encontrar arquivos de música semelhantes sem um método de similaridade selecionado core_failed_to_spawn_command = Falhou em spawn comando: { $reason } core_ffmpeg_failed_with_status = ffmpeg falhou com o status { $status }: { $stderr } (comando: { $command }) core_failed_to_load_image_frame = Falha ao carregar o quadro de imagem: { $reason } core_failed_to_extract_frame = Falhou em extrair o quadro em { $time } segundos de "{ $file }": { $reason } core_failed_to_save_thumbnail = Falhou ao salvar a miniatura para "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Falhou em obter o quadro no timestamp { $timestamp } de "{ $file }": { $reason } core_failed_get_frame_from_file = Falhou ao obter o quadro de "{ $file }" no timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Retângulo de cultivo inválido: esquerda={ $left }, superior={ $top }, direita={ $right }, inferior={ $bottom } core_failed_to_crop_video_file = Falha ao cortar o arquivo de vídeo "{ $file }": { $reason } core_cropped_video_not_created = Arquivo de vídeo cortado não foi criado: { $temp } core_unable_check_hash_of_file = Não foi possível verificar o hash do arquivo "{ $file }", motivo { $reason } core_error_checking_hash_of_file = Erro ocorreu ao verificar o hash do arquivo "{ $file }", motivo { $reason } core_image_zero_dimensions = A imagem tem largura ou altura zero "{ $path }" core_image_open_failed = Não é possível abrir o arquivo de imagem "{ $path }": { $reason } core_not_directory_remove = Tentando remover a pasta "{ $path }" que não é um diretório core_cannot_read_directory = Não é possível ler o diretório "{ $path }" core_cannot_read_entry_from_directory = Não é possível ler entrada do diretório "{ $path }" core_folder_contains_file_inside = A pasta contém o arquivo "{ $entry }" dentro de "{ $folder }" core_unknown_directory_entry = Não foi possível determinar o tipo de arquivo da entrada de diretório "{ $entry }" dentro de "{ $path }" core_video_height_exceeds_limit = Vídeo altura { $height } excede o limite de { $limit } core_failed_to_process_video = Falha ao processar o arquivo de vídeo { $file }: { $reason } core_unknown_codec = Codec desconhecido: { $codec } core_invalid_video_optimizer_mode = Modo otimizador de vídeo inválido: '{ $mode }'. Valores permitidos: transcodar, cortar core_path_not_directory = O caminho não é um diretório: { $folder } core_test_error_for_folder = Erro de teste para a pasta: { $folder } core_unknown_exif_tag_group = Grupo EXIF desconhecido: { $tag } core_failed_to_generate_thumbnail_frames_different_dimensions = Falhou ao gerar miniatura para "{ $file }": os quadros extraídos têm dimensões diferentes core_failed_to_generate_thumbnail = Falhou ao gerar miniatura para "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Falhou em extrair o quadro em { $time } segundos de "{ $file }": { $reason } core_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 }" core_failed_to_get_video_metadata = Falhou ao obter os metadados de vídeo para o arquivo "{ $file }": { $reason } core_failed_to_get_video_codec = Falhou ao obter o codec de vídeo para o arquivo "{ $file }" core_failed_to_get_video_duration = Falhou ao obter a duração do vídeo para o arquivo "{ $file }" core_failed_to_get_video_dimensions = Falhou ao obter as dimensões do vídeo para o arquivo "{ $file }" core_frame_dimensions_mismatch = Dimensões do quadro para timestamp { $timestamp } não correspondem às dimensões do primeiro quadro ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Falha ao carregar dados do ficheiro de cache { $file }, motivo { $reason } core_failed_to_load_data_from_json_cache = Falha ao carregar dados do ficheiro de cache json { $file }, motivo { $reason } core_failed_to_replace_with_optimized = Falhou ao substituir o arquivo "{ $file }" pela versão otimizada: { $reason } core_failed_to_write_data_to_cache = Não é possível escrever dados para o ficheiro de cache "{ $file }", motivo { $reason } core_properly_saved_cache_entries = Salvado corretamente para o arquivo { $count } entradas de cache. core_video_processing_stopped_by_user = O processamento de vídeo foi interrompido pelo utilizador core_thumbnail_generation_stopped_by_user = Geração de miniaturas foi interrompida pelo usuário core_failed_to_optimize_video = Falha ao otimizar o vídeo "{ $file }": { $reason } core_failed_to_crop_video = Falha ao cortar o vídeo "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Falhou ao obter metadados do arquivo otimizado "{ $file }": { $reason } core_cannot_create_config_folder = Não é possível criar a pasta de configuração "{ $folder }", a razão { $reason } core_cannot_create_cache_folder = Não é possível criar a pasta de cache "{ $folder }", a razão { $reason } core_cannot_create_or_open_cache_file = Não é possível criar ou abrir o arquivo de cache "{ $file }", motivo { $reason } core_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. core_invalid_extension_contains_space = { $extension } não é uma extensão válida porque contém espaço em branco no interior core_invalid_extension_contains_dot = { $extension } não é uma extensão válida porque contém ponto dentro core_failed_to_check_process_status = Falha ao verificar o status do processo: { $reason } core_failed_to_wait_for_process = Falha ao aguardar a conclusão do processo: { $reason } core_failed_to_read_video_properties = Falha ao ler as propriedades do vídeo: { $reason } core_failed_to_execute_ffmpeg = Falha ao executar o ffmpeg: { $reason } core_video_width_exceeds_limit = Largura do vídeo { $width } excede o limite de { $limit } core_optimized_file_larger = O arquivo otimizado { $optimized } (tamanho: { $new_size }) não é menor que o arquivo original { $original } (tamanho: { $original_size }) core_folder_does_not_exist = Pasta não encontrada: { $folder } core_error_comparing_fingerprints = Erro ao comparar impressões digitais: { $reason } ================================================ FILE: czkawka_core/i18n/ro/czkawka_core.ftl ================================================ # Core core_similarity_original = Originală core_similarity_very_high = Foarte Mare core_similarity_high = Ridicat core_similarity_medium = Medie core_similarity_small = Mică core_similarity_very_small = Foarte mic core_similarity_minimal = Minimă core_cannot_open_dir = Nu se poate deschide dir { $dir }, motiv { $reason } core_cannot_read_entry_dir = Nu se poate citi intrarea în dir { $dir }, motivul { $reason } core_cannot_read_metadata_dir = Metadatele nu pot fi citite în dir { $dir }, motivul { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Fișierul { $name } pare să fi fost modificat înainte de Epocul Unix core_folder_modified_before_epoch = Dosarul { $name } pare să fi fost modificat înainte de Epocul Unix core_file_no_modification_date = Imposibil de obținut data modificării din fișierul { $name }, motivul { $reason } core_folder_no_modification_date = Imposibil de obținut data modificării din dosarul { $name }, motivul { $reason } core_cannot_start_scan_no_included_paths = Nu se poate începe scanarea, deoarece nu există căi incluse core_skip_exist_check_all_included_paths_nonexistent = Nu se poate începe scanarea, deoarece toate căile incluse nu există core_missing_no_chosen_included_path = Nu a fost selectat niciun drum inclus valid (drumurile excluse ar fi putut exclude toate drumurile incluse) core_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 core_path_must_exists = Calea furnizată trebuie să existe, ignorând { $path } core_must_be_directory_or_file = Fiul indicat trebuie să indice un director sau fișier valid, ignorând { $path } core_excluded_paths_pointless_slash = Excluderea / este inutilă, deoarece înseamnă că niciun fișier nu va fi scanat core_paths_unable_to_get_device_id = Nu se poate obține ID-ul dispozitivului din folder { $path } core_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 core_needs_allowed_extensions = Nu se poate începe scanarea, când toate extensiile au fost excluse din scan core_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 core_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 core_ffmpeg_not_found = Nu se poate găsi o instalare adecvată a FFmpeg sau FFprobe. Acestea sunt programe externe care trebuie instalate manual. core_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ă core_invalid_symlink_infinite_recursion = Recepţie infinită core_invalid_symlink_non_existent_destination = Fișier destinație inexistent core_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. core_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. core_error_moving_to_trash = Eroare la mutarea "{ $file }" în coșul de gunoi: { $error } core_error_removing = Eroare la eliminarea "{ $file }": { $error } core_no_similarity_method_selected = Nu pot găsi fișiere muzicale similare fără o metodă de similaritate selectată core_failed_to_spawn_command = Eșuare la generarea comenzii: { $reason } core_failed_to_check_process_status = Eșec la verificarea stării procesului: { $reason } core_failed_to_wait_for_process = Eșec la așteptarea procesului: { $reason } core_failed_to_read_video_properties = Eșec la citirea proprietăților videoclipului: { $reason } core_failed_to_execute_ffmpeg = Eșec la execuția ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg a eșuat cu status { $status }: { $stderr } (comanda: { $command }) core_failed_to_load_image_frame = Nu s-a putut încărca cadrul imaginii: { $reason } core_failed_to_extract_frame = Eșec la extragerea cadrului la { $time } secunde din "{ $file }": { $reason } core_failed_to_save_thumbnail = Nu s-a putut salva miniatură pentru "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Eșec la obținerea cadrului la timestamp { $timestamp } din "{ $file }": { $reason } core_failed_get_frame_from_file = Nu s-a putut obține cadrul din "{ $file }" la timestamp { $timestamp }: { $reason } core_invalid_crop_rectangle = Dreptunghiul de selecție este invalid: stânga={ $left }, sus={ $top }, dreapta={ $right }, jos={ $bottom } core_failed_to_crop_video_file = Eșec la tăierea fișierului video "{ $file }": { $reason } core_cropped_video_not_created = Fișierul video tăiat nu a fost creat: { $temp } core_unable_check_hash_of_file = Nu se poate verifica hash-ul fișierului "{ $file }", motivul { $reason } core_error_checking_hash_of_file = Eroare a apărut la verificarea hash-ului fișierului "{ $file }", motivul { $reason } core_image_zero_dimensions = Imaginea are o lățime sau înălțime zero "{ $path }" core_image_open_failed = Nu se poate deschide fișierul de imagine "{ $path }": { $reason } core_not_directory_remove = Încercarea de a elimina folderul "{ $path }" care nu este un director core_cannot_read_directory = Nu pot citi directorul "{ $path }" core_cannot_read_entry_from_directory = Nu pot citi intrarea din directorul "{ $path }" core_folder_contains_file_inside = Folder conține fișierul "{ $entry }" în interiorul "{ $folder }" core_unknown_directory_entry = Nu se poate determina tipul fișierului intrării din director "{ $entry }" în "{ $path }" core_video_width_exceeds_limit = Video lățime { $width } depășește limita de { $limit } core_video_height_exceeds_limit = Video înălțime { $height } depășește limita de { $limit } core_failed_to_process_video = Fișierul video { $file } nu a putut fi procesat: { $reason } core_optimized_file_larger = Fișier optimizat { $optimized } (dimensiune: { $new_size }) nu este mai mic decât originalul { $original } (dimensiune: { $original_size }) core_unknown_codec = Codc necunoscut: { $codec } core_invalid_video_optimizer_mode = Mod de optimizare video invalid: '{ $mode }'. Valorile permise: transcode, crop core_folder_does_not_exist = Dosar nu există: { $folder } core_path_not_directory = Calele nu este un director: { $folder } core_test_error_for_folder = Eroare test pentru folder: { $folder } core_unknown_exif_tag_group = Grupul EXIF necunoscut: { $tag } core_error_comparing_fingerprints = Eroare la compararea amprentelor: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Eșec la generarea miniaturii pentru "{ $file }": cadrele extrase au dimensiuni diferite core_failed_to_generate_thumbnail = Eșec la generarea miniaturii pentru "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Eșec la extragerea cadrului la { $time } secunde din "{ $file }": { $reason } core_video_file_does_not_exist = Fișier video inexistent (poate fi eliminat între scanare/pașii ulteriori): "{ $path }" core_image_too_large = Imaginea este prea mare ({ $width }x{ $height }) - mai mult decât suportat { $max } pixeli core_failed_to_get_video_metadata = Eșec la obținerea metadatelor video pentru fișierul "{ $file }": { $reason } core_failed_to_get_video_codec = Eșec la obținerea codec-ului video pentru fișierul "{ $file }" core_failed_to_get_video_duration = Nu s-a putut obține durata video pentru fișierul "{ $file }" core_failed_to_get_video_dimensions = Eșec la obținerea dimensiunilor video pentru fișierul "{ $file }" core_frame_dimensions_mismatch = Dimensiunile cadrului pentru marcajul de timp { $timestamp } nu corespund cu dimensiunile primului cadru ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Nu s-a putut încărca date din fișierul cache { $file }, motivul { $reason } core_failed_to_load_data_from_json_cache = Nu s-a putut încărca date din fișierul cache JSON { $file }, motiv { $reason } core_failed_to_replace_with_optimized = Eșec la înlocuirea fișierului "{ $file }" cu versiunea optimizată: { $reason } core_failed_to_write_data_to_cache = Nu se poate scrie date în fișierul cache "{ $file }", motiv { $reason } core_properly_saved_cache_entries = Salvat corect în fișier { $count } intrări în cache. core_video_processing_stopped_by_user = Procesarea video a fost oprită de utilizator core_thumbnail_generation_stopped_by_user = Generarea miniaturilor a fost oprită de utilizator core_failed_to_optimize_video = Eșec la optimizarea videoclipului "{ $file }": { $reason } core_failed_to_crop_video = Eșec la tăierea videoclipului "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Eșec la obținerea metadatelor fișierului optimizat "{ $file }": { $reason } core_cannot_create_config_folder = Nu se poate crea folderul de configurare "{ $folder }", motivul { $reason } core_cannot_create_cache_folder = Nu se poate crea folderul de cache "{ $folder }", motiv { $reason } core_cannot_create_or_open_cache_file = Nu se poate crea sau deschide fișierul de cache "{ $file }", motiv { $reason } core_cannot_set_config_cache_path = Nu se poate seta calea de configurare/cache - configurarea și cache-ul nu vor fi utilizate. core_invalid_extension_contains_space = { $extension } nu este o extensie validă deoarece conține spații goale în interior core_invalid_extension_contains_dot = { $extension } nu este o extensie validă deoarece conține punct în interior ================================================ FILE: czkawka_core/i18n/ru/czkawka_core.ftl ================================================ # Core core_similarity_original = Оригинальное core_similarity_very_high = Очень высокое core_similarity_high = Высокое core_similarity_medium = Среднее core_similarity_small = Низкое core_similarity_very_small = Очень низкое core_similarity_minimal = Минимальное core_cannot_open_dir = Невозможно открыть каталог { $dir }, причина: { $reason } core_cannot_read_entry_dir = Невозможно прочитать запись в директории { $dir }, причина: { $reason } core_cannot_read_metadata_dir = Невозможно прочитать метаданные в директории { $dir }, причина: { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Файл { $name } был изменен до эпохи Unix core_folder_modified_before_epoch = Папка { $name } была изменена до начала Unix Epoch core_file_no_modification_date = Не удаётся получить дату изменения из файла { $name }, причина: { $reason } core_folder_no_modification_date = Не удаётся получить дату изменения из папки { $name }, причина: { $reason } core_cannot_start_scan_no_included_paths = Невозможно начать сканирование, так как не указаны пути core_skip_exist_check_all_included_paths_nonexistent = Невозможно начать сканирование, потому что все включенные пути не существуют core_missing_no_chosen_included_path = Не был выбран действительный путь, который был включен (исключающие пути могли исключить все включенные пути) core_reference_included_paths_same = Невозможно начать сканирование, где все допустимые включенные пути также являются ссылочными путями, попробуйте проверить или отключить ссылочные пути core_path_must_exists = Предоставленный путь должен существовать, игнорируя { $path } core_must_be_directory_or_file = Предоставленный путь должен указывать на действительную директорию или файл, игнорируя { $path } core_excluded_paths_pointless_slash = Исключать / бессмысленно, потому что это означает, что файлы не будут сканированы core_paths_unable_to_get_device_id = Невозможно получить идентификатор устройства из папки { $path } core_needs_allowed_extensions_limited_by_tool = Невозможно начать сканирование, когда все расширения, доступные в этом инструменте ({ $extensions }), были исключены из сканирования core_needs_allowed_extensions = Невозможно начать сканирование, когда все расширения исключены из сканирования core_needs_to_set_at_least_one_broken_option = Невозможно начать сканирование, когда не указана опция «поврежденный» для сканирования core_needs_to_set_at_least_one_bad_name_option = Невозможно начать сканирование, когда опция «плохое имя» не установлена для сканирования core_ffmpeg_not_found = Не удается найти надлежащую установку FFmpeg или FFprobe. Это внешние программы, которые должны быть установлены вручную. core_ffmpeg_not_found_windows = Убедитесь, что ffmpeg.exe и ffprobe.exe доступны в PATH или находятся непосредственно в той же папке, что и приложение core_invalid_symlink_infinite_recursion = Бесконечная рекурсия core_invalid_symlink_non_existent_destination = Не найден конечный файл core_messages_limit_reached_characters = Количество сообщений превысило установленный лимит ({ $current }/{ $limit } символов), поэтому вывод был усечен. Чтобы прочитать весь вывод, отключите опцию ограничения в настройках. core_messages_limit_reached_lines = Количество сообщений превысило установленный лимит ({ $current }/{ $limit } строк), поэтому вывод был усечен. Чтобы прочитать весь вывод, отключите опцию ограничения в настройках. core_error_moving_to_trash = Ошибка при перемещении "{ $file }" в корзину: { $error } core_error_removing = Ошибка при удалении "{ $file }": { $error } core_no_similarity_method_selected = Не удается найти похожие музыкальные файлы без выбранного метода сходства core_failed_to_spawn_command = Не удалось запустить команду: { $reason } core_failed_to_check_process_status = Не удалось проверить статус процесса: { $reason } core_failed_to_wait_for_process = Не удалось дождаться процесса: { $reason } core_failed_to_read_video_properties = Не удалось прочитать свойства видео: { $reason } core_failed_to_execute_ffmpeg = Не удалось выполнить ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg завершился с кодом ошибки { $status }: { $stderr } (команда: { $command }) core_failed_to_load_image_frame = Не удалось загрузить кадр изображения: { $reason } core_failed_to_extract_frame = Не удалось извлечь кадр в { $time } секундах из "{ $file }": { $reason } core_failed_to_save_thumbnail = Не удалось сохранить миниатюру для "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Не удалось получить кадр в метке времени { $timestamp } из "{ $file }": { $reason } core_failed_get_frame_from_file = Не удалось получить кадр из "{ $file }" в метке времени { $timestamp }: { $reason } core_invalid_crop_rectangle = Неверный прямоугольник обрезки: лево={ $left }, сверху={ $top }, право={ $right }, снизу={ $bottom } core_failed_to_crop_video_file = Не удалось обрезать видеофайл "{ $file }": { $reason } core_cropped_video_not_created = Обрезённый видеофайл не был создан: { $temp } core_unable_check_hash_of_file = Невозможно проверить хэш файла "{ $file }", причина { $reason } core_error_checking_hash_of_file = Ошибка при проверке хэша файла "{ $file }", причина { $reason } core_image_zero_dimensions = Изображение имеет нулевую ширину или высоту "{ $path }" core_image_open_failed = Невозможно открыть файл изображения "{ $path }": { $reason } core_not_directory_remove = Попытка удалить папку "{ $path }" которая не является директорией core_cannot_read_directory = Невозможно прочитать каталог "{ $path }" core_cannot_read_entry_from_directory = Невозможно прочитать запись из каталога "{ $path }" core_folder_contains_file_inside = Папка содержит файл "{ $entry }" внутри "{ $folder }" core_unknown_directory_entry = Невозможно определить тип файла для записи каталога "{ $entry }" внутри "{ $path }" core_video_width_exceeds_limit = Видео ширина { $width } превышает лимит { $limit } core_video_height_exceeds_limit = Видео высота { $height } превышает лимит { $limit } core_failed_to_process_video = Не удалось обработать видеофайл { $file }: { $reason } core_optimized_file_larger = Оптимизированный файл { $optimized } (размер: { $new_size }) не меньше, чем исходный { $original } (размер: { $original_size }) core_unknown_codec = Неизвестный кодек: { $codec } core_invalid_video_optimizer_mode = Недопустимый режим оптимизации видео: '{ $mode }'. Разрешенные значения: transcode, crop core_folder_does_not_exist = Папка не существует: { $folder } core_path_not_directory = Путь не является каталогом: { $folder } core_test_error_for_folder = Ошибка теста для папки: { $folder } core_unknown_exif_tag_group = Неизвестная группа EXIF тегов: { $tag } core_error_comparing_fingerprints = Ошибка при сравнении отпечатков пальцев: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Не удалось сгенерировать миниатюру для "{ $file }": извлеченные кадры имеют разные размеры core_failed_to_generate_thumbnail = Не удалось сгенерировать миниатюру для "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Не удалось извлечь кадр в { $time } секундах из "{ $file }": { $reason } core_video_file_does_not_exist = Файл видео не существует (может быть удален между сканированием/поздними шагами): "{ $path }" core_image_too_large = Изображение слишком большое ({ $width }x{ $height }) - больше, чем поддерживаемые { $max } пикселей core_failed_to_get_video_metadata = Не удалось получить метаданные видео для файла "{ $file }": { $reason } core_failed_to_get_video_codec = Не удалось получить видеокодек для файла "{ $file }" core_failed_to_get_video_duration = Не удалось получить продолжительность видео для файла "{ $file }" core_failed_to_get_video_dimensions = Не удалось получить размеры видео для файла "{ $file }" core_frame_dimensions_mismatch = Размеры кадра для метки времени { $timestamp } не совпадают с размерами первого кадра ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Не удалось загрузить данные из кэш-файла { $file }, причина { $reason } core_failed_to_load_data_from_json_cache = Не удалось загрузить данные из файла кэша json { $file }, причина { $reason } core_failed_to_replace_with_optimized = Не удалось заменить файл "{ $file }" на оптимизированную версию: { $reason } core_failed_to_write_data_to_cache = Невозможно записать данные в кэш-файл "{ $file }", причина { $reason } core_properly_saved_cache_entries = Правильно сохранено в файл { $count } кэш-записей. core_video_processing_stopped_by_user = Обработка видео была остановлена пользователем core_thumbnail_generation_stopped_by_user = Генерация превью была остановлена пользователем core_failed_to_optimize_video = Не удалось оптимизировать видео "{ $file }": { $reason } core_failed_to_crop_video = Не удалось обрезать видео "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Не удалось получить метаданные оптимизированного файла "{ $file }": { $reason } core_cannot_create_config_folder = Невозможно создать папку конфигурации "{ $folder }", причина { $reason } core_cannot_create_cache_folder = Невозможно создать папку кэша "{ $folder }", причина { $reason } core_cannot_create_or_open_cache_file = Невозможно создать или открыть кэш-файл "{ $file }", причина { $reason } core_cannot_set_config_cache_path = Невозможно установить путь к config/cache - config и cache не будут использоваться. core_invalid_extension_contains_space = { $extension } не является допустимым расширением, поскольку оно содержит пробел внутри core_invalid_extension_contains_dot = { $extension } не является допустимым расширением, так как оно содержит точку внутри ================================================ FILE: czkawka_core/i18n/sv-SE/czkawka_core.ftl ================================================ # Core core_similarity_original = Ursprunglig core_similarity_very_high = Mycket Hög core_similarity_high = Hög core_similarity_medium = Mellan core_similarity_small = Litet core_similarity_very_small = Väldigt Liten core_similarity_minimal = Minimalt core_cannot_open_dir = Kan inte öppna dir { $dir }anledning { $reason } core_cannot_read_entry_dir = Kan inte läsa post i dir { $dir }, anledning { $reason } core_cannot_read_metadata_dir = Kan inte läsa metadata i dir { $dir }, anledning { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Filen { $name } verkar ha ändrats innan Unix Epoch core_folder_modified_before_epoch = Folder { $name } verkar ha ändrats innan Unix Epoch core_file_no_modification_date = Det går inte att hämta ändringsdatum från filen { $name }, anledning { $reason } core_folder_no_modification_date = Det går inte att hämta ändringsdatum från mappen { $name }, anledning { $reason } core_cannot_start_scan_no_included_paths = Kan inte starta skanning, eftersom det inte finns några inkluderade sökvägar core_skip_exist_check_all_included_paths_nonexistent = Kan inte starta skanningen, eftersom alla inkluderade sökvägar inte finns core_missing_no_chosen_included_path = Ingen giltande inkluderad sökväg valdes (uteslutna sökvägar kunde ha uteslutit alla inkluderade sökvägar) core_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 core_path_must_exists = Angiven sökväg måste existera, ignorera { $path } core_must_be_directory_or_file = Angiven sökväg måste peka på en giltig katalog eller fil, och ignorera { $path } core_excluded_paths_pointless_slash = Exkludera / är meningslöst, eftersom det innebär att inga filer kommer att skannas core_paths_unable_to_get_device_id = Kan inte hämta enhets-ID från mappen { $path } core_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 core_needs_allowed_extensions = Kan inte starta skanning, när alla tillägg har tagits ur skanning core_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 core_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 core_ffmpeg_not_found = Kan inte hitta en korrekt installation av FFmpeg eller FFprobe. Dessa är externa program som måste installeras manuellt. core_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 core_invalid_symlink_infinite_recursion = Oändlig recursion core_invalid_symlink_non_existent_destination = Icke-existerande målfil core_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. core_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. core_error_moving_to_trash = Fel vid flyttning av "{ $file }" till papperskorgen: { $error } core_error_removing = Fel vid borttagning av "{ $file }": { $error } core_no_similarity_method_selected = Kan inte hitta liknande musikfiler utan en vald likhetsmetod core_failed_to_spawn_command = Misslyckades med att generera kommando: { $reason } core_failed_to_check_process_status = Misslyckades med att kontrollera processstatus: { $reason } core_failed_to_wait_for_process = Misslyckades med att vänta på processen: { $reason } core_failed_to_read_video_properties = Kunde inte läsa videobegränsningar: { $reason } core_failed_to_execute_ffmpeg = Kunde inte köra ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg misslyckades med status { $status }: { $stderr } (kommando: { $command }) core_failed_to_load_image_frame = Misslyckades med att ladda bildram: { $reason } core_failed_to_extract_frame = Misslyckades med att extrahera bildruta vid { $time } sekunder från "{ $file }": { $reason } core_failed_to_save_thumbnail = Misslyckades med att spara miniatyrbild för "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Misslyckades med att hämta bildrutor vid tidstämpling { $timestamp } från "{ $file }": { $reason } core_failed_get_frame_from_file = Misslyckades med att hämta bildrutor från "{ $file }" vid tidstämplen { $timestamp }: { $reason } core_invalid_crop_rectangle = Ogiltig odlingsrektangel: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom } core_failed_to_crop_video_file = Misslyckades med att beskära videofilen "{ $file }": { $reason } core_cropped_video_not_created = Skuren videofil var inte skapad: { $temp } core_unable_check_hash_of_file = Kunde inte kontrollera hash för fil "{ $file }", anledning { $reason } core_error_checking_hash_of_file = Fel inträffade vid kontroll av hash för filen "{ $file }", anledning { $reason } core_image_zero_dimensions = Bilden har noll bredd eller höjd "{ $path }" core_image_open_failed = Kan inte öppna bildfilen "{ $path }": { $reason } core_not_directory_remove = Försöker ta bort mappen "{ $path }" vilket inte är en katalog core_cannot_read_directory = Kan inte läsa katalogen "{ $path }" core_cannot_read_entry_from_directory = Kan inte läsa inlägg från katalogen "{ $path }" core_folder_contains_file_inside = Mappen innehåller filen "{ $entry }" inuti "{ $folder }" core_unknown_directory_entry = Kan inte fastställa filtyp för katalogposten "{ $entry }" inuti "{ $path }" core_video_width_exceeds_limit = Video bredd { $width } överskrider gränsen för { $limit } core_video_height_exceeds_limit = Video höjd { $height } överskrider gränsen för { $limit } core_optimized_file_larger = Optimerad fil { $optimized } (storlek: { $new_size }) är inte mindre än original { $original } (storlek: { $original_size }) core_unknown_codec = Okänt codec: { $codec } core_folder_does_not_exist = Mappexisterar inte: { $folder } core_path_not_directory = Sökvägen är inte en katalog: { $folder } core_test_error_for_folder = Feltest för mapp: { $folder } core_unknown_exif_tag_group = Okänt EXIF-tag grupp: { $tag } core_error_comparing_fingerprints = Fel vid jämförelse av fingeravtryck: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Misslyckades med att generera miniatyrbild för "{ $file }": extraherade ramar har olika dimensioner core_failed_to_generate_thumbnail = Misslyckades med att generera miniatyrbild för "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Misslyckades med att extrahera bildruta vid { $time } sekunder från "{ $file }": { $reason } core_video_file_does_not_exist = Videofilen finns inte (kan tas bort mellan skanning/senare steg): "{ $path }" core_image_too_large = Bilden är för stor ({ $width }x{ $height }) - mer än stödda { $max } pixlar core_failed_to_get_video_metadata = Misslyckades med att hämta videodata för fil "{ $file }": { $reason } core_failed_to_get_video_codec = Misslyckades med att hämta videocodec för filen "{ $file }" core_failed_to_get_video_duration = Kunde inte få videolängd för fil "{ $file }" core_failed_to_get_video_dimensions = Misslyckades med att få videons dimensioner för filen "{ $file }" core_frame_dimensions_mismatch = Bildens mått för tidstämplar { $timestamp } stämmer inte överens med bildens mått ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Misslyckades med att ladda data från cachefil { $file }, anledning { $reason } core_failed_to_load_data_from_json_cache = Kunde inte ladda data från json-cache-fil { $file}, anledning { $reason } core_failed_to_replace_with_optimized = Misslyckades med att ersätta filen "{ $file }" med den optimerade versionen: { $reason } core_failed_to_write_data_to_cache = Kan inte skriva data till cachefil "{ $file }", anledning { $reason } core_properly_saved_cache_entries = Spara korrekt till fil { $count } cacheposter. core_video_processing_stopped_by_user = Videobearbetningen stoppades av användaren core_thumbnail_generation_stopped_by_user = Miniatyrgenerering stoppades av användare core_failed_to_optimize_video = Misslyckades med att optimera video "{ $file }": { $reason } core_failed_to_crop_video = Misslyckades med att beskära videon "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Misslyckades med att hämta metadata för den optimerade filen "{ $file }": { $reason } core_cannot_create_config_folder = Kan inte skapa konfigurationsmappen "{ $folder }", anledningen är { $reason } core_cannot_create_cache_folder = Kan inte skapa cachemappen "{ $folder }", anledningen { $reason } core_cannot_create_or_open_cache_file = Kan inte skapa eller öppna cachefil "{ $file }", anledning { $reason } core_cannot_set_config_cache_path = Kan inte ställa in config/cache-sökväg - config och cache kommer inte att användas. core_invalid_extension_contains_space = { $extension } är inte en giltig filändelse eftersom den innehåller tomma utrymmen däri core_invalid_extension_contains_dot = { $extension } är inte en giltig filändelse eftersom den innehåller en punkt inuti core_failed_to_process_video = Kunde inte bearbeta videofil { $file }: { $reason } core_invalid_video_optimizer_mode = Ogiltigt optimeringsläge för video: '{ $mode }'. Tillåtna värden: transcode, crop ================================================ FILE: czkawka_core/i18n/tr/czkawka_core.ftl ================================================ # Core core_similarity_original = Asıl core_similarity_very_high = Çok Yüksek core_similarity_high = Yüksek core_similarity_medium = Orta core_similarity_small = Düşük core_similarity_very_small = Çok Düşük core_similarity_minimal = Aşırı Düşük core_cannot_open_dir = { $dir } dizini açılamıyor, nedeni: { $reason } core_cannot_read_entry_dir = { $dir } dizinindeki girdi okunamıyor, nedeni: { $reason } core_cannot_read_metadata_dir = { $dir } dizinindeki metaveri okunamıyor, nedei: { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch core_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch core_file_no_modification_date = { $name } dosyasının değişiklik tarihine erişilemiyor, nedeni: { $reason } core_folder_no_modification_date = { $name } klasörünün değişiklik tarihine erişilemiyor, nedeni: { $reason } core_cannot_start_scan_no_included_paths = Tarama başlatılamıyor, çünkü hiçbir dahil yol yok core_skip_exist_check_all_included_paths_nonexistent = Tarama başlatılamıyor, çünkü tüm dahil yollar mevcut değil core_missing_no_chosen_included_path = Geçerli bir dahil yol seçilemedi (hariç tutulan yollar tüm dahil yolları hariç bırakmış olabilir) core_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 core_must_be_directory_or_file = Verilen yol geçer bir dizine veya dosyaya işaret etmelidir, { $path }'i göz ardı ederek core_excluded_paths_pointless_slash = Hariç tutmak / anlamsızdır, çünkü bu, hiçbir dosyanın taranmayacağı anlamına gelir core_paths_unable_to_get_device_id = Cihaz kimliğini { $path } klasöründen elde edilemiyor core_needs_allowed_extensions_limited_by_tool = Tarama başlatılamıyor, tüm uzantılar bu araçta ({ $extensions }) mevcutken taramadan hariç tutulduğunda core_needs_allowed_extensions = Tarama başlatılamıyor, tüm uzantılar taramadan hariç tutulduğunda core_needs_to_set_at_least_one_broken_option = Tarama başlatılamıyor, kırık seçenek aranırken ayarlanmamışsa core_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 core_ffmpeg_not_found = FFmpeg veya FFprobe için uygun bir kurulum bulunamıyor. Bunlar, manuel olarak kurulması gereken harici programlardır. core_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 core_invalid_symlink_infinite_recursion = Sonsuz özyineleme core_invalid_symlink_non_existent_destination = Var olmayan hedef dosya core_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. core_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. core_error_moving_to_trash = { $file }'yi atlaşına taşıma sırasında hata: { $error } core_error_removing = "{ $file }" kaldırılırken hata: { $error } core_no_similarity_method_selected = Seçilen benzerlik metoduna olmadan benzer müzik dosyası bulamıyor core_failed_to_spawn_command = Komutun oluşturulması başarısız oldu: { $reason } core_failed_to_check_process_status = İşlem durumunu kontrol edemedi: { $reason } core_failed_to_wait_for_process = İşlem beklemede başarısız oldu: { $reason } core_failed_to_read_video_properties = Video özelliklerini okuyamıyor: { $reason } core_failed_to_execute_ffmpeg = ffmpeg'i çalıştırmada başarısız: { $reason } core_ffmpeg_failed_with_status = ffmpeg başarısız oldu durum { $status }: { $stderr } (komut: { $command }) core_failed_to_load_image_frame = Resim çerçevesi yüklenemedi: { $reason } core_failed_to_extract_frame = { $time } saniyede kareyi "{ $file }" dosyasından çıkarılamadı: { $reason } core_failed_to_save_thumbnail = "{ $file }" için önizlemeyi kaydedemedi: { $reason } core_failed_get_frame_at_timestamp = { $timestamp } zaman damgası üzerinde çerçeve alınamadı "{ $file }": { $reason } core_failed_get_frame_from_file = "{ $file }" adlı dosyadaki çerçeve alınamadı zaman damgası { $timestamp }'te: { $reason } core_invalid_crop_rectangle = Geçersiz tarım dikdörtgeni: sol={ $left }, üst={ $top }, sağ={ $right }, alt={ $bottom } core_cropped_video_not_created = Kesilmiş video dosyası oluşturulmadı: { $temp } core_unable_check_hash_of_file = Dosyanın hash'ini "{ $file }" kontrol edilemedi, nedeni { $reason } core_error_checking_hash_of_file = Dosya "{ $file }" için hash kontrolünde hata oluştu, nedeni { $reason } core_image_zero_dimensions = Görüntü sıfır genişliğe veya yüksekliğe sahiptir "{ $path }" core_image_open_failed = "{ $path }" adlı görüntü dosyasını açamıyoruz: { $reason } core_not_directory_remove = "{ $path }$" adlı klasörü silmeye çalışıyor, bu bir dizin değil core_cannot_read_directory = "{ $path }" dizinini okuyamıyor core_cannot_read_entry_from_directory = "{ $path }" dizininden girişi okuyamaz core_folder_contains_file_inside = Klasör içinde "{ $entry }" dosyası "{ $folder }" içinde bulunmaktadır core_unknown_directory_entry = Dosya türünü "{ $entry }" girişine ait "{ $path }" içinde belirleyemiyorum core_video_width_exceeds_limit = Video genişliği { $width } limiti { $limit } değerini aşmaktadır core_video_height_exceeds_limit = Video yüksekliği { $height } limiti { $limit }'i aşmaktadır core_optimized_file_larger = Optimize edilmiş dosya { $optimized } (boyut: { $new_size }) orijinal { $original } (boyut: { $original_size })'den daha küçük değil core_unknown_codec = Bilinmeyen codec: { $codec } core_invalid_video_optimizer_mode = Geçersiz video optimizasyon modu: '{ $mode }'. İzin verilen değerler: transcode, crop core_folder_does_not_exist = Klasör bulunamadı: { $folder } core_path_not_directory = Yol bir dizin değildir: { $folder } core_test_error_for_folder = Dosya hatası için klasör: { $folder } core_unknown_exif_tag_group = Bilinmeyen EXIF etiket grubu: { $tag } core_error_comparing_fingerprints = Parmak izlerini karşılaştırmada hata: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = "{ $file }" için önizlemeyi oluşturulamadı: Çıkarılan kareler farklı boyutlarda core_failed_to_generate_thumbnail = "{ $file }" için önizlemeyi oluşturulamadı: { $reason } core_failed_to_extract_frame_at_seek_time = { $time } saniyede kareyi "{ $file }" dosyasından çıkarılamadı: { $reason } core_video_file_does_not_exist = Video dosyası mevcut değil (tarama/daha sonra adımları arasında kaldırılabilir): "{ $path }" core_image_too_large = Görüntü çok büyü ({$width}x{$height}) - desteklenmeyen { $max } pikselden fazla core_failed_to_get_video_metadata = Video meta verilerini dosyayı "{ $file }" için elde edilemedi: { $reason } core_failed_to_get_video_codec = Dosya "{ $file }" için video codec'i alınamadı core_failed_to_get_video_duration = Video süresini "{ $file }" dosyası için elde edilemedi core_failed_to_get_video_dimensions = Video boyutlarını dosya için "{ $file }" alınamadı core_frame_dimensions_mismatch = Çerçeve boyutları { $timestamp } zaman damgası için ilk çerçeve boyutlarıyla (%{$first_w}x%{$first_h}) uyuşmuyor core_failed_to_load_data_from_cache = Verilen dosyadaki {$file} verisi yüklenemedi, nedeni { $reason } core_failed_to_replace_with_optimized = Dosya "{ $file }" optimize edilmiş versiyon ile değiştirilemedi: { $reason } core_failed_to_write_data_to_cache = Katalog dosyasına "{ $file }" yazamıyor, nedeni { $reason } core_properly_saved_cache_entries = Doğru şekilde dosyaya { $count } önbellek girişi kaydedildi. core_video_processing_stopped_by_user = Video işleme kullanıcının tarafından durduruldu core_thumbnail_generation_stopped_by_user = Miniature oluşturma durduruldu kullanıcı tarafından core_failed_to_optimize_video = Video "{ $file }" optimizasyonu başarısız: { $reason } core_failed_to_crop_video = Video kırpma başarısız: "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Optimizasyonlu dosya "{ $file }": { $reason } meta verisi alınamadı core_cannot_create_config_folder = Konfigürasyon klasörü "{ $folder }" oluşturulamazdı, nedeni { $reason } core_cannot_create_cache_folder = "{ $folder }" önbellek klasörü oluşturulamaz, nedeni { $reason } core_cannot_set_config_cache_path = Config/cache yolu yapılamadı - config ve cache kullanılmayacak. core_invalid_extension_contains_space = { $extension } geçerli bir uzantı değildir çünkü içinde boşluk içermektedir core_invalid_extension_contains_dot = { $extension } geçerli bir uzantı değildir çünkü içinde nokta içeriyor core_path_must_exists = Verilen yolun mevcut olması gerekmektedir, ancak {$path} kısmını dikkate almayınız core_failed_to_crop_video_file = "{ $file }" video dosyasını kırpmakta bir sorun oluştu: { $reason } core_failed_to_process_video = Video dosyasını işleme sırasında bir hata oluştu: { $file } - Neden: { $reason } core_failed_to_load_data_from_json_cache = JSON önbellek dosyasından veri yüklenemedi: { $file }, nedeni: { $reason } core_cannot_create_or_open_cache_file = "{ $file }" adlı geçici dosyayı oluşturamadı veya açamadı, nedeni: { $reason } ================================================ FILE: czkawka_core/i18n/uk/czkawka_core.ftl ================================================ # Core core_similarity_original = Оригінал core_similarity_very_high = Дуже висока core_similarity_high = Висока core_similarity_medium = Середня core_similarity_small = Низька core_similarity_very_small = Дуже низька core_similarity_minimal = Мінімальна core_cannot_open_dir = Не вдалося відкрити каталог { $dir }, причина: { $reason } core_cannot_read_entry_dir = Не вдалося прочитати запис в каталозі { $dir }, причина: { $reason } core_cannot_read_metadata_dir = Не вдалося прочитати метадані в каталозі { $dir }, причина: { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = Файл { $name } здається змінено до Unix Epoch core_folder_modified_before_epoch = Папка { $name } здається була змінена до Unix Epoch core_file_no_modification_date = Не вдалося отримати дату модифікації з файлу { $name }, причина: { $reason } core_folder_no_modification_date = Не вдалося отримати дату модифікації з каталогу { $name }, причина: { $reason } core_cannot_start_scan_no_included_paths = Не вдається запустити сканування, оскільки відсутні включені шляхи core_skip_exist_check_all_included_paths_nonexistent = Не вдається запустити сканування, оскільки всі включені шляхи не існують core_missing_no_chosen_included_path = Не було вибрано жодного валідного включеного шляху (виключені шляхи могли виключити всі включені шляхи) core_reference_included_paths_same = Не вдається запустити сканування, де всі валідні включені шляхи також є шляхами, на які посилаються, спробуйте перевірити їх або вимкнути шляхи, на які посилаються core_path_must_exists = Наданий шлях повинен існувати, ігноруючи { $path } core_must_be_directory_or_file = Наданий шлях повинен вказувати на дійсну директорію або файл, ігноруючи { $path } core_excluded_paths_pointless_slash = Виключення / марне, оскільки це означає, що жодні файли не будуть скановані core_paths_unable_to_get_device_id = Не вдається отримати ідентифікатор пристрою з папки { $path } core_needs_allowed_extensions_limited_by_tool = Не вдається запустити сканування, коли всі розширення, доступні в цьому інструменті ({ $extensions }), були виключені зі скану core_needs_allowed_extensions = Не вдається запустити сканування, коли всі розширення були виключені зі скану core_needs_to_set_at_least_one_broken_option = Не вдається запустити сканування, коли відсутній встановлений опція для сканування пошкоджених core_needs_to_set_at_least_one_bad_name_option = Не вдається запустити сканування, коли опція "поганого імені" не встановлена для сканування core_ffmpeg_not_found = Не вдається знайти налесну установку FFmpeg або FFprobe. Це зовнішні програми, які необхідно встановити вручну. core_ffmpeg_not_found_windows = Переконайтеся, що ffmpeg.exe і ffprobe.exe доступні в PATH або розташовані безпосередньо в тій же папці, що і виконуваний додаток core_invalid_symlink_infinite_recursion = Нескінченна рекурсія core_invalid_symlink_non_existent_destination = Неіснуючий файл призначення core_messages_limit_reached_characters = Кількість повідомлень перевищило встановлене обмеження ({ $current }/{ $limit } символів), тому результат обрізано. Щоб прочитати весь вихід, вимкніть обмежувальну опцію в налаштуваннях. core_messages_limit_reached_lines = Кількість повідомлень перевищило встановлене обмеження ({ $current }/{ $limit } рядка), тому вихід було скорочено. Щоб прочитати весь вихід, вимкніть обмежувальну опцію в налаштуваннях. core_error_moving_to_trash = Помилка при переміщенні "{ $file }" у кошик: { $error } core_error_removing = Помилка при видаленні "{ $file }": { $error } core_no_similarity_method_selected = Не вдається знайти подібні музичні файли без обраного методу подібності core_failed_to_spawn_command = Не вдалося запустити команду: { $reason } core_failed_to_check_process_status = Не вдалося перевірити статус процесу: { $reason } core_failed_to_wait_for_process = Не вдалося дочекатися процесу: { $reason } core_failed_to_read_video_properties = Не вдалося прочитати властивості відео: { $reason } core_failed_to_execute_ffmpeg = Не вдалося виконати ffmpeg: { $reason } core_ffmpeg_failed_with_status = ffmpeg не вдалося виконатися зі статусом { $status }: { $stderr } (команда: { $command }) core_failed_to_load_image_frame = Не вдалося завантажити кадр зображення: { $reason } core_failed_to_extract_frame = Не вдалося витягти кадр на { $time } секундах з "{ $file }": { $reason } core_failed_to_save_thumbnail = Не вдалося зберегти мініатюру для "{ $file }": { $reason } core_failed_get_frame_at_timestamp = Не вдалося отримати кадр о мітки часу { $timestamp } з "{ $file }": { $reason } core_failed_get_frame_from_file = Не вдалося отримати кадр з "{ $file }" у відбитку часу { $timestamp }: { $reason } core_invalid_crop_rectangle = Недійсний прямокутник обрізки: left={ $left }, top={ $top }, right={ $right }, bottom={ $bottom } core_failed_to_crop_video_file = Не вдалося обрізати відеофайл "{ $file }": { $reason } core_cropped_video_not_created = Обрізаний відеофайл не був створений: { $temp } core_unable_check_hash_of_file = Не вдалося перевірити хеш файлу "{ $file }", причина { $reason } core_error_checking_hash_of_file = Помилка сталася під час перевірки хешу файлу "{ $file }", причина { $reason } core_image_zero_dimensions = Зображення має нульову ширину або висоту "{ $path }" core_image_open_failed = Не вдається відкрити файл зображення "{ $path }": { $reason } core_not_directory_remove = Намагаюся видалити папку "{ $path }" яка не є директорією core_cannot_read_directory = Не вдається прочитати каталог "{ $path }" core_cannot_read_entry_from_directory = Не вдається прочитати запис із каталогу "{ $path }" core_folder_contains_file_inside = Папка містить файл "{ $entry }" всередині "{ $folder }" core_unknown_directory_entry = Не вдається визначити тип файлу запису каталогу "{ $entry }" всередині "{ $path }" core_video_width_exceeds_limit = Відео ширина { $width } перевищує ліміт { $limit } core_video_height_exceeds_limit = Відео висота { $height } перевищує ліміт { $limit } core_failed_to_process_video = Не вдалося обробити файл відео { $file }: { $reason } core_optimized_file_larger = Оптимізований файл { $optimized } (розмір: { $new_size }) не менший за оригінальний { $original } (розмір: { $original_size }) core_unknown_codec = Невідомий кодек: { $codec } core_invalid_video_optimizer_mode = Недійсний режим оптимізатора відео: '{ $mode }'. Допустимі значення: transcode, crop core_folder_does_not_exist = Папка не існує: { $folder } core_path_not_directory = Шлях не є директорією: { $folder } core_test_error_for_folder = Тест помилка для папки: { $folder } core_unknown_exif_tag_group = Невідомий EXIF тег група: { $tag } core_error_comparing_fingerprints = Помилка при порівнянні відбитків пальців: { $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = Не вдалося згенерувати мініатюру для "{ $file }": витягнуті кадри мають різні розміри core_failed_to_generate_thumbnail = Не вдалося згенерувати мініатюру для "{ $file }": { $reason } core_failed_to_extract_frame_at_seek_time = Не вдалося витягти кадр на { $time } секундах з "{ $file }": { $reason } core_video_file_does_not_exist = Файл відео не існує (може бути видалено між скануванням/пізнішими кроками): "{ $path }" core_image_too_large = Зображення занадто велике ({ $width }x{ $height }) - більше ніж підтримується { $max } пікселів core_failed_to_get_video_metadata = Не вдалося отримати метадані відео для файлу "{ $file }": { $reason } core_failed_to_get_video_codec = Не вдалося отримати відеокодек для файлу "{ $file }" core_failed_to_get_video_duration = Не вдалося отримати тривалість відео для файлу "{ $file }" core_failed_to_get_video_dimensions = Не вдалося отримати розміри відео для файлу "{ $file }" core_frame_dimensions_mismatch = Розміри кадру для відмітку часу { $timestamp } не відповідають розмірам першого кадру ({ $first_w }x{ $first_h }) core_failed_to_load_data_from_cache = Не вдалося завантажити дані з файлу кешу { $file }, причина { $reason } core_failed_to_load_data_from_json_cache = Не вдалося завантажити дані з JSON файлу кешу { $file}, причина { $reason } core_failed_to_replace_with_optimized = Не вдалося замінити файл "{ $file }" на оптимізовану версію: { $reason } core_failed_to_write_data_to_cache = Не вдається записати дані до кешованого файлу "{ $file }", причина { $reason } core_properly_saved_cache_entries = Правильно збережено у файл { $count } записів кешу. core_video_processing_stopped_by_user = Обробка відео була зупинена користувачем core_thumbnail_generation_stopped_by_user = Генерація превью була зупинена користувачем core_failed_to_optimize_video = Не вдалося оптимізувати відео "{ $file }": { $reason } core_failed_to_crop_video = Не вдалося обрізати відео "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = Не вдалося отримати метадані оптимізованого файлу "{ $file }": { $reason } core_cannot_create_config_folder = Не вдається створити папку конфігурації "{ $folder }", причина { $reason } core_cannot_create_cache_folder = Не вдається створити кешовий каталог "{ $folder }", причина { $reason } core_cannot_create_or_open_cache_file = Не вдається створити або відкрити файл кешу "{ $file }", причина { $reason } core_cannot_set_config_cache_path = Не вдається встановити шлях до конфігурації/кешу - конфігурація та кеш не будуть використані. core_invalid_extension_contains_space = { $extension } не є допустимим розширенням, оскільки воно містить порожній простір всередині core_invalid_extension_contains_dot = { $extension } не є допустимим розширенням, оскільки воно містить крапку всередині ================================================ FILE: czkawka_core/i18n/zh-CN/czkawka_core.ftl ================================================ # Core core_similarity_original = 原版 core_similarity_very_high = 非常高 core_similarity_high = 高 core_similarity_medium = 中 core_similarity_small = 小的 core_similarity_very_small = 非常小 core_similarity_minimal = 最小化 core_cannot_open_dir = 无法打开目录 { $dir },因为 { $reason } core_cannot_read_entry_dir = 无法在目录 { $dir } 中读取条目,因为 { $reason } core_cannot_read_metadata_dir = 无法读取目录 { $dir } 中的元数据,因为 { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = 文件 { $name } 似乎在Unix Epoch前被修改 core_folder_modified_before_epoch = 文件夹 { $name } 似乎已在Unix Epoch前被修改 core_file_no_modification_date = 无法从文件 { $name } 获取修改日期,因为 { $reason } core_folder_no_modification_date = 无法从文件夹 { $name } 获取修改日期,因为 { $reason } core_cannot_start_scan_no_included_paths = 无法启动扫描,因为没有包含的路径 core_skip_exist_check_all_included_paths_nonexistent = 无法启动扫描,因为所有包含的路径都不存在 core_missing_no_chosen_included_path = 未选择有效的包含路径(排除路径可能排除了所有包含路径) core_reference_included_paths_same = 无法在所有有效包含路径也同时是引用路径的位置开始扫描,请尝试验证或禁用引用路径 core_path_must_exists = 提供的路径必须存在,忽略 { $path } core_must_be_directory_or_file = 提供的路径必须指向一个有效的目录或文件,忽略 { $path } core_excluded_paths_pointless_slash = 排除/毫无意义,因为这意味着不会扫描任何文件 core_paths_unable_to_get_device_id = 无法从文件夹 { $path } 获取设备ID core_needs_allowed_extensions_limited_by_tool = 无法开始扫描,当此工具中所有可用扩展程序 ({ $extensions }) 都已从扫描中排除时 core_needs_allowed_extensions = 无法开始扫描,当所有扩展都已从扫描中排除时 core_needs_to_set_at_least_one_broken_option = 无法开始扫描,当未设置任何损坏选项以进行扫描 core_needs_to_set_at_least_one_bad_name_option = 无法启动扫描,当未设置“坏名称”选项进行扫描 core_ffmpeg_not_found = 无法找到合适的 FFmpeg 或 FFprobe 安装。这些是必须手动安装的外部程序。. core_ffmpeg_not_found_windows = 请确保ffmpeg.exe 和 ffprobe.exe 在 PATH 中可用或直接放入与应用可执行文件相同的文件夹 core_invalid_symlink_infinite_recursion = 无限递归性 core_invalid_symlink_non_existent_destination = 目标文件不存在 core_messages_limit_reached_characters = 消息数量超过了设置的限制 ({ $current }/{ $limit } 字符),所以输出被截断。 要读取全部输出,在设置中禁用限制选项。. core_messages_limit_reached_lines = 消息数量超过了设置的限制 ({ $current }/{ $limit } 行),所以输出被截断。 要读取全部输出,在设置中禁用限制选项。. core_error_moving_to_trash = 移动 "{ $file }" 到回收站时出错:{ $error } core_error_removing = 删除 "{ $file }" 时出错:{ $error } core_no_similarity_method_selected = 无法找到没有选择相似方法相似的音乐文件 core_failed_to_spawn_command = 未能生成命令:{ $reason } core_failed_to_check_process_status = 未能检查进程状态:{ $reason } core_failed_to_wait_for_process = 未能等待进程:{ $reason } core_failed_to_read_video_properties = 读取视频属性失败:{ $reason } core_failed_to_execute_ffmpeg = 未能执行 ffmpeg:{ $reason } core_ffmpeg_failed_with_status = ffmpeg 失败,状态 { $status }:{ $stderr } (命令:{ $command }) core_failed_to_load_image_frame = 加载图像帧失败:{ $reason } core_failed_to_extract_frame = 未能从 { $time } 秒的“{ $file }”中提取帧:{ $reason } core_failed_to_save_thumbnail = 缩略图保存失败“{ $file }”: { $reason } core_failed_get_frame_at_timestamp = 未能获取帧于时间戳 { $timestamp } 来自 "{ $file }": { $reason } core_failed_get_frame_from_file = 未能从 "{ $file }" 获取帧于 { $timestamp }:{ $reason } core_invalid_crop_rectangle = 无效作物矩形:左={ $left },上={ $top },右={ $right },下={ $bottom } core_failed_to_crop_video_file = 视频文件 "{ $file }" 裁剪失败:{ $reason } core_cropped_video_not_created = 裁剪视频文件未创建:{ $temp } core_unable_check_hash_of_file = 无法检查文件 "{ $file }" 的哈希值,原因 { $reason } core_error_checking_hash_of_file = 检查文件“{ $file }”的哈希时发生错误,原因 { $reason } core_image_zero_dimensions = 图片宽度或高度为零 "{ $path }" core_image_open_failed = 无法打开图像文件 "{ $path }": { $reason } core_not_directory_remove = 尝试删除文件夹 "{ $path }",它不是一个目录 core_cannot_read_directory = 无法读取目录 "{ $path }" core_cannot_read_entry_from_directory = 无法从目录 "{ $path }" 读取条目 core_folder_contains_file_inside = 文件夹包含文件 "{ $entry }" 在内 "{ $folder }" core_unknown_directory_entry = 无法确定目录条目 "{ $entry }" 在 "{ $path }" 内部的文件类型 core_video_width_exceeds_limit = 视频宽度 { $width } 超出 { $limit } 的限制 core_video_height_exceeds_limit = 视频高度 { $height } 超出 { $limit } 的限制 core_failed_to_process_video = 未能处理视频文件 { $file }: { $reason } core_optimized_file_larger = 优化文件 { $optimized } (大小:{ $new_size }) 未小于原始 { $original } (大小:{ $original_size }) core_unknown_codec = 未知编解码器:{ $codec } core_invalid_video_optimizer_mode = 无效视频优化模式:'{ $mode }'。允许的值:transcode, crop core_folder_does_not_exist = 文件夹不存在:{ $folder } core_path_not_directory = 路径不是一个目录:{ $folder } core_test_error_for_folder = 文件夹测试错误:{ $folder } core_unknown_exif_tag_group = 未知EXIF标签组:{ $tag } core_error_comparing_fingerprints = 比较指纹时出错:{ $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = 未能生成缩略图“{ $file }”: 提取的帧具有不同的尺寸 core_failed_to_generate_thumbnail = 未能生成缩略图“{ $file }”: { $reason } core_failed_to_extract_frame_at_seek_time = 未能从 { $time } 秒的“{ $file }”中提取帧:{ $reason } core_video_file_does_not_exist = 视频文件不存在(可删除在扫描/后续步骤之间):"{ $path }" core_image_too_large = 图片太大 ({ $width }x{ $height }) - 超过支持的 { $max } 像素 core_failed_to_get_video_metadata = 获取文件 "{ $file }" 的视频元数据失败:{ $reason } core_failed_to_get_video_codec = 无法获取文件“{ $file }”的视频编解码器 core_failed_to_get_video_duration = 无法获取文件 "{ $file }" 的视频时长 core_failed_to_get_video_dimensions = 无法获取文件 "{ $file }" 的视频尺寸 core_frame_dimensions_mismatch = 帧尺寸对于时间戳 { $timestamp } 不与第一帧尺寸 ({ $first_w }x{ $first_h }) 匹配 core_failed_to_load_data_from_cache = 无法从缓存文件 { $file } 加载数据,原因 { $reason } core_failed_to_load_data_from_json_cache = 未能从 json 缓存文件 { $file } 加载数据,原因 { $reason } core_failed_to_replace_with_optimized = 未能将文件“{ $file }”替换为优化版本:{ $reason } core_failed_to_write_data_to_cache = 无法将数据写入缓存文件 "{ $file }", 原因 { $reason } core_properly_saved_cache_entries = 正确保存到文件 { $count } 个缓存条目。. core_video_processing_stopped_by_user = 用户已停止视频处理 core_thumbnail_generation_stopped_by_user = 缩略图生成已由用户停止 core_failed_to_optimize_video = 视频优化失败 "{ $file }": { $reason } core_failed_to_crop_video = 视频裁剪失败 "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = 获取优化文件 "{ $file }" 的元数据失败:{ $reason } core_cannot_create_config_folder = 无法创建配置文件夹 "{ $folder }",原因 { $reason } core_cannot_create_cache_folder = 无法创建缓存文件夹 "{ $folder }", 原因 { $reason } core_cannot_create_or_open_cache_file = 无法创建或打开缓存文件 "{ $file }", 原因 { $reason } core_cannot_set_config_cache_path = 无法设置配置/缓存路径 - 配置和缓存将不会被使用。. core_invalid_extension_contains_space = { $extension } 不是一个有效的扩展名,因为它包含内部的空格 core_invalid_extension_contains_dot = { $extension } 不是一个有效的扩展名,因为它包含在点内 ================================================ FILE: czkawka_core/i18n/zh-TW/czkawka_core.ftl ================================================ # Core core_similarity_original = 原始 core_similarity_very_high = 極高 core_similarity_high = 高 core_similarity_medium = 中等 core_similarity_small = 小 core_similarity_very_small = 非常小 core_similarity_minimal = 最小 core_cannot_open_dir = 無法開啟目錄 { $dir },原因是 { $reason } core_cannot_read_entry_dir = 無法讀取目錄 { $dir } 中的項目,原因是 { $reason } core_cannot_read_metadata_dir = 無法讀取目錄 { $dir } 的中繼資料,原因是 { $reason } core_cannot_read_metadata_file = Cannot read metadata of file { $file }, reason { $reason } core_file_modified_before_epoch = File { $name } seems to have been modified before the Unix Epoch core_folder_modified_before_epoch = Folder { $name } seems to have been modified before the Unix Epoch core_file_no_modification_date = 無法取得檔案 { $name } 的修改日期,原因是 { $reason } core_folder_no_modification_date = 無法取得資料夾 { $name } 的修改日期,原因是 { $reason } core_cannot_start_scan_no_included_paths = 無法開始掃描,因為沒有包含的路徑 core_skip_exist_check_all_included_paths_nonexistent = 無法開始掃描,因為所有包含的路徑都不存在 core_missing_no_chosen_included_path = 無有效包含路徑被選擇 (排除路徑可能排除所有包含路徑) core_reference_included_paths_same = 無法在所有有效包含路徑同時也是參照路徑處開始掃描,請嘗試驗證或停用參照路徑 core_path_must_exists = 提供的路徑必須存在,忽略 { $path } core_must_be_directory_or_file = 提供的路徑必須指向一個有效的目錄或檔案,忽略 { $path } core_excluded_paths_pointless_slash = 排除 / 是沒有用的,因為它意味著不會掃描任何檔案 core_paths_unable_to_get_device_id = 無法從資料夾 { $path } 取得裝置 ID core_needs_allowed_extensions_limited_by_tool = 無法開始掃描,當此工具 ({ $extensions }) 中所有可用的擴充功能都已從掃描中排除時 core_needs_allowed_extensions = 無法開始掃描,當所有擴展功能已從掃描中排除 core_needs_to_set_at_least_one_broken_option = 無法開始掃描,當沒有將「損壞選項」設定為掃描時 core_needs_to_set_at_least_one_bad_name_option = 無法開始掃描,當沒有將「不良名稱」選項設定為掃描時 core_ffmpeg_not_found = 無法找到正確的 FFmpeg 或 FFprobe 安裝。這些是必須手動安裝的外部程式。. core_ffmpeg_not_found_windows = 請確保ffmpeg.exe和ffprobe.exe可用於PATH,或直接放置在與應用程式執行檔同一資料夾中。 core_invalid_symlink_infinite_recursion = 無限遞迴 core_invalid_symlink_non_existent_destination = 目標檔案不存在 core_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. core_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. core_error_moving_to_trash = 移動"{ $file }"到垃圾桶時出現錯誤:{ $error } core_error_removing = 移除"{ $file }"時發生錯誤:{ $error } core_no_similarity_method_selected = 無法找到沒有選擇相似度方法的類似音樂檔案 core_failed_to_spawn_command = 未能生成命令:{ $reason } core_failed_to_check_process_status = 無法檢查進程狀態:{ $reason } core_failed_to_wait_for_process = 未能等待處理:{ $reason } core_failed_to_read_video_properties = 無法讀取影片屬性:{ $reason } core_failed_to_execute_ffmpeg = 無法執行 ffmpeg:{ $reason } core_ffmpeg_failed_with_status = ffmpeg 失敗,狀態為 { $status }:{ $stderr } (命令:{ $command }) core_failed_to_load_image_frame = 無法載入圖片畫面:{ $reason } core_failed_to_extract_frame = 在 { $time } 秒處未能提取幀來自 "{ $file }": { $reason } core_failed_to_save_thumbnail = 無法儲存 "{ $file }" 的縮圖:{ $reason } core_failed_get_frame_at_timestamp = 未能於時間戳 { $timestamp } 從 "{ $file }" 取得畫面:{ $reason } core_failed_get_frame_from_file = 從 "{ $file }" 在 { $timestamp } 標記時間取得畫面失敗:{ $reason } core_invalid_crop_rectangle = 無效的作物矩形:左={ $left },上={ $top },右={ $right },下={ $bottom } core_failed_to_crop_video_file = 無法裁剪影片檔案 "{ $file }": { $reason } core_cropped_video_not_created = 裁剪後的影片檔案未建立:{ $temp } core_unable_check_hash_of_file = 無法檢查檔案 "{ $file }" 的雜項,原因 { $reason } core_error_checking_hash_of_file = 檢查檔案 "{ $file }" 的雜項時發生錯誤,原因 { $reason } core_image_zero_dimensions = 圖片具有零寬度或高度 "{ $path }" core_image_open_failed = 無法開啟圖片檔案 "{ $path }": { $reason } core_not_directory_remove = 嘗試移除檔案夾 "{ $path }",它不是一個目錄 core_cannot_read_directory = 無法讀取目錄 "{ $path }" core_cannot_read_entry_from_directory = 無法從目錄 "{ $path }" 讀取資料 core_folder_contains_file_inside = 資料夾內包含檔案 "{ $entry }" 位於 "{ $folder }" core_unknown_directory_entry = 無法判斷目錄入口 "{ $entry }" 內部的檔案類型 inside "{ $path }" core_video_width_exceeds_limit = 影片寬度 { $width } 超過 { $limit } 的限制 core_video_height_exceeds_limit = 影片高度 { $height } 超過 { $limit } 的限制 core_failed_to_process_video = 無法處理影片檔案 { $file }: { $reason } core_optimized_file_larger = 優化檔案 { $optimized } (大小:{ $new_size }) 未超過原始檔案 { $original } (大小:{ $original_size }) core_unknown_codec = 未知編碼格式:{ $codec } core_invalid_video_optimizer_mode = 無效的影片優化模式:'{ $mode }'。允許的值:transcode, crop core_folder_does_not_exist = 資料夾不存在:{ $folder } core_path_not_directory = 路徑不是一個目錄:{ $folder } core_test_error_for_folder = 測試資料夾錯誤:{ $folder } core_unknown_exif_tag_group = 未知的 EXIF 標籤群組:{ $tag } core_error_comparing_fingerprints = 比較指紋時發生錯誤:{ $reason } core_failed_to_generate_thumbnail_frames_different_dimensions = 未能為 "{ $file }" 產生縮圖:提取的幀有不同尺寸 core_failed_to_generate_thumbnail = 未能為 "{ $file }" 產生縮圖:{ $reason } core_failed_to_extract_frame_at_seek_time = 在 { $time } 秒處未能提取幀來自 "{ $file }": { $reason } core_video_file_does_not_exist = 影片檔案不存在 (可移除於掃描/後續步驟中):"{ $path }" core_image_too_large = 圖片太大 ({ $width }x{ $height }) - 超過支援 { $max } 像素 core_failed_to_get_video_metadata = 無法取得檔案 "{ $file }" 的影片元資料:{ $reason } core_failed_to_get_video_codec = 無法取得檔案 "{ $file }" 的影片碼通 core_failed_to_get_video_duration = 無法取得檔案 "{ $file }" 的影片長度 core_failed_to_get_video_dimensions = 無法取得檔案 "{ $file }" 的影片尺寸 core_frame_dimensions_mismatch = 時間戳 { $timestamp } 的畫框尺寸與第一個畫框尺寸 ({ $first_w }x{ $first_h }) 不符 core_failed_to_load_data_from_cache = 無法從快取檔案 { $file } 加載資料,原因 { $reason } core_failed_to_load_data_from_json_cache = 未能從 JSON 緩存檔案 { $file } 加載資料,原因 { $reason } core_failed_to_replace_with_optimized = 未能將檔案 "{ $file }" 替換為優化版本:{ $reason } core_failed_to_write_data_to_cache = 無法將資料寫入快取檔案 "{ $file }", 原因 { $reason } core_properly_saved_cache_entries = 正確儲存至檔案 { $count } 個緩存條目。. core_video_processing_stopped_by_user = 使用者已停止影片處理 core_thumbnail_generation_stopped_by_user = 使用者已停止生成縮圖 core_failed_to_optimize_video = 無法優化影片 "{ $file }": { $reason } core_failed_to_crop_video = 視頻裁剪失敗 "{ $file }": { $reason } core_failed_to_get_metadata_of_optimized_file = 無法取得優化檔案 "{ $file }" 的元資料:{ $reason } core_cannot_create_config_folder = 無法建立配置資料夾 "{ $folder }",原因 { $reason } core_cannot_create_cache_folder = 無法建立快取資料夾 "{ $folder }",原因 { $reason } core_cannot_create_or_open_cache_file = 無法建立或開啟快取檔案 "{ $file }",原因 { $reason } core_cannot_set_config_cache_path = 無法設定 config/cache 路径 - config 和 cache 将不会被使用。. core_invalid_extension_contains_space = { $extension } 不是一個有效的擴充類型,因為它包含內部的空白 core_invalid_extension_contains_dot = { $extension } 不是一個有效的擴充類型,因為它包含內部的點。 ================================================ FILE: czkawka_core/i18n.toml ================================================ # (Required) The language identifier of the language used in the # source code for gettext system, and the primary fallback language # (for which all strings must be present) when using the fluent # system. fallback_language = "en" # Use the fluent localization system. [fluent] # (Required) The path to the assets directory. # The paths inside the assets directory should be structured like so: # `assets_dir/{language}/{domain}.ftl` assets_dir = "i18n" ================================================ FILE: czkawka_core/src/common/basic_gui_cli.rs ================================================ use std::process; use log::{error, warn}; use crate::common::config_cache_path::get_config_cache_path; use crate::{CZKAWKA_VERSION, flc}; #[derive(Clone, Debug)] pub struct CliResult { pub included_items: Vec, pub excluded_items: Vec, pub referenced_items: Vec, } enum ExpectedArgs { Include, Exclude, Referenced, } // Manual processing of CLI arguments, because Clap would be too heavy for this simple task #[expect(clippy::print_stdout)] #[expect(clippy::print_stderr)] pub fn process_cli_args(app_display: &str, app_exec: &str, args: Vec) -> Option { if ["--help", "-h"].iter().any(|&arg| args.contains(&arg.to_string())) { println!("{app_display}"); println!("{app_display} allows you to specify folders to search for files via the CLI, and also to exclude or reference folders."); println!("If used, it will automatically apply the last preset and load its options."); println!("Running the app without arguments will launch the {app_display} with default or saved options."); println!("Usage: {app_exec} [OPTIONS] [FOLDERS...]"); println!("Options:"); println!(" FOLDER Include a folder in the search"); println!(" -e FOLDER, --exclude FOLDER Exclude a folder from the search"); println!(" -r FOLDER, --referenced FOLDER Include a folder and set it as referenced"); println!(" --cache, -c Opens the cache folder"); println!(" --config, -C Opens the config folder"); println!(" --help, -h Show this help message"); println!(" --version, -v Show version information"); println!("Examples:"); println!(" {app_exec} /path/absolute/to/folder -e relative_path/2 -r /path/to/referenced"); println!(" {app_exec} . folder2 folder3"); println!("If no folders are specified, the program will exit without doing anything."); process::exit(0); } if ["--version", "-v"].iter().any(|&arg| args.contains(&arg.to_string())) { let git_commit = env!("CZKAWKA_GIT_COMMIT_SHORT"); let official_build = if env!("CZKAWKA_OFFICIAL_BUILD") == "1" { "O" // Official build } else { "U" // Unofficial build }; let git_date = env!("CZKAWKA_GIT_COMMIT_DATE"); println!("{app_display} version {CZKAWKA_VERSION}({git_commit} {official_build} {git_date})"); process::exit(0); } let mut expected_arg = ExpectedArgs::Include; let mut cli_result = CliResult { included_items: Vec::new(), excluded_items: Vec::new(), referenced_items: Vec::new(), }; let mut errors = Vec::new(); for arg in args { if arg.starts_with("-") { match arg.as_str() { "-e" | "--exclude" => expected_arg = ExpectedArgs::Exclude, "-r" | "--referenced" => expected_arg = ExpectedArgs::Referenced, "-c" | "--cache" => { if let Some(cfg) = get_config_cache_path() { if let Err(e) = open::that(&cfg.cache_folder) { error!("Failed to open cache folder \"{}\": {e}", cfg.cache_folder.to_string_lossy()); process::exit(1); } process::exit(0); } else { error!("Failed to get cache folder path"); process::exit(1); } } "-C" | "--config" => { if let Some(cfg) = get_config_cache_path() { if let Err(e) = open::that(&cfg.config_folder) { error!("Failed to open config folder \"{}\": {e}", cfg.config_folder.to_string_lossy()); process::exit(1); } process::exit(0); } else { error!("Failed to get config folder path"); process::exit(1); } } _ => { eprintln!("Unknown option: {arg}"); process::exit(1); } } } else { match expected_arg { ExpectedArgs::Include => match check_if_folder_is_valid(&arg) { Ok(folder) => cli_result.included_items.push(folder), Err(e) => errors.push(e), }, ExpectedArgs::Exclude => match check_if_folder_is_valid(&arg) { Ok(folder) => cli_result.excluded_items.push(folder), Err(e) => errors.push(e), }, ExpectedArgs::Referenced => match check_if_folder_is_valid(&arg) { Ok(folder) => { cli_result.included_items.push(folder.clone()); cli_result.referenced_items.push(folder); } Err(e) => errors.push(e), }, } expected_arg = ExpectedArgs::Include; } } deduplicate_folders(&mut cli_result.included_items); deduplicate_folders(&mut cli_result.excluded_items); deduplicate_folders(&mut cli_result.referenced_items); if !errors.is_empty() { warn!("Errors encountered while processing CLI arguments:"); } for error in &errors { warn!("{error}"); } if cli_result.included_items.is_empty() && cli_result.excluded_items.is_empty() && cli_result.referenced_items.is_empty() { None } else { Some(cli_result) } } fn deduplicate_folders(folder_list: &mut Vec) { folder_list.sort(); folder_list.dedup(); } #[cfg(not(test))] fn check_if_folder_is_valid(folder: &str) -> Result { let path = std::path::Path::new(folder); if !path.exists() { return Err(flc!("core_folder_does_not_exist", folder = folder)); } if !path.is_dir() { return Err(flc!("core_path_not_directory", folder = folder)); } let canonical_path = dunce::canonicalize(path).map_err(|e| format!("Failed to canonicalize path: {folder}. Error: {e}"))?; Ok(canonical_path.to_string_lossy().to_string()) } #[cfg(test)] fn check_if_folder_is_valid(folder: &str) -> Result { if folder.contains("test_error") { return Err(flc!("core_test_error_for_folder", folder = folder)); } Ok(folder.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn processes_include_folder() { let args = vec!["/valid/folder".to_string()]; let result = process_cli_args("A", "B", args).expect("TEST"); assert_eq!(result.included_items, vec!["/valid/folder".to_string()]); assert!(result.excluded_items.is_empty()); assert!(result.referenced_items.is_empty()); } #[test] fn processes_exclude_folder() { let args = vec!["-e".to_string(), "/valid/folder".to_string()]; let result = process_cli_args("A", "B", args).expect("TEST"); assert!(result.included_items.is_empty()); assert_eq!(result.excluded_items, vec!["/valid/folder".to_string()]); assert!(result.referenced_items.is_empty()); } #[test] fn processes_referenced_folder() { let args = vec!["-r".to_string(), "/valid/folder".to_string()]; let result = process_cli_args("A", "B", args).expect("TEST"); assert_eq!(result.included_items, vec!["/valid/folder".to_string()]); assert!(result.excluded_items.is_empty()); assert_eq!(result.referenced_items, vec!["/valid/folder".to_string()]); } #[test] fn processes_multiple_same_folder() { let args = [ "-r", "/valid/folder", "-r", "/valid/folder", "normal_folder", "abcd", "abcd", "-e", "/exclu", "normal_folder", ] .iter() .map(|s| s.to_string()) .collect(); let result = process_cli_args("A", "B", args).expect("TEST"); assert_eq!(result.included_items, vec!["/valid/folder".to_string(), "abcd".to_string(), "normal_folder".to_string()]); assert_eq!(result.excluded_items, vec!["/exclu".to_string()]); assert_eq!(result.referenced_items, vec!["/valid/folder".to_string()]); } #[test] fn handles_invalid_folder() { let args = vec!["/invalid/test_error".to_string()]; let result = process_cli_args("A", "B", args); assert!(result.is_none()); } #[test] fn handles_no_arguments() { let args = Vec::new(); let result = process_cli_args("A", "B", args); assert!(result.is_none()); } } ================================================ FILE: czkawka_core/src/common/cache/cleaning.rs ================================================ use std::fs; use std::io::{BufReader, BufWriter}; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use bincode::Options; use crossbeam_channel::Sender; use fun_time::fun_time; use log::{debug, error}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use crate::common::cache::{ CACHE_BROKEN_FILES_VERSION, CACHE_CLEANING_INTERVAL_SECONDS, CACHE_DUPLICATE_VERSION, CACHE_IMAGE_VERSION, CACHE_VERSION, CACHE_VIDEO_OPTIMIZE_VERSION, CACHE_VIDEO_VERSION, CLEANING_TIMESTAMPS_FILE, MEMORY_LIMIT, }; use crate::common::config_cache_path::get_config_cache_path; use crate::common::traits::ResultEntry; use crate::tools::broken_files::BrokenEntry; use crate::tools::duplicate::DuplicateEntry; use crate::tools::exif_remover::ExifEntry; use crate::tools::same_music::MusicEntry; use crate::tools::similar_images::ImagesEntry; use crate::tools::similar_videos::VideosEntry; use crate::tools::video_optimizer::{VideoCropEntry, VideoTranscodeEntry}; #[derive(Debug, Clone, Default)] pub struct CacheCleaningStatistics { pub total_files_found: usize, pub successfully_cleaned: usize, pub files_with_errors: usize, pub total_entries_before: usize, pub total_entries_removed: usize, pub total_entries_left: usize, pub total_size_before: u64, pub total_size_after: u64, pub errors: Vec, } #[derive(Debug, Clone, Default)] pub struct CacheProgressCleaning { pub current_cache_file: usize, pub total_cache_files: usize, pub current_file_name: String, pub checked_entries: usize, pub all_entries: usize, } #[derive(Deserialize, Serialize, Debug)] struct CleaningTimestamps { timestamps: Vec, } #[derive(Deserialize, Serialize, Debug)] struct SingleCleaningTimestamp { cache_file_name: String, last_cleaned_timestamp: u64, } fn get_timestamps_file_path() -> Option { get_config_cache_path().map(|config| config.cache_folder.join(CLEANING_TIMESTAMPS_FILE)) } pub(crate) fn should_clean_cache(cache_file_name: &str) -> bool { let Some(timestamps_file) = get_timestamps_file_path() else { return true; }; let Ok(content) = fs::read_to_string(×tamps_file) else { return true; }; let cleaning_timestamps = match serde_json::from_str::(&content) { Ok(t) => t, Err(e) => { error!( "Failed to parse cleaning timestamps file \"{}\" while processing cache file \"{cache_file_name}\" - {e:?}", timestamps_file.to_string_lossy() ); return true; } }; let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); if let Some(timestamp) = cleaning_timestamps.timestamps.iter().find(|t| t.cache_file_name == cache_file_name) { let time_since_last_cleaning = current_time.saturating_sub(timestamp.last_cleaned_timestamp); if time_since_last_cleaning < *CACHE_CLEANING_INTERVAL_SECONDS { debug!( "Last cleaning for {} was {} seconds ago, which is less than the configured interval of {} seconds. Skipping cleaning.", cache_file_name, time_since_last_cleaning, *CACHE_CLEANING_INTERVAL_SECONDS ); return false; } debug!( "Last cleaning for {} was {} seconds ago, which exceeds the configured interval of {} seconds. Proceeding with cleaning.", cache_file_name, time_since_last_cleaning, *CACHE_CLEANING_INTERVAL_SECONDS ); return true; } debug!("No cleaning timestamp found for {cache_file_name}, cache cleaning should run"); true } pub(crate) fn update_cleaning_timestamp(cache_file_name: &str) { let Some(timestamps_file) = get_timestamps_file_path() else { return; }; let mut cleaning_timestamps = if let Ok(content) = fs::read_to_string(×tamps_file) { serde_json::from_str::(&content).unwrap_or_else(|e| { error!("Failed to parse cleaning timestamps file \"{}\" content - {e:?}", timestamps_file.to_string_lossy()); CleaningTimestamps { timestamps: Vec::new() } }) } else { CleaningTimestamps { timestamps: Vec::new() } }; let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); if let Some(timestamp) = cleaning_timestamps.timestamps.iter_mut().find(|t| t.cache_file_name == cache_file_name) { timestamp.last_cleaned_timestamp = current_time; } else { cleaning_timestamps.timestamps.push(SingleCleaningTimestamp { cache_file_name: cache_file_name.to_string(), last_cleaned_timestamp: current_time, }); } if let Ok(serialized) = serde_json::to_string_pretty(&cleaning_timestamps) { if let Err(e) = fs::write(×tamps_file, serialized) { error!("Failed to write cleaning timestamps to file {}: {e}", timestamps_file.to_string_lossy()); } } else { error!("Failed to serialize cleaning timestamps"); } } #[derive(Debug)] enum CacheType { Duplicates, MusicTags, MusicFingerprints, SimilarImages, SimilarVideos, BrokenFiles, ExifRemover, VideoTranscode, VideoCrop, } impl CacheType { fn from_filename(filename: &str) -> Option { if filename.starts_with("cache_duplicates_") && filename.ends_with(&format!("_{CACHE_DUPLICATE_VERSION}.bin")) { Some(Self::Duplicates) } else if filename == format!("cache_same_music_tags_{CACHE_VERSION}.bin") { Some(Self::MusicTags) } else if filename == format!("cache_same_music_fingerprints_{CACHE_VERSION}.bin") { Some(Self::MusicFingerprints) } else if filename.starts_with("cache_similar_images_") && filename.ends_with(&format!("_{CACHE_IMAGE_VERSION}.bin")) { Some(Self::SimilarImages) } else if filename.starts_with(&format!("cache_similar_videos_{CACHE_VIDEO_VERSION}__")) && filename.ends_with(".bin") { Some(Self::SimilarVideos) } else if filename == format!("cache_broken_files_{CACHE_BROKEN_FILES_VERSION}.bin") { Some(Self::BrokenFiles) } else if filename == format!("cache_exif_remover_{CACHE_VERSION}.bin") { Some(Self::ExifRemover) } else if filename == format!("cache_video_transcode_{CACHE_VIDEO_OPTIMIZE_VERSION}.bin") { Some(Self::VideoTranscode) } else if filename.starts_with(&format!("cache_video_crop_{CACHE_VIDEO_OPTIMIZE_VERSION}_")) && filename.ends_with(".bin") { Some(Self::VideoCrop) } else { None } } } #[fun_time(message = "clean_all_cache_files", level = "debug")] pub fn clean_all_cache_files(stop_flag: &Arc, cache_progress_sender: Option<&Sender>) -> Result { let mut stats = CacheCleaningStatistics::default(); let Some(config_cache_path) = get_config_cache_path() else { return Err("Cannot get cache folder path".to_string()); }; let cache_folder = &config_cache_path.cache_folder; let entries = fs::read_dir(cache_folder).map_err(|e| format!("Cannot read cache folder \"{}\": {}", cache_folder.to_string_lossy(), e))?; let cache_files: Vec<_> = entries .flatten() .filter_map(|entry| { let path = entry.path(); if !path.is_file() { return None; } let file_name = path.file_name()?.to_str()?.to_string(); let cache_type = CacheType::from_filename(&file_name)?; Some((path, file_name, cache_type)) }) .collect(); let total_files = cache_files.len(); let current_file = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let current_file_name = Arc::new(std::sync::Mutex::new(String::new())); let checked_entries = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let all_entries = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let progress_thread = cache_progress_sender.map(|sender| { let sender = sender.clone(); let stop_flag = stop_flag.clone(); let current_file = current_file.clone(); let current_file_name = current_file_name.clone(); let checked_entries = checked_entries.clone(); let all_entries = all_entries.clone(); std::thread::spawn(move || { while !stop_flag.load(Ordering::Relaxed) { std::thread::sleep(std::time::Duration::from_millis(100)); let current = current_file.load(Ordering::Relaxed); let name = current_file_name.lock().expect("Mutex poisoned").clone(); let checked = checked_entries.load(Ordering::Relaxed); let all = all_entries.load(Ordering::Relaxed); if current > 0 { let _ = sender.send(CacheProgressCleaning { current_cache_file: current, total_cache_files: total_files, current_file_name: name, checked_entries: checked, all_entries: all, }); } } }) }); for (current_file_idx, (path, file_name, cache_type)) in cache_files.into_iter().enumerate() { if stop_flag.load(Ordering::Relaxed) { return Err("Operation stopped by user".to_string()); } stats.total_files_found += 1; debug!("Found cache file to clean: {file_name} (type: {cache_type:?})"); current_file.store(current_file_idx + 1, Ordering::Relaxed); *current_file_name.lock().expect("Lock poisoned") = file_name.clone(); checked_entries.store(0, Ordering::Relaxed); all_entries.store(0, Ordering::Relaxed); let result = match cache_type { CacheType::Duplicates => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::MusicTags | CacheType::MusicFingerprints => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::SimilarImages => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::SimilarVideos => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::BrokenFiles => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::ExifRemover => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::VideoTranscode => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), CacheType::VideoCrop => clean_cache_file_typed::(&path, stop_flag, &checked_entries, &all_entries), }; match result { Ok(Some((before, after, size_before, size_after))) => { stats.successfully_cleaned += 1; stats.total_entries_before += before; stats.total_entries_left += after; stats.total_entries_removed += before - after; stats.total_size_before += size_before; stats.total_size_after += size_after; update_cleaning_timestamp(&file_name); } Ok(None) => { debug!("Cleaning of cache file {file_name} was skipped due to stop flag"); return Err("Operation stopped by user".to_string()); } Err(e) => { stats.files_with_errors += 1; stats.errors.push(format!("{file_name}: {e}")); } } } stop_flag.store(true, Ordering::Relaxed); if let Some(handle) = progress_thread { let _ = handle.join(); } Ok(stats) } fn clean_cache_file_typed( cache_path: &Path, stop_flag: &Arc, checked_entries: &Arc, all_entries: &Arc, ) -> Result, String> where for<'a> T: Deserialize<'a> + ResultEntry + Serialize + Clone + Send, { let size_before = fs::metadata(cache_path).map(|m| m.len()).unwrap_or(0); let file = fs::File::open(cache_path).map_err(|e| format!("Cannot open file: {e}"))?; let reader = BufReader::new(file); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); let entries: Vec = options.deserialize_from(reader).map_err(|e| format!("Cannot deserialize file: {e}"))?; let original_count = entries.len(); all_entries.store(original_count, Ordering::Relaxed); let checked_entries_clone = checked_entries.clone(); let filtered_entries: Vec = entries .into_par_iter() .map(|cached_entry| { if stop_flag.load(Ordering::Relaxed) { return None; } checked_entries_clone.fetch_add(1, Ordering::Relaxed); let Ok(metadata) = fs::metadata(cached_entry.get_path()) else { return Some(None); }; if metadata.len() != cached_entry.get_size() { return Some(None); } if let Ok(modified_time) = metadata.modified() { if let Ok(duration_since_epoch) = modified_time.duration_since(std::time::UNIX_EPOCH) { if duration_since_epoch.as_secs() != cached_entry.get_modified_date() { return Some(None); } } else { return Some(None); } } Some(Some(cached_entry)) }) .while_some() .flatten() .collect(); if stop_flag.load(Ordering::Relaxed) { return Ok(None); } let remaining_count = filtered_entries.len(); let removed_count = original_count - remaining_count; let size_after = if removed_count > 0 { let tmp_file_path = cache_path.with_extension("tmp"); let tmp_file = fs::File::create(&tmp_file_path).map_err(|e| format!("Cannot create temporary file: {e}"))?; let writer = BufWriter::new(tmp_file); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); options .serialize_into(writer, &filtered_entries) .map_err(|e| format!("Cannot serialize cleaned data to temporary file: {e}"))?; let new_size = fs::metadata(&tmp_file_path).map(|m| m.len()).unwrap_or(size_before); fs::rename(&tmp_file_path, cache_path).map_err(|e| format!("Cannot replace original cache file: {e}"))?; debug!( "Cleaned cache file \"{}\": removed {} entries, {} remaining, size reduced from {} to {} bytes", cache_path.to_string_lossy(), removed_count, filtered_entries.len(), size_before, new_size ); new_size } else { size_before }; Ok(Some((original_count, remaining_count, size_before, size_after))) } #[cfg(test)] mod tests { use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::UNIX_EPOCH; use bincode::Options; use serde::{Deserialize, Serialize}; use tempfile::TempDir; use super::*; use crate::common::cache::tests::setup_cache_path; #[derive(Clone, Debug, Serialize, Deserialize)] struct TestCacheEntry { path: PathBuf, size: u64, modified_date: u64, data: String, } impl ResultEntry for TestCacheEntry { fn get_path(&self) -> &Path { &self.path } fn get_size(&self) -> u64 { self.size } fn get_modified_date(&self) -> u64 { self.modified_date } } fn setup_test_env() -> (PathBuf, PathBuf) { setup_cache_path(); let config_cache = get_config_cache_path().unwrap(); (config_cache.cache_folder.clone(), config_cache.config_folder) } fn create_test_file(dir: &Path, name: &str, content: &str) -> (PathBuf, u64, u64) { let path = dir.join(name); fs::write(&path, content).unwrap(); let metadata = fs::metadata(&path).unwrap(); let modified = metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(); (path, metadata.len(), modified) } fn create_cache_file(cache_dir: &Path, name: &str, entries: &[TestCacheEntry]) -> PathBuf { let cache_path = cache_dir.join(name); let file = fs::File::create(&cache_path).unwrap(); let writer = BufWriter::new(file); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); options.serialize_into(writer, entries).unwrap(); cache_path } #[test] fn test_timestamp_operations_and_should_clean() { let (_cache_dir, _config_dir) = setup_test_env(); let cache_name = format!("test_cache_{}", std::process::id()); assert!(should_clean_cache(&cache_name)); update_cleaning_timestamp(&cache_name); assert!(!should_clean_cache(&cache_name)); update_cleaning_timestamp(&cache_name); assert!(!should_clean_cache(&cache_name)); let different_cache = format!("different_cache_{}", std::process::id()); assert!(should_clean_cache(&different_cache)); update_cleaning_timestamp(&different_cache); assert!(!should_clean_cache(&different_cache)); assert!(!should_clean_cache(&cache_name)); } #[test] fn test_clean_cache_file_typed_mixed_scenarios() { let (cache_dir, _config_dir) = setup_test_env(); let data_dir = TempDir::new().unwrap(); let (valid_path, valid_size, valid_modified) = create_test_file(data_dir.path(), "valid.txt", "valid content"); let (modified_path, _, old_modified) = create_test_file(data_dir.path(), "modified.txt", "old content"); std::thread::sleep(std::time::Duration::from_millis(100)); fs::write(&modified_path, "new content with different size").unwrap(); let (deleted_path, deleted_size, deleted_modified) = create_test_file(data_dir.path(), "deleted.txt", "to be deleted"); fs::remove_file(&deleted_path).unwrap(); let entries = vec![ TestCacheEntry { path: valid_path.clone(), size: valid_size, modified_date: valid_modified, data: "valid".to_string(), }, TestCacheEntry { path: modified_path, size: 11, modified_date: old_modified, data: "modified".to_string(), }, TestCacheEntry { path: deleted_path, size: deleted_size, modified_date: deleted_modified, data: "deleted".to_string(), }, ]; let cache_path = create_cache_file(cache_dir.as_path(), "test_cache.bin", &entries); let stop_flag = Arc::new(AtomicBool::new(false)); let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let all = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let result = clean_cache_file_typed::(&cache_path, &stop_flag, &checked, &all).unwrap(); assert!(result.is_some()); let (original, remaining, _, _) = result.unwrap(); assert_eq!(original, 3); assert_eq!(remaining, 1); assert_eq!(checked.load(Ordering::Relaxed), 3); assert_eq!(all.load(Ordering::Relaxed), 3); let file = fs::File::open(&cache_path).unwrap(); let reader = BufReader::new(file); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); let cleaned_entries: Vec = options.deserialize_from(reader).unwrap(); assert_eq!(cleaned_entries.len(), 1); assert_eq!(cleaned_entries[0].path, valid_path); } #[test] fn test_clean_cache_file_with_stop_flag() { let (cache_dir, _config_dir) = setup_test_env(); let data_dir = TempDir::new().unwrap(); const ENTRIES_NUMBER: usize = 100; let mut entries = Vec::new(); for i in 0..ENTRIES_NUMBER { let (path, size, modified) = create_test_file(data_dir.path(), &format!("file_{i}.txt"), &format!("content {i}")); entries.push(TestCacheEntry { path, size, modified_date: modified, data: format!("data {i}"), }); } let cache_path = create_cache_file(cache_dir.as_path(), "test_stop.bin", &entries); let stop_flag = Arc::new(AtomicBool::new(false)); let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let all = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let stop_flag_clone = stop_flag.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(1)); stop_flag_clone.store(true, Ordering::Relaxed); }); // Well - it may fail in any place, so we just cannot check exact number of checked entries let result = clean_cache_file_typed::(&cache_path, &stop_flag, &checked, &all).unwrap(); if result.is_some() { assert!(checked.load(Ordering::Relaxed) <= ENTRIES_NUMBER); } } #[test] fn test_cache_type_from_filename_all_variants() { assert!(matches!( CacheType::from_filename(&format!("cache_duplicates_hash_{CACHE_DUPLICATE_VERSION}.bin")), Some(CacheType::Duplicates) )); assert!(matches!( CacheType::from_filename(&format!("cache_duplicates_size_{CACHE_DUPLICATE_VERSION}.bin")), Some(CacheType::Duplicates) )); assert!(matches!( CacheType::from_filename(&format!("cache_same_music_tags_{CACHE_VERSION}.bin")), Some(CacheType::MusicTags) )); assert!(matches!( CacheType::from_filename(&format!("cache_same_music_fingerprints_{CACHE_VERSION}.bin")), Some(CacheType::MusicFingerprints) )); assert!(matches!( CacheType::from_filename(&format!("cache_similar_images_8_{CACHE_IMAGE_VERSION}.bin")), Some(CacheType::SimilarImages) )); assert!(matches!( CacheType::from_filename(&format!("cache_similar_videos_{CACHE_VIDEO_VERSION}__10.bin")), Some(CacheType::SimilarVideos) )); assert!(matches!( CacheType::from_filename(&format!("cache_broken_files_{CACHE_BROKEN_FILES_VERSION}.bin")), Some(CacheType::BrokenFiles) )); assert!(matches!( CacheType::from_filename(&format!("cache_exif_remover_{CACHE_VERSION}.bin")), Some(CacheType::ExifRemover) )); assert!(matches!( CacheType::from_filename(&format!("cache_video_transcode_{CACHE_VIDEO_OPTIMIZE_VERSION}.bin")), Some(CacheType::VideoTranscode) )); assert!(matches!( CacheType::from_filename(&format!("cache_video_crop_{CACHE_VIDEO_OPTIMIZE_VERSION}_test.bin")), Some(CacheType::VideoCrop) )); assert!(CacheType::from_filename("invalid_cache.bin").is_none()); assert!(CacheType::from_filename("cache_duplicates_99.bin").is_none()); assert!(CacheType::from_filename("random_file.txt").is_none()); } #[test] fn test_clean_cache_file_no_changes_needed() { let (cache_dir, _config_dir) = setup_test_env(); let data_dir = TempDir::new().unwrap(); let mut entries = Vec::new(); for i in 0..5 { let (path, size, modified) = create_test_file(data_dir.path(), &format!("valid_{i}.txt"), &format!("valid content {i}")); entries.push(TestCacheEntry { path, size, modified_date: modified, data: format!("data {i}"), }); } let cache_path = create_cache_file(cache_dir.as_path(), "test_no_changes.bin", &entries); let size_before = fs::metadata(&cache_path).unwrap().len(); let stop_flag = Arc::new(AtomicBool::new(false)); let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let all = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let result = clean_cache_file_typed::(&cache_path, &stop_flag, &checked, &all).unwrap(); assert!(result.is_some()); let (original, remaining, size_before_result, size_after) = result.unwrap(); assert_eq!(original, 5); assert_eq!(remaining, 5); assert_eq!(size_before_result, size_before); assert_eq!(size_after, size_before); } #[test] fn test_clean_cache_file_all_entries_invalid() { let (cache_dir, _config_dir) = setup_test_env(); let data_dir = TempDir::new().unwrap(); let (deleted1, size1, mod1) = create_test_file(data_dir.path(), "del1.txt", "content 1"); let (deleted2, size2, mod2) = create_test_file(data_dir.path(), "del2.txt", "content 2"); let (deleted3, size3, mod3) = create_test_file(data_dir.path(), "del3.txt", "content 3"); fs::remove_file(&deleted1).unwrap(); fs::remove_file(&deleted2).unwrap(); fs::remove_file(&deleted3).unwrap(); let entries = vec![ TestCacheEntry { path: deleted1, size: size1, modified_date: mod1, data: "1".to_string(), }, TestCacheEntry { path: deleted2, size: size2, modified_date: mod2, data: "2".to_string(), }, TestCacheEntry { path: deleted3, size: size3, modified_date: mod3, data: "3".to_string(), }, ]; let cache_path = create_cache_file(cache_dir.as_path(), "test_all_invalid.bin", &entries); let stop_flag = Arc::new(AtomicBool::new(false)); let checked = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let all = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let result = clean_cache_file_typed::(&cache_path, &stop_flag, &checked, &all).unwrap(); assert!(result.is_some()); let (original, remaining, _, _) = result.unwrap(); assert_eq!(original, 3); assert_eq!(remaining, 0); let file = fs::File::open(&cache_path).unwrap(); let reader = BufReader::new(file); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); let cleaned_entries: Vec = options.deserialize_from(reader).unwrap(); assert_eq!(cleaned_entries.len(), 0); } #[test] fn test_cache_progress_cleaning_struct() { let progress = CacheProgressCleaning { current_cache_file: 3, total_cache_files: 10, current_file_name: "test_cache.bin".to_string(), checked_entries: 50, all_entries: 100, }; assert_eq!(progress.current_cache_file, 3); assert_eq!(progress.total_cache_files, 10); assert_eq!(progress.current_file_name, "test_cache.bin"); assert_eq!(progress.checked_entries, 50); assert_eq!(progress.all_entries, 100); } #[test] fn test_cleaning_timestamps_serialization() { let timestamps = CleaningTimestamps { timestamps: vec![ SingleCleaningTimestamp { cache_file_name: "cache1.bin".to_string(), last_cleaned_timestamp: 1000, }, SingleCleaningTimestamp { cache_file_name: "cache2.bin".to_string(), last_cleaned_timestamp: 2000, }, ], }; let serialized = serde_json::to_string(×tamps).unwrap(); let deserialized: CleaningTimestamps = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.timestamps.len(), 2); assert_eq!(deserialized.timestamps[0].cache_file_name, "cache1.bin"); assert_eq!(deserialized.timestamps[0].last_cleaned_timestamp, 1000); assert_eq!(deserialized.timestamps[1].cache_file_name, "cache2.bin"); assert_eq!(deserialized.timestamps[1].last_cleaned_timestamp, 2000); } } ================================================ FILE: czkawka_core/src/common/cache.rs ================================================ #![allow(clippy::useless_let_if_seq)] mod cleaning; use std::collections::BTreeMap; use std::io::{BufReader, BufWriter}; use std::path::Path; use std::{fs, mem}; use bincode::Options; pub use cleaning::{CacheCleaningStatistics, CacheProgressCleaning, clean_all_cache_files}; use fun_time::fun_time; use humansize::{BINARY, format_size}; use indexmap::IndexMap; use log::{debug, error}; use once_cell::sync::Lazy; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; use crate::common::cache::cleaning::{should_clean_cache, update_cleaning_timestamp}; use crate::common::config_cache_path::open_cache_folder; use crate::common::tool_data::CommonData; use crate::common::traits::ResultEntry; use crate::flc; use crate::helpers::messages::Messages; pub(crate) const CACHE_VERSION: u8 = 100; pub(crate) const CACHE_DUPLICATE_VERSION: u8 = 100; pub(crate) const CACHE_IMAGE_VERSION: u8 = 100; pub(crate) const CACHE_VIDEO_VERSION: u8 = 110; pub(crate) const CACHE_BROKEN_FILES_VERSION: u8 = 110; pub(crate) const CACHE_VIDEO_OPTIMIZE_VERSION: u8 = 110; const MEMORY_LIMIT: u64 = 8 * 1024 * 1024 * 1024; const CLEANING_TIMESTAMPS_FILE: &str = "cleaning_timestamps.json"; static CACHE_CLEANING_INTERVAL_SECONDS: Lazy = Lazy::new(|| { option_env!("CZKAWKA_CACHE_CLEANING_INTERVAL_SECONDS") .and_then(|s| s.parse::().ok()) .unwrap_or(7 * 24 * 60 * 60) }); fn get_cache_size(file_name: &Path) -> String { fs::metadata(file_name).map_or_else(|_| "".to_string(), |metadata| format_size(metadata.len(), BINARY)) } #[fun_time(message = "save_cache_to_file_generalized", level = "debug")] pub fn save_cache_to_file_generalized(cache_file_name: &str, hashmap: &BTreeMap, save_also_as_json: bool, minimum_file_size: u64) -> Messages where T: Serialize + ResultEntry + Sized + Send + Sync, { let mut text_messages = Messages::new(); 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) { let hashmap_to_save = hashmap.values().filter(|t| t.get_size() >= minimum_file_size).collect::>(); { let writer = BufWriter::new(file_handler.expect("Cannot fail, because for saving, this always exists")); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); if let Err(e) = options.serialize_into(writer, &hashmap_to_save) { text_messages .warnings .push(flc!("core_failed_to_write_data_to_cache", file = cache_file.to_string_lossy(), reason = e.to_string())); debug!("Failed to save cache to file \"{}\" - {e}", cache_file.to_string_lossy()); return text_messages; } debug!("Saved cache to binary file \"{}\" with size {}", cache_file.to_string_lossy(), get_cache_size(&cache_file)); } if save_also_as_json && let Some(file_handler_json) = file_handler_json { let writer = BufWriter::new(file_handler_json); if let Err(e) = serde_json::to_writer(writer, &hashmap_to_save) { text_messages .warnings .push(flc!("core_failed_to_write_data_to_cache", file = cache_file_json.to_string_lossy(), reason = e.to_string())); debug!("Failed to save cache to file \"{}\" - {e}", cache_file_json.to_string_lossy()); return text_messages; } debug!( "Saved cache to json file \"{}\" with size {}", cache_file_json.to_string_lossy(), get_cache_size(&cache_file_json) ); } text_messages.messages.push(flc!("core_properly_saved_cache_entries", count = hashmap.len())); debug!("Properly saved to file {} cache entries.", hashmap.len()); } else { debug!("Failed to save cache to file {cache_file_name} because not exists"); } text_messages } pub(crate) fn extract_loaded_cache( loaded_hash_map: &BTreeMap, files_to_check: BTreeMap, records_already_cached: &mut BTreeMap, non_cached_files_to_check: &mut BTreeMap, ) where T: Clone, { for (name, file_entry) in files_to_check { if let Some(cached_file_entry) = loaded_hash_map.get(&name) { records_already_cached.insert(name, cached_file_entry.clone()); } else { non_cached_files_to_check.insert(name, file_entry); } } } #[fun_time(message = "load_cache_from_file_generalized_by_path", level = "debug")] pub fn load_cache_from_file_generalized_by_path(cache_file_name: &str, delete_outdated_cache: bool, used_files: &BTreeMap) -> (Messages, Option>) where for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, { let check_file = |file_entry: &T| { let file_entry_path_str = file_entry.get_path().to_string_lossy(); let key: &str = file_entry_path_str.as_ref(); if let Some(used_file) = used_files.get(key) { if file_entry.get_size() != used_file.get_size() { return false; } if file_entry.get_modified_date() != used_file.get_modified_date() { return false; } } true }; let (text_messages, vec_loaded_cache) = load_cache_from_file_generalized(cache_file_name, delete_outdated_cache, check_file); let Some(vec_loaded_entries) = vec_loaded_cache else { return (text_messages, None); }; debug!("Converting cache Vec into BTreeMap"); let number_of_entries = vec_loaded_entries.len(); let start_time = std::time::Instant::now(); let map_loaded_entries: BTreeMap = vec_loaded_entries .into_iter() .map(|file_entry| (file_entry.get_path().to_string_lossy().into_owned(), file_entry)) .collect(); debug!("Converted cache Vec({number_of_entries} results) into BTreeMap in {:?}", start_time.elapsed()); (text_messages, Some(map_loaded_entries)) } #[fun_time(message = "load_cache_from_file_generalized_by_size", level = "debug")] pub fn load_cache_from_file_generalized_by_size( cache_file_name: &str, delete_outdated_cache: bool, cache_not_converted: &BTreeMap>, ) -> (Messages, Option>>) where for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, { debug!("Converting cache BtreeMap> into IndexMap"); let used_files: IndexMap = cache_not_converted .iter() .flat_map(|(size, vec)| { vec.iter() .map(move |file_entry| (file_entry.get_path().to_string_lossy().into_owned(), (*size, file_entry.get_modified_date()))) }) .collect(); debug!("Converted cache BtreeMap> into IndexMap"); let check_file = |file_entry: &T| { let file_entry_path_str = file_entry.get_path().to_string_lossy(); let key: &str = file_entry_path_str.as_ref(); if let Some((size, modification_date)) = used_files.get(key) { if file_entry.get_size() != *size { return false; } if file_entry.get_modified_date() != *modification_date { return false; } } true }; let (text_messages, vec_loaded_cache) = load_cache_from_file_generalized(cache_file_name, delete_outdated_cache, check_file); let Some(vec_loaded_entries) = vec_loaded_cache else { return (text_messages, None); }; debug!("Converting cache Vec into BTreeMap>"); let number_of_entries = vec_loaded_entries.len(); let start_time = std::time::Instant::now(); let mut map_loaded_entries: BTreeMap> = Default::default(); for file_entry in vec_loaded_entries { map_loaded_entries.entry(file_entry.get_size()).or_default().push(file_entry); } debug!( "Converted cache Vec({number_of_entries} results) into BTreeMap> in {:?}", start_time.elapsed() ); (text_messages, Some(map_loaded_entries)) } #[fun_time(message = "load_cache_from_file_generalized", level = "debug")] fn load_cache_from_file_generalized(cache_file_name: &str, delete_outdated_cache: bool, check_func: F) -> (Messages, Option>) where for<'a> T: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, F: Fn(&T) -> bool + Send + Sync, { let mut text_messages = Messages::new(); 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) { let cache_full_name; let mut vec_loaded_entries: Vec; if let Some(file_handler) = file_handler { cache_full_name = cache_file.clone(); let reader = BufReader::new(file_handler); let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT); vec_loaded_entries = match options.deserialize_from(reader) { Ok(t) => t, Err(e) => { text_messages .warnings .push(flc!("core_failed_to_load_data_from_cache", file = cache_file.to_string_lossy(), reason = e.to_string())); error!("Failed to load cache from file {} - {e}", cache_file.to_string_lossy()); return (text_messages, None); } }; } else { cache_full_name = cache_file_json.clone(); let reader = BufReader::new(file_handler_json.expect("This cannot fail, because if file_handler is None, then this cannot be None")); vec_loaded_entries = match serde_json::from_reader(reader) { Ok(t) => t, Err(e) => { text_messages.warnings.push(flc!( "core_failed_to_load_data_from_json_cache", file = cache_file_json.to_string_lossy(), reason = e.to_string() )); debug!("Failed to load cache from file {} - {e}", cache_file_json.to_string_lossy()); return (text_messages, None); } }; } let should_clean = should_clean_cache(cache_file_name); debug!( "Starting removing outdated cache entries (removing non existent files from cache - {delete_outdated_cache}, should_clean - {should_clean}, entries number - {})", vec_loaded_entries.len() ); let initial_number_of_entries = vec_loaded_entries.len(); let deleting_start_time = std::time::Instant::now(); let effective_delete_outdated = delete_outdated_cache && should_clean; vec_loaded_entries = vec_loaded_entries .into_par_iter() .filter(|file_entry| { if !check_func(file_entry) { return false; } if effective_delete_outdated && !file_entry.get_path().exists() { return false; } true }) .collect(); if effective_delete_outdated { update_cleaning_timestamp(cache_file_name); } debug!( "Completed removing outdated cache entries, removed {} out of all {} entries in {:?}", initial_number_of_entries - vec_loaded_entries.len(), initial_number_of_entries, deleting_start_time.elapsed() ); text_messages.messages.push(format!("Properly loaded {} cache entries.", vec_loaded_entries.len())); debug!( "Loaded cache from file {cache_file_name} (or json alternative) - {} results - size {}", vec_loaded_entries.len(), get_cache_size(&cache_full_name) ); return (text_messages, Some(vec_loaded_entries)); } debug!("Failed to load cache from file {cache_file_name} because not exists"); (text_messages, None) } pub(crate) fn load_and_split_cache_generalized_by_path( cache_file_name: &str, mut items_to_check: BTreeMap, common_data: &mut C, ) -> (BTreeMap, BTreeMap, BTreeMap) where for<'a> K: Deserialize<'a> + ResultEntry + Sized + Send + Sync + Clone, { if !common_data.get_use_cache() { return (Default::default(), Default::default(), items_to_check); } let loaded_hash_map; let mut records_already_cached: BTreeMap = Default::default(); let mut non_cached_files_to_check: BTreeMap = Default::default(); let (messages, loaded_items) = load_cache_from_file_generalized_by_path::(cache_file_name, common_data.get_delete_outdated_cache(), &items_to_check); common_data.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); debug!("load_cache - Starting to check for differences"); extract_loaded_cache( &loaded_hash_map, mem::take(&mut items_to_check), &mut records_already_cached, &mut non_cached_files_to_check, ); debug!( "load_cache - completed diff between loaded and prechecked files, {}({}) - non cached, {}({}) - already cached", non_cached_files_to_check.len(), format_size(non_cached_files_to_check.values().map(|e| e.get_size()).sum::(), BINARY), records_already_cached.len(), format_size(records_already_cached.values().map(|e| e.get_size()).sum::(), BINARY), ); (loaded_hash_map, records_already_cached, non_cached_files_to_check) } pub(crate) fn save_and_connect_cache_generalized_by_path(cache_file_name: &str, vec_file_entry: &[K], loaded_hash_map: BTreeMap, common_data: &mut C) where K: Serialize + ResultEntry + Sized + Send + Sync + Clone, { if !common_data.get_use_cache() { return; } let mut all_results: BTreeMap = Default::default(); for file_entry in vec_file_entry.iter().cloned() { all_results.insert(file_entry.get_path().to_string_lossy().to_string(), file_entry); } for (name, file_entry) in loaded_hash_map { all_results.insert(name, file_entry); } let messages = save_cache_to_file_generalized(cache_file_name, &all_results, common_data.get_save_also_as_json(), 0); common_data.get_text_messages_mut().extend_with_another_messages(messages); } #[cfg(test)] mod tests { use std::collections::BTreeMap; use std::fs; use std::path::PathBuf; use std::sync::Once; use tempfile::TempDir; use super::*; use crate::common::config_cache_path::set_config_cache_path_test; static INIT: Once = Once::new(); pub(crate) fn setup_cache_path() { INIT.call_once(|| { let temp_cache_dir = TempDir::new().expect("Failed to create temp cache dir"); let temp_config_dir = TempDir::new().expect("Failed to create temp config dir"); let cache_path = temp_cache_dir.path().to_path_buf(); let config_path = temp_config_dir.path().to_path_buf(); set_config_cache_path_test(cache_path, config_path); // Leak the TempDir to keep directories alive for the duration of tests std::mem::forget(temp_cache_dir); std::mem::forget(temp_config_dir); }); } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] struct TestEntry { path: PathBuf, size: u64, modified_date: u64, value: u32, } impl ResultEntry for TestEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl TestEntry { fn new(path: &str, size: u64, modified_date: u64, value: u32) -> Self { Self { path: PathBuf::from(path), size, modified_date, value, } } } #[test] fn test_extract_loaded_cache() { let mut loaded_cache = BTreeMap::new(); loaded_cache.insert("file1".to_string(), TestEntry::new("/tmp/file1", 100, 1000, 10)); loaded_cache.insert("file2".to_string(), TestEntry::new("/tmp/file2", 200, 2000, 20)); let mut files_to_check = BTreeMap::new(); files_to_check.insert("file1".to_string(), TestEntry::new("/tmp/file1", 100, 1000, 10)); files_to_check.insert("file3".to_string(), TestEntry::new("/tmp/file3", 300, 3000, 30)); files_to_check.insert("file2".to_string(), TestEntry::new("/tmp/file2", 200, 2000, 20)); let mut records_already_cached = BTreeMap::new(); let mut non_cached_files_to_check = BTreeMap::new(); extract_loaded_cache(&loaded_cache, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check); assert_eq!(records_already_cached.len(), 2); assert_eq!(non_cached_files_to_check.len(), 1); assert!(records_already_cached.contains_key("file1")); assert!(records_already_cached.contains_key("file2")); assert!(non_cached_files_to_check.contains_key("file3")); assert_eq!(records_already_cached.get("file1").unwrap().value, 10); assert_eq!(non_cached_files_to_check.get("file3").unwrap().value, 30); } #[test] fn test_extract_loaded_cache_empty() { let loaded_cache: BTreeMap = BTreeMap::new(); let mut files_to_check = BTreeMap::new(); files_to_check.insert("file1".to_string(), TestEntry::new("/tmp/file1", 100, 1000, 10)); files_to_check.insert("file2".to_string(), TestEntry::new("/tmp/file2", 200, 2000, 20)); let mut records_already_cached = BTreeMap::new(); let mut non_cached_files_to_check = BTreeMap::new(); extract_loaded_cache(&loaded_cache, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check); assert_eq!(records_already_cached.len(), 0, "No entries should be cached"); assert_eq!(non_cached_files_to_check.len(), 2, "All entries should be non-cached"); } #[test] fn test_extract_loaded_cache_all_cached() { let mut loaded_cache = BTreeMap::new(); loaded_cache.insert("file1".to_string(), TestEntry::new("/tmp/file1", 100, 1000, 10)); loaded_cache.insert("file2".to_string(), TestEntry::new("/tmp/file2", 200, 2000, 20)); let mut files_to_check = BTreeMap::new(); files_to_check.insert("file1".to_string(), TestEntry::new("/tmp/file1", 100, 1000, 10)); files_to_check.insert("file2".to_string(), TestEntry::new("/tmp/file2", 200, 2000, 20)); let mut records_already_cached = BTreeMap::new(); let mut non_cached_files_to_check = BTreeMap::new(); extract_loaded_cache(&loaded_cache, files_to_check, &mut records_already_cached, &mut non_cached_files_to_check); assert_eq!(records_already_cached.len(), 2, "All entries should be cached"); assert_eq!(non_cached_files_to_check.len(), 0, "No entries should be non-cached"); } #[test] fn test_save_and_load_cache_by_path() { setup_cache_path(); let temp_dir = TempDir::new().unwrap(); let temp_file = temp_dir.path().join("test_file.txt"); fs::write(&temp_file, "test content").unwrap(); let metadata = fs::metadata(&temp_file).unwrap(); let mut cache_to_save = BTreeMap::new(); cache_to_save.insert( temp_file.to_string_lossy().to_string(), TestEntry::new(temp_file.to_str().unwrap(), metadata.len(), metadata.modified().unwrap().elapsed().unwrap().as_secs(), 42), ); // Save cache let cache_name = format!("test_cache_by_path_{}", std::process::id()); let messages = save_cache_to_file_generalized(&cache_name, &cache_to_save, false, 0); assert!(messages.warnings.is_empty(), "Should not have warnings when saving"); assert!(!messages.messages.is_empty(), "Should have success messages when saving"); // Load cache let (load_messages, loaded_cache) = load_cache_from_file_generalized_by_path::(&cache_name, false, &cache_to_save); assert!(load_messages.warnings.is_empty(), "Should not have warnings when loading"); assert!(!load_messages.messages.is_empty(), "Should have success messages when loading"); assert!(loaded_cache.is_some(), "Should load cache successfully"); let loaded = loaded_cache.unwrap(); assert_eq!(loaded.len(), 1, "Should load 1 entry"); assert!(loaded.contains_key(temp_file.to_str().unwrap()), "Should contain the test file"); } #[test] fn test_save_and_load_cache_by_size() { setup_cache_path(); let temp_dir = TempDir::new().unwrap(); let temp_file1 = temp_dir.path().join("test_file1.txt"); let temp_file2 = temp_dir.path().join("test_file2.txt"); fs::write(&temp_file1, "test content 1").unwrap(); fs::write(&temp_file2, "test content 2").unwrap(); let metadata1 = fs::metadata(&temp_file1).unwrap(); let metadata2 = fs::metadata(&temp_file2).unwrap(); let mut cache_to_save: BTreeMap> = BTreeMap::new(); cache_to_save.entry(metadata1.len()).or_default().push(TestEntry::new( temp_file1.to_str().unwrap(), metadata1.len(), metadata1.modified().unwrap().elapsed().unwrap().as_secs(), 10, )); cache_to_save.entry(metadata2.len()).or_default().push(TestEntry::new( temp_file2.to_str().unwrap(), metadata2.len(), metadata2.modified().unwrap().elapsed().unwrap().as_secs(), 20, )); // Convert to flat map for saving let mut flat_cache = BTreeMap::new(); for entries in cache_to_save.values() { for entry in entries { flat_cache.insert(entry.path.to_string_lossy().to_string(), entry.clone()); } } // Save cache let cache_name = format!("test_cache_by_size_{}", std::process::id()); let messages = save_cache_to_file_generalized(&cache_name, &flat_cache, false, 0); assert!(messages.warnings.is_empty(), "Should not have warnings when saving"); // Load cache let (load_messages, loaded_cache) = load_cache_from_file_generalized_by_size::(&cache_name, false, &cache_to_save); assert!(load_messages.warnings.is_empty(), "Should not have warnings when loading"); assert!(loaded_cache.is_some(), "Should load cache successfully"); let loaded = loaded_cache.unwrap(); assert!(!loaded.is_empty(), "Should load entries"); } #[test] fn test_save_cache_with_minimum_file_size() { setup_cache_path(); let temp_dir = TempDir::new().unwrap(); let temp_file = temp_dir.path().join("test_file.txt"); fs::write(&temp_file, "test").unwrap(); let mut cache_to_save = BTreeMap::new(); cache_to_save.insert("small_file".to_string(), TestEntry::new("/tmp/small", 10, 1000, 1)); cache_to_save.insert("large_file".to_string(), TestEntry::new("/tmp/large", 1000, 2000, 2)); // Save cache with minimum file size of 100 bytes let cache_name = format!("test_cache_min_size_{}", std::process::id()); let messages = save_cache_to_file_generalized(&cache_name, &cache_to_save, false, 100); assert!(messages.warnings.is_empty(), "Should not have warnings"); // Load cache - should only contain large file let files_to_check = cache_to_save.clone(); let (_, loaded_cache) = load_cache_from_file_generalized_by_path::(&cache_name, false, &files_to_check); if let Some(loaded) = loaded_cache { // Only the large file should be saved (size >= 100) for (_, entry) in loaded { assert!(entry.size >= 100, "All loaded entries should be >= minimum size"); } } } #[test] fn test_load_cache_with_outdated_entries() { setup_cache_path(); let temp_dir = TempDir::new().unwrap(); let temp_file = temp_dir.path().join("test_file.txt"); fs::write(&temp_file, "test content").unwrap(); let metadata = fs::metadata(&temp_file).unwrap(); let mut cache_to_save = BTreeMap::new(); cache_to_save.insert( temp_file.to_string_lossy().to_string(), TestEntry::new(temp_file.to_str().unwrap(), metadata.len(), metadata.modified().unwrap().elapsed().unwrap().as_secs(), 42), ); // Save cache let cache_name = format!("test_cache_outdated_{}", std::process::id()); save_cache_to_file_generalized(&cache_name, &cache_to_save, false, 0); // Modify the file std::thread::sleep(std::time::Duration::from_millis(100)); fs::write(&temp_file, "modified content").unwrap(); // Create new files_to_check with updated metadata let new_metadata = fs::metadata(&temp_file).unwrap(); let mut files_to_check = BTreeMap::new(); files_to_check.insert( temp_file.to_string_lossy().to_string(), TestEntry::new( temp_file.to_str().unwrap(), new_metadata.len(), new_metadata.modified().unwrap().elapsed().unwrap().as_secs(), 42, ), ); // Load cache - should filter out the outdated entry let (_, loaded_cache) = load_cache_from_file_generalized_by_path::(&cache_name, false, &files_to_check); if let Some(loaded) = loaded_cache { // Should be empty because size/modified date changed assert!(loaded.is_empty() || loaded.len() < cache_to_save.len(), "Outdated entries should be filtered"); } } #[test] fn test_load_nonexistent_cache() { setup_cache_path(); let cache_name = format!("nonexistent_cache_{}", std::process::id()); let files_to_check: BTreeMap = BTreeMap::new(); let (messages, loaded_cache) = load_cache_from_file_generalized_by_path::(&cache_name, false, &files_to_check); assert!(loaded_cache.is_none(), "Should return None for nonexistent cache"); assert!(messages.warnings.is_empty(), "Should not have warnings for nonexistent cache"); } #[test] fn test_save_cache_with_json() { setup_cache_path(); let temp_dir = TempDir::new().unwrap(); let temp_file = temp_dir.path().join("test_file.txt"); fs::write(&temp_file, "test content").unwrap(); let mut cache_to_save = BTreeMap::new(); cache_to_save.insert("test_key".to_string(), TestEntry::new("/tmp/test", 100, 1000, 42)); // Save cache with JSON enabled let cache_name = format!("test_cache_json_{}", std::process::id()); let messages = save_cache_to_file_generalized(&cache_name, &cache_to_save, true, 0); assert!(messages.warnings.is_empty(), "Should not have warnings when saving with JSON"); } #[test] fn test_get_cache_size_nonexistent() { let nonexistent_path = Path::new("/nonexistent/path/to/cache.bin"); let size_str = get_cache_size(nonexistent_path); assert_eq!(size_str, "", "Should return unknown size for nonexistent file"); } } ================================================ FILE: czkawka_core/src/common/config_cache_path.rs ================================================ use std::fs::{File, OpenOptions}; use std::path::PathBuf; use std::{env, fs}; use directories_next::ProjectDirs; use log::{info, warn}; use once_cell::sync::OnceCell; use crate::flc; static CONFIG_CACHE_PATH: OnceCell> = OnceCell::new(); #[derive(Debug, Clone)] pub struct ConfigCachePath { pub config_folder: PathBuf, pub cache_folder: PathBuf, } pub fn get_config_cache_path() -> Option { CONFIG_CACHE_PATH.get().expect("Cannot fail if set_config_cache_path was called before").clone() } /// On Android `ProjectDirs` always returns `None` because there is no concept of a home /// directory accessible via standard UNIX paths. Instead we use the app-private data /// directory exposed by the Android runtime through the `DATA_DIR` or `HOME` env variable. /// If neither variable is set, `None` is returned and the caller should handle the missing /// path (e.g. via the `CZKAWKA_CACHE_PATH` / `CZKAWKA_CONFIG_PATH` env overrides). /// /// The base directory is expected to be set by the host application (e.g. cedinia) before /// calling `set_config_cache_path`, so that `czkawka_core` stays package-agnostic. /// /// Android paths used: /// cache – $DATA_DIR/cache/ /// config – $DATA_DIR/files/ #[cfg(target_os = "android")] fn android_default_dirs(cache_name: &str, config_name: &str) -> (Option, Option) { let base = match env::var("DATA_DIR").or_else(|_| env::var("HOME")) { Ok(path) => PathBuf::from(path), Err(_) => return (None, None), }; let cache_folder = Some(base.join("cache").join(cache_name)); let config_folder = Some(base.join("files").join(config_name)); (cache_folder, config_folder) } #[cfg(not(target_os = "android"))] fn android_default_dirs(_cache_name: &str, _config_name: &str) -> (Option, Option) { (None, None) } fn resolve_folder(env_var: &str, default_folder: Option, name: &'static str, warnings: &mut Vec) -> Option { let default_folder_str = default_folder.as_ref().map_or("".to_string(), |t| t.to_string_lossy().to_string()); if env_var.is_empty() { default_folder } else { let folder_path = PathBuf::from(env_var); let _ = fs::create_dir_all(&folder_path); if !folder_path.exists() { warnings.push(format!( "{name} folder \"{}\" does not exist, using default folder \"{}\"", folder_path.to_string_lossy(), default_folder_str )); return default_folder; } if !folder_path.is_dir() { warnings.push(format!( "{name} folder \"{}\" is not a directory, using default folder \"{}\"", folder_path.to_string_lossy(), default_folder_str )); return default_folder; } match dunce::canonicalize(folder_path) { Ok(t) => Some(t), Err(_e) => { warnings.push(format!( "Cannot canonicalize {} folder \"{}\", using default folder \"{}\"", name.to_ascii_lowercase(), env_var, default_folder_str )); default_folder } } } } #[cfg(test)] pub fn set_config_cache_path_test(cache_path: PathBuf, config_path: PathBuf) { CONFIG_CACHE_PATH .set(Some(ConfigCachePath { cache_folder: cache_path, config_folder: config_path, })) .expect("Cannot set config cache path"); } pub struct ConfigCachePathSetResult { pub infos: Vec, pub warnings: Vec, pub config_env_set: bool, pub cache_env_set: bool, pub default_cache_path_exists: bool, pub default_config_path_exists: bool, } // This function must be executed, to not crash, when gathering config/cache path pub fn set_config_cache_path(cache_name: &'static str, config_name: &'static str) -> ConfigCachePathSetResult { // By default, such folders are used: // Lin: /home/username/.config/czkawka // LinFlatpak: /home/username/.var/app/com.github.qarmin.czkawka/config/czkawka // Win: C:\Users\Username\AppData\Roaming\Qarmin\Czkawka\config // Mac: /Users/Username/Library/Application Support/pl.Qarmin.Czkawka let mut infos = Vec::new(); let mut warnings = Vec::new(); let config_folder_env = env::var("CZKAWKA_CONFIG_PATH").unwrap_or_default().trim().to_string(); let cache_folder_env = env::var("CZKAWKA_CACHE_PATH").unwrap_or_default().trim().to_string(); let (android_cache_folder, android_config_folder) = android_default_dirs(cache_name, config_name); let default_cache_folder = ProjectDirs::from("pl", "Qarmin", cache_name) .map(|proj_dirs| proj_dirs.cache_dir().to_path_buf()) .or(android_cache_folder); let default_config_folder = ProjectDirs::from("pl", "Qarmin", config_name) .map(|proj_dirs| proj_dirs.config_dir().to_path_buf()) .or(android_config_folder); let default_config_path_exists = default_config_folder.as_ref().is_some_and(|t| t.exists()); let default_cache_path_exists = default_cache_folder.as_ref().is_some_and(|t| t.exists()); let config_folder = resolve_folder(&config_folder_env, default_config_folder, "Config", &mut warnings); let cache_folder = resolve_folder(&cache_folder_env, default_cache_folder, "Cache", &mut warnings); let config_cache_path = if let (Some(config_folder), Some(cache_folder)) = (config_folder, cache_folder) { infos.push(format!( "Config folder set to \"{}\" and cache folder set to \"{}\"", config_folder.to_string_lossy(), cache_folder.to_string_lossy() )); if !config_folder.exists() && let Err(e) = fs::create_dir_all(&config_folder) { warnings.push(flc!("core_cannot_create_config_folder", folder = config_folder.to_string_lossy(), reason = e.to_string())); } if !cache_folder.exists() && let Err(e) = fs::create_dir_all(&cache_folder) { warnings.push(flc!("core_cannot_create_cache_folder", folder = cache_folder.to_string_lossy(), reason = e.to_string())); } Some(ConfigCachePath { config_folder, cache_folder }) } else { warnings.push(flc!("core_cannot_set_config_cache_path")); None }; CONFIG_CACHE_PATH.set(config_cache_path).expect("Cannot set config/cache path twice"); ConfigCachePathSetResult { infos, warnings, config_env_set: !config_folder_env.is_empty(), cache_env_set: !cache_folder_env.is_empty(), default_cache_path_exists, default_config_path_exists, } } pub(crate) fn open_cache_folder( cache_file_name: &str, save_to_cache: bool, use_json: bool, warnings: &mut Vec, ) -> Option<((Option, PathBuf), (Option, PathBuf))> { let cache_dir = get_config_cache_path()?.cache_folder; let cache_file = cache_dir.join(cache_file_name); let cache_file_json = cache_dir.join(cache_file_name.replace(".bin", ".json")); let mut file_handler_default = None; let mut file_handler_json = None; if save_to_cache { file_handler_default = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file) { Ok(t) => t, Err(e) => { warnings.push(flc!("core_cannot_create_or_open_cache_file", file = cache_file.to_string_lossy(), reason = e.to_string())); return None; } }); if use_json { file_handler_json = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file_json) { Ok(t) => t, Err(e) => { warnings.push(flc!( "core_cannot_create_or_open_cache_file", file = cache_file_json.to_string_lossy(), reason = e.to_string() )); return None; } }); } } else if let Ok(t) = OpenOptions::new().read(true).open(&cache_file) { file_handler_default = Some(t); } else if use_json { file_handler_json = Some(OpenOptions::new().read(true).open(&cache_file_json).ok()?); } else { return None; } Some(((file_handler_default, cache_file), (file_handler_json, cache_file_json))) } // When initializing logger or settings config/cache folders, logger is not yet initialized, // so we need to delay them until logger is initialized pub fn print_infos_and_warnings(infos: Vec, warnings: Vec) { for info in infos { info!("{info}"); } for warning in warnings { warn!("{warning}"); } } ================================================ FILE: czkawka_core/src/common/consts.rs ================================================ pub const DEFAULT_THREAD_SIZE: usize = 8 * 1024 * 1024; // 8 MB pub const DEFAULT_WORKER_THREAD_SIZE: usize = 4 * 1024 * 1024; // 4 MB pub 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 pub const RAW_IMAGE_EXTENSIONS: &[&str] = &[ "ari", "cr3", "cr2", "crw", "erf", "raf", "3fr", "kdc", "dcs", "dcr", "iiq", "mos", "mef", "mrw", "nef", "nrw", "orf", "rw2", "pef", "srw", "arw", "srf", "sr2", ]; #[cfg(feature = "libavif")] pub const IMAGE_RS_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "bmp", "tiff", "tif", "tga", "ff", "jif", "jfi", "webp", "gif", "ico", "exr", "qoi", "jxl", "avif", ]; #[cfg(not(feature = "libavif"))] pub const IMAGE_RS_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "bmp", "tiff", "tif", "tga", "ff", "jif", "jfi", "webp", "gif", "ico", "exr", "qoi", "jxl", ]; #[cfg(feature = "libavif")] pub const IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "bmp", "webp", "exr", "qoi", "jxl", "avif"]; #[cfg(not(feature = "libavif"))] pub const IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "bmp", "webp", "exr", "qoi", "jxl"]; #[cfg(feature = "libavif")] pub const IMAGE_RS_BROKEN_FILES_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "gif", "bmp", "ico", "jfif", "jpe", "pnz", "dib", "webp", "exr", "avif", "jxl", ]; #[cfg(not(feature = "libavif"))] pub const IMAGE_RS_BROKEN_FILES_EXTENSIONS: &[&str] = &[ "jpg", "jpeg", "png", "tiff", "tif", "tga", "ff", "jif", "jfi", "gif", "bmp", "ico", "jfif", "jpe", "pnz", "dib", "webp", "exr", "jxl", ]; pub const HEIC_EXTENSIONS: &[&str] = &["heif", "heifs", "heic", "heics", "avci", "avcs", "hif"]; pub const ZIP_FILES_EXTENSIONS: &[&str] = &["zip", "jar"]; pub const PDF_FILES_EXTENSIONS: &[&str] = &["pdf"]; pub const AUDIO_FILES_EXTENSIONS: &[&str] = &[ "mp3", "flac", "wav", "ogg", "m4a", "aac", "aiff", "pcm", "aif", "aiff", "aifc", "m3a", "mp2", "mp4a", "mp2a", "mpga", "wave", "weba", "wma", "oga", ]; pub const VIDEO_FILES_EXTENSIONS: &[&str] = &[ "mp4", "m4v", "mkv", "avi", "mov", "webm", "flv", "wmv", // Popular "mpeg", "mpg", "mp2", "mpe", "m2ts", "vob", "evo", // MPEG / broadcast, "ts" "3gp", "3g2", "f4v", "f4p", "f4a", "f4b", // Mobile / legacy "qt", "m4p", "mpv", // Apple / ISO BMFF "ogv", "rm", "rmvb", "asf", // Streaming / recording "dv", "mxf", "roq", "nsv", "yuv", // Professional "y4m", "h264", "h265", "hevc", "av1", "vp8", "vp9", // Raw / uncompressed "amv", "drc", "gifv", "smk", "bik", // Older / games ]; pub const TEXT_FILES_EXTENSIONS: &[&str] = &["txt", "md", "csv", "log", "ini", "json", "xml", "yaml", "yml", "toml", "doc", "docx", "rtf", "odt"]; // "dng" - is theoretically a tiff file, but little_exif have problem with saving metadata to it pub const EXIF_FILES_EXTENSIONS: &[&str] = &["jpg", "jpeg", "jfif", "png", "tiff", "tif", "avif", "jxl", "webp", "heic", "heif"]; ================================================ FILE: czkawka_core/src/common/dir_traversal.rs ================================================ use std::collections::BTreeMap; use std::fs; use std::fs::{DirEntry, FileType, Metadata}; #[cfg(target_family = "unix")] use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::UNIX_EPOCH; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::directories::Directories; use crate::common::extensions::Extensions; use crate::common::items::ExcludedItems; use crate::common::model::{CheckingMethod, FileEntry, ToolType}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::CommonToolData; use crate::flc; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum Collect { InvalidSymlinks, Files, } #[derive(Eq, PartialEq, Copy, Clone, Debug)] enum EntryType { File, Dir, Symlink, Other, } pub struct DirTraversalBuilder<'b, F> { group_by: Option, root_dirs: Vec, root_files: Vec, stop_flag: Option>, progress_sender: Option<&'b Sender>, minimal_file_size: Option, maximal_file_size: Option, checking_method: CheckingMethod, collect: Collect, recursive_search: bool, directories: Option, excluded_items: Option, extensions: Option, tool_type: ToolType, } #[derive(Debug)] pub struct DirTraversal<'b, F> { group_by: F, root_dirs: Vec, root_files: Vec, stop_flag: Arc, progress_sender: Option<&'b Sender>, recursive_search: bool, directories: Directories, excluded_items: ExcludedItems, extensions: Extensions, minimal_file_size: u64, maximal_file_size: u64, checking_method: CheckingMethod, tool_type: ToolType, collect: Collect, } impl Default for DirTraversalBuilder<'_, ()> { fn default() -> Self { Self::new() } } impl DirTraversalBuilder<'_, ()> { pub fn new() -> Self { DirTraversalBuilder { group_by: None, root_dirs: Vec::new(), root_files: Vec::new(), stop_flag: None, progress_sender: None, checking_method: CheckingMethod::None, minimal_file_size: None, maximal_file_size: None, collect: Collect::Files, recursive_search: false, directories: None, extensions: None, excluded_items: None, tool_type: ToolType::None, } } } impl<'b, F> DirTraversalBuilder<'b, F> { pub(crate) fn common_data(mut self, common_tool_data: &CommonToolData) -> Self { self.root_dirs = common_tool_data.directories.included_directories.clone(); self.root_files = common_tool_data.directories.included_files.clone(); self.extensions = Some(common_tool_data.extensions.clone()); self.excluded_items = Some(common_tool_data.excluded_items.clone()); self.recursive_search = common_tool_data.recursive_search; self.minimal_file_size = Some(common_tool_data.minimal_file_size); self.maximal_file_size = Some(common_tool_data.maximal_file_size); self.tool_type = common_tool_data.tool_type; self.directories = Some(common_tool_data.directories.clone()); self } pub(crate) fn stop_flag(mut self, stop_flag: &Arc) -> Self { self.stop_flag = Some(stop_flag.clone()); self } pub(crate) fn progress_sender(mut self, progress_sender: Option<&'b Sender>) -> Self { self.progress_sender = progress_sender; self } pub(crate) fn checking_method(mut self, checking_method: CheckingMethod) -> Self { self.checking_method = checking_method; self } pub(crate) fn minimal_file_size(mut self, minimal_file_size: u64) -> Self { self.minimal_file_size = Some(minimal_file_size); self } pub(crate) fn maximal_file_size(mut self, maximal_file_size: u64) -> Self { self.maximal_file_size = Some(maximal_file_size); self } pub(crate) fn collect(mut self, collect: Collect) -> Self { self.collect = collect; self } pub(crate) fn group_by(self, group_by: G) -> DirTraversalBuilder<'b, G> where G: Fn(&FileEntry) -> T, { DirTraversalBuilder { group_by: Some(group_by), root_dirs: self.root_dirs, root_files: self.root_files, stop_flag: self.stop_flag, progress_sender: self.progress_sender, directories: self.directories, extensions: self.extensions, excluded_items: self.excluded_items, recursive_search: self.recursive_search, maximal_file_size: self.maximal_file_size, minimal_file_size: self.minimal_file_size, collect: self.collect, checking_method: self.checking_method, tool_type: self.tool_type, } } pub(crate) fn build(self) -> DirTraversal<'b, F> { DirTraversal { group_by: self.group_by.expect("could not build"), root_dirs: self.root_dirs, root_files: self.root_files, stop_flag: self.stop_flag.expect("Stop flag must be always initialized"), progress_sender: self.progress_sender, checking_method: self.checking_method, minimal_file_size: self.minimal_file_size.unwrap_or(0), maximal_file_size: self.maximal_file_size.unwrap_or(u64::MAX), collect: self.collect, directories: self.directories.expect("could not build"), excluded_items: self.excluded_items.expect("could not build"), extensions: self.extensions.unwrap_or_default(), recursive_search: self.recursive_search, tool_type: self.tool_type, } } } pub enum DirTraversalResult { SuccessFiles { warnings: Vec, grouped_file_entries: BTreeMap>, }, Stopped, } fn entry_type(file_type: FileType) -> EntryType { if file_type.is_dir() { EntryType::Dir } else if file_type.is_symlink() { EntryType::Symlink } else if file_type.is_file() { EntryType::File } else { EntryType::Other } } impl DirTraversal<'_, F> where F: Fn(&FileEntry) -> T, T: Ord + PartialOrd, { #[fun_time(message = "run(collecting files/dirs)", level = "debug")] pub(crate) fn run(self) -> DirTraversalResult { assert_ne!(self.tool_type, ToolType::None, "Tool type cannot be None"); let mut all_warnings = Vec::new(); let mut grouped_file_entries: BTreeMap> = BTreeMap::new(); // Add root folders and files for finding let mut folders_to_check: Vec = self.root_dirs.clone(); let mut files_to_check: Vec = self.root_files.clone(); let progress_handler = prepare_thread_handler_common(self.progress_sender, CurrentStage::CollectingFiles, 0, (self.tool_type, self.checking_method), 0); let DirTraversal { collect, directories, excluded_items, extensions, recursive_search, minimal_file_size, maximal_file_size, stop_flag, .. } = self; let mut file_results = Vec::new(); // File traversal while let Some(current_file) = files_to_check.pop() { let Some(metadata) = common_get_metadata_from_path(¤t_file, &mut all_warnings) else { continue; }; let file_type = metadata.file_type(); match (entry_type(file_type), collect) { (EntryType::File, Collect::Files) => { progress_handler.increase_items(1); process_file_in_file_mode_path_check( ¤t_file, &metadata, &mut all_warnings, &mut file_results, &extensions, &excluded_items, &directories, minimal_file_size, maximal_file_size, ); } (EntryType::File, Collect::InvalidSymlinks) => { progress_handler.increase_items(1); } (EntryType::Symlink, Collect::InvalidSymlinks) => { progress_handler.increase_items(1); process_symlink_in_symlink_mode_path_check(¤t_file, &metadata, &mut all_warnings, &mut file_results, &extensions, &excluded_items); } (EntryType::Symlink | EntryType::Dir | EntryType::Other, _) => { // nothing to do } } } file_results.sort_by_cached_key(|fe| fe.path.to_string_lossy().to_string()); for fe in file_results { let key = (self.group_by)(&fe); grouped_file_entries.entry(key).or_default().push(fe); } // Folder traversal while !folders_to_check.is_empty() { if check_if_stop_received(&stop_flag) { progress_handler.join_thread(); return DirTraversalResult::Stopped; } let segments: Vec<_> = folders_to_check .into_par_iter() .with_max_len(2) // Avoiding checking too many folders in batch .map(|current_folder| { let mut dir_result = Vec::new(); let mut warnings = Vec::new(); let mut fe_result = Vec::new(); let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return Some((dir_result, warnings, fe_result)); }; let mut counter = 0; // Check every sub folder/file/link etc. for entry in read_dir { if check_if_stop_received(&stop_flag) { return None; } let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, ¤t_folder) else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue }; match (entry_type(file_type), collect) { (EntryType::Dir, Collect::Files | Collect::InvalidSymlinks) => { process_dir_in_file_symlink_mode(recursive_search, entry_data, &directories, &mut dir_result, &mut warnings, &excluded_items); } (EntryType::File, Collect::Files) => { counter += 1; process_file_in_file_mode( entry_data, &mut warnings, &mut fe_result, &extensions, &directories, &excluded_items, minimal_file_size, maximal_file_size, ); } (EntryType::File, Collect::InvalidSymlinks) => { counter += 1; } (EntryType::Symlink, Collect::InvalidSymlinks) => { counter += 1; process_symlink_in_symlink_mode(entry_data, &mut warnings, &mut fe_result, &extensions, &directories, &excluded_items); } (EntryType::Symlink, Collect::Files) | (EntryType::Other, _) => { // nothing to do } } } if counter > 0 { // Increase counter in batch, because usually it may be slow to add multiple times atomic value progress_handler.increase_items(counter); } Some((dir_result, warnings, fe_result)) }) .while_some() .collect(); let required_size = segments.iter().map(|(segment, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, mut fe_result) in segments { folders_to_check.extend(segment); all_warnings.extend(warnings); fe_result.sort_by_cached_key(|fe| fe.path.to_string_lossy().to_string()); for fe in fe_result { let key = (self.group_by)(&fe); grouped_file_entries.entry(key).or_default().push(fe); } } } progress_handler.join_thread(); debug!("Collected {} files", grouped_file_entries.values().map(Vec::len).sum::()); match collect { Collect::Files | Collect::InvalidSymlinks => DirTraversalResult::SuccessFiles { grouped_file_entries, warnings: all_warnings, }, } } } fn process_file_in_file_mode( entry_data: &DirEntry, warnings: &mut Vec, fe_result: &mut Vec, extensions: &Extensions, directories: &Directories, excluded_items: &ExcludedItems, minimal_file_size: u64, maximal_file_size: u64, ) { if !extensions.check_if_entry_have_valid_extension(&entry_data.file_name()) { return; } let current_file_name = entry_data.path(); if excluded_items.is_excluded(¤t_file_name) { return; } if directories.is_excluded_file(¤t_file_name) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(¤t_file_name) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } #[cfg(windows)] let _ = directories; // Silence unused variable warning on Windows let Some(metadata) = common_get_metadata_dir(entry_data, warnings, ¤t_file_name) else { return; }; if (minimal_file_size..=maximal_file_size).contains(&metadata.len()) { // Creating new file entry let fe: FileEntry = FileEntry { size: metadata.len(), modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), path: current_file_name, }; fe_result.push(fe); } } // Same as above, but working with Path instead of DirEntry // Sadly this cannot be merged, due to a little crazy optimizations done in this functions fn process_file_in_file_mode_path_check( path: &Path, metadata: &Metadata, warnings: &mut Vec, fe_result: &mut Vec, extensions: &Extensions, excluded_items: &ExcludedItems, directories: &Directories, minimal_file_size: u64, maximal_file_size: u64, ) { let Some(file_name) = path.file_name() else { return; }; if !extensions.check_if_entry_have_valid_extension(file_name) { return; } if directories.is_excluded_file(path) { return; } if directories.is_excluded_item_in_dir(path) { return; } if excluded_items.is_excluded(path) { return; } if (minimal_file_size..=maximal_file_size).contains(&metadata.len()) { // Creating new file entry let fe: FileEntry = FileEntry { size: metadata.len(), modified_date: get_modified_time(metadata, warnings, path, false), path: path.to_path_buf(), }; fe_result.push(fe); } } fn process_dir_in_file_symlink_mode( recursive_search: bool, entry_data: &DirEntry, directories: &Directories, dir_result: &mut Vec, warnings: &mut Vec, excluded_items: &ExcludedItems, ) { if !recursive_search { return; } let dir_path = entry_data.path(); if directories.is_excluded_dir(&dir_path) { return; } if excluded_items.is_excluded(&dir_path) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&dir_path) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } #[cfg(windows)] let _ = warnings; // Silence unused variable warning on Windows dir_result.push(dir_path); } fn process_symlink_in_symlink_mode( entry_data: &DirEntry, warnings: &mut Vec, fe_result: &mut Vec, extensions: &Extensions, directories: &Directories, excluded_items: &ExcludedItems, ) { if !extensions.check_if_entry_have_valid_extension(&entry_data.file_name()) { return; } let current_file_name = entry_data.path(); if excluded_items.is_excluded(¤t_file_name) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(¤t_file_name) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } #[cfg(windows)] let _ = directories; // Silence unused variable warning on Windows let Some(metadata) = common_get_metadata_dir(entry_data, warnings, ¤t_file_name) else { return; }; // Creating new file entry let fe: FileEntry = FileEntry { size: metadata.len(), modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), path: current_file_name, }; fe_result.push(fe); } fn process_symlink_in_symlink_mode_path_check( path: &Path, metadata: &Metadata, warnings: &mut Vec, fe_result: &mut Vec, extensions: &Extensions, excluded_items: &ExcludedItems, ) { let Some(file_name) = path.file_name() else { return; }; if !extensions.check_if_entry_have_valid_extension(file_name) { return; } if excluded_items.is_excluded(path) { return; } // Creating new file entry let fe: FileEntry = FileEntry { size: metadata.len(), modified_date: get_modified_time(metadata, warnings, path, false), path: path.to_path_buf(), }; fe_result.push(fe); } pub(crate) fn common_read_dir(current_folder: &Path, warnings: &mut Vec) -> Option>> { match fs::read_dir(current_folder) { Ok(t) => Some(t.collect()), Err(e) => { warnings.push(flc!("core_cannot_open_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string())); None } } } pub(crate) fn common_get_entry_data<'a>(entry: &'a Result, warnings: &mut Vec, current_folder: &Path) -> Option<&'a DirEntry> { let entry_data = match entry { Ok(t) => t, Err(e) => { warnings.push(flc!( "core_cannot_read_entry_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string() )); return None; } }; Some(entry_data) } pub(crate) fn common_get_metadata_dir(entry_data: &DirEntry, warnings: &mut Vec, current_folder: &Path) -> Option { let metadata: Metadata = match entry_data.metadata() { Ok(t) => t, Err(e) => { warnings.push(flc!( "core_cannot_read_metadata_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string() )); return None; } }; Some(metadata) } pub(crate) fn common_get_metadata_from_path(path: &Path, warnings: &mut Vec) -> Option { let metadata: Metadata = match fs::metadata(path) { Ok(t) => t, Err(e) => { warnings.push(flc!("core_cannot_read_metadata_file", file = path.to_string_lossy().to_string(), reason = e.to_string())); return None; } }; Some(metadata) } pub(crate) fn get_modified_time(metadata: &Metadata, warnings: &mut Vec, current_file_name: &Path, is_folder: bool) -> u64 { match metadata.modified() { Ok(t) => match t.duration_since(UNIX_EPOCH) { Ok(d) => d.as_secs(), Err(_inspected) => { if is_folder { warnings.push(flc!("core_folder_modified_before_epoch", name = current_file_name.to_string_lossy().to_string())); } else { warnings.push(flc!("core_file_modified_before_epoch", name = current_file_name.to_string_lossy().to_string())); } 0 } }, Err(e) => { if is_folder { warnings.push(flc!( "core_folder_no_modification_date", name = current_file_name.to_string_lossy().to_string(), reason = e.to_string() )); } else { warnings.push(flc!( "core_file_no_modification_date", name = current_file_name.to_string_lossy().to_string(), reason = e.to_string() )); } 0 } } } #[cfg(target_family = "windows")] pub(crate) fn inode(_fe: &FileEntry) -> Option { None } #[cfg(target_family = "unix")] pub(crate) fn inode(fe: &FileEntry) -> Option { if let Ok(meta) = fs::metadata(&fe.path) { Some(meta.ino()) } else { None } } pub(crate) fn take_1_per_inode((k, mut v): (Option, Vec)) -> Vec { if k.is_some() { v.drain(1..); } v } #[cfg(test)] mod tests { use std::fs::File; use std::io::prelude::*; use std::time::{Duration, SystemTime}; use std::{fs, io}; use indexmap::IndexSet; use super::*; use crate::common::tool_data::*; impl CommonData for CommonToolData { type Info = (); type Parameters = (); fn get_information(&self) -> Self::Info {} fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { self } fn get_cd_mut(&mut self) -> &mut CommonToolData { self } fn found_any_items(&self) -> bool { false } } static NOW: std::sync::LazyLock = std::sync::LazyLock::new(|| SystemTime::UNIX_EPOCH + Duration::new(100, 0)); const CONTENT: &[u8; 1] = b"a"; fn normalize_path(item: &Path) -> PathBuf { let canonicalized = if cfg!(windows) { // Only canonicalize if it's not a network path // This can be done by checking if path starts with \\?\UNC\ if let Ok(dir_can) = item.canonicalize() && let Some(dir_can_str) = dir_can.to_string_lossy().strip_prefix(r"\\?\") && dir_can_str.chars().nth(1) == Some(':') { PathBuf::from(dir_can_str) } else { item.to_path_buf() } } else { if let Ok(dir) = item.canonicalize() { dir } else { item.to_path_buf() } }; #[cfg(target_family = "windows")] return crate::common::normalize_windows_path(&canonicalized); #[cfg(not(target_family = "windows"))] return canonicalized; } fn create_files(dir: &Path) -> io::Result<(PathBuf, PathBuf, PathBuf)> { let (src, hard, other_file) = (dir.join("a"), dir.join("b"), dir.join("c")); let mut file = File::create(&src)?; file.write_all(CONTENT)?; fs::hard_link(&src, &hard)?; file.set_modified(*NOW)?; let mut file = File::create(&other_file)?; file.write_all(CONTENT)?; file.set_modified(*NOW)?; Ok((normalize_path(&src), normalize_path(&hard), normalize_path(&other_file))) } #[test] fn test_traversal() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let dir_path = normalize_path(dir.path()); let (src, hard, other_file) = create_files(&dir_path)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail calculating duration since epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_paths([dir.path().to_owned()].to_vec()); common_data.set_minimal_file_size(0); match DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(&Arc::default()) .common_data(&common_data) .build() .run() { DirTraversalResult::SuccessFiles { warnings: _, grouped_file_entries, } => { let actual: IndexSet<_> = grouped_file_entries.into_values().flatten().collect(); assert_eq!( IndexSet::from([ FileEntry { path: normalize_path(&src), size: 1, modified_date: secs, }, FileEntry { path: normalize_path(&hard), size: 1, modified_date: secs, }, FileEntry { path: normalize_path(&other_file), size: 1, modified_date: secs, }, ]), actual ); } DirTraversalResult::Stopped => { panic!("Expect SuccessFiles."); } } Ok(()) } fn create_temp_structure(dir: &Path) -> io::Result<(PathBuf, PathBuf, PathBuf)> { let global_file = dir.join("global_file.txt"); let other_dir = dir.join("other_file"); fs::create_dir_all(&other_dir)?; let other_file = other_dir.join("other_file.txt"); let mut f = File::create(&global_file)?; f.write_all(b"global_file")?; f.set_modified(*NOW)?; let mut f2 = File::create(&other_file)?; f2.write_all(b"other_file")?; f2.set_modified(*NOW)?; let global_file = normalize_path(&global_file); let other_file = normalize_path(&other_file); let other_dir = normalize_path(&other_dir); Ok((global_file, other_file, other_dir)) } fn run_traversal(common_data: &CommonToolData) -> Vec { match DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(&Arc::default()) .common_data(common_data) .build() .run() { DirTraversalResult::SuccessFiles { grouped_file_entries, .. } => grouped_file_entries.into_values().flatten().collect(), DirTraversalResult::Stopped => panic!("Expect SuccessFiles."), } } #[test] #[expect(clippy::needless_for_each)] fn test_traversal_with_and_without_excluded_dir() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let dir_path = dir.path().to_path_buf(); let dir_path = normalize_path(&dir_path); let (global_file, other_file, other_dir) = create_temp_structure(&dir_path)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail calculating duration since epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_paths([dir.path().to_owned()].to_vec()); common_data.set_minimal_file_size(0); let mut actual: Vec<_> = run_traversal(&common_data); actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path)); assert_eq!(2, actual.len()); assert!(actual.contains(&FileEntry { path: global_file.clone(), size: 11, modified_date: secs })); assert!(actual.contains(&FileEntry { path: other_file.clone(), size: 10, modified_date: secs })); let mut common_data2 = CommonToolData::new(ToolType::SimilarImages); common_data2.directories.set_included_paths([dir.path().to_owned()].to_vec()); common_data2.directories.set_excluded_paths([other_dir].to_vec()); common_data2.set_minimal_file_size(0); let mut actual: Vec<_> = run_traversal(&common_data2); actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path)); assert_eq!(1, actual.len()); assert!(actual.contains(&FileEntry { path: global_file.clone(), size: 11, modified_date: secs })); let mut common_data3 = CommonToolData::new(ToolType::SimilarImages); common_data3.directories.set_included_paths([dir.path().to_owned()].to_vec()); common_data3.directories.set_excluded_paths([other_file.clone()].to_vec()); common_data3.set_minimal_file_size(0); let mut actual: Vec<_> = run_traversal(&common_data3); actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path)); assert_eq!(1, actual.len()); assert!(actual.contains(&FileEntry { path: global_file.clone(), size: 11, modified_date: secs })); let mut common_data4 = CommonToolData::new(ToolType::SimilarImages); common_data4.directories.set_included_paths([global_file.clone()].to_vec()); common_data4.set_minimal_file_size(0); let mut actual: Vec<_> = run_traversal(&common_data4); actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path)); assert_eq!(1, actual.len()); assert!(actual.contains(&FileEntry { path: global_file.clone(), size: 11, modified_date: secs })); let mut common_data5 = CommonToolData::new(ToolType::SimilarImages); common_data5.directories.set_included_paths([global_file.clone(), other_file.clone()].to_vec()); common_data5.set_minimal_file_size(0); let mut actual: Vec<_> = run_traversal(&common_data5); actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path)); assert_eq!(2, actual.len()); assert!(actual.contains(&FileEntry { path: global_file.clone(), size: 11, modified_date: secs })); assert!(actual.contains(&FileEntry { path: other_file.clone(), size: 10, modified_date: secs })); // 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 let mut common_data6 = CommonToolData::new(ToolType::SimilarImages); common_data6.directories.set_included_paths([global_file.clone(), other_file.clone()].to_vec()); common_data6.directories.set_excluded_paths([other_file].to_vec()); common_data6.set_minimal_file_size(0); let mut actual: Vec<_> = run_traversal(&common_data6); actual.iter_mut().for_each(|e| e.path = normalize_path(&e.path)); assert_eq!(1, actual.len()); assert!(actual.contains(&FileEntry { path: global_file, size: 11, modified_date: secs })); // This test is invalid - other dir should be removed by optimizer // let mut common_data7 = CommonToolData::new(ToolType::SimilarImages); // common_data7.directories.set_included_paths([other_file.clone()].to_vec()); // common_data7.directories.set_excluded_paths([other_dir.clone()].to_vec()); // common_data7.set_minimal_file_size(0); // // let actual: IndexSet<_> = run_traversal(&common_data7).into_iter().collect(); // assert_eq!(0, actual.len()); Ok(()) } #[cfg(target_family = "unix")] #[test] fn test_traversal_group_by_inode() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let dir_path = normalize_path(dir.path()); let (src, _, other) = create_files(&dir_path)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail calculating duration since epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_paths([dir.path().to_owned()].to_vec()); common_data.set_minimal_file_size(0); match DirTraversalBuilder::new() .group_by(inode) .stop_flag(&Arc::default()) .common_data(&common_data) .build() .run() { DirTraversalResult::SuccessFiles { warnings: _, grouped_file_entries, } => { let actual: IndexSet<_> = grouped_file_entries.into_iter().flat_map(take_1_per_inode).collect(); assert_eq!( IndexSet::from([ FileEntry { path: normalize_path(&src), size: 1, modified_date: secs, }, FileEntry { path: normalize_path(&other), size: 1, modified_date: secs, }, ]), actual ); } DirTraversalResult::Stopped => { panic!("Expect SuccessFiles."); } } Ok(()) } #[cfg(target_family = "windows")] #[test] fn test_traversal_group_by_inode() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let dir_path = normalize_path(&dir.path()); let (src, hard, other) = create_files(&dir_path)?; let secs = NOW.duration_since(SystemTime::UNIX_EPOCH).expect("Cannot fail duration from epoch").as_secs(); let mut common_data = CommonToolData::new(ToolType::SimilarImages); common_data.directories.set_included_paths([dir_path.to_owned()].to_vec()); common_data.set_minimal_file_size(0); match DirTraversalBuilder::new() .group_by(inode) .stop_flag(&Arc::default()) .common_data(&common_data) .build() .run() { DirTraversalResult::SuccessFiles { warnings: _, grouped_file_entries, } => { let actual: IndexSet<_> = grouped_file_entries.into_iter().flat_map(take_1_per_inode).collect(); assert_eq!( IndexSet::from([ FileEntry { path: src, size: 1, modified_date: secs, }, FileEntry { path: hard, size: 1, modified_date: secs, }, FileEntry { path: other, size: 1, modified_date: secs, }, ]), actual ); } _ => { panic!("Expect SuccessFiles."); } }; Ok(()) } } ================================================ FILE: czkawka_core/src/common/directories.rs ================================================ use std::path::{Path, PathBuf}; #[cfg(target_family = "unix")] use std::{fs, os::unix::fs::MetadataExt}; use crate::common::traits::ResultEntry; use crate::flc; use crate::helpers::messages::Messages; #[derive(Debug, Clone, Default)] pub struct Directories { pub(crate) included_directories: Vec, pub(crate) excluded_directories: Vec, pub(crate) reference_directories: Vec, pub(crate) included_files: Vec, pub(crate) excluded_files: Vec, pub(crate) reference_files: Vec, pub(crate) original_included_paths: Vec, pub(crate) original_excluded_paths: Vec, pub(crate) original_reference_paths: Vec, pub(crate) exclude_other_filesystems: Option, #[cfg(target_family = "unix")] pub(crate) included_dev_ids: Vec, } impl Directories { pub fn new() -> Self { Default::default() } pub(crate) fn set_reference_paths(&mut self, reference_paths: Vec) -> Messages { self.reference_files = Vec::new(); self.reference_directories = Vec::new(); self.original_reference_paths = reference_paths.clone(); self.process_paths(reference_paths, true, false) } pub(crate) fn set_included_paths(&mut self, included_paths: Vec) -> Messages { self.included_files = Vec::new(); self.included_directories = Vec::new(); self.original_included_paths = included_paths.clone(); self.process_paths(included_paths, false, false) } pub(crate) fn set_excluded_paths(&mut self, excluded_paths: Vec) -> Messages { self.excluded_files = Vec::new(); self.excluded_directories = Vec::new(); self.original_excluded_paths = excluded_paths.clone(); self.process_paths(excluded_paths, false, true) } fn process_paths(&mut self, paths: Vec, is_reference: bool, is_excluded: bool) -> Messages { let mut messages: Messages = Messages::new(); if paths.is_empty() { return messages; } for path in paths { if is_excluded && path.to_string_lossy() == "/" { messages.errors.push(flc!("core_excluded_paths_pointless_slash")); break; } let (dir, msg) = Self::canonicalize_and_clear_path(&path, is_excluded); messages.extend_with_another_messages(msg); if let Some(dir) = dir { #[cfg(target_family = "windows")] let dir = crate::common::normalize_windows_path(&dir); match (dir.is_file(), is_reference, is_excluded) { (false, true, false) => self.reference_directories.push(dir), (false, false, false) => self.included_directories.push(dir), (false, false, true) => self.excluded_directories.push(dir), (true, true, false) => self.reference_files.push(dir), (true, false, false) => self.included_files.push(dir), (true, false, true) => self.excluded_files.push(dir), _ => unreachable!("Invalid combination of parameters in process_paths"), } } } messages } fn canonicalize_and_clear_path(path: &Path, is_excluded: bool) -> (Option, Messages) { let mut messages = Messages::new(); let mut path = path.to_path_buf(); if !path.exists() { if !is_excluded { messages.warnings.push(flc!("core_path_must_exists", path = path.to_string_lossy().to_string())); } return (None, messages); } if !path.is_dir() && !path.is_file() { messages.warnings.push(flc!("core_must_be_directory_or_file", path = path.to_string_lossy().to_string())); return (None, messages); } // Try to canonicalize them if cfg!(windows) { // Only canonicalize if it's not a network path // This can be done by checking if path starts with \\?\UNC\ if let Ok(dir_can) = path.canonicalize() && let Some(dir_can_str) = dir_can.to_string_lossy().strip_prefix(r"\\?\") && dir_can_str.chars().nth(1) == Some(':') { path = PathBuf::from(dir_can_str); } } else { if let Ok(dir) = path.canonicalize() { path = dir; } } #[cfg(target_family = "windows")] let path = crate::common::normalize_windows_path(&path); (Some(path), messages) } #[cfg(target_family = "unix")] pub(crate) fn set_exclude_other_filesystems(&mut self, exclude_other_filesystems: bool) { self.exclude_other_filesystems = Some(exclude_other_filesystems); } pub(crate) fn optimize_directories(&mut self, recursive_search: bool, skip_exist_check: bool) -> Result { let mut messages: Messages = Messages::new(); if self.original_included_paths.is_empty() { messages.critical = Some(flc!("core_cannot_start_scan_no_included_paths")); return Err(messages); } if self.included_directories.is_empty() && self.included_files.is_empty() { messages.critical = Some(flc!("core_skip_exist_check_all_included_paths_nonexistent")); return Err(messages); } // Remove duplicated entries like: "/", "/" for items in &mut [ &mut self.included_directories, &mut self.excluded_directories, &mut self.reference_directories, &mut self.included_files, &mut self.excluded_files, &mut self.reference_files, ] { items.sort_unstable(); items.dedup(); } // Optimize for duplicated included directories - "/", "/home". "/home/Pulpit" to "/" // Do not use when not using recursive search if recursive_search && !self.exclude_other_filesystems.unwrap_or(false) { for kk in [&mut self.included_directories, &mut self.excluded_directories] { let cloned = kk.clone(); kk.retain(|item| !cloned.iter().any(|other_item| item != other_item && item.starts_with(other_item))); } } // Remove included directories which are inside any excluded directory // Same with included files for kk in [&mut self.included_directories, &mut self.included_files] { kk.retain(|id| !self.excluded_directories.iter().any(|ed| id.starts_with(ed))); } // Remove included files inside included directories { let kk = &mut self.included_files; kk.retain(|id| !self.included_directories.iter().any(|ed| id.starts_with(ed))); } // Also check if files are not excluded directly { let kk = &mut self.included_files; kk.retain(|id| !self.excluded_directories.iter().any(|ed| id == ed)); } // Remove non existed directories and files if !skip_exist_check { for kk in [ &mut self.excluded_files, &mut self.excluded_directories, &mut self.included_files, &mut self.included_directories, ] { kk.retain(|path| path.exists()); } } // Excluded paths must are inside included path, because otherwise they are pointless // So first, removing included files, that are inside excluded directories // So this will allow to remove excluded directories outside included directories self.included_files.retain(|ifile| !self.excluded_directories.iter().any(|ed| ifile.starts_with(ed))); self.excluded_directories.retain(|ed| self.included_directories.iter().any(|id| ed.starts_with(id))); // Selecting Reference folders { self.reference_directories.retain(|folder| self.included_directories.iter().any(|e| folder.starts_with(e))); self.reference_files .retain(|file| self.included_directories.iter().any(|e| file.starts_with(e)) || self.included_files.iter().any(|f| file == f)); } // Not needed, but better is to have sorted everything for items in &mut [ &mut self.included_directories, &mut self.excluded_directories, &mut self.reference_directories, &mut self.included_files, &mut self.excluded_files, &mut self.reference_files, ] { items.sort_unstable(); } // 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 #[cfg(target_family = "unix")] if self.exclude_other_filesystems() { for d in &self.included_directories { match fs::metadata(d) { Ok(m) => self.included_dev_ids.push(m.dev()), Err(_) => messages.errors.push(flc!("core_paths_unable_to_get_device_id", path = d.to_string_lossy().to_string())), } } } if self.included_directories.is_empty() && self.included_files.is_empty() { messages.critical = Some(flc!("core_missing_no_chosen_included_path")); return Err(messages); } if self.reference_directories == self.included_directories && self.included_files == self.reference_files { messages.critical = Some(flc!("core_reference_included_paths_same")); return Err(messages); } Ok(messages) } pub(crate) fn is_in_referenced_directory(&self, path: &Path) -> bool { self.reference_directories.iter().any(|e| path.starts_with(e)); self.reference_files.iter().any(|e| e.as_path() == path); self.reference_directories.iter().any(|e| path.starts_with(e)) || self.reference_files.iter().any(|e| e.as_path() == path) } pub(crate) fn is_excluded_dir(&self, path: &Path) -> bool { #[cfg(target_family = "windows")] let path = crate::common::normalize_windows_path(path); // We're assuming that `excluded_directories` are already normalized self.excluded_directories.iter().any(|p| p.as_path() == path) } pub(crate) fn is_excluded_file(&self, path: &Path) -> bool { #[cfg(target_family = "windows")] let path = crate::common::normalize_windows_path(path); // We're assuming that `excluded_files` are already normalized self.excluded_files.iter().any(|p| p.as_path() == path) } // Usually it is not required, because if main directory is excluded, then we don't run check on // every single children, different situation is with excluded single file pub(crate) fn is_excluded_item_in_dir(&self, path: &Path) -> bool { #[cfg(target_family = "windows")] let path = crate::common::normalize_windows_path(path); #[cfg(target_family = "windows")] let path = &path; // We're assuming that `excluded_directories` are already normalized self.excluded_directories.iter().any(|p| path.starts_with(p)) } #[cfg(target_family = "unix")] pub(crate) fn exclude_other_filesystems(&self) -> bool { self.exclude_other_filesystems.unwrap_or(false) } #[cfg(target_family = "unix")] pub(crate) fn is_on_other_filesystems>(&self, path: P) -> Result { let path = path.as_ref(); match fs::metadata(path) { Ok(m) => { if m.file_type().is_file() && !self.included_files.is_empty() && self.included_files.contains(&path.to_path_buf()) { return Ok(false); // Exact equality for included files is always allowed } Ok(!self.included_dev_ids.iter().any(|&id| id == m.dev())) } Err(_) => Err(flc!("core_paths_unable_to_get_device_id", path = path.to_string_lossy().to_string())), } } pub(crate) fn filter_reference_folders(&self, entries_to_check: Vec>) -> Vec<(T, Vec)> where T: ResultEntry, { entries_to_check .into_iter() .filter_map(|vec_file_entry| { 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())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>() } } #[cfg(test)] mod tests { use std::path::PathBuf; use super::*; #[test] fn test_no_included_paths_errors() { let mut d = Directories::new(); let msgs = d.optimize_directories(true, true).unwrap_err(); assert!(msgs.critical.is_some()); } #[test] fn test_dedup_included_directories() { let p = PathBuf::from("/this/path/does/not/exist/dedup"); let mut d = Directories::new(); d.included_directories.push(p.clone()); d.included_directories.push(p.clone()); d.original_included_paths.push(p.clone()); d.original_included_paths.push(p.clone()); let _msgs = d.optimize_directories(true, true).unwrap(); assert_eq!(d.included_directories, vec![p]); } #[test] fn test_excluded_removes_included_inside() { let base = PathBuf::from("/this/base/does/not/exist"); let sub = base.join("sub"); let mut d = Directories::new(); d.included_directories.push(sub.clone()); d.original_included_paths.push(sub); d.excluded_directories.push(base); let _msgs = d.optimize_directories(true, true).unwrap_err(); assert_eq!(d.included_directories, Vec::::new()); } #[test] fn test_optimize_nested_included_directories_dedup() { let mut d = Directories::new(); d.included_directories.push(PathBuf::from("/")); d.original_included_paths.push(PathBuf::from("/")); d.included_directories.push(PathBuf::from("/home")); d.original_included_paths.push(PathBuf::from("/home")); d.included_directories.push(PathBuf::from("/home/Pulpit")); d.original_included_paths.push(PathBuf::from("/home/Pulpit")); // use recursive_search = true and skip_exist_check = true as requested let msgs = d.optimize_directories(true, true).unwrap(); // only root should remain after dedup assert_eq!(d.included_directories, vec![PathBuf::from("/")]); assert!(msgs.critical.is_none()); } #[test] fn test_excluded_directories_pruned_to_inside_included() { let mut d = Directories::new(); d.included_directories.push(PathBuf::from("/this/include")); d.original_included_paths.push(PathBuf::from("/this/include")); d.excluded_directories.push(PathBuf::from("/this/include/sub")); d.excluded_directories.push(PathBuf::from("/other/place")); d.original_excluded_paths.push(PathBuf::from("/this/include/sub")); d.original_excluded_paths.push(PathBuf::from("/other/place")); let _msgs = d.optimize_directories(true, true).unwrap(); assert_eq!(d.included_directories, vec![PathBuf::from("/this/include")]); assert_eq!(d.excluded_directories, vec![PathBuf::from("/this/include/sub")]); } #[test] fn test_reference_dirs_and_files_retained_correctly() { let mut d = Directories::new(); d.included_directories.push(PathBuf::from("/a")); d.original_included_paths.push(PathBuf::from("/a")); d.included_files.push(PathBuf::from("/a/included_file.txt")); d.original_included_paths.push(PathBuf::from("/a/included_file.txt")); d.reference_directories.push(PathBuf::from("/a/sub")); d.reference_directories.push(PathBuf::from("/other")); d.reference_files.push(PathBuf::from("/a/included_file.txt")); d.reference_files.push(PathBuf::from("/other/file2.txt")); let _msgs = d.optimize_directories(true, true).unwrap(); assert_eq!(d.included_directories, vec![PathBuf::from("/a")]); assert_eq!(d.excluded_directories, Vec::::new()); assert_eq!(d.included_files, Vec::::new()); assert_eq!(d.excluded_files, Vec::::new()); assert_eq!(d.reference_directories, vec![PathBuf::from("/a/sub")]); assert_eq!(d.reference_files, vec![PathBuf::from("/a/included_file.txt")]); } #[test] fn test_reference_equals_included_error() { let mut d = Directories::new(); d.included_directories.push(PathBuf::from("/same")); d.reference_directories.push(PathBuf::from("/same")); d.included_files = Vec::new(); d.reference_files = Vec::new(); let msgs = d.optimize_directories(true, true).unwrap_err(); assert!(msgs.critical.is_some()); } #[test] fn test_included_files_removed_when_equal_to_excluded_directory() { let mut d = Directories::new(); d.included_directories.push(PathBuf::from("/base")); d.original_included_paths.push(PathBuf::from("/base")); d.included_files.push(PathBuf::from("/base/file")); d.original_included_paths.push(PathBuf::from("/base/file")); // excluded directory equals included file path d.excluded_directories.push(PathBuf::from("/base/file")); d.original_excluded_paths.push(PathBuf::from("/base/file")); let _msgs = d.optimize_directories(true, true).unwrap(); // included_files should be removed because it equals an excluded directory assert!(d.included_files.is_empty()); // excluded_directories should be retained as it's inside included_directories assert_eq!(d.excluded_directories, vec![PathBuf::from("/base/file")]); assert_eq!(d.included_directories, vec![PathBuf::from("/base")]); } } ================================================ FILE: czkawka_core/src/common/extensions.rs ================================================ use std::ffi::OsStr; use indexmap::IndexSet; use crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_EXTENSIONS, TEXT_FILES_EXTENSIONS, VIDEO_FILES_EXTENSIONS}; use crate::flc; use crate::helpers::messages::Messages; #[derive(Debug, Clone, Default)] pub struct Extensions { allowed_extensions_hashset: IndexSet, excluded_extensions_hashset: IndexSet, } impl Extensions { pub fn new() -> Self { Default::default() } pub(crate) fn filter_extensions(file_extensions: Vec) -> (IndexSet, Messages) { let mut messages = Messages::new(); let extensions_hashset: IndexSet = file_extensions .into_iter() .flat_map(|e| match e.trim().trim_start_matches(".").to_lowercase().as_str() { "image" => IMAGE_RS_EXTENSIONS.iter().map(|s| s.to_string()).collect(), "video" => VIDEO_FILES_EXTENSIONS.iter().map(|s| s.to_string()).collect(), "music" => AUDIO_FILES_EXTENSIONS.iter().map(|s| s.to_string()).collect(), "text" => TEXT_FILES_EXTENSIONS.iter().map(|s| s.to_string()).collect(), _ => vec![e], }) .filter_map(|extension| { let e = extension.trim().trim_start_matches(".").to_lowercase(); if e.is_empty() { return None; } if e.contains(' ') { messages.warnings.push(flc!("core_invalid_extension_contains_space", extension = extension)); return None; } if e.contains('.') { messages.warnings.push(flc!("core_invalid_extension_contains_dot", extension = extension)); return None; } Some(e) }) .collect(); (extensions_hashset, messages) } pub(crate) fn set_allowed_extensions(&mut self, allowed_extensions: Vec) -> Messages { let (extensions, messages) = Self::filter_extensions(allowed_extensions); self.allowed_extensions_hashset = extensions; messages } pub(crate) fn set_excluded_extensions(&mut self, excluded_extensions: Vec) -> Messages { let (extensions, messages) = Self::filter_extensions(excluded_extensions); self.excluded_extensions_hashset = extensions; messages } #[expect(clippy::string_slice)] // Valid, because we address go to dot, which is known ascii character pub(crate) fn check_if_entry_have_valid_extension(&self, file_name: &OsStr) -> bool { if self.allowed_extensions_hashset.is_empty() && self.excluded_extensions_hashset.is_empty() { return true; } // Using entry_data.path().extension() is a lot of slower, even 5 times let Some(file_name_str) = file_name.to_str() else { return false }; let Some(extension_idx) = file_name_str.rfind('.') else { return false }; let extension = &file_name_str[extension_idx + 1..]; if !self.allowed_extensions_hashset.is_empty() { if extension.chars().all(|c| c.is_ascii_lowercase()) { self.allowed_extensions_hashset.contains(extension) } else { self.allowed_extensions_hashset.contains(&extension.to_lowercase()) } } else if extension.chars().all(|c| c.is_ascii_lowercase()) { !self.excluded_extensions_hashset.contains(extension) } else { !self.excluded_extensions_hashset.contains(&extension.to_lowercase()) } } // E.g. when using similar videos, user can provide extensions like "mp4,flv", but if user provide "mp4,jpg" then // it will be only "mp4" because "jpg" is not valid extension for videos fn intersection_allowed_extensions(&mut self, file_extensions: &[&str]) { self.allowed_extensions_hashset.retain(|ext| file_extensions.contains(&ext.as_str())); } // Tool extensions may be set by the tool itself, e.g. similar images may only use image extensions pub(crate) fn set_and_validate_extensions(&mut self, tool_extensions: Option<&[&str]>) -> Result<(), String> { let user_set_any_allowed_extensions = !self.allowed_extensions_hashset.is_empty(); let tool_have_any_extensions = tool_extensions.is_some(); // If user not set any extensions and tool not have any allowed extension, it is fine if !user_set_any_allowed_extensions && !tool_have_any_extensions { return Ok(()); } if let Some(tool_extensions) = tool_extensions { // If there is no selected allowed extensions, that means that are all allowed // If there are some allowed extensions, we need to do intersection with tool extensions if user_set_any_allowed_extensions { self.intersection_allowed_extensions(tool_extensions); } else { self.allowed_extensions_hashset = tool_extensions.iter().map(|ext| ext.trim_start_matches('.').to_string()).collect(); } } let both_extensions = self.allowed_extensions_hashset.intersection(&self.excluded_extensions_hashset).cloned().collect::>(); self.allowed_extensions_hashset.retain(|ext| !both_extensions.contains(ext)); self.excluded_extensions_hashset.retain(|ext| !both_extensions.contains(ext)); if self.allowed_extensions_hashset.is_empty() { if let Some(tool_extensions) = tool_extensions { Err(flc!("core_needs_allowed_extensions_limited_by_tool", extensions = tool_extensions.join(", "))) } else { Err(flc!("core_needs_allowed_extensions")) } } else { Ok(()) } } } #[cfg(test)] mod tests { use std::fs; use super::*; #[test] fn test_filter_extensions_basic_and_replacements() { // Empty string let (exts, msgs) = Extensions::filter_extensions(Vec::new()); assert!(exts.is_empty()); assert!(msgs.messages.is_empty() && msgs.warnings.is_empty() && msgs.errors.is_empty()); // Basic extensions let (exts, msgs) = Extensions::filter_extensions(vec!["jpg".to_string(), "png".to_string(), "gif".to_string()]); assert_eq!(exts.len(), 3); assert!(exts.contains("jpg") && exts.contains("png") && exts.contains("gif")); assert!(msgs.warnings.is_empty()); // With dots let (exts, _) = Extensions::filter_extensions(vec![".jpg".to_string(), ".png".to_string()]); assert_eq!(exts.len(), 2); assert!(exts.contains("jpg") && exts.contains("png")); // IMAGE replacement let (exts, _) = Extensions::filter_extensions(vec!["IMAGE".to_string()]); assert!(exts.contains("jpg") && exts.contains("png") && exts.contains("bmp")); // VIDEO replacement let (exts, _) = Extensions::filter_extensions(vec!["VIDEO".to_string()]); assert!(exts.contains("mp4") && exts.contains("mkv") && exts.contains("avi")); // Invalid extensions with dot inside let (exts, msgs) = Extensions::filter_extensions(vec!["jpg".to_string(), "test.bad".to_string(), "png".to_string()]); assert_eq!(exts.len(), 2); assert!(!exts.contains("test.bad")); assert!(msgs.warnings.iter().any(|w| w.contains("test.bad"))); // Invalid extensions with space let (exts, msgs) = Extensions::filter_extensions(vec!["jpg".to_string(), "bad ext".to_string(), "png".to_string()]); assert!(!exts.contains("bad ext")); assert!(msgs.warnings.iter().any(|w| w.contains("bad ext"))); } #[test] fn test_check_if_entry_have_valid_extension() { let temp_dir = tempfile::tempdir().unwrap(); let file_jpg = temp_dir.path().join("test.jpg"); let file_png = temp_dir.path().join("test.PNG"); let file_gif = temp_dir.path().join("test.gif"); let file_txt = temp_dir.path().join("test.txt"); let file_no_ext = temp_dir.path().join("noext"); fs::write(&file_jpg, "test").unwrap(); fs::write(&file_png, "test").unwrap(); fs::write(&file_gif, "test").unwrap(); fs::write(&file_txt, "test").unwrap(); fs::write(&file_no_ext, "test").unwrap(); // No extensions set - all should pass let ext = Extensions::new(); assert!( ext.check_if_entry_have_valid_extension( &fs::read_dir(&temp_dir) .unwrap() .find(|e| e.as_ref().unwrap().file_name() == "test.jpg") .unwrap() .unwrap() .file_name() ) ); // Allowed extensions let mut ext = Extensions::new(); ext.set_allowed_extensions(vec!["jpg".to_string(), "png".to_string()]); let entries: Vec<_> = fs::read_dir(&temp_dir).unwrap().map(|e| e.unwrap()).collect(); assert!(ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == "test.jpg").unwrap().file_name())); assert!(ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == "test.PNG").unwrap().file_name())); // case insensitive assert!(!ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == "test.gif").unwrap().file_name())); assert!(!ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == "noext").unwrap().file_name())); // Excluded extensions let mut ext = Extensions::new(); ext.set_excluded_extensions(vec!["txt".to_string()]); assert!(ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == "test.jpg").unwrap().file_name())); assert!(!ext.check_if_entry_have_valid_extension(&entries.iter().find(|e| e.file_name() == "test.txt").unwrap().file_name())); } } ================================================ FILE: czkawka_core/src/common/ffmpeg_utils.rs ================================================ use std::process::{Command, Stdio}; use crate::common::process_utils::disable_windows_console_window; pub fn check_if_ffprobe_ffmpeg_exists() -> bool { let mut ffmpeg_command = Command::new("ffmpeg"); disable_windows_console_window(&mut ffmpeg_command); let ffmpeg_ok = ffmpeg_command .arg("-version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false); let mut ffprobe_command = Command::new("ffprobe"); disable_windows_console_window(&mut ffprobe_command); let ffprobe_ok = ffprobe_command .arg("-version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false); ffprobe_ok && ffmpeg_ok } ================================================ FILE: czkawka_core/src/common/image.rs ================================================ use std::fmt::Debug; use std::fs::File; use std::panic; use std::path::Path; use fast_image_resize::{FilterType as FirFilterType, ResizeAlg, ResizeOptions as FirResizeOptions, Resizer}; use image::{DynamicImage, ImageReader}; use log::{error, trace}; use nom_exif::{ExifIter, ExifTag, MediaParser, MediaSource}; use crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS}; use crate::common::create_crash_message; use crate::flc; const MAXIMUM_IMAGE_PIXELS: u32 = 2_000_000_000; pub fn register_image_decoding_hooks() { #[cfg(feature = "heif")] libheif_rs::integration::image::register_all_decoding_hooks(); jxl_oxide::integration::register_image_decoding_hook(); } // Using this instead of image::open because image::open only reads content of files if extension matches content // This is not really helpful when trying to show preview of files with wrong extensions pub(crate) fn decode_normal_image(path: &str) -> Result { let file = File::open(path).map_err(|e| e.to_string())?; let reader = ImageReader::new(std::io::BufReader::new(file)).with_guessed_format().map_err(|e| e.to_string())?; let img = reader.decode().map_err(|e| e.to_string())?; Ok(img) } pub struct LoadedImage { pub image: DynamicImage, pub original_width: u32, pub original_height: u32, } impl Debug for LoadedImage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LoadedImage") .field("original_width", &self.original_width) .field("original_height", &self.original_height) .field( "image", &format!( "DynamicImage of type {:?} with dimensions {}x{}", self.image.color(), self.image.width(), self.image.height() ), ) .finish() } } pub fn get_dynamic_image_from_path(path: &str, opts: Option) -> Result { let path_lower = Path::new(path).extension().unwrap_or_default().to_string_lossy().to_lowercase(); trace!("decoding file \"{path}\""); let res = panic::catch_unwind(|| { let img = if RAW_IMAGE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext)) { get_raw_image(path).map_err(|e| flc!("core_image_open_failed", path = path, reason = e)) } else { // Heic files must be registered in image-rs decode_normal_image(path).map_err(|e| flc!("core_image_open_failed", path = path, reason = e)) }?; if img.width() == 0 || img.height() == 0 { return Err(flc!("core_image_zero_dimensions", path = path)); } if img.width() as u64 * img.height() as u64 > MAXIMUM_IMAGE_PIXELS as u64 { return Err(flc!("core_image_too_large", width = img.width(), height = img.height(), max = MAXIMUM_IMAGE_PIXELS)); } let original_width = img.width(); let original_height = img.height(); if let Some(opts) = opts { Ok((resize_image(img, opts), original_width, original_height)) } else { Ok((img, original_width, original_height)) } }); if let Ok(res) = res { match res { Ok((img, w, h)) => { let rotation = get_rotation_from_exif(path).unwrap_or(None); let img_rotated = match rotation { Some(ExifOrientation::Normal) | None => img, Some(ExifOrientation::MirrorHorizontal) => img.fliph(), Some(ExifOrientation::Rotate180) => img.rotate180(), Some(ExifOrientation::MirrorVertical) => img.flipv(), Some(ExifOrientation::MirrorHorizontalAndRotate270CW) => img.fliph().rotate270(), Some(ExifOrientation::Rotate90CW) => img.rotate90(), Some(ExifOrientation::MirrorHorizontalAndRotate90CW) => img.fliph().rotate90(), Some(ExifOrientation::Rotate270CW) => img.rotate270(), }; Ok(LoadedImage { image: img_rotated, original_width: w, original_height: h, }) } Err(e) => Err(flc!("core_image_open_failed", path = path, reason = e)), } } else { let message = create_crash_message("Image-rs or libraw-rs or jxl-oxide", path, "https://github.com/image-rs/image/issues"); error!("{message}"); Err(message) } } #[derive(Debug, Clone, Copy)] pub struct ImgResizeOptions { pub max_width: u32, pub max_height: u32, pub filter: FirFilterType, } fn resize_image(img: DynamicImage, opts: ImgResizeOptions) -> DynamicImage { let orig_w = img.width(); let orig_h = img.height(); if orig_w <= opts.max_width && orig_h <= opts.max_height { return img; } let scale = f32::min(opts.max_width as f32 / orig_w as f32, opts.max_height as f32 / orig_h as f32); let new_w = ((orig_w as f32 * scale) as u32).max(1).min(img.width()); let new_h = ((orig_h as f32 * scale) as u32).max(1).min(img.height()); let mut dst = DynamicImage::new(new_w, new_h, img.color()); let fir_opts = FirResizeOptions::new().resize_alg(ResizeAlg::Interpolation(opts.filter)); match Resizer::new().resize(&img, &mut dst, Some(&fir_opts)) { Ok(()) => dst, Err(_) => { // Fall back to the image-rs built-in resizer if fast_image_resize fails, quite unlikely img.resize(new_w, new_h, image::imageops::FilterType::Lanczos3) } } } #[cfg(feature = "libraw")] pub(crate) fn get_raw_image>(path: P) -> Result { let buf = std::fs::read(path.as_ref()).map_err(|e| format!("Error reading image: {e}"))?; let processor = libraw::Processor::new(); let processed = processor.process_8bit(&buf).map_err(|e| format!("Error processing RAW image: {e}"))?; let width = processed.width(); let height = processed.height(); let data = processed.to_vec(); let data_len = data.len(); let buffer = image::ImageBuffer::from_raw(width, height, data).ok_or(format!( "Cannot create ImageBuffer from raw image with width: {width} and height: {height} and data length: {data_len}", ))?; Ok(DynamicImage::ImageRgb8(buffer)) } #[cfg(not(feature = "libraw"))] pub(crate) fn get_raw_image + std::fmt::Debug>(path: P) -> Result { use rawler::decoders::RawDecodeParams; use rawler::imgop::develop::RawDevelop; use rawler::rawsource::RawSource; let mut timer = crate::helpers::debug_timer::Timer::new("Rawler"); 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()))?; timer.checkpoint("Created RawSource"); let decoder = rawler::get_decoder(&raw_source).map_err(|e| e.to_string())?; timer.checkpoint("Got decoder"); let params = RawDecodeParams::default(); // TODO - Nef currently disabled, due really bad quality of some extracted images https://github.com/dnglab/dnglab/issues/638, waiting for new release if !path.as_ref().to_string_lossy().to_ascii_lowercase().ends_with(".nef") && let Some(extracted_dynamic_image) = decoder.full_image(&raw_source, ¶ms).ok().flatten() { timer.checkpoint("Decoded full image"); trace!("{}", timer.report("Everything", false)); return Ok(extracted_dynamic_image); } let raw_image = decoder.raw_image(&raw_source, ¶ms, false).map_err(|e| e.to_string())?; timer.checkpoint("Decoded raw image"); let developer = RawDevelop::default(); let developed_image = developer.develop_intermediate(&raw_image).map_err(|e| e.to_string())?; timer.checkpoint("Developed raw image"); let dynamic_image = developed_image.to_dynamic_image().ok_or("Failed to convert image to DynamicImage".to_string())?; timer.checkpoint("Converted to DynamicImage"); trace!("{}", timer.report("Everything", false)); Ok(dynamic_image) } pub fn check_if_can_display_image(path: &str) -> bool { let Some(extension) = Path::new(path).extension() else { return false; }; let extension_str = extension.to_string_lossy().to_lowercase(); #[cfg(feature = "heif")] let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat(); #[cfg(not(feature = "heif"))] let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat(); allowed_extensions.iter().any(|ext| &extension_str == ext) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExifOrientation { Normal, MirrorHorizontal, Rotate180, MirrorVertical, MirrorHorizontalAndRotate270CW, Rotate90CW, MirrorHorizontalAndRotate90CW, Rotate270CW, } pub(crate) fn get_rotation_from_exif(path: &str) -> Result, nom_exif::Error> { if let Some(extension) = Path::new(path).extension() && HEIC_EXTENSIONS.contains(&extension.to_string_lossy().to_lowercase().as_str()) { return Ok(None); // libheif already applies orientation } let res = panic::catch_unwind(|| { let mut parser = MediaParser::new(); let ms = MediaSource::file_path(path)?; if !ms.has_exif() { return Ok(None); } let exif_iter: ExifIter = parser.parse(ms)?; for exif_entry in exif_iter { if exif_entry.tag() == Some(ExifTag::Orientation) && let Some(value) = exif_entry.get_value() { return match value.to_string().as_str() { "1" => Ok(Some(ExifOrientation::Normal)), "2" => Ok(Some(ExifOrientation::MirrorHorizontal)), "3" => Ok(Some(ExifOrientation::Rotate180)), "4" => Ok(Some(ExifOrientation::MirrorVertical)), "5" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate270CW)), "6" => Ok(Some(ExifOrientation::Rotate90CW)), "7" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate90CW)), "8" => Ok(Some(ExifOrientation::Rotate270CW)), _ => Ok(None), }; } } Ok(None) }); res.unwrap_or_else(|_| { let message = create_crash_message("nom-exif", path, "https://github.com/mindeng/nom-exif"); error!("{message}"); Err(nom_exif::Error::IOError(std::io::Error::other("Panic in get_rotation_from_exif"))) }) } #[cfg(test)] mod tests { use super::*; const TEST_NORMAL_IMAGE: &str = "test_resources/images/normal.jpg"; const TEST_ROTATED_IMAGE: &str = "test_resources/images/rotated.jpg"; #[test] fn test_image_loading_and_exif_rotation() { let normal_img = get_dynamic_image_from_path(TEST_NORMAL_IMAGE, None).unwrap().image; let rotated_img = get_dynamic_image_from_path(TEST_ROTATED_IMAGE, None).unwrap().image; assert!(normal_img.width() > 0 && normal_img.height() > 0); assert!(rotated_img.width() > 0 && rotated_img.height() > 0); let normal_exif = get_rotation_from_exif(TEST_NORMAL_IMAGE).ok(); let rotated_exif = get_rotation_from_exif(TEST_ROTATED_IMAGE).ok(); if let Some(normal_orientation) = normal_exif { assert!(normal_orientation == Some(ExifOrientation::Normal) || normal_orientation.is_none()); } if let Some(rotated_orientation) = rotated_exif && rotated_orientation.is_some() { let raw_rotated = decode_normal_image(TEST_ROTATED_IMAGE).unwrap(); if rotated_orientation == Some(ExifOrientation::Rotate90CW) || rotated_orientation == Some(ExifOrientation::Rotate270CW) { assert_eq!(rotated_img.width(), raw_rotated.height()); assert_eq!(rotated_img.height(), raw_rotated.width()); } } } #[test] fn test_check_if_can_display_image() { assert!(check_if_can_display_image("test.jpg")); assert!(check_if_can_display_image("test.png")); assert!(check_if_can_display_image("test.webp")); assert!(check_if_can_display_image("test.jxl")); assert!(check_if_can_display_image("test.cr2")); assert!(check_if_can_display_image("test.JPG")); assert!(!check_if_can_display_image("test.txt")); assert!(!check_if_can_display_image("test.mp4")); assert!(!check_if_can_display_image("test")); } #[test] fn test_error_handling() { get_dynamic_image_from_path("nonexistent.jpg", None).unwrap_err(); decode_normal_image("nonexistent.jpg").unwrap_err(); get_rotation_from_exif("nonexistent.jpg").unwrap_err(); } } ================================================ FILE: czkawka_core/src/common/items.rs ================================================ use std::path::Path; use crate::common::regex_check; use crate::helpers::messages::Messages; #[cfg(target_family = "unix")] pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &["/proc", "/dev", "/sys", "/snap"]; #[cfg(not(target_family = "unix"))] pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &["C:\\Windows"]; #[cfg(all(target_family = "unix", target_os = "macos"))] pub const DEFAULT_EXCLUDED_ITEMS: &str = "*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,/Users/*/Library/Caches/*"; #[cfg(all(target_family = "unix", not(target_os = "macos")))] pub const DEFAULT_EXCLUDED_ITEMS: &str = "*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,*/snap/*,/home/*/.cache/*,/home/*/.var/app/,/home/*/.*"; #[cfg(not(target_family = "unix"))] pub 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"; #[derive(Debug, Clone, Default)] pub struct ExcludedItems { expressions: Vec, connected_expressions: Vec, } #[derive(Debug, Clone, Default)] pub struct SingleExcludedItem { pub expression: String, pub expression_splits: Vec, pub unique_extensions_splits: Vec, } impl ExcludedItems { pub fn new() -> Self { Default::default() } pub fn new_from(excluded_items: Vec) -> Self { let mut s = Self::new(); s.set_excluded_items(excluded_items); s } pub(crate) fn set_excluded_items(&mut self, excluded_items: Vec) -> Messages { let mut warnings: Vec = Vec::new(); if excluded_items.is_empty() { return Messages::new(); } let expressions: Vec = excluded_items; let mut checked_expressions: Vec = Vec::new(); for expression in expressions { let expression: String = expression.trim().to_string(); if expression.is_empty() { continue; } #[cfg(target_family = "windows")] let expression = expression.replace("/", "\\"); if expression == "DEFAULT" { checked_expressions.push(DEFAULT_EXCLUDED_ITEMS.to_string()); continue; } if !expression.contains('*') { warnings.push("Excluded Items Warning: Wildcard * is required in expression, ignoring ".to_string() + expression.as_str()); continue; } checked_expressions.push(expression); } for checked_expression in &checked_expressions { let item = new_excluded_item(checked_expression); self.expressions.push(item.expression.clone()); self.connected_expressions.push(item); } Messages { critical: None, messages: Vec::new(), warnings, errors: Vec::new(), } } pub(crate) fn get_excluded_items(&self) -> &Vec { &self.expressions } pub(crate) fn is_excluded(&self, path: &Path) -> bool { if self.connected_expressions.is_empty() { return false; } #[cfg(target_family = "windows")] let path = crate::common::normalize_windows_path(path); let path_str = path.to_string_lossy(); for expression in &self.connected_expressions { if regex_check(expression, &path_str) { return true; } } false } } pub fn new_excluded_item(expression: &str) -> SingleExcludedItem { let expression = expression.trim().to_string(); let expression_splits: Vec = expression.split('*').filter_map(|e| if e.is_empty() { None } else { Some(e.to_string()) }).collect(); let mut unique_extensions_splits = expression_splits.clone(); unique_extensions_splits.sort(); unique_extensions_splits.dedup(); unique_extensions_splits.sort_by_key(|b| std::cmp::Reverse(b.len())); SingleExcludedItem { expression, expression_splits, unique_extensions_splits, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_excluded_items_new_and_basic_operations() { let items = ExcludedItems::new(); assert!(items.expressions.is_empty()); assert!(items.connected_expressions.is_empty()); let items = ExcludedItems::new_from(vec!["*/.git/*".to_string(), "*/node_modules/*".to_string()]); assert_eq!(items.expressions.len(), 2); assert_eq!(items.get_excluded_items().len(), 2); } #[test] fn test_set_excluded_items_with_default() { let mut items = ExcludedItems::new(); let msgs = items.set_excluded_items(vec!["DEFAULT".to_string()]); assert!(msgs.warnings.is_empty()); assert_eq!(items.expressions.len(), 1); assert!(items.expressions[0].contains(".git") || items.expressions[0].contains("node_modules")); } #[test] fn test_set_excluded_items_warnings() { let mut items = ExcludedItems::new(); let msgs = items.set_excluded_items(vec!["no_wildcard".to_string(), " ".to_string()]); assert_eq!(msgs.warnings.len(), 1); assert!(msgs.warnings[0].contains("Wildcard * is required")); assert!(items.expressions.is_empty()); } #[test] fn test_is_excluded() { let mut items = ExcludedItems::new(); items.set_excluded_items(vec!["*/.git/*".to_string(), "*/node_modules/*".to_string(), "/home/*/.*".to_string()]); assert!(items.is_excluded(Path::new("/home/user/.git/config"))); assert!(items.is_excluded(Path::new("/home/user/.abscd/config"))); assert!(items.is_excluded(Path::new("/project/node_modules/package.json"))); assert!(!items.is_excluded(Path::new("/home/user/file.txt"))); // Empty items - nothing excluded let items_empty = ExcludedItems::new(); assert!(!items_empty.is_excluded(Path::new("/any/path"))); } #[test] fn test_new_excluded_item() { let item = new_excluded_item(" */test/*.txt "); assert_eq!(item.expression, "*/test/*.txt"); assert_eq!(item.expression_splits, vec!["/test/", ".txt"]); assert_eq!(item.unique_extensions_splits.len(), 2); let item2 = new_excluded_item("*abc*def*abc*"); assert_eq!(item2.expression_splits, vec!["abc", "def", "abc"]); // unique_extensions_splits should be deduplicated and sorted by length assert_eq!(item2.unique_extensions_splits, vec!["abc", "def"]); } } ================================================ FILE: czkawka_core/src/common/logger.rs ================================================ use std::env; use file_rotate::compression::Compression; use file_rotate::suffix::{AppendTimestamp, FileLimit}; use file_rotate::{ContentLimit, FileRotate}; use handsome_logger::{ColorChoice, CombinedLogger, ConfigBuilder, FormatText, SharedLogger, TermLogger, TerminalMode, TimeFormat, WriteLogger}; use log::{LevelFilter, Record, info, warn}; use crate::CZKAWKA_VERSION; use crate::common::config_cache_path::get_config_cache_path; use crate::common::get_all_available_threads; pub fn setup_logger(disabled_terminal_printing: bool, app_name: &str, filtering_messages_func: fn(&Record) -> bool) { log_panics::init(); let terminal_log_level = if disabled_terminal_printing && ![Ok("1"), Ok("true")].contains(&env::var("ENABLE_TERMINAL_LOGS_IN_CLI").as_deref()) { LevelFilter::Off } else { LevelFilter::Info }; let file_log_level = LevelFilter::Debug; let term_config = ConfigBuilder::default() .set_level(terminal_log_level) .set_message_filtering(Some(filtering_messages_func)) .build(); let file_config = ConfigBuilder::default() .set_level(file_log_level) .set_write_once(true) .set_message_filtering(Some(filtering_messages_func)) .set_time_format(TimeFormat::DateTimeWithMicro, None) .set_format_text(FormatText::DefaultWithThreadFile.get(), None) .build(); let combined_logger = (|| { let Some(config_cache_path) = get_config_cache_path() else { // println!("No config cache path configured, using default config folder"); return None; }; let cache_logs_path = config_cache_path.cache_folder.join(format!("{app_name}.log")); let write_rotater = FileRotate::new( &cache_logs_path, AppendTimestamp::default(FileLimit::MaxFiles(3)), ContentLimit::BytesSurpassed(100 * 1024 * 1024), Compression::None, None, ); let combined_logs: Vec> = if [Ok("1"), Ok("true")].contains(&env::var("DISABLE_FILE_LOGGING").as_deref()) { vec![TermLogger::new_from_config(term_config.clone())] } else { vec![TermLogger::new_from_config(term_config.clone()), WriteLogger::new(file_config, write_rotater)] }; CombinedLogger::init(combined_logs).ok().inspect(|()| { info!("Logging to file \"{}\" and terminal", cache_logs_path.to_string_lossy()); }) })(); if combined_logger.is_none() { let _ = TermLogger::init(term_config, TerminalMode::Mixed, ColorChoice::Always); info!("Logging to terminal only, file logging is disabled"); } } pub fn filtering_messages(record: &Record) -> bool { if let Some(module_path) = record.module_path() { // Printing not supported modules // if !["krokiet", "czkawka", "log_panics", "smithay_client_toolkit", "sctk_adwaita"] // .iter() // .any(|t| module_path.starts_with(t)) // { // println!("{:?}", module_path); // return true; // } else { // return false; // } ["krokiet", "czkawka", "log_panics", "cedinia"].iter().any(|t| module_path.starts_with(t)) } else { true } } #[allow(clippy::allow_attributes)] #[allow(unfulfilled_lint_expectations)] // Happens only on release build #[expect(clippy::vec_init_then_push)] #[expect(unused_mut)] pub fn print_version_mode(app: &str) { let rust_version = env!("RUST_VERSION_INTERNAL"); let debug_release = if cfg!(debug_assertions) { "debug" } else { "release" }; let processors = get_all_available_threads(); let info = os_info::get(); let mut features: Vec<&str> = Vec::new(); #[cfg(feature = "heif")] features.push("heif"); #[cfg(feature = "libavif")] features.push("libavif"); #[cfg(feature = "libraw")] features.push("libraw"); let mut app_cpu_version = "Baseline"; let mut os_cpu_version = "Baseline"; if cfg!(target_feature = "sse2") { app_cpu_version = "x86-64-v1 (SSE2)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("sse2") { os_cpu_version = "x86-64-v1 (SSE2)"; } if cfg!(target_feature = "popcnt") { app_cpu_version = "x86-64-v2 (SSE4.2 + POPCNT)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("popcnt") { os_cpu_version = "x86-64-v2 (SSE4.2 + POPCNT)"; } if cfg!(target_feature = "avx2") { app_cpu_version = "x86-64-v3 (AVX2)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("avx2") { os_cpu_version = "x86-64-v3 (AVX2)"; } if cfg!(target_feature = "avx512f") { app_cpu_version = "x86-64-v4 (AVX-512)"; } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if is_x86_feature_detected!("avx512f") { os_cpu_version = "x86-64-v4 (AVX-512)"; } let musl_or_glibc = if cfg!(target_os = "linux") { let libc_versions_str = match glibc_musl_version::get_os_libc_versions() { Ok(libc_versions) => { let libc_versions_str = libc_versions.to_string(); match option_env!("CZKAWKA_LIBC_VERSIONS") { Some(env) if env == libc_versions_str => { format!(" [build + runtime ({libc_versions_str})]") } Some(env) => { format!(" [build ({env}), runtime ({libc_versions_str})]") } None => { format!(" [build (unknown), runtime ({libc_versions_str})]") } } } Err(e) => { warn!("Cannot get libc version: {e}"); "".to_string() } }; format!(", libc {}{libc_versions_str}", option_env!("CZKAWKA_LIBC").unwrap_or("unknown(cross-compilation?)")) } else { "".to_string() }; let git_commit = env!("CZKAWKA_GIT_COMMIT_SHORT"); let official_build = if env!("CZKAWKA_OFFICIAL_BUILD") == "1" { "O" // Official build } else { "U" // Unofficial build }; let git_date = env!("CZKAWKA_GIT_COMMIT_DATE"); info!( "{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}", info.os_type(), info.version(), env::consts::ARCH, info.bitness(), features.len(), features.join(", "), ); if cfg!(debug_assertions) { warn!("You are running debug version of app which is a lot of slower than release version."); } if option_env!("USING_CRANELIFT").is_some() { warn!("You are running app with cranelift which is intended only for fast compilation, not runtime performance."); } if cfg!(panic = "abort") { warn!("You are running app compiled with panic='abort', which may cause panics when processing untrusted data."); } } ================================================ FILE: czkawka_core/src/common/mod.rs ================================================ pub mod basic_gui_cli; pub mod cache; pub mod config_cache_path; pub mod consts; pub mod dir_traversal; pub mod directories; pub mod extensions; pub mod ffmpeg_utils; pub mod image; pub mod items; pub mod logger; pub mod model; pub mod process_utils; pub mod progress_data; pub mod progress_stop_handler; pub mod tool_data; pub mod traits; pub mod video_utils; use std::cmp::Ordering; use std::ffi::OsString; use std::io::Error; use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::Duration; use std::{fs, io, thread}; use items::SingleExcludedItem; use log::debug; use crate::common::consts::DEFAULT_WORKER_THREAD_SIZE; use crate::flc; static NUMBER_OF_THREADS: std::sync::LazyLock>> = std::sync::LazyLock::new(|| Mutex::new(None)); static ALL_AVAILABLE_THREADS: std::sync::LazyLock>> = std::sync::LazyLock::new(|| Mutex::new(None)); const MAX_SYMLINK_HARDLINK_ATTEMPTS: u8 = 5; #[cfg(all(feature = "xdg_portal_trash", target_os = "linux"))] thread_local! { static TOKIO_RT: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; } #[cfg(all(feature = "xdg_portal_trash", target_os = "linux"))] fn with_runtime(f: F) -> Result where F: FnOnce(&tokio::runtime::Runtime) -> Result, { TOKIO_RT.with(|cell| { let mut opt = cell.borrow_mut(); if opt.is_none() { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .map_err(|e| format!("Failed to build Tokio runtime: {e}")); *opt = Some(rt); } match opt.as_ref().expect("Tokio runtime is initialized before") { Ok(rt) => f(rt), Err(e) => Err(e.clone()), } }) } pub fn get_number_of_threads() -> usize { let data = NUMBER_OF_THREADS.lock().expect("Cannot fail").expect("Should be set before get"); if data >= 1 { data } else { get_all_available_threads() } } pub fn get_all_available_threads() -> usize { let mut available_threads = ALL_AVAILABLE_THREADS.lock().expect("Cannot fail"); if let Some(available_threads) = *available_threads { available_threads } else { let threads = thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1); *available_threads = Some(threads); threads } } pub fn set_number_of_threads(thread_number: usize) { *NUMBER_OF_THREADS.lock().expect("Cannot fail") = Some(thread_number); let additional_message = if thread_number == 0 { format!( " (0 - means that all available threads will be used({}))", thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1) ) } else { "".to_string() }; debug!("Number of threads set to {thread_number}{additional_message}"); rayon::ThreadPoolBuilder::new() .num_threads(get_number_of_threads()) .stack_size(DEFAULT_WORKER_THREAD_SIZE) .build_global() .expect("Cannot set number of threads"); } pub fn check_if_folder_contains_only_empty_folders>(path: P) -> Result<(), String> { let path = path.as_ref(); if !path.is_dir() { return Err(flc!("core_not_directory_remove", path = path.to_string_lossy())); } let mut entries_to_check = Vec::new(); let Ok(initial_entry) = path.read_dir() else { return Err(flc!("core_cannot_read_directory", path = path.to_string_lossy())); }; for entry in initial_entry { if let Ok(entry) = entry { entries_to_check.push(entry); } else { return Err(flc!("core_cannot_read_entry_from_directory", path = path.to_string_lossy())); } } loop { let Some(entry) = entries_to_check.pop() else { break; }; let Some(file_type) = entry.file_type().ok() else { return Err(flc!( "core_unknown_directory_entry", entry = entry.path().to_string_lossy().to_string(), path = path.to_string_lossy() )); }; if !file_type.is_dir() { return Err(flc!( "core_folder_contains_file_inside", entry = entry.path().to_string_lossy().to_string(), folder = path.to_string_lossy() )); } let Ok(internal_read_dir) = entry.path().read_dir() else { return Err(flc!("core_cannot_read_directory", path = path.to_string_lossy().to_string())); }; for internal_elements in internal_read_dir { if let Ok(internal_element) = internal_elements { entries_to_check.push(internal_element); } else { return Err(flc!("core_cannot_read_entry_from_directory", path = path.to_string_lossy().to_string())); } } } Ok(()) } /// A wrapper around `trash::delete`. Note that for platforms that do not have native trash support /// (Android, iOS), this function will always return an [`Error`]. When the `xdg_portal_trash` feature is /// enabled, the portal-based implementation will only be used on Linux; on other desktop OSes the /// regular `trash::delete` fallback will be used instead. fn trash_delete>(path: P) -> Result<(), String> { let path = path.as_ref(); #[cfg(not(any(target_os = "android", target_os = "ios", all(feature = "xdg_portal_trash", target_os = "linux"))))] { trash::delete(path).map_err(|err| err.to_string()) } #[cfg(all(feature = "xdg_portal_trash", target_os = "linux"))] { use std::os::fd::AsFd; let file = std::fs::OpenOptions::new().write(true).read(true).open(path).map_err(|err| err.to_string())?; with_runtime(|rt| rt.block_on(async move { ashpd::desktop::trash::trash_file(&file.as_fd()).await.map_err(|e| e.to_string()) }))?; Ok(()) } #[cfg(any(target_os = "android", target_os = "ios"))] { let _path = path; Err("trash is not supported on this platform".to_string()) } } /// Remove the folder if it only contains empty folders/is empty. If `remove_to_trash` is set, the folder /// will instead be sent to the system's recycle bin/trash equivalent rather than being deleted. /// /// Note: if used on Android or iOS platforms, ensure `remove_to_trash` is false, as trash is not supported /// and will always return an [`Error`]. pub fn remove_folder_if_contains_only_empty_folders>(path: P, remove_to_trash: bool) -> Result<(), String> { check_if_folder_contains_only_empty_folders(&path)?; let path = path.as_ref(); if remove_to_trash { trash_delete(path).map_err(|e| format!("Cannot move folder \"{}\" to trash, reason {e}", path.to_string_lossy())) } else { fs::remove_dir_all(path).map_err(|e| format!("Cannot remove directory \"{}\", reason {e}", path.to_string_lossy())) } } /// Remove a single file. If `remove_to_trash` is set, the folder will instead be sent to the system's /// recycle bin/trash equivalent rather than being deleted. /// /// Note: if used on Android or iOS platforms, ensure `remove_to_trash` is false, as trash is not supported /// and will always return an [`Error`]. pub fn remove_single_file>(full_path: P, remove_to_trash: bool) -> Result<(), String> { if remove_to_trash { if let Err(e) = trash_delete(&full_path) { return Err(flc!("core_error_moving_to_trash", file = full_path.as_ref().to_string_lossy().to_string(), error = e)); } } else { if let Err(e) = fs::remove_file(&full_path) { return Err(flc!("core_error_removing", file = full_path.as_ref().to_string_lossy().to_string(), error = e.to_string())); } } Ok(()) } /// Remove a single folder recursively. If `remove_to_trash` is set, the folder will instead be sent to the system's /// recycle bin/trash equivalent rather than being deleted. /// /// Note: if used on Android or iOS platforms, ensure `remove_to_trash` is false, as trash is not supported /// and will always return an [`Error`]. pub fn remove_single_folder(full_path: &str, remove_to_trash: bool) -> Result<(), String> { if remove_to_trash { if let Err(e) = trash_delete(full_path) { return Err(flc!("core_error_moving_to_trash", file = full_path, error = e)); } } else { if let Err(e) = fs::remove_dir_all(full_path) { return Err(flc!("core_error_removing", file = full_path, error = e.to_string())); } } Ok(()) } pub fn split_path(path: &Path) -> (String, String) { match (path.parent(), path.file_name()) { (Some(dir), Some(file)) => (dir.to_string_lossy().to_string(), file.to_string_lossy().into_owned()), (Some(dir), None) => (dir.to_string_lossy().to_string(), String::new()), (None, _) => (String::new(), String::new()), } } pub fn split_path_compare(path_a: &Path, path_b: &Path) -> Ordering { match path_a.parent().cmp(&path_b.parent()) { Ordering::Equal => path_a.file_name().cmp(&path_b.file_name()), other => other, } } pub fn format_time(duration: Duration) -> String { let hours = duration.as_secs() / 3600; let minutes = duration.as_secs() % 3600 / 60; let secs = duration.as_secs() % 60; let millis = duration.subsec_millis(); if hours == 0 && minutes == 0 && secs == 0 { format!("{millis}ms") } else if hours == 0 && minutes == 0 { if millis / 10 == 0 { format!("{secs}s") } else { format!("{secs}.{:02}s", millis / 10) } } else if hours == 0 { if secs == 0 { format!("{minutes}m") } else { format!("{minutes}m {secs}s") } } else { if secs == 0 && minutes == 0 { format!("{hours}h") } else if secs == 0 { format!("{hours}h {minutes}m") } else { format!("{hours}h {minutes}m {secs}s") } } } pub(crate) fn create_crash_message(library_name: &str, file_path: &str, home_library_url: &str) -> String { format!( "{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}" ) } #[expect(clippy::string_slice)] #[expect(clippy::indexing_slicing)] pub fn regex_check(expression_item: &SingleExcludedItem, directory_name: &str) -> bool { if expression_item.expression_splits.is_empty() { return true; } // Early checking if directory contains all parts needed by expression for split in &expression_item.unique_extensions_splits { if !directory_name.contains(split) { return false; } } // `git*` shouldn't be true for `/gitsfafasfs` if !expression_item.expression.starts_with('*') && directory_name .find(&expression_item.expression_splits[0]) .expect("Cannot fail, because split must exists in directory_name") > 0 { return false; } // `*home` shouldn't be true for `/homeowner` if !expression_item.expression.ends_with('*') && !directory_name.ends_with(expression_item.expression_splits.last().expect("Cannot fail, because at least one item is available")) { return false; } // At the end we check if parts between * are correctly positioned let mut last_split_point = directory_name.find(&expression_item.expression_splits[0]).expect("Cannot fail, because is checked earlier"); let mut current_index: usize = 0; let mut found_index: usize; for spl in &expression_item.expression_splits[1..] { found_index = match directory_name[current_index..].find(spl) { Some(t) => t, None => return false, }; current_index = last_split_point + spl.len(); last_split_point = found_index + current_index; } true } #[expect(clippy::string_slice)] // Is in char boundary pub fn normalize_windows_path>(path_to_change: P) -> PathBuf { let path = path_to_change.as_ref(); // Don't do anything, because network path may be case intensive if path.to_string_lossy().starts_with('\\') { return path.to_path_buf(); } match path.to_str() { Some(path) if path.is_char_boundary(1) => { let replaced = path.replace('/', "\\"); let mut new_path = OsString::new(); if replaced[1..].starts_with(':') { new_path.push(replaced[..1].to_ascii_uppercase()); new_path.push(replaced[1..].to_ascii_lowercase()); } else { new_path.push(replaced.to_ascii_lowercase()); } PathBuf::from(new_path) } _ => path.to_path_buf(), } } // Function to create hardlink, when destination exists // This is always true in this app, because creating hardlink, to newly created file is pointless pub fn make_hard_link, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { let src = src.as_ref(); let dst = dst.as_ref(); let dst_dir = dst.parent().ok_or_else(|| Error::other("No parent"))?; let mut temp; let mut attempts = MAX_SYMLINK_HARDLINK_ATTEMPTS; loop { temp = dst_dir.join(format!("{}.czkawka_tmp", rand::random::())); if !temp.exists() { break; } attempts -= 1; if attempts == 0 { return Err(Error::other("Cannot choose temporary file for hardlink creation")); } } fs::rename(dst, temp.as_path())?; match fs::hard_link(src, dst) { Ok(()) => { fs::remove_file(&temp)?; Ok(()) } Err(e) => { let _ = fs::rename(&temp, dst); Err(e) } } } #[cfg(any(target_family = "unix", target_family = "windows"))] pub fn make_file_symlink, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { let src = src.as_ref(); let dst = dst.as_ref(); let dst_dir = dst.parent().ok_or_else(|| Error::other("No parent"))?; let mut temp; let mut attempts = MAX_SYMLINK_HARDLINK_ATTEMPTS; loop { temp = dst_dir.join(format!("{}.czkawka_tmp", rand::random::())); if !temp.exists() { break; } attempts -= 1; if attempts == 0 { return Err(Error::other("Cannot choose temporary file for symlink creation")); } } fs::rename(dst, temp.as_path())?; let result: Result<_, _>; #[cfg(target_family = "unix")] { result = std::os::unix::fs::symlink(src, dst); } #[cfg(target_family = "windows")] { result = std::os::windows::fs::symlink_file(src, dst); } match result { Ok(()) => { fs::remove_file(&temp)?; Ok(()) } Err(e) => { let _ = fs::rename(&temp, dst); Err(e) } } } #[cfg(not(any(target_family = "unix", target_family = "windows")))] pub fn make_file_symlink, Q: AsRef>(src: P, dst: Q) -> io::Result<()> { Err(Error::new(io::ErrorKind::Other, "Soft links are not supported on this platform")) } pub fn debug_save_file(path: &str, data: &str) { use std::io::Write; if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(path) { let _ = writeln!(f, "{data}"); } } #[cfg(test)] mod test { use std::fs::{File, Metadata, read_dir}; use std::io::Write; #[cfg(target_family = "unix")] use std::os::unix::fs::MetadataExt; use tempfile::tempdir; use super::*; use crate::common::items::new_excluded_item; #[cfg(target_family = "unix")] fn assert_inode(before: &Metadata, after: &Metadata) { assert_eq!(before.ino(), after.ino()); } #[cfg(target_family = "windows")] fn assert_inode(_: &Metadata, _: &Metadata) {} #[cfg(target_family = "unix")] fn assert_different_inode(before: &Metadata, after: &Metadata) { assert_ne!(before.ino(), after.ino()); } #[cfg(target_family = "windows")] fn assert_different_inode(_before: &Metadata, _after: &Metadata) {} #[test] fn test_make_hard_link() -> io::Result<()> { // Test 1: Basic hardlink creation { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&src)?; let metadata = fs::metadata(&src)?; File::create(&dst)?; let dst_metadata_before = fs::metadata(&dst)?; assert_different_inode(&metadata, &dst_metadata_before); make_hard_link(&src, &dst)?; make_hard_link(&src, &dst)?; assert_inode(&metadata, &fs::metadata(&dst)?); assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?); assert_inode(&metadata, &fs::metadata(&src)?); assert_eq!(metadata.permissions(), fs::metadata(&src)?.permissions()); assert_eq!(metadata.modified()?, fs::metadata(&src)?.modified()?); let mut actual = read_dir(&dir)?.flatten().map(|e| e.path()).collect::>(); actual.sort_unstable(); assert_eq!(vec![src, dst], actual); } // Test 2: Hardlink creation fails when source doesn't exist { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&dst)?; let metadata = fs::metadata(&dst)?; assert!(make_hard_link(&src, &dst).is_err()); assert_inode(&metadata, &fs::metadata(&dst)?); assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?); assert_eq!(vec![dst], read_dir(&dir)?.flatten().map(|e| e.path()).collect::>()); } // Test 3: Hardlink with content preservation { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("src_file"), dir.path().join("dst_file")); let content = "test content for hardlink"; { let mut f = File::create(&src)?; writeln!(f, "{content}")?; } { let mut f = File::create(&dst)?; writeln!(f, "old content")?; } let src_metadata = fs::metadata(&src)?; let dst_metadata_before = fs::metadata(&dst)?; assert_different_inode(&src_metadata, &dst_metadata_before); make_hard_link(&src, &dst)?; let src_content = fs::read_to_string(&src)?; let dst_content = fs::read_to_string(&dst)?; assert_eq!(src_content, dst_content); assert_eq!(src_content, format!("{content}\n")); assert_inode(&src_metadata, &fs::metadata(&dst)?); } // Test 4: Hardlink on readonly file #[cfg(target_family = "unix")] { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("readonly_src"), dir.path().join("readonly_dst")); { let mut f = File::create(&src)?; writeln!(f, "readonly content")?; } let mut perms = fs::metadata(&src)?.permissions(); perms.set_readonly(true); fs::set_permissions(&src, perms)?; assert!(fs::metadata(&src)?.permissions().readonly()); { let mut f = File::create(&dst)?; writeln!(f, "dst content")?; } let src_metadata_before = fs::metadata(&src)?; let dst_metadata_before = fs::metadata(&dst)?; assert_different_inode(&src_metadata_before, &dst_metadata_before); make_hard_link(&src, &dst).unwrap(); assert_inode(&src_metadata_before, &fs::metadata(&dst)?); assert_eq!(fs::read_to_string(&src)?, fs::read_to_string(&dst)?); assert!(fs::metadata(&src)?.permissions().readonly()); assert!(fs::metadata(&dst)?.permissions().readonly()); } // Test 5: Hardlink on readonly destination file #[cfg(target_family = "unix")] { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("src_normal"), dir.path().join("dst_readonly")); { let mut f = File::create(&src)?; writeln!(f, "source content")?; } { let mut f = File::create(&dst)?; writeln!(f, "destination content")?; } let mut perms = fs::metadata(&dst)?.permissions(); perms.set_readonly(true); fs::set_permissions(&dst, perms)?; assert!(fs::metadata(&dst)?.permissions().readonly()); let src_metadata = fs::metadata(&src)?; let dst_metadata_before = fs::metadata(&dst)?; assert_different_inode(&src_metadata, &dst_metadata_before); make_hard_link(&src, &dst).unwrap(); assert_inode(&src_metadata, &fs::metadata(&dst)?); assert_eq!(fs::read_to_string(&src)?, fs::read_to_string(&dst)?); } // Test 6: Hardlink when destination doesn't exist - should fail { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("src"), dir.path().join("nonexistent")); File::create(&src)?; let result = make_hard_link(&src, &dst); assert!(result.is_err(), "Should fail when destination doesn't exist"); } // Test 7: Hardlink preserves file size { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("large_src"), dir.path().join("large_dst")); let large_content = "x".repeat(10000); { let mut f = File::create(&src)?; write!(f, "{large_content}")?; } File::create(&dst)?; let src_size = fs::metadata(&src)?.len(); let src_metadata = fs::metadata(&src)?; let dst_metadata_before = fs::metadata(&dst)?; assert_different_inode(&src_metadata, &dst_metadata_before); make_hard_link(&src, &dst)?; assert_eq!(src_size, fs::metadata(&dst)?.len()); assert_eq!(large_content, fs::read_to_string(&dst)?); } // Test 8: Multiple hardlinks to same file { let dir = tempfile::Builder::new().tempdir()?; let src = dir.path().join("original"); let dst1 = dir.path().join("link1"); let dst2 = dir.path().join("link2"); { let mut f = File::create(&src)?; writeln!(f, "original")?; } File::create(&dst1)?; File::create(&dst2)?; let src_metadata = fs::metadata(&src)?; let dst1_metadata_before = fs::metadata(&dst1)?; let dst2_metadata_before = fs::metadata(&dst2)?; // Before hardlinks - all files should have different inodes assert_different_inode(&src_metadata, &dst1_metadata_before); assert_different_inode(&src_metadata, &dst2_metadata_before); assert_different_inode(&dst1_metadata_before, &dst2_metadata_before); make_hard_link(&src, &dst1)?; make_hard_link(&src, &dst2)?; assert_inode(&src_metadata, &fs::metadata(&dst1)?); assert_inode(&src_metadata, &fs::metadata(&dst2)?); } Ok(()) } // Windows needs super user permissions #[cfg(target_family = "unix")] #[test] fn test_make_file_symlink() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); let content = "hello softlink"; { let mut f = File::create(&src)?; writeln!(f, "{content}")?; } File::create(&dst)?; make_file_symlink(&src, &dst)?; let symlink_meta = fs::symlink_metadata(&dst)?; assert!(symlink_meta.file_type().is_symlink()); let src_content = fs::read_to_string(&src)?; let dst_content = fs::read_to_string(&dst)?; assert_eq!(src_content, dst_content); let mut actual = read_dir(&dir)?.flatten().map(|e| e.path()).collect::>(); actual.sort_unstable(); assert_eq!(vec![src, dst], actual); Ok(()) } #[cfg(target_family = "unix")] #[test] fn test_make_file_symlink_fails() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); { let mut f = File::create(&dst)?; writeln!(f, "original")?; } let metadata = fs::metadata(&dst)?; match make_file_symlink(&src, &dst) { Err(_) => { assert_eq!(fs::read_to_string(&dst)?, "original\n"); assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); } Ok(()) => { let symlink_meta = fs::symlink_metadata(&dst)?; assert!(symlink_meta.file_type().is_symlink()); fs::read_to_string(&dst).unwrap_err(); } } Ok(()) } #[test] fn test_remove_folder_if_contains_only_empty_folders() { let dir = tempdir().expect("Cannot create temporary directory"); let sub_dir = dir.path().join("sub_dir"); fs::create_dir(&sub_dir).expect("Cannot create directory"); // Test with empty directory remove_folder_if_contains_only_empty_folders(&sub_dir, false).unwrap(); assert!(!Path::new(&sub_dir).exists()); // Test with directory containing an empty directory fs::create_dir(&sub_dir).expect("Cannot create directory"); fs::create_dir(sub_dir.join("empty_sub_dir")).expect("Cannot create directory"); remove_folder_if_contains_only_empty_folders(&sub_dir, false).unwrap(); assert!(!Path::new(&sub_dir).exists()); // Test with directory containing a file fs::create_dir(&sub_dir).expect("Cannot create directory"); let mut file = File::create(sub_dir.join("file.txt")).expect("Cannot create file"); writeln!(file, "Hello, world!").expect("Cannot write to file"); assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_err()); assert!(Path::new(&sub_dir).exists()); } #[test] fn test_regex() { assert!(regex_check(&new_excluded_item("*"), "/home/rafal")); assert!(regex_check(&new_excluded_item("*home*"), "/home/rafal")); assert!(regex_check(&new_excluded_item("*home"), "/home")); assert!(regex_check(&new_excluded_item("*home/"), "/home/")); assert!(regex_check(&new_excluded_item("*home/*"), "/home/")); assert!(regex_check(&new_excluded_item("*.git*"), "/home/.git")); assert!(regex_check(&new_excluded_item("/home/*/.*"), "/home/user/.random")); assert!(regex_check(&new_excluded_item("*/home/rafal*rafal*rafal*rafal*"), "/home/rafal/rafalrafalrafal")); assert!(regex_check(&new_excluded_item("AAA"), "AAA")); assert!(regex_check(&new_excluded_item("AAA*"), "AAABDGG/QQPW*")); assert!(!regex_check(&new_excluded_item("*home"), "/home/")); assert!(!regex_check(&new_excluded_item("*home"), "/homefasfasfasfasf/")); assert!(!regex_check(&new_excluded_item("*home"), "/homefasfasfasfasf")); assert!(!regex_check(&new_excluded_item("rafal*afal*fal"), "rafal")); assert!(!regex_check(&new_excluded_item("rafal*a"), "rafal")); assert!(!regex_check(&new_excluded_item("AAAAAAAA****"), "/AAAAAAAAAAAAAAAAA")); assert!(!regex_check(&new_excluded_item("*.git/*"), "/home/.git")); assert!(!regex_check(&new_excluded_item("*home/*koc"), "/koc/home/")); assert!(!regex_check(&new_excluded_item("*home/"), "/home")); assert!(!regex_check(&new_excluded_item("*TTT"), "/GGG")); assert!(regex_check( &new_excluded_item("*/home/*/.local/share/containers"), "/var/home/roman/.local/share/containers" )); if cfg!(target_family = "windows") { assert!(regex_check(&new_excluded_item("*\\home"), "C:\\home")); } } #[test] fn test_windows_path() { assert_eq!(PathBuf::from("C:\\path.txt"), normalize_windows_path("c:/PATH.tXt")); assert_eq!(PathBuf::from("H:\\reka\\weza\\roman.txt"), normalize_windows_path("h:/RekA/Weza\\roMan.Txt")); assert_eq!(PathBuf::from("T:\\a"), normalize_windows_path("T:\\A")); assert_eq!(PathBuf::from("\\\\aBBa"), normalize_windows_path("\\\\aBBa")); assert_eq!(PathBuf::from("a"), normalize_windows_path("a")); assert_eq!(PathBuf::from(""), normalize_windows_path("")); } #[test] fn test_format_time() { assert_eq!(format_time(Duration::from_millis(0)), "0ms"); assert_eq!(format_time(Duration::from_millis(1)), "1ms"); assert_eq!(format_time(Duration::from_millis(999)), "999ms"); assert_eq!(format_time(Duration::from_millis(1000)), "1s"); assert_eq!(format_time(Duration::from_millis(1234)), "1.23s"); assert_eq!(format_time(Duration::from_millis(5678)), "5.67s"); assert_eq!(format_time(Duration::from_secs(59)), "59s"); assert_eq!(format_time(Duration::from_secs(60)), "1m"); assert_eq!(format_time(Duration::from_secs(61)), "1m 1s"); assert_eq!(format_time(Duration::from_millis(61234)), "1m 1s"); assert_eq!(format_time(Duration::from_secs(125)), "2m 5s"); assert_eq!(format_time(Duration::from_secs(3599)), "59m 59s"); assert_eq!(format_time(Duration::from_secs(3600)), "1h"); assert_eq!(format_time(Duration::from_secs(3661)), "1h 1m 1s"); assert_eq!(format_time(Duration::from_secs(7384)), "2h 3m 4s"); assert_eq!(format_time(Duration::from_secs(86400)), "24h"); assert_eq!(format_time(Duration::from_millis(999)), "999ms"); assert_eq!(format_time(Duration::from_millis(1001)), "1s"); assert_eq!(format_time(Duration::from_millis(59999)), "59.99s"); assert_eq!(format_time(Duration::from_millis(60000)), "1m"); assert_eq!(format_time(Duration::from_millis(60100)), "1m"); assert_eq!(format_time(Duration::from_millis(120000)), "2m"); } } ================================================ FILE: czkawka_core/src/common/model.rs ================================================ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use xxhash_rust::xxh3::Xxh3; use crate::common::traits::ResultEntry; use crate::tools::duplicate::MyHasher; #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub enum ToolType { Duplicate, EmptyFolders, EmptyFiles, InvalidSymlinks, BrokenFiles, BadExtensions, BadNames, BigFile, SameMusic, SimilarImages, SimilarVideos, TemporaryFiles, ExifRemover, VideoOptimizer, #[default] None, } impl ToolType { pub fn may_use_reference_paths(self) -> bool { matches!(self, Self::Duplicate | Self::SameMusic | Self::SimilarImages | Self::SimilarVideos) } } #[derive(PartialEq, Eq, Clone, Debug, Copy, Default, Deserialize, Serialize)] pub enum CheckingMethod { #[default] None, Name, SizeName, Size, Hash, AudioTags, AudioContent, } #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct FileEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, } impl ResultEntry for FileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(PartialEq, Eq, Clone, Debug, Copy, Default)] pub enum HashType { #[default] Blake3, Crc32, Xxh3, } impl HashType { pub(crate) fn hasher(self) -> Box { match self { Self::Blake3 => Box::new(blake3::Hasher::new()), Self::Crc32 => Box::new(crc32fast::Hasher::new()), Self::Xxh3 => Box::new(Xxh3::new()), } } } #[derive(Debug, PartialEq)] pub enum WorkContinueStatus { Continue, Stop, } #[cfg(test)] mod tests { use super::*; #[test] fn test_file_entry_basic_operations() { let entry = FileEntry { path: PathBuf::from("/test/file.txt"), size: 1024, modified_date: 123456, }; assert_eq!(entry.get_path(), Path::new("/test/file.txt")); assert_eq!(entry.get_size(), 1024); assert_eq!(entry.get_modified_date(), 123456); let entry2 = entry.clone(); assert_eq!(entry, entry2); } #[test] fn test_hash_type_creates_hashers() { let blake3_hasher = HashType::Blake3.hasher(); let crc32_hasher = HashType::Crc32.hasher(); let xxh3_hasher = HashType::Xxh3.hasher(); // Just verify they can be created assert!(std::mem::size_of_val(&blake3_hasher) > 0); assert!(std::mem::size_of_val(&crc32_hasher) > 0); assert!(std::mem::size_of_val(&xxh3_hasher) > 0); } #[test] fn test_checking_method_default() { assert_eq!(CheckingMethod::default(), CheckingMethod::None); } #[test] fn test_tool_type_default() { assert_eq!(ToolType::default(), ToolType::None); } #[test] fn test_delete_method_default() { use crate::common::tool_data::DeleteMethod; assert_eq!(DeleteMethod::default(), DeleteMethod::None); } } ================================================ FILE: czkawka_core/src/common/process_utils.rs ================================================ use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; use log::{error, warn}; use crate::flc; #[expect(clippy::needless_pass_by_ref_mut)] pub fn disable_windows_console_window(command: &mut Command) { #[cfg(target_os = "windows")] { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; command.creation_flags(CREATE_NO_WINDOW); } #[cfg(not(target_os = "windows"))] { let _ = command; } } pub struct CommandOutput { pub status: std::process::ExitStatus, pub stdout: String, pub stderr: String, } // Remember - Ok returned by this function does not necessarily mean that the command executed successfully // it only means that the command was executed and its output was captured. // The actual success of the command should be determined by checking the `status` field of the returned `CommandOutput`. pub fn run_command_interruptible(mut command: Command, stop_flag: &Arc) -> Option> { if stop_flag.load(Ordering::Relaxed) { return None; } disable_windows_console_window(&mut command); command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = match command.spawn() { Ok(c) => c, Err(e) => return Some(Err(flc!("core_failed_to_spawn_command", reason = e.to_string()))), }; let Some(mut stdout) = child.stdout.take() else { error!("Failed to take stdout from child process"); return Some(Err("Failed to take stdout from child process".to_string())); }; let Some(mut stderr) = child.stderr.take() else { error!("Failed to take stderr from child process"); return Some(Err("Failed to take stderr from child process".to_string())); }; let stdout_buf = Arc::new(Mutex::new(Vec::new())); let stderr_buf = Arc::new(Mutex::new(Vec::new())); let out_buf = stdout_buf.clone(); let err_buf = stderr_buf.clone(); let out_handle = thread::spawn(move || { let mut buf = Vec::new(); let _ = std::io::copy(&mut stdout, &mut buf); match out_buf.lock() { Ok(mut lock) => *lock = buf, Err(e) => error!("Failed to lock stdout buffer: {e}"), } }); let err_handle = thread::spawn(move || { let mut buf = Vec::new(); let _ = std::io::copy(&mut stderr, &mut buf); match err_buf.lock() { Ok(mut lock) => *lock = buf, Err(e) => error!("Failed to lock stderr buffer: {e}"), } }); let start_time = Instant::now(); let warning_steps = [50, 250, 1250, 6000]; let mut next_warning_idx = 0; loop { if stop_flag.load(Ordering::Relaxed) { let _ = child.kill(); let _ = child.wait(); break; } let elapsed_secs = start_time.elapsed().as_secs(); if let Some(warning_time) = warning_steps.get(next_warning_idx) && elapsed_secs >= *warning_time { warn!("Command is still running after {warning_time} seconds, for command: {command:?}"); next_warning_idx += 1; } match child.try_wait() { Ok(Some(_)) => break, Ok(None) => thread::sleep(Duration::from_millis(100)), Err(e) => return Some(Err(flc!("core_failed_to_check_process_status", reason = e.to_string()))), } } let status = match child.wait() { Ok(s) => s, Err(e) => return Some(Err(flc!("core_failed_to_wait_for_process", reason = e.to_string()))), }; let _ = out_handle.join(); let _ = err_handle.join(); if stop_flag.load(Ordering::Relaxed) { return None; } let stdout = match Arc::try_unwrap(stdout_buf) { Ok(mutex) => match mutex.into_inner() { Ok(buf) => buf, Err(e) => { error!("Failed to get stdout inner buffer: {e}"); return Some(Err("Failed to get stdout inner buffer".to_string())); } }, Err(_) => { error!("Failed to unwrap stdout Arc - multiple references still exist"); return Some(Err("Failed to unwrap stdout Arc".to_string())); } }; let stderr = match Arc::try_unwrap(stderr_buf) { Ok(mutex) => match mutex.into_inner() { Ok(buf) => buf, Err(e) => { error!("Failed to get stderr inner buffer: {e}"); return Some(Err("Failed to get stderr inner buffer".to_string())); } }, Err(_) => { error!("Failed to unwrap stderr Arc - multiple references still exist"); return Some(Err("Failed to unwrap stderr Arc".to_string())); } }; Some(Ok(CommandOutput { status, stdout: String::from_utf8_lossy(&stdout).to_string(), stderr: String::from_utf8_lossy(&stderr).to_string(), })) } ================================================ FILE: czkawka_core/src/common/progress_data.rs ================================================ use log::error; use crate::common::model::{CheckingMethod, ToolType}; // Empty files // 0 - Collecting files // Empty folders // 0 - Collecting folders // Big files // 0 - Collecting files // Same music // 0 - Collecting files // 1 - Loading cache // 2 - Checking tags // 3 - Saving cache // 4 - TAGS - Comparing tags // 4 - CONTENT - Loading cache // 5 - CONTENT - Calculating fingerprints // 6 - CONTENT - Saving cache // 7 - CONTENT - Comparing fingerprints // Similar images // 0 - Collecting files // 1 - Scanning images // 2 - Comparing hashes // Similar videos // 0 - Collecting files // 1 - Scanning videos // 2 - Creating thumbnails // Temporary files // 0 - Collecting files // Invalid symlinks // 0 - Collecting files // Broken files // 0 - Collecting files // 1 - Scanning files // Bad extensions // 0 - Collecting files // 1 - Scanning files // Exif Remover // 0 - Collecting files // 1 - Loading cache // 2 - Extracting tags // 3 - Saving cache // Duplicates - Hash // 0 - Collecting files // 1 - Loading cache // 2 - Hash - first 1KB file // 3 - Saving cache // 4 - Loading cache // 5 - Hash - normal hash // 6 - Saving cache // Duplicates - Name or SizeName or Size // 0 - Collecting files // Deleting files // Renaming files #[derive(Debug, Clone, Copy)] pub struct ProgressData { pub sstage: CurrentStage, pub checking_method: CheckingMethod, pub current_stage_idx: u8, pub max_stage_idx: u8, pub entries_checked: usize, pub entries_to_check: usize, pub bytes_checked: u64, pub bytes_to_check: u64, pub tool_type: ToolType, } impl ProgressData { pub fn get_empty_state(current_stage: CurrentStage) -> Self { Self { sstage: current_stage, checking_method: CheckingMethod::None, current_stage_idx: 0, max_stage_idx: 0, entries_checked: 0, entries_to_check: 0, bytes_checked: 0, bytes_to_check: 0, tool_type: ToolType::None, } } } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum CurrentStage { DeletingFiles, RenamingFiles, MovingFiles, HardlinkingFiles, SymlinkingFiles, OptimizingVideos, CleaningExif, CollectingFiles, DuplicateCacheSaving, DuplicateCacheLoading, DuplicatePreHashCacheSaving, DuplicatePreHashCacheLoading, DuplicateScanningName, DuplicateScanningSizeName, DuplicateScanningSize, DuplicatePreHashing, DuplicateFullHashing, SameMusicCacheSavingTags, SameMusicCacheLoadingTags, SameMusicCacheSavingFingerprints, SameMusicCacheLoadingFingerprints, SameMusicReadingTags, SameMusicCalculatingFingerprints, SameMusicComparingTags, SameMusicComparingFingerprints, SimilarImagesCalculatingHashes, SimilarImagesComparingHashes, SimilarVideosCalculatingHashes, SimilarVideosCreatingThumbnails, BrokenFilesChecking, BadExtensionsChecking, BadNamesChecking, ExifRemoverCacheLoading, ExifRemoverExtractingTags, ExifRemoverCacheSaving, VideoOptimizerCreatingThumbnails, VideoOptimizerProcessingVideos, } impl ProgressData { pub(crate) fn validate(&self) { assert!( self.current_stage_idx <= self.max_stage_idx, "Current stage index: {}, max stage index: {}, stage {:?}", self.current_stage_idx, self.max_stage_idx, self.sstage ); assert_eq!( self.max_stage_idx, self.tool_type.get_max_stage(self.checking_method), "Max stage index: {}, tool type: {:?}, checking method: {:?}", self.max_stage_idx, self.tool_type, self.checking_method ); if self.sstage != CurrentStage::CollectingFiles { assert!( self.entries_checked <= self.entries_to_check, "Entries checked: {}, entries to check: {}, stage {:?}", self.entries_checked, self.entries_to_check, self.sstage ); } // This could be an assert, but it is possible that in duplicate finder, file that will // be checked, will increase the size of the file between collecting file to scan and // scanning it. So it is better to just log it if self.bytes_checked > self.bytes_to_check { error!("Bytes checked: {}, bytes to check: {}, stage {:?}", self.bytes_checked, self.bytes_to_check, self.sstage); } let tool_type_checking_method: Option = match self.checking_method { CheckingMethod::AudioTags | CheckingMethod::AudioContent => Some(ToolType::SameMusic), CheckingMethod::Name | CheckingMethod::SizeName | CheckingMethod::Size | CheckingMethod::Hash => Some(ToolType::Duplicate), CheckingMethod::None => None, }; if let Some(tool_type) = tool_type_checking_method { assert_eq!(self.tool_type, tool_type, "Tool type: {:?}, checking method: {:?}", self.tool_type, self.checking_method); } let tool_type_current_stage: Option = match self.sstage { CurrentStage::CollectingFiles | CurrentStage::DeletingFiles | CurrentStage::RenamingFiles | CurrentStage::MovingFiles | CurrentStage::HardlinkingFiles | CurrentStage::SymlinkingFiles | CurrentStage::OptimizingVideos | CurrentStage::CleaningExif => None, CurrentStage::DuplicateCacheSaving | CurrentStage::DuplicateCacheLoading | CurrentStage::DuplicatePreHashCacheSaving | CurrentStage::DuplicatePreHashCacheLoading => { Some(ToolType::Duplicate) } CurrentStage::DuplicateScanningName | CurrentStage::DuplicateScanningSizeName | CurrentStage::DuplicateScanningSize | CurrentStage::DuplicatePreHashing | CurrentStage::DuplicateFullHashing => Some(ToolType::Duplicate), CurrentStage::SameMusicCacheLoadingTags | CurrentStage::SameMusicCacheSavingTags | CurrentStage::SameMusicCacheLoadingFingerprints | CurrentStage::SameMusicCacheSavingFingerprints | CurrentStage::SameMusicComparingTags | CurrentStage::SameMusicReadingTags | CurrentStage::SameMusicComparingFingerprints | CurrentStage::SameMusicCalculatingFingerprints => Some(ToolType::SameMusic), CurrentStage::SimilarImagesCalculatingHashes | CurrentStage::SimilarImagesComparingHashes => Some(ToolType::SimilarImages), CurrentStage::SimilarVideosCalculatingHashes | CurrentStage::SimilarVideosCreatingThumbnails => Some(ToolType::SimilarVideos), CurrentStage::BrokenFilesChecking => Some(ToolType::BrokenFiles), CurrentStage::BadExtensionsChecking => Some(ToolType::BadExtensions), CurrentStage::BadNamesChecking => Some(ToolType::BadNames), CurrentStage::ExifRemoverCacheLoading | CurrentStage::ExifRemoverExtractingTags | CurrentStage::ExifRemoverCacheSaving => Some(ToolType::ExifRemover), CurrentStage::VideoOptimizerCreatingThumbnails | CurrentStage::VideoOptimizerProcessingVideos => Some(ToolType::VideoOptimizer), }; if let Some(tool_type) = tool_type_current_stage { assert_eq!(self.tool_type, tool_type, "Tool type: {:?}, stage {:?}", self.tool_type, self.sstage); } } } impl ToolType { pub(crate) fn get_max_stage(self, checking_method: CheckingMethod) -> u8 { match self { Self::Duplicate => 6, Self::EmptyFolders | Self::EmptyFiles | Self::InvalidSymlinks | Self::BigFile | Self::TemporaryFiles => 0, Self::BrokenFiles | Self::BadExtensions | Self::BadNames => 1, Self::SimilarImages | Self::SimilarVideos | Self::VideoOptimizer => 2, Self::ExifRemover => 3, Self::None => unreachable!("ToolType::None is not allowed"), Self::SameMusic => match checking_method { CheckingMethod::AudioTags => 4, CheckingMethod::AudioContent => 7, _ => unreachable!("CheckingMethod {checking_method:?} in same music mode is not allowed"), }, } } } impl CurrentStage { pub fn is_special_non_tool_stage(self) -> bool { matches!( self, Self::DeletingFiles | Self::RenamingFiles | Self::MovingFiles | Self::HardlinkingFiles | Self::SymlinkingFiles | Self::OptimizingVideos | Self::CleaningExif ) } pub fn get_current_stage(self) -> u8 { #[expect(clippy::match_same_arms)] // Now it is easier to read match self { Self::DeletingFiles => 0, Self::RenamingFiles => 0, Self::MovingFiles => 0, Self::HardlinkingFiles => 0, Self::SymlinkingFiles => 0, Self::OptimizingVideos => 0, Self::CleaningExif => 0, Self::CollectingFiles => 0, Self::DuplicateScanningName => 0, Self::DuplicateScanningSizeName => 0, Self::DuplicateScanningSize => 0, Self::DuplicatePreHashCacheLoading => 1, Self::DuplicatePreHashing => 2, Self::DuplicatePreHashCacheSaving => 3, Self::DuplicateCacheLoading => 4, Self::DuplicateFullHashing => 5, Self::DuplicateCacheSaving => 6, Self::SimilarImagesCalculatingHashes => 1, Self::SimilarImagesComparingHashes => 2, Self::SimilarVideosCalculatingHashes => 1, Self::SimilarVideosCreatingThumbnails => 2, Self::BrokenFilesChecking => 1, Self::BadExtensionsChecking => 1, Self::BadNamesChecking => 1, Self::VideoOptimizerCreatingThumbnails => 2, Self::VideoOptimizerProcessingVideos => 1, Self::SameMusicCacheLoadingTags => 1, Self::SameMusicReadingTags => 2, Self::SameMusicCacheSavingTags => 3, Self::SameMusicComparingTags => 4, Self::SameMusicCacheLoadingFingerprints => 4, Self::SameMusicCalculatingFingerprints => 5, Self::SameMusicCacheSavingFingerprints => 6, Self::SameMusicComparingFingerprints => 7, Self::ExifRemoverCacheLoading => 1, Self::ExifRemoverExtractingTags => 2, Self::ExifRemoverCacheSaving => 3, } } pub fn check_if_loading_saving_cache(self) -> bool { self.check_if_saving_cache() || self.check_if_loading_cache() } pub fn check_if_loading_cache(self) -> bool { matches!( self, Self::SameMusicCacheLoadingFingerprints | Self::SameMusicCacheLoadingTags | Self::DuplicateCacheLoading | Self::DuplicatePreHashCacheLoading | Self::ExifRemoverCacheLoading ) } pub fn check_if_saving_cache(self) -> bool { matches!( self, Self::SameMusicCacheSavingFingerprints | Self::SameMusicCacheSavingTags | Self::DuplicateCacheSaving | Self::DuplicatePreHashCacheSaving | Self::ExifRemoverCacheSaving ) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_tool_type_and_current_stage_integration() { assert_eq!(ToolType::Duplicate.get_max_stage(CheckingMethod::Hash), 6); assert_eq!(ToolType::SameMusic.get_max_stage(CheckingMethod::AudioTags), 4); assert_eq!(ToolType::SameMusic.get_max_stage(CheckingMethod::AudioContent), 7); assert_eq!(ToolType::SimilarImages.get_max_stage(CheckingMethod::None), 2); assert_eq!(ToolType::BrokenFiles.get_max_stage(CheckingMethod::None), 1); assert_eq!(CurrentStage::DuplicateFullHashing.get_current_stage(), 5); assert_eq!(CurrentStage::SameMusicComparingFingerprints.get_current_stage(), 7); assert!(CurrentStage::DeletingFiles.is_special_non_tool_stage()); assert!(!CurrentStage::CollectingFiles.is_special_non_tool_stage()); } #[test] fn test_cache_operations_detection() { assert!(CurrentStage::DuplicateCacheLoading.check_if_loading_cache()); assert!(CurrentStage::DuplicateCacheSaving.check_if_saving_cache()); assert!(CurrentStage::SameMusicCacheLoadingTags.check_if_loading_saving_cache()); assert!(!CurrentStage::DuplicateFullHashing.check_if_loading_saving_cache()); } #[test] fn test_progress_data_validation_and_empty_state() { let empty = ProgressData::get_empty_state(CurrentStage::CollectingFiles); assert_eq!(empty.entries_checked, 0); assert_eq!(empty.tool_type, ToolType::None); let valid = ProgressData { sstage: CurrentStage::DuplicateFullHashing, checking_method: CheckingMethod::Hash, current_stage_idx: 5, max_stage_idx: 6, entries_checked: 50, entries_to_check: 100, bytes_checked: 1000, bytes_to_check: 2000, tool_type: ToolType::Duplicate, }; valid.validate(); } #[test] #[should_panic(expected = "Current stage index")] fn test_validation_invalid_stage_idx() { ProgressData { sstage: CurrentStage::DuplicateFullHashing, checking_method: CheckingMethod::Hash, current_stage_idx: 7, max_stage_idx: 6, entries_checked: 0, entries_to_check: 100, bytes_checked: 0, bytes_to_check: 1000, tool_type: ToolType::Duplicate, } .validate(); } #[test] #[should_panic(expected = "Entries checked")] fn test_validation_too_many_entries() { ProgressData { sstage: CurrentStage::DuplicateFullHashing, checking_method: CheckingMethod::Hash, current_stage_idx: 5, max_stage_idx: 6, entries_checked: 150, entries_to_check: 100, bytes_checked: 0, bytes_to_check: 1000, tool_type: ToolType::Duplicate, } .validate(); } } ================================================ FILE: czkawka_core/src/common/progress_stop_handler.rs ================================================ use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize}; use std::sync::{Arc, atomic}; use std::thread; use std::thread::{JoinHandle, sleep}; use std::time::{Duration, Instant}; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::{CheckingMethod, ToolType}; use crate::common::progress_data::{CurrentStage, ProgressData}; pub const LOOP_DURATION: u32 = 20; pub const SEND_PROGRESS_DATA_TIME_BETWEEN: u32 = 200; pub(crate) struct ProgressThreadHandler { progress_thread_handle: JoinHandle<()>, progress_thread_running: Arc, progress_status: ProgressStatus, } impl ProgressThreadHandler { pub fn new(progress_thread_handle: JoinHandle<()>, progress_thread_running: Arc, progress_status: ProgressStatus) -> Self { Self { progress_thread_handle, progress_thread_running, progress_status, } } pub fn join_thread(self) { self.progress_thread_running.store(false, atomic::Ordering::Relaxed); self.progress_thread_handle .join() .expect("Cannot join progress thread - quite fatal error, but I hope, that it will never happen :)"); } pub fn increase_items(&self, count: usize) { self.progress_status.items_counter.fetch_add(count, atomic::Ordering::Relaxed); } pub fn increase_size(&self, size: u64) { self.progress_status.size_counter.fetch_add(size, atomic::Ordering::Relaxed); } pub fn items_counter(&self) -> &Arc { &self.progress_status.items_counter } pub fn size_counter(&self) -> &Arc { &self.progress_status.size_counter } } #[derive(Clone)] pub(crate) struct ProgressStatus { items_counter: Arc, size_counter: Arc, } impl ProgressStatus { pub fn new() -> Self { Self { items_counter: Arc::new(AtomicUsize::new(0)), size_counter: Arc::new(AtomicU64::new(0)), } } } pub(crate) fn prepare_thread_handler_common( progress_sender: Option<&Sender>, sstage: CurrentStage, max_items: usize, test_type: (ToolType, CheckingMethod), max_size: u64, ) -> ProgressThreadHandler { let (tool_type, checking_method) = test_type; assert_ne!(tool_type, ToolType::None, "Cannot send progress data for ToolType::None"); let progress_status = ProgressStatus::new(); let progress_thread_running = Arc::new(AtomicBool::new(true)); let progress_thread_sender = if let Some(progress_sender) = progress_sender.cloned() { let progress_status = progress_status.clone(); let progress_thread_running = progress_thread_running.clone(); thread::spawn(move || { // Use earlier time, to send immediately first message let mut time_since_last_send = Instant::now().checked_sub(Duration::from_secs(10u64)).unwrap_or_else(Instant::now); loop { if time_since_last_send.elapsed().as_millis() > SEND_PROGRESS_DATA_TIME_BETWEEN as u128 { let progress_data = ProgressData { sstage, checking_method, current_stage_idx: sstage.get_current_stage(), max_stage_idx: tool_type.get_max_stage(checking_method), entries_checked: progress_status.items_counter.load(atomic::Ordering::Relaxed), entries_to_check: max_items, bytes_checked: progress_status.size_counter.load(atomic::Ordering::Relaxed), bytes_to_check: max_size, tool_type, }; progress_data.validate(); progress_sender.send(progress_data).expect("Cannot send progress data"); time_since_last_send = Instant::now(); } if !progress_thread_running.load(atomic::Ordering::Relaxed) { break; } sleep(Duration::from_millis(LOOP_DURATION as u64)); } }) } else { thread::spawn(|| {}) }; ProgressThreadHandler::new(progress_thread_sender, progress_thread_running, progress_status) } #[inline] pub(crate) fn check_if_stop_received(stop_flag: &Arc) -> bool { stop_flag.load(atomic::Ordering::Relaxed) } #[fun_time(message = "send_info_and_wait_for_ending_all_threads", level = "debug")] pub(crate) fn send_info_and_wait_for_ending_all_threads(progress_thread_run: &Arc, progress_thread_handle: JoinHandle<()>) { progress_thread_run.store(false, atomic::Ordering::Relaxed); progress_thread_handle.join().expect("Cannot join progress thread - quite fatal error, but happens rarely"); } #[cfg(test)] mod tests { use super::*; #[test] fn test_progress_status_and_stop_flag() { let status = ProgressStatus::new(); assert_eq!(status.items_counter.load(atomic::Ordering::Relaxed), 0); assert_eq!(status.size_counter.load(atomic::Ordering::Relaxed), 0); status.items_counter.fetch_add(10, atomic::Ordering::Relaxed); status.size_counter.fetch_add(1024, atomic::Ordering::Relaxed); assert_eq!(status.items_counter.load(atomic::Ordering::Relaxed), 10); assert_eq!(status.size_counter.load(atomic::Ordering::Relaxed), 1024); let stop_flag = Arc::new(AtomicBool::new(false)); assert!(!check_if_stop_received(&stop_flag)); stop_flag.store(true, atomic::Ordering::Relaxed); assert!(check_if_stop_received(&stop_flag)); } #[test] fn test_progress_thread_handler_with_sender() { let (sender, _receiver) = crossbeam_channel::unbounded(); let handler = prepare_thread_handler_common(Some(&sender), CurrentStage::DuplicateFullHashing, 100, (ToolType::Duplicate, CheckingMethod::Hash), 10000); assert_eq!(handler.items_counter().load(atomic::Ordering::Relaxed), 0); assert_eq!(handler.size_counter().load(atomic::Ordering::Relaxed), 0); handler.increase_items(5); handler.increase_size(512); handler.increase_items(3); handler.increase_size(256); assert_eq!(handler.items_counter().load(atomic::Ordering::Relaxed), 8); assert_eq!(handler.size_counter().load(atomic::Ordering::Relaxed), 768); handler.join_thread(); } #[test] fn test_progress_thread_handler_without_sender() { let handler = prepare_thread_handler_common(None, CurrentStage::CollectingFiles, 50, (ToolType::EmptyFiles, CheckingMethod::None), 5000); handler.increase_items(10); handler.increase_size(1000); assert_eq!(handler.items_counter().load(atomic::Ordering::Relaxed), 10); assert_eq!(handler.size_counter().load(atomic::Ordering::Relaxed), 1000); handler.join_thread(); } #[test] #[should_panic(expected = "Cannot send progress data for ToolType::None")] fn test_panics_on_none_tool_type() { prepare_thread_handler_common(None, CurrentStage::CollectingFiles, 50, (ToolType::None, CheckingMethod::None), 5000); } } ================================================ FILE: czkawka_core/src/common/tool_data.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Duration; use crossbeam_channel::Sender; use humansize::{BINARY, format_size}; use log::info; use rayon::prelude::*; use crate::common::directories::Directories; use crate::common::extensions::Extensions; use crate::common::items::ExcludedItems; use crate::common::model::{CheckingMethod, ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::check_if_stop_received; use crate::common::traits::ResultEntry; use crate::common::{make_hard_link, remove_folder_if_contains_only_empty_folders, remove_single_file}; use crate::helpers::delayed_sender::DelayedSender; use crate::helpers::messages::Messages; #[derive(Debug, Clone, Default)] pub struct CommonToolData { pub(crate) tool_type: ToolType, pub(crate) text_messages: Messages, pub(crate) directories: Directories, pub(crate) extensions: Extensions, pub(crate) excluded_items: ExcludedItems, pub(crate) recursive_search: bool, pub(crate) delete_method: DeleteMethod, pub(crate) maximal_file_size: u64, pub(crate) minimal_file_size: u64, pub(crate) stopped_search: bool, pub(crate) use_cache: bool, pub(crate) delete_outdated_cache: bool, pub(crate) save_also_as_json: bool, pub(crate) use_reference_folders: bool, pub(crate) dry_run: bool, pub(crate) move_to_trash: bool, pub(crate) hide_hard_links: bool, } #[derive(Debug, Clone, Default)] pub struct DeleteResult { deleted_files: usize, gained_bytes: u64, failed_to_delete_files: usize, errors: Vec, infos: Vec, } impl DeleteResult { pub(crate) fn add_to_messages(&self, messages: &mut Messages) { messages.errors.extend(self.errors.clone()); messages.messages.extend(self.infos.clone()); } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum DeleteItemType { DeletingFiles(Vec), DeletingFolders(Vec), HardlinkingFiles(Vec<(T, Vec)>), } impl DeleteItemType { fn calculate_size_to_delete(&self) -> u64 { match &self { Self::DeletingFiles(items) | Self::DeletingFolders(items) => items.iter().map(|item| item.get_size()).sum(), Self::HardlinkingFiles(items) => items.iter().map(|(item, _)| item.get_size()).sum(), } } fn calculate_entries_to_delete(&self) -> usize { match &self { Self::DeletingFiles(items) | Self::DeletingFolders(items) => items.len(), Self::HardlinkingFiles(items) => items.iter().map(|(_original, files)| files.len()).sum(), } } } #[derive(Eq, PartialEq, Clone, Debug, Copy, Default)] pub enum DeleteMethod { #[default] None, Delete, // Just delete items AllExceptNewest, AllExceptOldest, OneOldest, OneNewest, HardLink, AllExceptBiggest, AllExceptSmallest, OneBiggest, OneSmallest, } impl CommonToolData { pub fn new(tool_type: ToolType) -> Self { Self { tool_type, text_messages: Messages::new(), directories: Directories::new(), extensions: Extensions::new(), excluded_items: ExcludedItems::new(), recursive_search: true, delete_method: DeleteMethod::None, maximal_file_size: u64::MAX, minimal_file_size: 0, stopped_search: false, use_cache: true, delete_outdated_cache: true, save_also_as_json: false, use_reference_folders: false, dry_run: false, move_to_trash: false, hide_hard_links: false, } } } pub trait CommonData { type Info; type Parameters; fn get_information(&self) -> Self::Info; fn get_params(&self) -> Self::Parameters; fn get_cd(&self) -> &CommonToolData; fn get_cd_mut(&mut self) -> &mut CommonToolData; fn get_check_method(&self) -> CheckingMethod { CheckingMethod::None } fn get_test_type(&self) -> (ToolType, CheckingMethod) { (self.get_cd().tool_type, self.get_check_method()) } fn found_any_items(&self) -> bool; fn get_tool_type(&self) -> ToolType { self.get_cd().tool_type } fn set_hide_hard_links(&mut self, hide_hard_links: bool) { self.get_cd_mut().hide_hard_links = hide_hard_links; } fn get_hide_hard_links(&self) -> bool { self.get_cd().hide_hard_links } fn set_dry_run(&mut self, dry_run: bool) { self.get_cd_mut().dry_run = dry_run; } fn get_dry_run(&self) -> bool { self.get_cd().dry_run } fn set_use_cache(&mut self, use_cache: bool) { self.get_cd_mut().use_cache = use_cache; } fn get_use_cache(&self) -> bool { self.get_cd().use_cache } fn set_delete_outdated_cache(&mut self, delete_outdated_cache: bool) { self.get_cd_mut().delete_outdated_cache = delete_outdated_cache; } fn get_delete_outdated_cache(&self) -> bool { self.get_cd().delete_outdated_cache } fn get_stopped_search(&self) -> bool { self.get_cd().stopped_search } fn set_stopped_search(&mut self, stopped_search: bool) { self.get_cd_mut().stopped_search = stopped_search; } fn set_maximal_file_size(&mut self, maximal_file_size: u64) { self.get_cd_mut().maximal_file_size = match maximal_file_size { 0 => 1, t => t, }; } fn get_maximal_file_size(&self) -> u64 { self.get_cd().maximal_file_size } fn set_minimal_file_size(&mut self, minimal_file_size: u64) { self.get_cd_mut().minimal_file_size = match minimal_file_size { 0 => 1, t => t, }; } fn get_minimal_file_size(&self) -> u64 { self.get_cd().minimal_file_size } #[cfg(target_family = "unix")] fn set_exclude_other_filesystems(&mut self, exclude_other_filesystems: bool) { self.get_cd_mut().directories.set_exclude_other_filesystems(exclude_other_filesystems); } #[cfg(not(target_family = "unix"))] fn set_exclude_other_filesystems(&mut self, _exclude_other_filesystems: bool) {} fn get_text_messages(&self) -> &Messages { &self.get_cd().text_messages } fn get_text_messages_mut(&mut self) -> &mut Messages { &mut self.get_cd_mut().text_messages } fn set_save_also_as_json(&mut self, save_also_as_json: bool) { self.get_cd_mut().save_also_as_json = save_also_as_json; } fn get_save_also_as_json(&self) -> bool { self.get_cd().save_also_as_json } fn set_recursive_search(&mut self, recursive_search: bool) { self.get_cd_mut().recursive_search = recursive_search; } fn get_recursive_search(&self) -> bool { self.get_cd().recursive_search } fn set_use_reference_folders(&mut self, use_reference_folders: bool) { self.get_cd_mut().use_reference_folders = use_reference_folders; } fn get_use_reference_folders(&self) -> bool { self.get_cd().use_reference_folders } fn set_delete_method(&mut self, delete_method: DeleteMethod) { self.get_cd_mut().delete_method = delete_method; } fn get_delete_method(&self) -> DeleteMethod { self.get_cd().delete_method } // Only used for internal deleting - probably only useful in CLI, but not in GUI which probably uses its own delete method selection fn set_move_to_trash(&mut self, move_to_trash: bool) { self.get_cd_mut().move_to_trash = move_to_trash; } fn get_move_to_trash(&self) -> bool { self.get_cd().move_to_trash } fn set_included_paths(&mut self, included_paths: Vec) { let messages = self.get_cd_mut().directories.set_included_paths(included_paths); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_excluded_paths(&mut self, excluded_paths: Vec) { let messages = self.get_cd_mut().directories.set_excluded_paths(excluded_paths); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_reference_paths(&mut self, reference_paths: Vec) { let messages = self.get_cd_mut().directories.set_reference_paths(reference_paths); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_allowed_extensions(&mut self, allowed_extensions: Vec) { let messages = self.get_cd_mut().extensions.set_allowed_extensions(allowed_extensions); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_excluded_extensions(&mut self, excluded_extensions: Vec) { let messages = self.get_cd_mut().extensions.set_excluded_extensions(excluded_extensions); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn set_excluded_items(&mut self, excluded_items: Vec) { let messages = self.get_cd_mut().excluded_items.set_excluded_items(excluded_items); self.get_cd_mut().text_messages.extend_with_another_messages(messages); } fn get_extensions_mut(&mut self) -> &mut Extensions { &mut self.get_cd_mut().extensions } #[expect(clippy::result_unit_err)] fn prepare_items(&mut self, tool_extensions: Option<&[&str]>) -> Result<(), ()> { let recursive_search = self.get_cd().recursive_search; // Optimizes directories and removes recursive calls match self.get_cd_mut().directories.optimize_directories(recursive_search, false) { Ok(messages) => { self.get_cd_mut().text_messages.extend_with_another_messages(messages); } Err(messages) => { self.get_cd_mut().text_messages.extend_with_another_messages(messages); return Err(()); } } if let Err(e) = self.get_extensions_mut().set_and_validate_extensions(tool_extensions) { self.get_cd_mut().text_messages.critical = Some(e); return Err(()); } Ok(()) } fn delete_simple_elements_and_add_to_messages( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, delete_item_type: DeleteItemType, ) -> WorkContinueStatus { let delete_results = self.delete_elements(stop_flag, progress_sender, delete_item_type); if check_if_stop_received(stop_flag) { WorkContinueStatus::Stop } else { delete_results.add_to_messages(self.get_text_messages_mut()); WorkContinueStatus::Continue } } #[expect(clippy::indexing_slicing)] // Safe, because input is always checked to have at least 1 element fn delete_advanced_elements_and_add_to_messages( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, files_to_process: Vec>, ) -> WorkContinueStatus { let delete_method = self.get_cd().delete_method; let sorting_by_size = matches!( delete_method, DeleteMethod::AllExceptBiggest | DeleteMethod::AllExceptSmallest | DeleteMethod::OneBiggest | DeleteMethod::OneSmallest ); let sort_items = |mut input: Vec| -> Vec { input.sort_unstable_by_key(if sorting_by_size { ResultEntry::get_size } else { ResultEntry::get_modified_date }); input }; let delete_results = if delete_method == DeleteMethod::HardLink { let res = files_to_process .into_iter() .map(|values| { let mut all_values = sort_items(values); let original = all_values.remove(0); (original, all_values) }) .collect::>(); self.delete_elements(stop_flag, progress_sender, DeleteItemType::HardlinkingFiles(res)) } else { let res = files_to_process .into_iter() .flat_map(|values| { // TODO - probably a little too much cloning, so later could be this optimized let len = values.len(); let all_values = sort_items(values); match delete_method { DeleteMethod::Delete => &all_values, DeleteMethod::AllExceptNewest | DeleteMethod::AllExceptBiggest => &all_values[..(len - 1)], DeleteMethod::AllExceptOldest | DeleteMethod::AllExceptSmallest => &all_values[1..], DeleteMethod::OneOldest | DeleteMethod::OneSmallest => &all_values[..1], DeleteMethod::OneNewest | DeleteMethod::OneBiggest => &all_values[(len - 1)..], DeleteMethod::HardLink | DeleteMethod::None => unreachable!("HardLink and None should be handled before"), } .to_vec() }) .collect::>(); self.delete_elements(stop_flag, progress_sender, DeleteItemType::DeletingFiles(res)) }; if check_if_stop_received(stop_flag) { WorkContinueStatus::Stop } else { delete_results.add_to_messages(self.get_text_messages_mut()); WorkContinueStatus::Continue } } fn delete_elements( &self, stop_flag: &Arc, progress_sender: Option<&Sender>, delete_item_type: DeleteItemType, ) -> DeleteResult { let dry_run = self.get_cd().dry_run; let move_to_trash = self.get_cd().move_to_trash; let mut progress = ProgressData::get_empty_state(CurrentStage::DeletingFiles); progress.bytes_to_check = delete_item_type.calculate_size_to_delete(); progress.entries_to_check = delete_item_type.calculate_entries_to_delete(); let is_hardlinking = matches!(delete_item_type, DeleteItemType::HardlinkingFiles(_)); let msg_common = format!( "{} items, total size: {} bytes, dry_run: {dry_run}", progress.entries_to_check, format_size(progress.bytes_to_check, BINARY) ); if is_hardlinking { info!("Hardlinking {msg_common}"); } else { info!("Deleting {msg_common}"); } let delayed_sender = progress_sender.map(|e| DelayedSender::new(e.clone(), Duration::from_millis(200))); let bytes_processed = Arc::new(std::sync::atomic::AtomicU64::new(0)); let files_processed = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let res = match delete_item_type { DeleteItemType::DeletingFiles(ref items) | DeleteItemType::DeletingFolders(ref items) => items .into_par_iter() .map(|e| { if check_if_stop_received(stop_flag) { return None; } let mut progress_tmp = progress; progress_tmp.bytes_checked = bytes_processed.fetch_add(e.get_size(), std::sync::atomic::Ordering::Relaxed); progress_tmp.entries_checked = files_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if let Some(e) = delayed_sender.as_ref() { e.send(progress_tmp); } if dry_run { return Some(vec![(e, None)]); } let delete_res = if matches!(delete_item_type, DeleteItemType::DeletingFiles(_)) { remove_single_file(e.get_path(), move_to_trash) } else { remove_folder_if_contains_only_empty_folders(e.get_path(), move_to_trash) }; match delete_res { Ok(()) => Some(vec![(e, None)]), Err(err) => Some(vec![(e, Some(err))]), } }) .while_some() .flatten() .collect::>(), DeleteItemType::HardlinkingFiles(ref items) => items .into_par_iter() .map(|(original, files)| { if check_if_stop_received(stop_flag) { return None; } let mut progress_tmp = progress; progress_tmp.bytes_checked = bytes_processed.fetch_add(files.iter().map(|e| e.get_size()).sum(), std::sync::atomic::Ordering::Relaxed); progress_tmp.entries_checked = files_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if let Some(e) = delayed_sender.as_ref() { e.send(progress_tmp); } if dry_run { return Some(files.iter().map(|e| (e, None)).collect::>()); } let res = files .iter() .map(|file| { let err = match make_hard_link(original.get_path(), file.get_path()) { Ok(()) => None, Err(err) => Some(format!( "Failed to hardlink \"{}\" to \"{}\": {err}", original.get_path().to_string_lossy(), file.get_path().to_string_lossy() )), }; (file, err) }) .collect::>(); Some(res) }) .while_some() .flatten() .collect::>(), }; let mut delete_result = DeleteResult::default(); for (file_entry, delete_err) in res { if let Some(err) = delete_err { delete_result.errors.push(err); delete_result.failed_to_delete_files += 1; } else { if dry_run { if is_hardlinking { delete_result.infos.push(format!( "Would hardlink: \"{}\" to \"{}\"", file_entry.get_path().to_string_lossy(), file_entry.get_path().to_string_lossy() )); } else { delete_result.infos.push(format!("Would delete: \"{}\"", file_entry.get_path().to_string_lossy())); } } delete_result.deleted_files += 1; delete_result.gained_bytes += file_entry.get_size(); } } if !dry_run { let action = if is_hardlinking { "hardlink" } else { "delete" }; let action2 = if is_hardlinking { "hardlinked" } else { "deleted" }; info!( "{} items {action2}, {} gained, {} failed to {action}", delete_result.deleted_files, format_size(delete_result.gained_bytes, BINARY), delete_result.failed_to_delete_files ); } delete_result } #[expect(clippy::print_stdout)] fn debug_print_common(&self) { println!("---------------DEBUG PRINT COMMON---------------"); println!("Included paths(before optimization) - {:?}", self.get_cd().directories.original_included_paths); println!("Excluded paths(before optimization) - {:?}", self.get_cd().directories.original_excluded_paths); println!("Reference paths(before optimization) - {:?}", self.get_cd().directories.original_reference_paths); println!("Included directories(optimized) - {:?}", self.get_cd().directories.included_directories); println!("Included files(optimized) - {:?}", self.get_cd().directories.included_files); println!("Excluded directories(optimized) - {:?}", self.get_cd().directories.excluded_directories); println!("Excluded files(optimized) - {:?}", self.get_cd().directories.excluded_files); println!("Reference directories(optimized) - {:?}", self.get_cd().directories.reference_directories); println!("Reference files(optimized) - {:?}", self.get_cd().directories.reference_files); println!("Tool type: {:?}", self.get_cd().tool_type); println!("Directories: {:?}", self.get_cd().directories); println!("Extensions: {:?}", self.get_cd().extensions); println!("Excluded items: {:?}", self.get_cd().excluded_items); println!("Recursive search: {}", self.get_cd().recursive_search); println!("Maximal file size: {}", self.get_cd().maximal_file_size); println!("Minimal file size: {}", self.get_cd().minimal_file_size); println!("Stopped search: {}", self.get_cd().stopped_search); println!("Use cache: {}", self.get_cd().use_cache); println!("Delete outdated cache: {}", self.get_cd().delete_outdated_cache); println!("Save also as json: {}", self.get_cd().save_also_as_json); println!("Delete method: {:?}", self.get_cd().delete_method); println!("Use reference folders: {}", self.get_cd().use_reference_folders); println!("Dry run: {}", self.get_cd().dry_run); println!("Hide hard links: {}", self.get_cd().hide_hard_links); println!("---------------DEBUG PRINT MESSAGES---------------"); println!("Errors size - {}", self.get_cd().text_messages.errors.len()); println!("Warnings size - {}", self.get_cd().text_messages.warnings.len()); println!("Messages size - {}", self.get_cd().text_messages.messages.len()); } } #[cfg(test)] mod tests { use std::fs; use tempfile::TempDir; use super::*; use crate::common::model::FileEntry; // Mock implementation for testing struct MockTool { common_data: CommonToolData, } impl CommonData for MockTool { type Info = (); type Parameters = (); fn get_information(&self) -> Self::Info {} fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { false } } impl MockTool { fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::Duplicate), } } } #[test] fn test_delete_result_add_to_messages() { let delete_result = DeleteResult { deleted_files: 5, gained_bytes: 1024, failed_to_delete_files: 2, errors: vec!["Error 1".to_string(), "Error 2".to_string()], infos: vec!["Info 1".to_string()], }; let mut messages = Messages::new(); delete_result.add_to_messages(&mut messages); assert_eq!(messages.errors.len(), 2); assert_eq!(messages.messages.len(), 1); assert!(messages.errors.contains(&"Error 1".to_string())); assert!(messages.messages.contains(&"Info 1".to_string())); } #[test] fn test_delete_item_type_calculate_size_and_entries() { let files = vec![ FileEntry { path: PathBuf::from("/a"), size: 100, modified_date: 1, }, FileEntry { path: PathBuf::from("/b"), size: 200, modified_date: 2, }, FileEntry { path: PathBuf::from("/c"), size: 300, modified_date: 3, }, ]; let delete_files = DeleteItemType::DeletingFiles(files.clone()); assert_eq!(delete_files.calculate_size_to_delete(), 600); assert_eq!(delete_files.calculate_entries_to_delete(), 3); let delete_folders = DeleteItemType::DeletingFolders(files.clone()); assert_eq!(delete_folders.calculate_size_to_delete(), 600); assert_eq!(delete_folders.calculate_entries_to_delete(), 3); let hardlink_files = DeleteItemType::HardlinkingFiles(vec![ (files[0].clone(), vec![files[1].clone()]), (files[2].clone(), vec![files[0].clone(), files[1].clone()]), ]); assert_eq!(hardlink_files.calculate_size_to_delete(), 400); assert_eq!(hardlink_files.calculate_entries_to_delete(), 3); } #[test] fn test_common_tool_data_new() { let tool_data = CommonToolData::new(ToolType::Duplicate); assert_eq!(tool_data.tool_type, ToolType::Duplicate); assert_eq!(tool_data.delete_method, DeleteMethod::None); assert_eq!(tool_data.maximal_file_size, u64::MAX); assert_eq!(tool_data.minimal_file_size, 0); assert!(tool_data.recursive_search); assert!(!tool_data.stopped_search); assert!(tool_data.use_cache); assert!(tool_data.delete_outdated_cache); assert!(!tool_data.save_also_as_json); assert!(!tool_data.use_reference_folders); assert!(!tool_data.dry_run); } #[test] fn test_delete_elements_dry_run() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); fs::write(&file1, "test content 1").unwrap(); fs::write(&file2, "test content 2").unwrap(); let files = vec![ FileEntry { path: file1.clone(), size: 14, modified_date: 1, }, FileEntry { path: file2.clone(), size: 14, modified_date: 2, }, ]; let mut tool = MockTool::new(); tool.common_data.dry_run = true; let stop_flag = Arc::new(AtomicBool::new(false)); let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files)); assert_eq!(delete_result.deleted_files, 2, "Should mark 2 files as deleted"); assert_eq!(delete_result.failed_to_delete_files, 0, "Should have no failed deletions"); assert_eq!(delete_result.gained_bytes, 28, "Should calculate gained bytes"); assert_eq!(delete_result.infos.len(), 2, "Should have 2 info messages in dry run"); assert!(file1.exists(), "File should still exist in dry run"); assert!(file2.exists(), "File should still exist in dry run"); } #[test] fn test_delete_elements_actual_deletion() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); fs::write(&file1, "test content 1").unwrap(); fs::write(&file2, "test content 2").unwrap(); let files = vec![ FileEntry { path: file1.clone(), size: 14, modified_date: 1, }, FileEntry { path: file2.clone(), size: 14, modified_date: 2, }, ]; let tool = MockTool::new(); let stop_flag = Arc::new(AtomicBool::new(false)); let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files)); assert_eq!(delete_result.deleted_files, 2, "Should delete 2 files"); assert_eq!(delete_result.failed_to_delete_files, 0, "Should have no failed deletions"); assert_eq!(delete_result.gained_bytes, 28, "Should gain 28 bytes"); assert!(!file1.exists(), "File 1 should be deleted"); assert!(!file2.exists(), "File 2 should be deleted"); } #[test] fn test_delete_elements_with_stop_flag() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); fs::write(&file1, "test content").unwrap(); let files = vec![FileEntry { path: file1.clone(), size: 12, modified_date: 1, }]; let tool = MockTool::new(); let stop_flag = Arc::new(AtomicBool::new(true)); // Stop flag set to true let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files)); assert_eq!(delete_result.deleted_files, 0, "Should not delete any files when stopped"); assert!(file1.exists(), "File should still exist"); } #[test] fn test_delete_elements_nonexistent_file() { let temp_dir = TempDir::new().unwrap(); let nonexistent_file = temp_dir.path().join("nonexistent.txt"); let files = vec![FileEntry { path: nonexistent_file, size: 100, modified_date: 1, }]; let tool = MockTool::new(); let stop_flag = Arc::new(AtomicBool::new(false)); let delete_result = tool.delete_elements(&stop_flag, None, DeleteItemType::DeletingFiles(files)); assert_eq!(delete_result.deleted_files, 0, "Should not delete nonexistent file"); assert_eq!(delete_result.failed_to_delete_files, 1, "Should report 1 failed deletion"); assert_eq!(delete_result.errors.len(), 1, "Should have 1 error message"); } #[test] fn test_delete_simple_elements_and_add_to_messages() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); fs::write(&file1, "content1").unwrap(); fs::write(&file2, "content2").unwrap(); let files = vec![ FileEntry { path: file1.clone(), size: 8, modified_date: 1, }, FileEntry { path: file2.clone(), size: 8, modified_date: 2, }, ]; let mut tool = MockTool::new(); let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_simple_elements_and_add_to_messages(&stop_flag, None, DeleteItemType::DeletingFiles(files)); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(!file1.exists(), "File 1 should be deleted"); assert!(!file2.exists(), "File 2 should be deleted"); assert_eq!(tool.common_data.text_messages.errors.len(), 0, "Should have no errors"); } #[test] fn test_delete_simple_elements_with_stop_flag() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); fs::write(&file1, "content").unwrap(); let files = vec![FileEntry { path: file1.clone(), size: 7, modified_date: 1, }]; let mut tool = MockTool::new(); let stop_flag = Arc::new(AtomicBool::new(true)); let status = tool.delete_simple_elements_and_add_to_messages(&stop_flag, None, DeleteItemType::DeletingFiles(files)); assert_eq!(status, WorkContinueStatus::Stop, "Should stop"); assert!(file1.exists(), "File should still exist"); } #[test] fn test_delete_advanced_elements_all_except_newest() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); fs::write(&file3, "c").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 1, modified_date: 2, }, FileEntry { path: file3.clone(), size: 1, modified_date: 3, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::AllExceptNewest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(!file1.exists(), "Oldest file should be deleted"); assert!(!file2.exists(), "Middle file should be deleted"); assert!(file3.exists(), "Newest file should be kept"); } #[test] fn test_delete_advanced_elements_all_except_oldest() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); fs::write(&file3, "c").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 1, modified_date: 2, }, FileEntry { path: file3.clone(), size: 1, modified_date: 3, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::AllExceptOldest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(file1.exists(), "Oldest file should be kept"); assert!(!file2.exists(), "Middle file should be deleted"); assert!(!file3.exists(), "Newest file should be deleted"); } #[test] fn test_delete_advanced_elements_one_oldest() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); fs::write(&file3, "c").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 1, modified_date: 2, }, FileEntry { path: file3.clone(), size: 1, modified_date: 3, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::OneOldest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(!file1.exists(), "Oldest file should be deleted"); assert!(file2.exists(), "Middle file should be kept"); assert!(file3.exists(), "Newest file should be kept"); } #[test] fn test_delete_advanced_elements_one_newest() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); fs::write(&file3, "c").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 1, modified_date: 2, }, FileEntry { path: file3.clone(), size: 1, modified_date: 3, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::OneNewest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(file1.exists(), "Oldest file should be kept"); assert!(file2.exists(), "Middle file should be kept"); assert!(!file3.exists(), "Newest file should be deleted"); } #[test] fn test_delete_advanced_elements_all_except_biggest() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "bb").unwrap(); fs::write(&file3, "ccc").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 2, modified_date: 1, }, FileEntry { path: file3.clone(), size: 3, modified_date: 1, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::AllExceptBiggest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(!file1.exists(), "Smallest file should be deleted"); assert!(!file2.exists(), "Middle file should be deleted"); assert!(file3.exists(), "Biggest file should be kept"); } #[test] fn test_delete_advanced_elements_all_except_smallest() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "bb").unwrap(); fs::write(&file3, "ccc").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 2, modified_date: 1, }, FileEntry { path: file3.clone(), size: 3, modified_date: 1, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::AllExceptSmallest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(file1.exists(), "Smallest file should be kept"); assert!(!file2.exists(), "Middle file should be deleted"); assert!(!file3.exists(), "Biggest file should be deleted"); } #[test] fn test_delete_advanced_elements_delete_all() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); let files_group = vec![vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 1, modified_date: 2, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::Delete; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(!file1.exists(), "All files should be deleted"); assert!(!file2.exists(), "All files should be deleted"); } #[test] fn test_delete_advanced_elements_multiple_groups() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); let file3 = temp_dir.path().join("file3.txt"); let file4 = temp_dir.path().join("file4.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); fs::write(&file3, "c").unwrap(); fs::write(&file4, "d").unwrap(); let files_group = vec![ vec![ FileEntry { path: file1.clone(), size: 1, modified_date: 1, }, FileEntry { path: file2.clone(), size: 1, modified_date: 2, }, ], vec![ FileEntry { path: file3.clone(), size: 1, modified_date: 1, }, FileEntry { path: file4.clone(), size: 1, modified_date: 2, }, ], ]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::AllExceptNewest; let stop_flag = Arc::new(AtomicBool::new(false)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Continue, "Should continue"); assert!(!file1.exists(), "Oldest from group 1 should be deleted"); assert!(file2.exists(), "Newest from group 1 should be kept"); assert!(!file3.exists(), "Oldest from group 2 should be deleted"); assert!(file4.exists(), "Newest from group 2 should be kept"); } #[test] fn test_delete_advanced_elements_with_stop_flag() { let temp_dir = TempDir::new().unwrap(); let file1 = temp_dir.path().join("file1.txt"); let file2 = temp_dir.path().join("file2.txt"); fs::write(&file1, "a").unwrap(); fs::write(&file2, "b").unwrap(); let files_group = vec![vec![ FileEntry { path: file1, size: 1, modified_date: 1, }, FileEntry { path: file2, size: 1, modified_date: 2, }, ]]; let mut tool = MockTool::new(); tool.common_data.delete_method = DeleteMethod::AllExceptNewest; let stop_flag = Arc::new(AtomicBool::new(true)); let status = tool.delete_advanced_elements_and_add_to_messages(&stop_flag, None, files_group); assert_eq!(status, WorkContinueStatus::Stop, "Should stop"); } } ================================================ FILE: czkawka_core/src/common/traits.rs ================================================ use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use serde::Serialize; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::CommonData; pub trait DebugPrint { fn debug_print(&self); } pub trait PrintResults: CommonData { fn write_base_search_paths(&self, writer: &mut T) -> std::io::Result<()> { let dirs = &self.get_cd().directories; let included_paths = dirs.included_files.iter().chain(dirs.included_directories.iter()).collect::>(); let excluded_paths = dirs.excluded_files.iter().chain(dirs.excluded_directories.iter()).collect::>(); let reference_paths = dirs.reference_files.iter().chain(dirs.reference_directories.iter()).collect::>(); let excluded_items = self.get_cd().excluded_items.get_excluded_items(); if self.get_cd().tool_type.may_use_reference_paths() { writeln!( writer, "Results of searching {included_paths:?} with reference paths {reference_paths:?}, excluded paths {excluded_paths:?} and excluded items {excluded_items:?}" )?; writeln!( writer, "(Before optimizations - included paths: {:?}, excluded paths: {:?}, reference paths: {:?})", dirs.original_included_paths, dirs.original_excluded_paths, dirs.original_reference_paths )?; } else { writeln!( writer, "Results of searching {included_paths:?} with excluded paths {excluded_paths:?} and excluded items {excluded_items:?}" )?; writeln!( writer, "(Before optimizations - included paths: {:?}, excluded paths: {:?})", dirs.original_included_paths, dirs.original_excluded_paths )?; } Ok(()) } fn write_results(&self, writer: &mut T) -> std::io::Result<()>; #[fun_time(message = "print_results_to_output", level = "debug")] fn print_results_to_output(&self) { let stdout = std::io::stdout(); let mut handle = stdout.lock(); // Panics here are allowed, because it is used only in CLI self.write_results(&mut handle).expect("Error while writing to stdout"); handle.flush().expect("Error while flushing stdout"); } #[fun_time(message = "print_results_to_file", level = "debug")] fn print_results_to_file(&self, file_name: &str) -> std::io::Result<()> { let file_name: String = match file_name { "" => "results.txt".to_string(), k => k.to_string(), }; let file_handler = File::create(file_name)?; let mut writer = BufWriter::new(file_handler); self.write_results(&mut writer)?; writer.flush()?; Ok(()) } #[fun_time(message = "print_results_to_writer", level = "debug")] fn print_results_to_writer(&self, writer: &mut T) -> std::io::Result<()> { self.write_results(writer) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()>; fn save_results_to_file_as_json_internal(&self, file_name: &str, item_to_serialize: &T, pretty_print: bool) -> std::io::Result<()> { if pretty_print { self.save_results_to_file_as_json_pretty(file_name, item_to_serialize) } else { self.save_results_to_file_as_json_compact(file_name, item_to_serialize) } } #[fun_time(message = "save_results_to_file_as_json_pretty", level = "debug")] fn save_results_to_file_as_json_pretty(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> { let file_handler = File::create(file_name)?; let mut writer = BufWriter::new(file_handler); serde_json::to_writer_pretty(&mut writer, item_to_serialize)?; Ok(()) } #[fun_time(message = "save_results_to_file_as_json_compact", level = "debug")] fn save_results_to_file_as_json_compact(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> { let file_handler = File::create(file_name)?; let mut writer = BufWriter::new(file_handler); serde_json::to_writer(&mut writer, item_to_serialize)?; Ok(()) } fn save_all_in_one(&self, folder: &str, base_file_name: &str) -> std::io::Result<()> { let pretty_name = format!("{folder}/{base_file_name}_pretty.json"); self.save_results_to_file_as_json(&pretty_name, true)?; let compact_name = format!("{folder}/{base_file_name}_compact.json"); self.save_results_to_file_as_json(&compact_name, false)?; let txt_name = format!("{folder}/{base_file_name}.txt"); self.print_results_to_file(&txt_name)?; Ok(()) } } pub trait DeletingItems { #[must_use] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus; } pub trait FixingItems { type FixParams; fn fix_items(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, fix_params: Self::FixParams); } pub trait ResultEntry { fn get_path(&self) -> &Path; fn get_modified_date(&self) -> u64; fn get_size(&self) -> u64; } pub trait Search { fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>); } pub trait AllTraits: DebugPrint + PrintResults + DeletingItems + CommonData + Search {} ================================================ FILE: czkawka_core/src/common/video_utils.rs ================================================ use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use blake3::Hasher; use image::{GenericImage, RgbImage}; use serde::{Deserialize, Serialize}; use crate::common::consts::VIDEO_RESOLUTION_LIMIT; use crate::common::process_utils::disable_windows_console_window; use crate::common::progress_stop_handler::check_if_stop_received; use crate::flc; use crate::helpers::ffprobe::ffprobe; pub const VIDEO_THUMBNAILS_SUBFOLDER: &str = "video_thumbnails"; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct VideoMetadata { pub fps: Option, pub codec: Option, pub bitrate: Option, pub width: Option, pub height: Option, pub duration: Option, } impl VideoMetadata { pub fn from_path(path: &Path) -> Result { let info = ffprobe(path).map_err(|e| flc!("core_failed_to_read_video_properties", reason = e.to_string()))?; let mut metadata = Self::default(); if let Some(duration_str) = &info.format.duration && let Ok(d) = duration_str.parse::() { metadata.duration = Some(d); } if let Some(stream) = info.streams.into_iter().find(|s| s.codec_type.as_deref() == Some("video")) { metadata.codec = stream.codec_name; if let Some(bit_rate_str) = stream.bit_rate.or(info.format.bit_rate) && let Ok(b) = bit_rate_str.parse::() { metadata.bitrate = Some(b); } if let Some(w) = stream.width && w >= 0 { if w > VIDEO_RESOLUTION_LIMIT as i64 { return Err(flc!("core_video_width_exceeds_limit", width = w, limit = VIDEO_RESOLUTION_LIMIT)); } metadata.width = Some(w as u32); } if let Some(h) = stream.height && h >= 0 { if h > VIDEO_RESOLUTION_LIMIT as i64 { return Err(flc!("core_video_height_exceeds_limit", height = h, limit = VIDEO_RESOLUTION_LIMIT)); } metadata.height = Some(h as u32); } let fps_opt = if !stream.avg_frame_rate.is_empty() && stream.avg_frame_rate != "0/0" { Some(stream.avg_frame_rate) } else if !stream.r_frame_rate.is_empty() && stream.r_frame_rate != "0/0" { Some(stream.r_frame_rate) } else { None }; if let Some(fps_str) = fps_opt { let fps_val = if fps_str.contains('/') { let mut parts = fps_str.splitn(2, '/'); if let (Some(n), Some(d)) = (parts.next(), parts.next()) { if let (Ok(nv), Ok(dv)) = (n.parse::(), d.parse::()) { if dv != 0.0 { Some(nv / dv) } else { None } } else { None } } else { None } } else { fps_str.parse::().ok() }; if let Some(fps_v) = fps_val { metadata.fps = Some(fps_v); } } } Ok(metadata) } } pub(crate) fn extract_frame_ffmpeg(video_path: &Path, timestamp: f32, max_values: Option<(u32, u32)>) -> Result { // This function returns strange status 234, when path contains non default UTF-8 characters, not sure why if !video_path.exists() { return Err(flc!("core_video_file_does_not_exist", path = video_path.to_string_lossy())); } let mut command = Command::new("ffmpeg"); let command_mut = &mut command; disable_windows_console_window(command_mut); command_mut.arg("-threads").arg("1").arg("-ss").arg(timestamp.to_string()).arg("-i").arg(video_path); if let Some((max_width, max_height)) = max_values { let vf_filter = format!("scale='min({max_width},iw)':'min({max_height},ih)':force_original_aspect_ratio=decrease"); command_mut.arg("-vf").arg(&vf_filter); } let output = command_mut .arg("-vframes") .arg("1") .arg("-f") .arg("image2pipe") .arg("-vcodec") .arg("png") .arg("pipe:1") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .output() .map_err(|e| flc!("core_failed_to_execute_ffmpeg", reason = e.to_string()))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).replace("\r\n", "\n").replace("\n", " "); return Err(flc!( "core_ffmpeg_failed_with_status", status = output.status.to_string(), stderr = stderr, command = format!("{:?}", command) )); } let img = image::load_from_memory(&output.stdout).map_err(|e| flc!("core_failed_to_load_image_frame", reason = e.to_string()))?; Ok(img.into_rgb8()) } pub fn generate_thumbnail( stop_flag: &Arc, video_path: &Path, size: u64, modified_date: u64, duration: Option, thumbnails_dir: &Path, thumbnail_video_percentage_from_start: u8, generate_grid_instead_of_single: bool, thumbnail_grid_tiles_per_side: u8, generate_thumbnails: bool, ) -> Result, String> { let mut hasher = Hasher::new(); if generate_grid_instead_of_single { hasher.update(format!("{size}___{modified_date}___{}___GRID_{thumbnail_grid_tiles_per_side}", video_path.to_string_lossy()).as_bytes()); } else { hasher.update( format!( "{thumbnail_video_percentage_from_start}___{size}___{modified_date}___{}___SINGLE", video_path.to_string_lossy() ) .as_bytes(), ); } let hash = hasher.finalize(); let thumbnail_filename = format!("{}.jpg", hash.to_hex()); let thumbnail_path = thumbnails_dir.join(thumbnail_filename); if thumbnail_path.exists() { let _ = filetime::set_file_mtime(&thumbnail_path, filetime::FileTime::now()); return Ok(Some(thumbnail_path)); } if !generate_thumbnails { return Ok(None); } let seek_time = duration.map_or(5.0, |d| d * (thumbnail_video_percentage_from_start as f64) / 100.0); 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); let max_height = 1080 / thumbnail_grid_tiles_per_side as u32; let max_width = 1920 / thumbnail_grid_tiles_per_side as u32; if generate_grid_instead_of_single { let frame_times = (0..(thumbnail_grid_tiles_per_side * thumbnail_grid_tiles_per_side)) .map(|i| duration_per_tile_items as f32 * (i + 1) as f32) .collect::>(); let mut imgs = Vec::new(); for ft in frame_times { if check_if_stop_received(stop_flag) { return Err(flc!("core_thumbnail_generation_stopped_by_user")); } match extract_frame_ffmpeg(video_path, ft, Some((max_width, max_height))) { Ok(img) => imgs.push(img), Err(e) => { let _ = fs::write(&thumbnail_path, b""); return Err(flc!("core_failed_to_extract_frame", time = ft, file = video_path.to_string_lossy(), reason = e)); } } } assert_eq!(imgs.len(), (thumbnail_grid_tiles_per_side * thumbnail_grid_tiles_per_side) as usize); let first_img = &imgs.first().expect("Cannot be empty here, because at least tiles_size^2 images are extracted"); if imgs.iter().any(|img| img.height() != first_img.height() || img.width() != first_img.width()) { let _ = fs::write(&thumbnail_path, b""); return Err(flc!("core_failed_to_generate_thumbnail_frames_different_dimensions", file = video_path.to_string_lossy())); } let mut new_thumbnail = RgbImage::new( first_img.width() * thumbnail_grid_tiles_per_side as u32, first_img.height() * thumbnail_grid_tiles_per_side as u32, ); for (idx, img) in imgs.iter().enumerate() { let x = (idx % thumbnail_grid_tiles_per_side as usize) as u32 * img.width(); let y = (idx / thumbnail_grid_tiles_per_side as usize) as u32 * img.height(); new_thumbnail .copy_from(img, x, y) .map_err(|e| flc!("core_failed_to_generate_thumbnail", file = video_path.to_string_lossy(), reason = e.to_string()))?; } if let Err(e) = new_thumbnail.save(&thumbnail_path) { let _ = fs::write(&thumbnail_path, b""); return Err(flc!("core_failed_to_save_thumbnail", file = video_path.to_string_lossy(), reason = e.to_string())); } } else { match extract_frame_ffmpeg(video_path, seek_time as f32, Some((max_width, max_height))) { Ok(img) => { if let Err(e) = img.save(&thumbnail_path) { let _ = fs::write(&thumbnail_path, b""); return Err(flc!("core_failed_to_save_thumbnail", file = video_path.to_string_lossy(), reason = e.to_string())); } } Err(e) => { let _ = fs::write(&thumbnail_path, b""); return Err(flc!( "core_failed_to_extract_frame_at_seek_time", time = seek_time, file = video_path.to_string_lossy(), reason = e )); } } } Ok(Some(thumbnail_path)) } ================================================ FILE: czkawka_core/src/helpers/audio_checker.rs ================================================ use std::fs::File; use std::io; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::errors::Error; use symphonia::core::errors::Error::IoError; use symphonia::core::io::MediaSourceStream; pub fn parse_audio_file(file_handler: File) -> Result<(), Error> { let mss = MediaSourceStream::new(Box::new(file_handler), Default::default()); let Ok(probed) = symphonia::default::get_probe().format(&Default::default(), mss, &Default::default(), &Default::default()) else { return Err(Error::Unsupported("probe info not available/file not recognized")); }; let mut format = probed.format; let Some(track) = format.tracks().iter().find(|t| t.codec_params.codec != CODEC_TYPE_NULL) else { return Err(Error::Unsupported("not supported audio track")); }; let Ok(mut decoder) = symphonia::default::get_codecs().make(&track.codec_params, &Default::default()) else { return Err(Error::Unsupported("not supported codec")); }; loop { let packet = match format.next_packet() { Ok(packet) => packet, Err(Error::ResetRequired) => { return Err(Error::ResetRequired); } Err(err) => { if let IoError(ref er) = err { // Catch eof, not sure how to do it properly if er.kind() == io::ErrorKind::UnexpectedEof { return Ok(()); } } return Err(err); } }; decoder.decode(&packet)?; } } ================================================ FILE: czkawka_core/src/helpers/debug_timer.rs ================================================ use std::time::{Duration, Instant}; /// Timer for measuring elapsed time between checkpoints. /// /// # How to use - examples /// /// Basic usage: /// ``` /// use czkawka_core::helpers::debug_timer::Timer; /// use std::thread::sleep; /// use std::time::Duration; /// /// let mut timer = Timer::new("MyTimer"); /// sleep(Duration::from_millis(50)); /// timer.checkpoint("step1"); /// sleep(Duration::from_millis(30)); /// timer.checkpoint("step2"); /// let report = timer.report("all_steps", false); /// println!("{}", report); /// ``` /// /// Output example: /// ```text /// MyTimer - step1: 50.0ms, /// MyTimer - step2: 30.0ms, /// MyTimer - all_steps: 80.0ms /// ``` /// /// One-line output: /// ``` /// use czkawka_core::helpers::debug_timer::Timer; /// use std::thread::sleep; /// use std::time::Duration; /// /// let mut timer = Timer::new("MyTimer"); /// sleep(Duration::from_millis(10)); /// timer.checkpoint("a"); /// sleep(Duration::from_millis(20)); /// timer.checkpoint("b"); /// let report = timer.report("total", true); /// println!("{}", report); /// ``` /// /// Output example: /// ```text /// MyTimer - a: 10.0ms, b: 20.0ms, total: 30.0ms /// ``` pub struct Timer { /// Name or label for the timer. base: String, /// Time when the timer was started. start_time: Instant, /// Time of the last checkpoint. last_time: Instant, /// List of (checkpoint name, duration since last checkpoint). times: Vec<(String, Duration)>, } impl Timer { /// Creates a new timer with a given label. pub fn new(base: &str) -> Self { Self { base: base.to_string(), start_time: Instant::now(), last_time: Instant::now(), times: Vec::new(), } } /// Records a checkpoint with the given name. pub fn checkpoint(&mut self, name: &str) { let elapsed = self.last_time.elapsed(); self.times.push((name.to_string(), elapsed)); self.last_time = Instant::now(); } /// Returns a formatted report of all checkpoints and total time. /// /// If `in_one_line` is true, outputs all checkpoints in a single line. /// Otherwise, outputs each checkpoint on a separate line. pub fn report(&mut self, all_steps_name: &str, in_one_line: bool) -> String { let all_elapsed = self.start_time.elapsed(); self.times.push((all_steps_name.to_string(), all_elapsed)); if in_one_line { let times = self.times.iter().map(|(name, time)| format!("{name}: {time:?}")).collect::>().join(", "); format!("{} - {}", self.base, times) } else { self.times .iter() .map(|(name, time)| format!("{} - {name}: {time:?}", self.base)) .collect::>() .join(", \n") } } } #[cfg(test)] mod tests { use std::thread::sleep; use super::*; #[test] fn test_timer_basic_functionality() { let mut timer = Timer::new("TestTimer"); assert_eq!(timer.base, "TestTimer"); assert_eq!(timer.times.len(), 0); sleep(Duration::from_millis(10)); timer.checkpoint("step1"); assert_eq!(timer.times.len(), 1); assert_eq!(timer.times[0].0, "step1"); sleep(Duration::from_millis(10)); timer.checkpoint("step2"); assert_eq!(timer.times.len(), 2); assert_eq!(timer.times[1].0, "step2"); } #[test] fn test_timer_report_multiline() { let mut timer = Timer::new("MultilineTimer"); sleep(Duration::from_millis(5)); timer.checkpoint("checkpoint1"); sleep(Duration::from_millis(5)); timer.checkpoint("checkpoint2"); let report = timer.report("total", false); assert!(report.contains("MultilineTimer - checkpoint1:")); assert!(report.contains("MultilineTimer - checkpoint2:")); assert!(report.contains("MultilineTimer - total:")); assert!(report.contains(", \n")); } #[test] fn test_timer_report_oneline() { let mut timer = Timer::new("OnelineTimer"); sleep(Duration::from_millis(5)); timer.checkpoint("a"); sleep(Duration::from_millis(5)); timer.checkpoint("b"); let report = timer.report("final", true); assert!(report.starts_with("OnelineTimer - ")); assert!(report.contains("a:")); assert!(report.contains("b:")); assert!(report.contains("final:")); assert!(report.contains(", ")); assert!(!report.contains("\n")); } #[test] fn test_timer_no_checkpoints() { let mut timer = Timer::new("EmptyTimer"); let report = timer.report("done", false); assert!(report.contains("EmptyTimer - done:")); assert_eq!(report.matches('\n').count(), 0); } #[test] fn test_timer_elapsed_time_accumulates() { let mut timer = Timer::new("AccumulateTimer"); sleep(Duration::from_millis(20)); timer.checkpoint("step1"); assert!(timer.times[0].1.as_millis() >= 15); sleep(Duration::from_millis(20)); timer.checkpoint("step2"); assert!(timer.times[1].1.as_millis() >= 15); } } ================================================ FILE: czkawka_core/src/helpers/delayed_sender.rs ================================================ //! DelayedSender: A utility for batching or throttling messages sent between threads. use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; /// A sender that delays sending values until a specified wait time has passed since the last sent value. /// /// This is useful for batching updates or reducing the frequency of sending messages in a multi-threaded environment. /// Note: Using mutexes in the send function from multiple threads can lead to performance issues (waiting for mutex release), /// but for now, the performance impact is minimal. In the future, a more efficient channel could be used. pub struct DelayedSender { slot: Arc>>, stop_flag: Arc, } impl DelayedSender { /// Creates a new DelayedSender. /// /// # Arguments /// * `sender` - The channel sender to forward values to. /// * `wait_time` - The minimum duration to wait between sends. pub fn new(sender: crossbeam_channel::Sender, wait_time: Duration) -> Self { let slot = Arc::new(Mutex::new(None)); let slot_clone = Arc::clone(&slot); let stop_flag = Arc::new(AtomicBool::new(false)); let stop_flag_clone = Arc::clone(&stop_flag); let _join = thread::spawn(move || { let mut last_send_time: Option = None; let duration_between_checks = Duration::from_secs_f64(wait_time.as_secs_f64() / 5.0); loop { if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { break; } if let Some(last_send_time) = last_send_time && last_send_time.elapsed() < wait_time { thread::sleep(duration_between_checks); continue; } let Some(value) = slot_clone.lock().expect("Failed to lock slot in DelayedSender").take() else { thread::sleep(duration_between_checks); continue; }; if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { break; } if let Err(e) = sender.send(value) { log::error!("Failed to send value: {e:?}"); } last_send_time = Some(Instant::now()); } }); Self { slot, stop_flag } } /// Sends a value, replacing any previous value that has not yet been sent. pub fn send(&self, value: T) { let mut slot = self.slot.lock().expect("Failed to lock slot in DelayedSender"); *slot = Some(value); } } impl Drop for DelayedSender { fn drop(&mut self) { // After dropping DelayedSender, no more values will be sent. // Previously, some values were cached and sent after later operations. self.stop_flag.store(true, std::sync::atomic::Ordering::Relaxed); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_delayed_sender_basic_send() { let (sender, receiver) = crossbeam_channel::unbounded(); let delayed_sender = DelayedSender::new(sender, Duration::from_millis(50)); delayed_sender.send(42); thread::sleep(Duration::from_millis(100)); let result = receiver.try_recv(); result.unwrap(); assert_eq!(result.unwrap(), 42); } #[test] fn test_delayed_sender_batching() { let (sender, receiver) = crossbeam_channel::unbounded(); let delayed_sender = DelayedSender::new(sender, Duration::from_millis(100)); delayed_sender.send(1); thread::sleep(Duration::from_millis(50)); let first = receiver.try_recv(); first.unwrap(); assert_eq!(first.unwrap(), 1); delayed_sender.send(2); thread::sleep(Duration::from_millis(10)); delayed_sender.send(3); thread::sleep(Duration::from_millis(10)); delayed_sender.send(4); thread::sleep(Duration::from_millis(150)); let result = receiver.try_recv(); result.unwrap(); assert_eq!(result.unwrap(), 4); let result2 = receiver.try_recv(); result2.unwrap_err(); } #[test] fn test_delayed_sender_multiple_sends() { let (sender, receiver) = crossbeam_channel::unbounded(); let delayed_sender = DelayedSender::new(sender, Duration::from_millis(50)); delayed_sender.send(10); thread::sleep(Duration::from_millis(100)); delayed_sender.send(20); thread::sleep(Duration::from_millis(100)); let first = receiver.try_recv(); first.unwrap(); assert_eq!(first.unwrap(), 10); let second = receiver.try_recv(); second.unwrap(); assert_eq!(second.unwrap(), 20); } #[test] fn test_delayed_sender_drop_stops_thread() { let (sender, receiver) = crossbeam_channel::unbounded(); { let delayed_sender = DelayedSender::new(sender, Duration::from_millis(50)); delayed_sender.send(100); } thread::sleep(Duration::from_millis(150)); let count = receiver.try_iter().count(); assert!(count <= 1); } #[test] fn test_delayed_sender_no_send_without_wait() { let (sender, receiver) = crossbeam_channel::unbounded(); let delayed_sender = DelayedSender::new(sender, Duration::from_millis(100)); delayed_sender.send(5); thread::sleep(Duration::from_millis(20)); let first = receiver.try_recv(); first.unwrap(); assert_eq!(first.unwrap(), 5); delayed_sender.send(10); thread::sleep(Duration::from_millis(20)); let result = receiver.try_recv(); result.unwrap_err(); // But should be sent after full wait_time thread::sleep(Duration::from_millis(100)); let result = receiver.try_recv(); result.unwrap(); assert_eq!(result.unwrap(), 10); } } ================================================ FILE: czkawka_core/src/helpers/ffprobe.rs ================================================ //! Simple wrapper for the [ffprobe](https://ffmpeg.org/ffprobe.html) CLI utility, //! which is part of the ffmpeg tool suite. //! //! This crate allows retrieving typed information about media files (images and videos) //! by invoking `ffprobe` with JSON output options and deserializing the data //! into convenient Rust types. //! //! //! //! ```rust, no_run //! use czkawka_core::helpers::ffprobe::ffprobe; //! match ffprobe("path/to/video.mp4") { //! Ok(info) => { //! dbg!(info); //! }, //! Err(err) => { //! eprintln!("Could not analyze file with ffprobe: {:?}", err); //! }, //! } //! ``` //! //! CODE IS COPIED FROM https://github.com/theduke/ffprobe-rs //! I WILL BE ABLE TO AGAIN USE IT AFTER A NEW VERSION IS RELEASED //! https://github.com/theduke/ffprobe-rs/issues/33 //! LICENSE: MIT #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; /// Execute ffprobe with default settings and return the extracted data. /// /// See [`ffprobe_config`] if you need to customize settings. pub fn ffprobe(path: impl AsRef) -> Result { ffprobe_config( Config { count_frames: false, ffprobe_bin: "ffprobe".into(), }, path, ) } /// Run ffprobe with a custom config. /// See [`ConfigBuilder`] for more details. pub fn ffprobe_config(config: Config, path: impl AsRef) -> Result { let path = path.as_ref(); let mut cmd = std::process::Command::new(config.ffprobe_bin); // Default args. cmd.args(["-v", "error", "-show_format", "-show_streams", "-print_format", "json"]); if config.count_frames { cmd.arg("-count_frames"); } cmd.arg(path); // Prevent CMD popup on Windows. #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); let out = cmd.output().map_err(FfProbeError::Io)?; if !out.status.success() { return Err(FfProbeError::Status(out)); } serde_json::from_slice::(&out.stdout).map_err(FfProbeError::Deserialize) } /// ffprobe configuration. /// /// Use [`Config::builder`] for constructing a new config. #[derive(Clone, Debug)] pub struct Config { count_frames: bool, ffprobe_bin: std::path::PathBuf, } impl Config { /// Construct a new ConfigBuilder. pub fn builder() -> ConfigBuilder { ConfigBuilder::new() } } /// Build the ffprobe configuration. pub struct ConfigBuilder { config: Config, } impl ConfigBuilder { pub fn new() -> Self { Self { config: Config { count_frames: false, ffprobe_bin: "ffprobe".into(), }, } } /// Enable the -count_frames setting. /// Will fully decode the file and count the frames. /// Frame count will be available in [`Stream::nb_read_frames`]. pub fn count_frames(mut self, count_frames: bool) -> Self { self.config.count_frames = count_frames; self } /// Specify which binary name (e.g. `"ffprobe-6"`) or path (e.g. `"/opt/bin/ffprobe"`) to use /// for executing `ffprobe`. pub fn ffprobe_bin(mut self, ffprobe_bin: impl AsRef) -> Self { self.config.ffprobe_bin = ffprobe_bin.as_ref().to_path_buf(); self } /// Finalize the builder into a [`Config`]. pub fn build(self) -> Config { self.config } /// Run ffprobe with the config produced by this builder. pub fn run(self, path: impl AsRef) -> Result { ffprobe_config(self.config, path) } } impl Default for ConfigBuilder { fn default() -> Self { Self::new() } } #[derive(Debug)] #[non_exhaustive] pub enum FfProbeError { Io(std::io::Error), Status(std::process::Output), Deserialize(serde_json::Error), } impl std::fmt::Display for FfProbeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Io(e) => e.fmt(f), Self::Status(o) => { write!(f, "ffprobe exited with status code {}: {}", o.status, String::from_utf8_lossy(&o.stderr)) } Self::Deserialize(e) => e.fmt(f), } } } impl std::error::Error for FfProbeError {} #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FfProbe { pub streams: Vec, pub format: Format, } #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Stream { pub index: i64, pub codec_name: Option, pub sample_aspect_ratio: Option, pub display_aspect_ratio: Option, pub color_range: Option, pub color_space: Option, pub bits_per_raw_sample: Option, pub channel_layout: Option, pub max_bit_rate: Option, pub nb_frames: Option, /// Number of frames seen by the decoder. /// Requires full decoding and is only available if the 'count_frames' /// setting was enabled. pub nb_read_frames: Option, pub codec_long_name: Option, pub codec_type: Option, pub codec_time_base: Option, pub codec_tag_string: String, pub codec_tag: String, pub sample_fmt: Option, pub sample_rate: Option, pub channels: Option, pub bits_per_sample: Option, pub r_frame_rate: String, pub avg_frame_rate: String, pub time_base: String, pub start_pts: Option, pub start_time: Option, pub duration_ts: Option, pub duration: Option, pub bit_rate: Option, pub disposition: Disposition, pub tags: Option, pub profile: Option, pub width: Option, pub height: Option, pub coded_width: Option, pub coded_height: Option, pub closed_captions: Option, pub has_b_frames: Option, pub pix_fmt: Option, pub level: Option, pub chroma_location: Option, pub refs: Option, pub is_avc: Option, pub nal_length: Option, pub nal_length_size: Option, pub field_order: Option, pub id: Option, #[serde(default)] pub side_data_list: Vec, } #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] // Allowed to prevent having to break compatibility of float fields are added. #[expect(clippy::derive_partial_eq_without_eq)] pub struct SideData { pub side_data_type: String, pub rotation: Option, } #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] // Allowed to prevent having to break compatibility of float fields are added. #[expect(clippy::derive_partial_eq_without_eq)] pub struct Disposition { pub default: i64, pub dub: i64, pub original: i64, pub comment: i64, pub lyrics: i64, pub karaoke: i64, pub forced: i64, pub hearing_impaired: i64, pub visual_impaired: i64, pub clean_effects: i64, pub attached_pic: i64, pub timed_thumbnails: i64, } #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] // Allowed to prevent having to break compatibility of float fields are added. #[expect(clippy::derive_partial_eq_without_eq)] pub struct StreamTags { pub language: Option, pub creation_time: Option, pub handler_name: Option, pub encoder: Option, pub timecode: Option, pub reel_name: Option, } #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Format { pub filename: String, pub nb_streams: i64, pub nb_programs: i64, pub format_name: String, pub format_long_name: Option, pub start_time: Option, pub duration: Option, pub size: Option, pub bit_rate: Option, pub probe_score: i64, pub tags: Option, } impl Format { /// Get the duration parsed into a [`std::time::Duration`]. pub fn try_get_duration(&self) -> Option> { self.duration.as_ref().map(|duration| match duration.parse::() { Ok(num) => Ok(std::time::Duration::from_secs_f64(num)), Err(error) => Err(error), }) } /// Get the duration parsed into a [`std::time::Duration`]. /// /// Will return [`None`] if no duration is available, or if parsing fails. /// See [`Self::try_get_duration`] for a method that returns an error. pub fn get_duration(&self) -> Option { self.try_get_duration()?.ok() } } #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FormatTags { #[serde(rename = "WMFSDKNeeded")] pub wmfsdkneeded: Option, #[serde(rename = "DeviceConformanceTemplate")] pub device_conformance_template: Option, #[serde(rename = "WMFSDKVersion")] pub wmfsdkversion: Option, #[serde(rename = "IsVBR")] pub is_vbr: Option, pub major_brand: Option, pub minor_version: Option, pub compatible_brands: Option, pub creation_time: Option, pub encoder: Option, #[serde(flatten)] pub extra: std::collections::HashMap, } ================================================ FILE: czkawka_core/src/helpers/messages.rs ================================================ //! Messages: Utility for collecting and printing messages, warnings, and errors. use crate::flc; /// Stores messages, warnings, and errors for reporting. #[derive(Debug, Default, Clone)] pub struct Messages { pub critical: Option, /// Informational messages. pub messages: Vec, /// Warning messages. pub warnings: Vec, /// Error messages. pub errors: Vec, } #[derive(Debug, Clone, Copy)] pub enum MessageLimit { NoLimit, Characters(usize), Lines(usize), } impl Messages { /// Creates a new, empty `Messages` struct. pub fn new() -> Self { Default::default() } /// Creates a new `Messages` struct with errors. pub fn new_from_errors(errors: Vec) -> Self { Self { errors, ..Default::default() } } /// Creates a new `Messages` struct with warnings. pub fn new_from_warnings(warnings: Vec) -> Self { Self { warnings, ..Default::default() } } /// Creates a new `Messages` struct with messages. pub fn new_from_messages(messages: Vec) -> Self { Self { messages, ..Default::default() } } /// Prints all messages, warnings, and errors to the provided writer. pub fn print_messages_to_writer(&self, writer: &mut T) -> std::io::Result<()> { let text = self.create_messages_text(MessageLimit::NoLimit); writer.write_all(text.as_bytes()) } /// Creates a formatted string containing all messages, warnings, and errors. pub fn create_messages_text(&self, limit: MessageLimit) -> String { let mut text_to_return: String = String::new(); if let Some(critical) = &self.critical { text_to_return += "------------------------------CRITICAL ERROR---------------------------\n"; text_to_return += critical; text_to_return += "\n"; text_to_return += "--------------------------END OF CRITICAL ERROR------------------------\n"; } if !self.errors.is_empty() { text_to_return += "--------------------------------ERRORS---------------------------------\n"; for i in &self.errors { text_to_return += i; text_to_return += "\n"; } text_to_return += "----------------------------END OF ERRORS------------------------------\n"; } if !self.messages.is_empty() { text_to_return += "-------------------------------MESSAGES--------------------------------\n"; for i in &self.messages { text_to_return += i; text_to_return += "\n"; } text_to_return += "---------------------------END OF MESSAGES-----------------------------\n"; } if !self.warnings.is_empty() { text_to_return += "-------------------------------WARNINGS--------------------------------\n"; for i in &self.warnings { text_to_return += i; text_to_return += "\n"; } text_to_return += "---------------------------END OF WARNINGS-----------------------------\n"; } let mut text_to_return = text_to_return.trim().to_string(); match limit { MessageLimit::NoLimit => {} MessageLimit::Characters(max_chars) => { let char_count = text_to_return.chars().count(); if char_count > max_chars { let truncated: String = text_to_return.chars().take(max_chars).collect(); text_to_return = truncated; text_to_return += "\n\n"; text_to_return += &flc!("core_messages_limit_reached_characters", current = char_count, limit = max_chars); text_to_return += "\n"; } } MessageLimit::Lines(max_lines) => { let line_count = text_to_return.lines().count(); if line_count > max_lines { let lines: Vec<&str> = text_to_return.lines().take(max_lines).collect(); text_to_return = lines.join("\n"); text_to_return += "\n\n"; text_to_return += &flc!("core_messages_limit_reached_lines", current = line_count, limit = max_lines); text_to_return += "\n"; } } } text_to_return } /// Extends this `Messages` struct with another, appending all messages, warnings, and errors. pub fn extend_with_another_messages(&mut self, messages: Self) { let (messages, warnings, errors, critical) = (messages.messages, messages.warnings, messages.errors, messages.critical); self.messages.extend(messages); self.warnings.extend(warnings); self.errors.extend(errors); if critical.is_some() { self.critical = critical; } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_messages_constructors_and_text_formatting() { // Test new() let msg = Messages::new(); assert!(msg.messages.is_empty()); assert!(msg.warnings.is_empty()); assert!(msg.errors.is_empty()); assert_eq!(msg.create_messages_text(MessageLimit::NoLimit), ""); // Test new_from_errors() let errors = vec!["Error 1".to_string(), "Error 2".to_string()]; let msg = Messages::new_from_errors(errors.clone()); assert_eq!(msg.errors, errors); let text = msg.create_messages_text(MessageLimit::NoLimit); assert!(text.contains("ERRORS")); assert!(text.contains("Error 1")); // Test new_from_warnings() let warnings = vec!["Warning 1".to_string()]; let msg = Messages::new_from_warnings(warnings.clone()); assert_eq!(msg.warnings, warnings); let text = msg.create_messages_text(MessageLimit::NoLimit); assert!(text.contains("WARNINGS")); // Test new_from_messages() let messages = vec!["Message 1".to_string()]; let msg = Messages::new_from_messages(messages.clone()); assert_eq!(msg.messages, messages); let text = msg.create_messages_text(MessageLimit::NoLimit); assert!(text.contains("MESSAGES")); // Test all types together let mut msg = Messages::new(); msg.messages.push("Info".to_string()); msg.warnings.push("Warn".to_string()); msg.errors.push("Err".to_string()); let text = msg.create_messages_text(MessageLimit::NoLimit); assert!(text.contains("MESSAGES")); assert!(text.contains("Info")); assert!(text.contains("WARNINGS")); assert!(text.contains("Warn")); assert!(text.contains("ERRORS")); assert!(text.contains("Err")); } #[test] fn test_extend_and_writer() { // Test extend_with_another_messages() let mut msg1 = Messages::new(); msg1.messages.push("Msg1".to_string()); msg1.warnings.push("Warn1".to_string()); msg1.errors.push("Err1".to_string()); let mut msg2 = Messages::new(); msg2.messages.push("Msg2".to_string()); msg2.warnings.push("Warn2".to_string()); msg2.errors.push("Err2".to_string()); msg1.extend_with_another_messages(msg2); assert_eq!(msg1.messages.len(), 2); assert_eq!(msg1.warnings.len(), 2); assert_eq!(msg1.errors.len(), 2); assert!(msg1.messages.contains(&"Msg1".to_string())); assert!(msg1.messages.contains(&"Msg2".to_string())); // Test print_messages_to_writer() let mut buffer = Vec::new(); let result = msg1.print_messages_to_writer(&mut buffer); result.unwrap(); let output = String::from_utf8(buffer).unwrap(); assert!(output.contains("Msg1")); assert!(output.contains("Warn2")); assert!(output.contains("Err1")); } } ================================================ FILE: czkawka_core/src/helpers/mod.rs ================================================ //! Helper modules: generic utilities, traits, structs, ready to copy/paste to other projects. pub mod audio_checker; pub mod debug_timer; pub mod delayed_sender; pub mod ffprobe; pub mod messages; ================================================ FILE: czkawka_core/src/lib.rs ================================================ pub mod common; pub mod helpers; pub mod localizer_core; pub mod tools; pub mod re_exported { pub use fast_image_resize::FilterType as FirFilterType; pub use image_hasher::{FilterType, HashAlg}; pub use vid_dup_finder_lib::Cropdetect; } pub const CZKAWKA_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const TOOLS_NUMBER: usize = 14; ================================================ FILE: czkawka_core/src/localizer_core.rs ================================================ use std::collections::HashMap; use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader}; use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer}; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; pub static LANGUAGE_LOADER_CORE: std::sync::LazyLock = std::sync::LazyLock::new(|| { let loader: FluentLanguageLoader = fluent_language_loader!(); loader.load_fallback_language(&Localizations).expect("Error while loading fallback language"); loader }); #[macro_export] macro_rules! flc { ( $($tt:tt)* ) => {{ i18n_embed_fl::fl!($crate::localizer_core::LANGUAGE_LOADER_CORE, $($tt)*) }}; } pub fn localizer_core() -> Box { Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_CORE, &Localizations)) } pub fn generate_translation_hashmap(vec: Vec<(&'static str, String)>) -> HashMap<&'static str, String> { let mut hashmap: HashMap<&'static str, String> = Default::default(); for (key, value) in vec { hashmap.insert(key, value); } hashmap } pub fn fnc_get_similarity_very_high() -> String { flc!("core_similarity_very_high") } pub fn fnc_get_similarity_minimal() -> String { flc!("core_similarity_minimal") } ================================================ FILE: czkawka_core/src/tools/bad_extensions/core.rs ================================================ use std::collections::BTreeSet; use std::mem; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crossbeam_channel::Sender; use fun_time::fun_time; use indexmap::IndexMap; use log::debug; use mime_guess::get_mime_extensions; use rayon::prelude::*; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{FileEntry, ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::bad_extensions::workarounds::{DISABLED_EXTENSIONS, WORKAROUNDS}; use crate::tools::bad_extensions::{BadExtensions, BadExtensionsParameters, BadFileEntry, Info}; // Text longer than 10 characters is not considered as extension const MAX_EXTENSION_LENGTH: usize = 10; impl BadExtensions { pub fn new(params: BadExtensionsParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BadExtensions), information: Info::default(), files_to_check: Default::default(), bad_extensions_files: Default::default(), params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.files_to_check = grouped_file_entries.into_values().flatten().collect(); self.common_data.text_messages.warnings.extend(warnings); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "look_for_bad_extensions_files", level = "debug")] pub(crate) fn look_for_bad_extensions_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.files_to_check.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::BadExtensionsChecking, self.files_to_check.len(), self.get_test_type(), 0); let files_to_check = mem::take(&mut self.files_to_check); let mut workarounds: IndexMap<&str, Vec<&str>> = Default::default(); for (proper, found) in WORKAROUNDS { workarounds.entry(found).or_default().push(proper); } self.bad_extensions_files = self.verify_extensions(files_to_check, progress_handler.items_counter(), stop_flag, &workarounds); progress_handler.join_thread(); // Break if stop was clicked if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } self.information.number_of_files_with_bad_extension = self.bad_extensions_files.len(); debug!("Found {} files with invalid extension.", self.information.number_of_files_with_bad_extension); WorkContinueStatus::Continue } fn verify_extension_of_file(&self, file_entry: FileEntry, workarounds: &IndexMap<&str, Vec<&str>>) -> Option { // Check what exactly content file contains let kind = match infer::get_from_path(&file_entry.path) { Ok(k) => k?, Err(_) => return None, }; let proper_extension = kind.extension(); let current_extension = Self::get_and_validate_extension(&file_entry, proper_extension)?; // Check for all extensions that file can use(not sure if it is worth to do it) let (mut all_available_extensions, valid_extensions) = Self::check_for_all_extensions_that_file_can_use(workarounds, ¤t_extension, proper_extension); if all_available_extensions.is_empty() { // Not found any extension return None; } else if current_extension.is_empty() { if !self.params.include_files_without_extension { return None; } } else if all_available_extensions.take(¤t_extension).is_some() { // Found proper extension return None; } Some(BadFileEntry { path: file_entry.path, modified_date: file_entry.modified_date, size: file_entry.size, current_extension, proper_extensions_group: valid_extensions, proper_extension: proper_extension.to_string(), }) } #[fun_time(message = "verify_extensions", level = "debug")] fn verify_extensions( &self, files_to_check: Vec, items_counter: &Arc, stop_flag: &Arc, workarounds: &IndexMap<&str, Vec<&str>>, ) -> Vec { files_to_check .into_par_iter() .map(|file_entry| { if check_if_stop_received(stop_flag) { return None; } let res = self.verify_extension_of_file(file_entry, workarounds); items_counter.fetch_add(1, Ordering::Relaxed); Some(res) }) .while_some() .flatten() .collect::>() } fn get_and_validate_extension(file_entry: &FileEntry, proper_extension: &str) -> Option { let current_extension; // Extract current extension from file if let Some(extension) = file_entry.path.extension() { let extension = extension.to_string_lossy().to_lowercase(); if DISABLED_EXTENSIONS.contains(&extension.as_str()) { return None; } if extension.len() > MAX_EXTENSION_LENGTH { current_extension = String::new(); } else { current_extension = extension; } } else { current_extension = String::new(); } // Already have proper extension, no need to do more things if current_extension == proper_extension { return None; } Some(current_extension) } fn check_for_all_extensions_that_file_can_use(workarounds: &IndexMap<&str, Vec<&str>>, current_extension: &str, proper_extension: &str) -> (BTreeSet, String) { let mut all_available_extensions: BTreeSet = Default::default(); for mim in mime_guess::from_ext(proper_extension) { if let Some(all_ext) = get_mime_extensions(&mim) { for ext in all_ext { all_available_extensions.insert((*ext).to_string()); } } } // Workarounds: if !current_extension.is_empty() && let Some(vec_pre) = workarounds.get(current_extension) { for pre in vec_pre { if all_available_extensions.contains(*pre) { all_available_extensions.insert(current_extension.to_string()); break; } } } let valid_extensions = if all_available_extensions.is_empty() { String::new() } else { let mut guessed_multiple_extensions = format!("({proper_extension}) - "); for ext in &all_available_extensions { guessed_multiple_extensions.push_str(ext); guessed_multiple_extensions.push(','); } guessed_multiple_extensions.pop(); guessed_multiple_extensions }; (all_available_extensions, valid_extensions) } #[fun_time(message = "fix_bad_extensions", level = "debug")] pub fn fix_bad_extensions(&mut self, _fix_params: super::BadExtensionsFixParams, stop_flag: &Arc) { let warnings: Vec<_> = mem::take(&mut self.bad_extensions_files) .into_par_iter() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } let new_path = entry.path.with_extension(&entry.proper_extension); if new_path.exists() { return Some(Some(format!("Cannot rename {:?} to {:?}: target file already exists", entry.path, new_path))); } match std::fs::rename(&entry.path, &new_path) { Ok(()) => Some(None), Err(e) => Some(Some(format!("Failed to rename {:?} to {:?}: {}", entry.path, new_path, e))), } }) .while_some() .flatten() .collect(); self.common_data.text_messages.warnings.extend(warnings); } } ================================================ FILE: czkawka_core/src/tools/bad_extensions/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; mod workarounds; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::Serialize; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; #[derive(Clone, Serialize, Debug)] pub struct BadFileEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub current_extension: String, pub proper_extensions_group: String, pub proper_extension: String, } impl ResultEntry for BadFileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_files_with_bad_extension: usize, pub scanning_time: Duration, } #[derive(Clone, Debug, Default, Copy)] pub struct BadExtensionsFixParams {} #[derive(Clone)] pub struct BadExtensionsParameters { pub include_files_without_extension: bool, } impl BadExtensionsParameters { pub fn new() -> Self { Self { include_files_without_extension: false, } } } impl Default for BadExtensionsParameters { fn default() -> Self { Self::new() } } pub struct BadExtensions { common_data: CommonToolData, information: Info, files_to_check: Vec, bad_extensions_files: Vec, params: BadExtensionsParameters, } impl BadExtensions { pub const fn get_bad_extensions_files(&self) -> &Vec { &self.bad_extensions_files } } ================================================ FILE: czkawka_core/src/tools/bad_extensions/tests.rs ================================================ use std::fs; use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::bad_extensions::{BadExtensions, BadExtensionsParameters}; #[test] fn test_find_bad_extension_png_as_jpg() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create a PNG file with .jpg extension let png_data = vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, // IHDR chunk ]; let mut file = fs::File::create(path.join("image.jpg")).unwrap(); file.write_all(&png_data).unwrap(); let params = BadExtensionsParameters::new(); let mut finder = BadExtensions::new(params); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let bad_files = finder.get_bad_extensions_files(); assert_eq!(bad_files.len(), 1, "Should find 1 file with bad extension"); assert_eq!(bad_files[0].current_extension, "jpg", "Current extension should be jpg"); assert_eq!(bad_files[0].proper_extension, "png", "Proper extension should be png"); } #[test] fn test_correct_extension() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create a PNG file with correct .png extension let png_data = vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, ]; let mut file = fs::File::create(path.join("image.png")).unwrap(); file.write_all(&png_data).unwrap(); let params = BadExtensionsParameters::new(); let mut finder = BadExtensions::new(params); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let bad_files = finder.get_bad_extensions_files(); assert_eq!(bad_files.len(), 0, "Should find no files with bad extension"); } #[test] fn test_file_without_extension_excluded() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create a PNG file without extension let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]; let mut file = fs::File::create(path.join("image_no_ext")).unwrap(); file.write_all(&png_data).unwrap(); let mut params = BadExtensionsParameters::new(); params.include_files_without_extension = false; let mut finder = BadExtensions::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let bad_files = finder.get_bad_extensions_files(); assert_eq!(bad_files.len(), 0, "Should not include files without extension when disabled"); } #[test] fn test_file_without_extension_included() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create a PNG file without extension let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]; let mut file = fs::File::create(path.join("image_no_ext")).unwrap(); file.write_all(&png_data).unwrap(); let mut params = BadExtensionsParameters::new(); params.include_files_without_extension = true; let mut finder = BadExtensions::new(params); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let bad_files = finder.get_bad_extensions_files(); assert_eq!(bad_files.len(), 1, "Should include files without extension when enabled"); assert_eq!(bad_files[0].current_extension, "", "Current extension should be empty"); assert_eq!(bad_files[0].proper_extension, "png"); } ================================================ FILE: czkawka_core/src/tools/bad_extensions/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search}; use crate::tools::bad_extensions::{BadExtensions, BadExtensionsFixParams, BadExtensionsParameters, Info}; impl AllTraits for BadExtensions {} impl Search for BadExtensions { #[fun_time(message = "find_bad_extensions_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.look_for_bad_extensions_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DeletingItems for BadExtensions { fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFolders(self.bad_extensions_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl FixingItems for BadExtensions { type FixParams = BadExtensionsFixParams; #[fun_time(message = "fix_items", level = "debug")] fn fix_items(&mut self, stop_flag: &Arc, _progress_sender: Option<&Sender>, fix_params: Self::FixParams) { self.fix_bad_extensions(fix_params, stop_flag); } } impl DebugPrint for BadExtensions { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for BadExtensions { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; writeln!(writer, "Found {} files with invalid extension.\n", self.information.number_of_files_with_bad_extension)?; for file_entry in &self.bad_extensions_files { writeln!(writer, "\"{}\" ----- {}", file_entry.path.to_string_lossy(), file_entry.proper_extensions_group)?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.bad_extensions_files, pretty_print) } } impl CommonData for BadExtensions { type Info = Info; type Parameters = BadExtensionsParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.get_information().number_of_files_with_bad_extension > 0 } } ================================================ FILE: czkawka_core/src/tools/bad_extensions/workarounds.rs ================================================ pub(crate) const DISABLED_EXTENSIONS: &[&str] = &["file", "cache", "bak", "data", "tmp"]; // Such files can have any type inside // This adds several workarounds for bugs/invalid recognizing types by external libraries // ("real_content_extension", "current_file_extension") pub(crate) const WORKAROUNDS: &[(&str, &str)] = &[ // Wine/Windows ("der", "cat"), ("exe", "acm"), ("exe", "ax"), ("exe", "bck"), ("exe", "com"), ("exe", "cpl"), ("exe", "dll16"), ("exe", "dll"), ("exe", "drv16"), ("exe", "drv"), ("exe", "ds"), ("exe", "efi"), ("exe", "exe16"), ("exe", "fon"), // Type of font or something else ("exe", "mod16"), ("exe", "msstyles"), ("exe", "mui"), ("exe", "mun"), ("exe", "orig"), ("exe", "ps1xml"), ("exe", "rll"), ("exe", "rs"), ("exe", "scr"), ("exe", "signed"), ("exe", "sys"), ("exe", "tlb"), ("exe", "tsp"), ("exe", "vdm"), ("exe", "vxd"), ("exe", "winmd"), ("gz", "loggz"), ("xml", "adml"), ("xml", "admx"), ("xml", "camp"), ("xml", "cdmp"), ("xml", "cdxml"), ("xml", "dgml"), ("xml", "diagpkg"), ("xml", "gmmp"), ("xml", "library-ms"), ("xml", "man"), ("xml", "manifest"), ("xml", "msc"), ("xml", "mum"), ("xml", "resx"), ("zip", "msix"), ("zip", "wmz"), // Games specific extensions - cannot be used here common extensions like zip ("gz", "h3m"), // Heroes 3 ("zip", "hashdb"), // Gog ("c2", "zip"), // King of the Dark Age ("c2", "bmp"), // King of the Dark Age ("c2", "avi"), // King of the Dark Age ("c2", "exe"), // King of the Dark Age // Raw images ("tif", "nef"), ("tif", "dng"), ("tif", "arw"), // Other ("der", "keystore"), // Godot/Android keystore ("exe", "pyd"), // Python/Mingw ("gz", "blend"), // Blender ("gz", "crate"), // Cargo ("gz", "svgz"), // Archive svg ("gz", "tgz"), // Archive ("heic", "heif"), // Image ("heif", "heic"), // Image ("html", "dtd"), // Mingw ("html", "ent"), // Mingw ("html", "md"), // Markdown ("html", "svelte"), // Svelte ("jpg", "jfif"), // Photo format ("m4v", "mp4"), // m4v and mp4 are interchangeable ("mobi", "azw3"), // Ebook format ("mpg", "vob"), // Weddings in parts have usually vob extension ("obj", "bin"), // Multiple apps, Czkawka, Nvidia, Windows ("obj", "o"), // Compilators ("odp", "otp"), // LibreOffice ("ods", "ots"), // Libreoffice ("odt", "ott"), // Libreoffice ("ogg", "ogv"), // Audio format ("pem", "key"), // curl, openssl ("png", "kpp"), // Krita presets ("pptx", "ppsx"), // Powerpoint ("sh", "bash"), // Linux ("sh", "guess"), // GNU ("sh", "lua"), // Lua ("sh", "js"), // Javascript ("sh", "pl"), // Gnome/Linux ("sh", "pm"), // Gnome/Linux ("sh", "py"), // Python ("sh", "pyx"), // Python ("sh", "rs"), // Rust ("sh", "sample"), // Git ("xml", "bsp"), // Quartus ("xml", "cbp"), // CodeBlocks config ("xml", "cfg"), // Multiple apps - Godot ("xml", "cmb"), // Cambalache ("xml", "conf"), // Multiple apps - Python ("xml", "config"), // Multiple apps - QT Creator ("xml", "dae"), // 3D models ("xml", "docbook"), // ("xml", "fb2"), // ("xml", "filters"), // Visual studio ("xml", "gir"), // GTK ("xml", "glade"), // Glade ("xml", "iml"), // Intelij Idea ("xml", "kdenlive"), // KDenLive ("xml", "lang"), // ? ("xml", "nuspec"), // Nuget ("xml", "policy"), // SystemD ("xml", "qsys"), // Quartus ("xml", "sopcinfo"), // Quartus ("xml", "svg"), // SVG ("xml", "ui"), // Cambalache, Glade ("xml", "user"), // Qtcreator ("xml", "vbox"), // VirtualBox ("xml", "vbox-prev"), // VirtualBox ("xml", "vcproj"), // VisualStudio ("xml", "vcxproj"), // VisualStudio ("xml", "xba"), // Libreoffice ("xml", "xcd"), // Libreoffice files ("zip", "apk"), // Android apk ("zip", "cbz"), // Comics ("zip", "dat"), // Multiple - python, brave ("zip", "doc"), // Word ("zip", "docx"), // Word ("zip", "epub"), // Ebook format ("zip", "jar"), // Java ("zip", "kra"), // Krita ("zip", "kgm"), // Krita ("zip", "nupkg"), // Nuget packages ("zip", "odg"), // Libreoffice ("zip", "pptx"), // Powerpoint ("zip", "whl"), // Python packages ("zip", "xlsx"), // Excel ("zip", "xpi"), // Firefox extensions ("zip", "zcos"), // Scilab // Probably invalid ("html", "svg"), ("xml", "html"), // Probably bug in external library ("msi", "ppt"), // Not sure why ppt is not recognized ("msi", "doc"), // Not sure why doc is not recognized ("exe", "xls"), // Not sure why xls is not recognized ]; ================================================ FILE: czkawka_core/src/tools/bad_names/core.rs ================================================ use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{fs, mem}; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::bad_names::{BadNameEntry, BadNames, BadNamesParameters, Info, NameFixerParams, NameIssues}; impl BadNames { pub fn new(params: BadNamesParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BadNames), information: Info::default(), files_to_check: Default::default(), bad_names_files: Default::default(), params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.files_to_check = grouped_file_entries.into_values().flatten().collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} files to check.", self.files_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "look_for_bad_names_files", level = "debug")] pub(crate) fn look_for_bad_names_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.files_to_check.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::BadNamesChecking, self.files_to_check.len(), self.get_test_type(), self.files_to_check.iter().map(|item| item.size).sum::(), ); let files_to_check = std::mem::take(&mut self.files_to_check); let checked_issues = self.params.checked_issues.clone(); debug!("look_for_bad_names_files - started checking for bad names"); let bad_names_files: Vec = files_to_check .into_par_iter() .filter_map(|file_entry| { if check_if_stop_received(stop_flag) { return None; } let size = file_entry.size; let result = check_and_generate_new_name(&file_entry.path, &checked_issues).map(|new_name| BadNameEntry { path: file_entry.path, modified_date: file_entry.modified_date, size: file_entry.size, new_name, }); progress_handler.increase_items(1); progress_handler.increase_size(size); result }) .collect(); debug!("look_for_bad_names_files - ended checking for bad names"); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } self.bad_names_files = bad_names_files; self.information.number_of_files_with_bad_names = self.bad_names_files.len(); debug!("Found {} files with bad names.", self.information.number_of_files_with_bad_names); WorkContinueStatus::Continue } #[fun_time(message = "fix_bad_names", level = "debug")] pub fn fix_bad_names(&mut self, _fix_params: NameFixerParams, stop_flag: &Arc) { let warnings: Vec<_> = mem::take(&mut self.bad_names_files) .into_par_iter() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } let new_path = entry.path.with_file_name(&entry.new_name); match fs::rename(&entry.path, &new_path) { Ok(()) => Some(None), Err(e) => Some(Some(format!("Failed to rename {:?}: {}", entry.path, e))), } }) .while_some() .flatten() .collect(); self.common_data.text_messages.warnings.extend(warnings); } } // Check file name against NameIssues and generate a new fixed name if issues are found pub fn check_and_generate_new_name(path: &Path, checked_issues: &NameIssues) -> Option { let file_name = path.file_name()?.to_string_lossy(); let mut stem = path.file_stem()?.to_string_lossy().to_string(); let mut extension = path.extension().map(|e| e.to_string_lossy().to_string()); if checked_issues.uppercase_extension && let Some(ref mut ext) = extension && ext.chars().any(|c| c.is_uppercase()) { *ext = ext.to_lowercase(); } if checked_issues.emoji_used { stem = stem.chars().filter(|c| !is_emoji(*c)).collect(); if let Some(ref mut ext) = extension { *ext = ext.chars().filter(|c| !is_emoji(*c)).collect(); } } if checked_issues.non_ascii_graphical { stem = deunicode::deunicode(&stem); if let Some(ref mut ext) = extension { *ext = deunicode::deunicode(ext).chars().filter(|e| e.is_ascii_graphic() || *e == ' ').collect(); } } if let Some(allowed_chars) = &checked_issues.restricted_charset_allowed { stem = deunicode::deunicode(&stem).chars().filter(|c| is_alphanumeric(*c) || allowed_chars.contains(c)).collect(); if let Some(ref mut ext) = extension { *ext = deunicode::deunicode(ext).chars().filter(|c| is_alphanumeric(*c) || allowed_chars.contains(c)).collect(); } } if checked_issues.remove_duplicated_non_alphanumeric { stem = remove_duplicated_non_alphanumeric(&stem); if let Some(ref mut ext) = extension { *ext = remove_duplicated_non_alphanumeric(ext); } } if checked_issues.space_at_start_or_end { stem = stem.trim().to_string(); if let Some(ref mut ext) = extension { *ext = ext.trim().to_string(); } } let new_name = if let Some(ext) = extension { if ext.is_empty() { stem } else { format!("{stem}.{ext}") } } else { stem }; if new_name != file_name.as_ref() as &str { Some(new_name) } else { None } } fn is_alphanumeric(c: char) -> bool { c.is_ascii_alphanumeric() } fn remove_duplicated_non_alphanumeric(s: &str) -> String { let mut result = String::with_capacity(s.len()); let mut chars = s.chars().peekable(); while let Some(c) = chars.next() { result.push(c); if !c.is_ascii_alphanumeric() { // Skip consecutive identical non-alphanumeric characters while let Some(&next_c) = chars.peek() { if next_c == c { chars.next(); } else { break; } } } } result } fn is_emoji(c: char) -> bool { let code = c as u32; matches!(code, // Misc symbols + pictographs 0x231A..=0x231B | 0x23E9..=0x23EC | 0x23F0 | 0x23F3 | 0x25FD..=0x25FE | 0x2600..=0x2604 | 0x2614..=0x2615 | 0x2648..=0x2653 | 0x267F | 0x2693 | 0x26A1 | 0x26AA..=0x26AB | 0x26BD..=0x26BE | 0x26C4..=0x26C8 | 0x26CE | 0x26D4 | 0x26EA | 0x26F2..=0x26F3 | 0x26F5 | 0x26FA | 0x26FD | 0x2705 | 0x270A..=0x270B | 0x2728 | 0x274C | 0x274E | 0x2753..=0x2757 | 0x2763..=0x2764 | 0x2795..=0x2797 | 0x27B0 | 0x27BF | 0x2B1B..=0x2B1C | 0x2B50 | 0x2B55 | // Enclosed characters 0x1F004 | 0x1F0CF | 0x1F18E | 0x1F191..=0x1F19A | 0x1F201 | 0x1F21A | 0x1F22F | 0x1F232..=0x1F23A | 0x1F250..=0x1F251 | // Main emoji blocks 0x1F300..=0x1F5FF | 0x1F600..=0x1F64F | 0x1F680..=0x1F6FF | 0x1F900..=0x1F9FF | // Regional indicator symbols (flags) 0x1F1E6..=0x1F1FF ) } ================================================ FILE: czkawka_core/src/tools/bad_names/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct BadNameEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub new_name: String, } impl ResultEntry for BadNameEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct NameIssues { pub uppercase_extension: bool, pub emoji_used: bool, pub space_at_start_or_end: bool, pub non_ascii_graphical: bool, pub restricted_charset_allowed: Option>, pub remove_duplicated_non_alphanumeric: bool, } impl NameIssues { pub fn all() -> Self { Self { uppercase_extension: true, emoji_used: true, space_at_start_or_end: true, non_ascii_graphical: true, restricted_charset_allowed: Some(vec!['_', '-', ' ', '.']), remove_duplicated_non_alphanumeric: true, } } pub fn none() -> Self { Self::default() } pub fn is_empty(&self) -> bool { !self.uppercase_extension && !self.emoji_used && !self.space_at_start_or_end && !self.non_ascii_graphical && self.restricted_charset_allowed.is_none() && !self.remove_duplicated_non_alphanumeric } } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct NameFixerParams { // Empty - fixing has no parameters } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_files_with_bad_names: usize, pub scanning_time: Duration, } #[derive(Clone)] pub struct BadNamesParameters { pub checked_issues: NameIssues, } impl BadNamesParameters { pub fn new(checked_issues: NameIssues) -> Self { Self { checked_issues } } } impl Default for BadNamesParameters { fn default() -> Self { Self { checked_issues: NameIssues::all(), } } } pub struct BadNames { common_data: CommonToolData, information: Info, files_to_check: Vec, bad_names_files: Vec, params: BadNamesParameters, } impl BadNames { pub const fn get_bad_names_files(&self) -> &Vec { &self.bad_names_files } pub fn get_params(&self) -> &BadNamesParameters { &self.params } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/bad_names/tests.rs ================================================ #[cfg(test)] mod tests2 { use std::fs; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::bad_names::{BadNames, BadNamesParameters, NameIssues}; #[test] fn test_uppercase_extension_detection() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("test.TXT"); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: true, emoji_used: false, space_at_start_or_end: false, non_ascii_graphical: false, restricted_charset_allowed: None, remove_duplicated_non_alphanumeric: false, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test.txt"); } #[test] fn test_emoji_detection() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("test😀.txt"); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: false, emoji_used: true, space_at_start_or_end: false, non_ascii_graphical: false, restricted_charset_allowed: None, remove_duplicated_non_alphanumeric: false, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test.txt"); } #[test] fn test_space_at_start_end_stem_detection() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join(" test .txt"); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: false, emoji_used: false, space_at_start_or_end: true, non_ascii_graphical: false, restricted_charset_allowed: None, remove_duplicated_non_alphanumeric: false, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test.txt"); } #[test] fn test_space_at_start_end_extension_detection() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("test. txt "); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: false, emoji_used: false, space_at_start_or_end: true, non_ascii_graphical: false, restricted_charset_allowed: None, remove_duplicated_non_alphanumeric: false, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test.txt"); } #[test] fn test_non_ascii_graphical_detection() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("tëst.txt"); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: false, emoji_used: false, space_at_start_or_end: false, non_ascii_graphical: true, restricted_charset_allowed: None, remove_duplicated_non_alphanumeric: false, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test.txt"); } #[test] fn test_restricted_charset_detection() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("test@file.txt"); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: false, emoji_used: false, space_at_start_or_end: false, non_ascii_graphical: false, restricted_charset_allowed: Some(vec!['_', '-', ' ']), remove_duplicated_non_alphanumeric: false, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "testfile.txt"); } #[test] fn test_duplicated_non_alphanumeric() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("test__file--name.txt"); fs::write(&test_file, "test").unwrap(); let params = BadNamesParameters::new(NameIssues { uppercase_extension: false, emoji_used: false, space_at_start_or_end: false, non_ascii_graphical: false, restricted_charset_allowed: None, remove_duplicated_non_alphanumeric: true, }); let mut bad_names = BadNames::new(params); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test_file-name.txt"); } #[test] fn test_multiple_issues() { let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join(" tëst😀 .TXT "); fs::write(&test_file, "test").unwrap(); let mut bad_names = BadNames::new(BadNamesParameters::new(NameIssues::all())); bad_names.get_cd_mut().directories.set_included_paths(vec![temp_dir.path().to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); bad_names.search(&stop_flag, None); assert_eq!(bad_names.get_bad_names_files().len(), 1); assert_eq!(bad_names.get_bad_names_files()[0].new_name, "test.txt"); } use std::path::Path; use crate::tools::bad_names::core::check_and_generate_new_name; #[test] fn test_uppercase_extension_unit() { let check_params = NameIssues { uppercase_extension: true, ..NameIssues::default() }; let mut errors = Vec::new(); let test_cases = [ ("test.TXT", "test.txt"), ("file.Jpg", "file.jpg"), ("document.PDF", "document.pdf"), ("image.PnG", "image.png"), ("video.MP4", "video.mp4"), ("archive.ZIP", "archive.zip"), ("data.CSV", "data.csv"), ("presentation.PPTX", "presentation.pptx"), ("script.Py", "script.py"), ("code.Js", "code.js"), ("style.Css", "style.css"), ("page.Html", "page.html"), ("config.Json", "config.json"), ("readme.Md", "readme.md"), ("Makefile.Mk", "Makefile.mk"), ("abc.cde.TXT", "abc.cde.txt"), ("file.backup.PDF", "file.backup.pdf"), ("my.file.name.JPG", "my.file.name.jpg"), ("test.1.2.3.Zip", "test.1.2.3.zip"), ("document.v2.0.Doc", "document.v2.0.doc"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } assert!(errors.is_empty(), "Uppercase extension tests failed:\n{}", errors.join("\n")); } #[test] fn test_emoji_removal_unit() { let check_params = NameIssues { emoji_used: true, ..NameIssues::default() }; let mut errors = Vec::new(); let test_cases = [ ("test😀.txt", "test.txt"), ("file🎉🎊.doc", "file.doc"), ("image❤.png", "image.png"), ("video🔥.mp4", "video.mp4"), ("doc👍.pdf", "doc.pdf"), ("report😊😊😊.xlsx", "report.xlsx"), ("photo🌟.jpg", "photo.jpg"), ("music🎵🎶.mp3", "music.mp3"), ("readme📝.md", "readme.md"), ("party🎈🎉🎊🎁.txt", "party.txt"), ("love💕💖💗💘.doc", "love.doc"), ("fire🔥🔥🔥.log", "fire.log"), ("star⭐.txt", "star.txt"), ("food🍕🍔🍟.jpg", "food.jpg"), ("weather☀🌧⛈.csv", "weather.csv"), ("test😀.backup.txt", "test.backup.txt"), ("my.file🎉.doc", "my.file.doc"), ("archive.v1.2🔥.zip", "archive.v1.2.zip"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } assert!(errors.is_empty(), "Emoji removal tests failed:\n{}", errors.join("\n")); } #[test] fn test_space_at_start_end_unit() { let check_params = NameIssues { space_at_start_or_end: true, ..NameIssues::default() }; let mut errors = Vec::new(); let test_cases = [ (" test.txt", "test.txt"), ("test .txt", "test.txt"), (" test .txt", "test.txt"), (" test .txt", "test.txt"), ("test. txt ", "test.txt"), (" file .doc", "file.doc"), ("image .png", "image.png"), (" video.mp4", "video.mp4"), ("document .pdf", "document.pdf"), (" report .xlsx", "report.xlsx"), (" data .csv", "data.csv"), ("photo . jpg ", "photo.jpg"), (" music .mp3", "music.mp3"), ("readme . md ", "readme.md"), (" archive . zip ", "archive.zip"), (" abc.cde.txt", "abc.cde.txt"), ("abc.cde .txt", "abc.cde.txt"), (" my.file.name .doc", "my.file.name.doc"), (" test.1.2 . pdf ", "test.1.2.pdf"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } assert!(errors.is_empty(), "Space at start/end tests failed:\n{}", errors.join("\n")); } #[test] fn test_non_ascii_graphical_unit() { let check_params = NameIssues { non_ascii_graphical: true, ..NameIssues::default() }; let mut errors = Vec::new(); let test_cases = [ ("tëst.txt", "test.txt"), ("café.pdf", "cafe.pdf"), ("Kraków.doc", "Krakow.doc"), ("Łódź.txt", "Lodz.txt"), ("naïve.doc", "naive.doc"), ("résumé.pdf", "resume.pdf"), ("São Paulo.txt", "Sao Paulo.txt"), ("Zürich.doc", "Zurich.doc"), ("Москва.txt", "Moskva.txt"), ("日本.txt", "Ri Ben.txt"), ("über.pdf", "uber.pdf"), ("señor.txt", "senor.txt"), ("Ærø.doc", "AEro.doc"), ("niño.txt", "nino.txt"), ("Björk.mp3", "Bjork.mp3"), ("François.doc", "Francois.doc"), ("Ñoño.txt", "Nono.txt"), ("Østergård.pdf", "Ostergard.pdf"), ("Łukasz.txt", "Lukasz.txt"), ("Müller.doc", "Muller.doc"), ("pièces", "pieces"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } assert!(errors.is_empty(), "Non-ASCII graphical tests failed:\n{}", errors.join("\n")); } #[test] fn test_restricted_charset_unit() { let check_params = NameIssues { restricted_charset_allowed: Some(vec!['_', '-', ' ']), ..NameIssues::default() }; let mut errors = Vec::new(); let test_cases = [ ("test@file.txt", "testfile.txt"), ("my#doc.pdf", "mydoc.pdf"), ("file$name.doc", "filename.doc"), ("data%set.csv", "dataset.csv"), ("script&code.js", "scriptcode.js"), ("image*pic.png", "imagepic.png"), ("video(1).mp4", "video1.mp4"), ("photo[2].jpg", "photo2.jpg"), ("doc{test}.pdf", "doctest.pdf"), ("file|name.txt", "filename.txt"), ("test:file.doc", "testfile.doc"), ("name;value.csv", "namevalue.csv"), ("file'name.txt", "filename.txt"), ("test\"quote.doc", "testquote.doc"), ("datamore.txt", "filemore.txt"), ("question?.log", "question.log"), ("wild*.txt", "wild.txt"), ("comma,.csv", "comma.csv"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } assert!(errors.is_empty(), "Restricted charset tests failed:\n{}", errors.join("\n")); } #[test] fn test_duplicated_non_alphanumeric_unit() { let check_params = NameIssues { remove_duplicated_non_alphanumeric: true, ..NameIssues::default() }; let mut errors = Vec::new(); let test_cases = [ ("test__file.txt", "test_file.txt"), ("my--doc.pdf", "my-doc.pdf"), ("file name.doc", "file name.doc"), ("data...set.csv", "data.set.csv"), ("script___code.js", "script_code.js"), ("image---pic.png", "image-pic.png"), ("test____file----name.txt", "test_file-name.txt"), ("multiple spaces.doc", "multiple spaces.doc"), ("under______score.log", "under_score.log"), ("dash-------line.txt", "dash-line.txt"), ("mixed__--__test.doc", "mixed_-_test.doc"), ("file,,,,name.csv", "file,name.csv"), ("test;;;;code.txt", "test;code.txt"), ("data::::value.xml", "data:value.xml"), ("triple___---...test.txt", "triple_-.test.txt"), ("many spaces.doc", "many spaces.doc"), ("dots......dots.txt", "dots.dots.txt"), ("under_score.txt", "under_score.txt"), ("normal-file.txt", "normal-file.txt"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); let result = check_and_generate_new_name(path, &check_params); if input == expected_output { // No change expected if let Some(result) = result { errors.push(format!("Input: '{input}' should not be modified but got: '{result}'")); } } else { // Change expected if let Some(new_name) = result { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } } assert!(errors.is_empty(), "Duplicated non-alphanumeric tests failed:\n{}", errors.join("\n")); } #[test] fn test_combined_all_issues_unit() { let check_params = NameIssues { uppercase_extension: true, emoji_used: true, space_at_start_or_end: true, non_ascii_graphical: true, restricted_charset_allowed: Some(vec!['_', '-', ' ']), remove_duplicated_non_alphanumeric: true, }; let mut errors = Vec::new(); let test_cases = [ (" tëst😀 .TXT ", "test.txt"), (" café☕ .Pdf ", "cafe.pdf"), (" über@file😊 .Txt ", "uberfile.txt"), ("test__😀__file.JPG", "test_file.jpg"), (" Kraków🎉 .Doc ", "Krakow.doc"), (" résumé## .PDF ", "resume.pdf"), ("São Paulo .TXT", "Sao Paulo.txt"), (" file___name😀😀.PNG ", "file_name.png"), ("test @@ emoji🎉.MP4", "test emoji.mp4"), (" Łódź---file .CSV ", "Lodz-file.csv"), ("über__müller😊.XLSX", "uber_muller.xlsx"), (" data___set🔥 . JSON ", "data_set.json"), ("test ## ëmoji😀.Doc", "test emoji.doc"), (" François___Müller .PDF ", "Francois_Muller.pdf"), ("multi___issue___test😀😀 .TXT ", "multi_issue_test.txt"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { if new_name != expected_output { errors.push(format!("Input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } let fixed_path = Path::new(&new_name); if check_and_generate_new_name(fixed_path, &check_params).is_some() { errors.push(format!("Double fix should return None for: '{new_name}'")); } } else { errors.push(format!("Input: '{input}' was not fixed")); } } assert!(errors.is_empty(), "Combined all issues tests failed:\n{}", errors.join("\n")); } #[test] fn test_no_issues_no_changes() { let check_params = NameIssues::all(); let mut errors = Vec::new(); let test_cases = [ "normal_file.txt", "test-file.doc", "MyDocument.pdf", "data_2024.csv", "image-001.jpg", "video_final.mp4", "report-2024-01.xlsx", "README.md", "config.json", "script.py", ]; for input in test_cases { let path = Path::new(input); if let Some(new_name) = check_and_generate_new_name(path, &check_params) { errors.push(format!("Input: '{input}' should not be changed but got: '{new_name}'")); } } assert!(errors.is_empty(), "No issues no changes tests failed:\n{}", errors.join("\n")); } #[test] fn test_edge_cases_unit() { let check_params = NameIssues::all(); let mut errors = Vec::new(); let test_cases = [ ("😀.txt", ".txt"), (" .TXT", ".txt"), ("😀😀😀.txt", ".txt"), ("___", "_"), ("---", "-"), ("...", "."), (" 😀 .TXT ", ".txt"), ("test.", "test"), (".test", ".test"), ]; for (input, expected_output) in test_cases { let path = Path::new(input); let result = check_and_generate_new_name(path, &check_params); if input == expected_output { if let Some(new_name) = result { errors.push(format!("Edge case input: '{input}' should not be modified but got: '{new_name}'")); } } else { if let Some(new_name) = result { if new_name != expected_output { errors.push(format!("Edge case input: '{input}', Expected: '{expected_output}', Got: '{new_name}'")); } } else { errors.push(format!("Edge case input: '{input}' was not fixed")); } } } assert!(errors.is_empty(), "Edge cases tests failed:\n{}", errors.join("\n")); } } ================================================ FILE: czkawka_core/src/tools/bad_names/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search}; use crate::flc; use crate::tools::bad_names::{BadNames, BadNamesParameters, Info, NameFixerParams}; impl AllTraits for BadNames {} impl Search for BadNames { #[fun_time(message = "find_bad_names", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.params.checked_issues.is_empty() { self.common_data.text_messages.critical = Some(flc!("core_needs_to_set_at_least_one_bad_name_option")); return; } if self.prepare_items(None).is_err() { return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.look_for_bad_names_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for BadNames { fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } self.debug_print_common(); } } impl PrintResults for BadNames { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.bad_names_files.is_empty() { writeln!(writer, "Found {} files with bad names.", self.information.number_of_files_with_bad_names)?; for file_entry in &self.bad_names_files { writeln!(writer, "\"{}\" -> \"{}\"", file_entry.path.to_string_lossy(), file_entry.new_name)?; } } else { write!(writer, "Not found any files with bad names.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.bad_names_files, pretty_print) } } impl DeletingItems for BadNames { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.bad_names_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl FixingItems for BadNames { type FixParams = NameFixerParams; #[fun_time(message = "fix_items", level = "debug")] fn fix_items(&mut self, stop_flag: &Arc, _progress_sender: Option<&Sender>, fix_params: Self::FixParams) { self.fix_bad_names(fix_params, stop_flag); } } impl CommonData for BadNames { type Info = Info; type Parameters = BadNamesParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_files_with_bad_names > 0 } } ================================================ FILE: czkawka_core/src/tools/big_file/core.rs ================================================ use std::cmp::Reverse; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::big_file::{BigFile, BigFileParameters, Info, SearchMode}; impl BigFile { pub fn new(params: BigFileParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BigFile), information: Info::default(), big_files: Default::default(), params, } } #[fun_time(message = "look_for_big_files", level = "debug")] pub(crate) fn look_for_big_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .minimal_file_size(1) .maximal_file_size(u64::MAX) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { let mut all_files = grouped_file_entries.into_values().flatten().collect::>(); if self.get_params().search_mode == SearchMode::BiggestFiles { all_files.par_sort_unstable_by_key(|fe| Reverse(fe.size)); } else { all_files.par_sort_unstable_by_key(|fe| fe.size); } all_files.truncate(self.get_params().number_of_files_to_check); self.big_files = all_files; self.common_data.text_messages.warnings.extend(warnings); self.information.number_of_real_files = self.big_files.len(); debug!("check_files - Found {} biggest/smallest files.", self.big_files.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } } ================================================ FILE: czkawka_core/src/tools/big_file/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::time::Duration; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum SearchMode { BiggestFiles, SmallestFiles, } #[derive(Debug, Default, Clone)] pub struct Info { pub number_of_real_files: usize, pub scanning_time: Duration, } #[derive(Clone)] pub struct BigFileParameters { pub number_of_files_to_check: usize, pub search_mode: SearchMode, } impl BigFileParameters { pub fn new(number_of_files: usize, search_mode: SearchMode) -> Self { Self { number_of_files_to_check: number_of_files.max(1), search_mode, } } } pub struct BigFile { common_data: CommonToolData, information: Info, big_files: Vec, params: BigFileParameters, } impl BigFile { pub const fn get_big_files(&self) -> &Vec { &self.big_files } } ================================================ FILE: czkawka_core/src/tools/big_file/tests.rs ================================================ use std::fs; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::big_file::{BigFile, BigFileParameters, SearchMode}; #[test] fn test_find_biggest_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create files with different sizes fs::write(path.join("small.txt"), b"12").unwrap(); // 2 bytes fs::write(path.join("medium.txt"), b"12345").unwrap(); // 5 bytes fs::write(path.join("large.txt"), vec![b'A'; 100]).unwrap(); // 100 bytes let params = BigFileParameters::new(2, SearchMode::BiggestFiles); let mut finder = BigFile::new(params); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let big_files = finder.get_big_files(); assert_eq!(big_files.len(), 2, "Should find 2 biggest files"); assert_eq!(big_files[0].size, 100, "First file should be 100 bytes"); assert_eq!(big_files[1].size, 5, "Second file should be 5 bytes"); } #[test] fn test_find_smallest_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create files with different sizes fs::write(path.join("small.txt"), b"12").unwrap(); // 2 bytes fs::write(path.join("medium.txt"), b"12345").unwrap(); // 5 bytes fs::write(path.join("large.txt"), vec![b'A'; 100]).unwrap(); // 100 bytes let params = BigFileParameters::new(2, SearchMode::SmallestFiles); let mut finder = BigFile::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let big_files = finder.get_big_files(); assert_eq!(big_files.len(), 2, "Should find 2 smallest files"); assert_eq!(big_files[0].size, 2, "First file should be 2 bytes"); assert_eq!(big_files[1].size, 5, "Second file should be 5 bytes"); } #[test] fn test_limit_number_of_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create 5 files for i in 1..=5 { fs::write(path.join(format!("file{i}.txt")), vec![b'A'; i * 10]).unwrap(); } let params = BigFileParameters::new(3, SearchMode::BiggestFiles); let mut finder = BigFile::new(params); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let big_files = finder.get_big_files(); assert_eq!(big_files.len(), 3, "Should limit results to 3 files"); } #[test] fn test_empty_directory() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let params = BigFileParameters::new(5, SearchMode::BiggestFiles); let mut finder = BigFile::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let big_files = finder.get_big_files(); assert!(big_files.is_empty(), "Should find no files in empty directory"); } ================================================ FILE: czkawka_core/src/tools/big_file/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::big_file::{BigFile, BigFileParameters, Info, SearchMode}; impl AllTraits for BigFile {} impl DeletingItems for BigFile { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.big_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl DebugPrint for BigFile { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("### INDIVIDUAL DEBUG PRINT ###"); println!("Info: {:?}", self.information); println!("Number of files to check - {}", self.get_params().number_of_files_to_check); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for BigFile { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if self.information.number_of_real_files != 0 { if self.get_params().search_mode == SearchMode::BiggestFiles { writeln!(writer, "{} the biggest files.\n\n", self.information.number_of_real_files)?; } else { writeln!(writer, "{} the smallest files.\n\n", self.information.number_of_real_files)?; } for file_entry in &self.big_files { writeln!( writer, "{} ({}) - \"{}\"", format_size(file_entry.size, BINARY), file_entry.size, file_entry.path.to_string_lossy() )?; } } else { writeln!(writer, "Not found any files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.big_files, pretty_print) } } impl Search for BigFile { #[fun_time(message = "find_big_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } if self.look_for_big_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl CommonData for BigFile { type Info = Info; type Parameters = BigFileParameters; fn get_information(&self) -> Self::Info { self.information.clone() } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_real_files > 0 } } ================================================ FILE: czkawka_core/src/tools/broken_files/core.rs ================================================ use std::collections::BTreeMap; use std::fs::File; use std::path::Path; use std::process::Command; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{mem, panic}; use crossbeam_channel::Sender; use fun_time::fun_time; use log::{debug, error}; use lopdf::Document; use rayon::prelude::*; use crate::common::cache::{CACHE_BROKEN_FILES_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path}; use crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_BROKEN_FILES_EXTENSIONS, PDF_FILES_EXTENSIONS, VIDEO_FILES_EXTENSIONS, ZIP_FILES_EXTENSIONS}; use crate::common::create_crash_message; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::process_utils::run_command_interruptible; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::helpers::audio_checker; use crate::tools::broken_files::{BrokenEntry, BrokenFiles, BrokenFilesParameters, Info, TypeOfFile}; impl BrokenFiles { pub fn new(params: BrokenFilesParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::BrokenFiles), information: Info::default(), files_to_check: Default::default(), broken_files: Default::default(), params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.files_to_check = grouped_file_entries .into_values() .flatten() .map(|fe| { let broken_entry = fe.into_broken_entry(); (broken_entry.path.to_string_lossy().to_string(), broken_entry) }) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} files to check.", self.files_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn check_broken_image(mut file_entry: BrokenEntry) -> BrokenEntry { let mut file_entry_clone = file_entry.clone(); panic::catch_unwind(|| { match image::open(&file_entry.path) { Ok(img) => { if img.width() == 0 || img.height() == 0 { file_entry.error_string = "Image has zero width or height".to_string(); } } Err(e) => { file_entry.error_string = e.to_string().trim().to_string(); } } file_entry }) .unwrap_or_else(|_| { let message = create_crash_message("Image-rs", &file_entry_clone.path.to_string_lossy(), "https://github.com/image-rs/image"); error!("{message}"); file_entry_clone.error_string = message; file_entry_clone }) } fn check_broken_zip(mut file_entry: BrokenEntry) -> Option { match File::open(&file_entry.path) { Ok(file) => { if let Err(e) = zip::ZipArchive::new(file) { file_entry.error_string = e.to_string().trim().to_string(); } Some(file_entry) } Err(_inspected) => None, } } fn check_broken_audio(mut file_entry: BrokenEntry) -> Option { match File::open(&file_entry.path) { Ok(file) => { let mut file_entry_clone = file_entry.clone(); panic::catch_unwind(|| { if let Err(e) = audio_checker::parse_audio_file(file) { let err_str = e.to_string(); if !err_str.contains("not supported codec") { file_entry.error_string = err_str.trim().to_string(); } } Some(file_entry) }) .unwrap_or_else(|_| { let message = create_crash_message("Symphonia", &file_entry_clone.path.to_string_lossy(), "https://github.com/pdeljanov/Symphonia"); error!("{message}"); file_entry_clone.error_string = message; Some(file_entry_clone) }) } Err(_inspected) => None, } } fn check_broken_pdf(mut file_entry: BrokenEntry) -> BrokenEntry { let mut file_entry_clone = file_entry.clone(); panic::catch_unwind(|| { match File::open(&file_entry.path) { Ok(file) => { if let Err(e) = Document::load_from(file) { file_entry.error_string = e.to_string().trim().to_string(); } } Err(e) => { file_entry.error_string = e.to_string().trim().to_string(); } } file_entry }) .unwrap_or_else(|_| { let message = create_crash_message("lopdf", &file_entry_clone.path.to_string_lossy(), "https://github.com/J-F-Liu/lopdf"); error!("{message}"); file_entry_clone.error_string = message; file_entry_clone }) } // None if stopped, otherwise Some fn check_broken_video(mut file_entry: BrokenEntry, stop_flag: &Arc) -> Option { let ffprobe_errors = [ ("moov atom not found", Some("broken file structure")), ("error reading header", Some("broken file structure")), ("EBML header parsing failed", None), ("exceeds containing master element", Some("broken file structure")), ("invalid frame index table", Some("broken file structure")), ("Invalid argument", Some("ffprobe seems to not recognize file format")), ]; let mut command = Command::new("ffprobe"); command.arg("-v").arg("error").arg(&file_entry.path); match run_command_interruptible(command, stop_flag) { None => return None, Some(Err(e)) => { debug!("Failed to run ffprobe on {:?}: {}", file_entry.path, e); file_entry.error_string = format!("Failed to run ffprobe: {e}").trim().to_string(); return Some(file_entry); } Some(Ok(output)) => { let combined = format!("{}{}", output.stdout.trim(), output.stderr.trim()); if let Some((error_message, additional_message)) = ffprobe_errors.iter().find(|(err, _)| combined.contains(err)) { file_entry.error_string = format!("{error_message}{}", additional_message.map(|e| format!(" ({e})")).unwrap_or_default()); return Some(file_entry); } else if !output.status.success() { // debug_save_file("ffprobe_failed_output.txt", &format!("{} --- \n{}", file_entry.path.to_string_lossy(), combined)); file_entry.error_string = format!("ffprobe exited with non-zero status: {}", output.status); return Some(file_entry); } } } let ffmpeg_message = [ ("Output file does not contain any stream", Some("cannot find video stream - possible not even video file")), ("missing mandatory atoms, broken header", Some("broken file structure")), ("Cannot determine format of input", None), ("decode_slice_header error", Some("corrupted video data, may be still fully/partially playable")), ("Truncating packet", Some("corrupted video data, may be still fully/partially playable")), ("Invalid NAL unit size", Some("corrupted video data, may be still fully/partially playable")), ( "exceeds containing master element ending", Some("corrupted video data, may be still fully/partially playable"), ), ("corrupt input packet in stream", Some("Possible corruption in audio/video stream, may be still playable")), ( "invalid as first byte of an EBML number", Some("corrupted video data, may be still fully/partially playable"), ), // Last resort for all other errors ("Invalid data found when processing input", Some("generic error")), // Must be last to not override more precise errors // Warnings ("corrupt decoded frame", Some("may be still playable")), ]; let ffmpeg_allowed_messages = [ "Input buffer exhausted before END element found", // Looks like quite popular message, so ignoring it "Invalid color space", // https://fftrac-bg.ffmpeg.org/ticket/11020 - seems to be non-fatal ]; let mut command = Command::new("ffmpeg"); command .arg("-v") .arg("error") .arg("-xerror") .arg("-threads") .arg("1") .arg("-i") .arg(&file_entry.path) .arg("-f") .arg("null") .arg("-"); match run_command_interruptible(command, stop_flag) { None => return None, Some(Err(e)) => { debug!("Failed to run ffmpeg on {:?}: {}", file_entry.path, e); file_entry.error_string = format!("Failed to run ffmpeg: {}", e.trim()); } Some(Ok(output)) => { let combined = format!("{}{}", output.stdout.trim(), output.stderr.trim()); if ffmpeg_allowed_messages.iter().any(|msg| combined.contains(msg)) { // Allowed message, do nothing } else if let Some((error_message, additional_message)) = ffmpeg_message.iter().find(|(err, _)| combined.contains(err)) { file_entry.error_string = format!("{error_message}{}", additional_message.map(|e| format!(" ({e})")).unwrap_or_default()); } else if !output.status.success() { // debug_save_file("ffmpeg_failed_output.txt", &format!("{} --- \n{}", file_entry.path.to_string_lossy(), combined)); file_entry.error_string = format!("ffmpeg exited with non-zero status: {}", output.status); } } } Some(file_entry) } #[fun_time(message = "load_cache", level = "debug")] fn load_cache(&mut self) -> (BTreeMap, BTreeMap, BTreeMap) { load_and_split_cache_generalized_by_path(&get_broken_files_cache_file(), mem::take(&mut self.files_to_check), self) } #[fun_time(message = "save_to_cache", level = "debug")] fn save_to_cache(&mut self, vec_file_entry: &[BrokenEntry], loaded_hash_map: BTreeMap) { save_and_connect_cache_generalized_by_path(&get_broken_files_cache_file(), vec_file_entry, loaded_hash_map, self); } fn check_file(file_entry: BrokenEntry, stop_flag: &Arc) -> Option> { match check_extension_availability(&file_entry.path) { TypeOfFile::Image => Some(Some(Self::check_broken_image(file_entry))), TypeOfFile::ArchiveZip => Some(Self::check_broken_zip(file_entry)), TypeOfFile::Audio => Some(Self::check_broken_audio(file_entry)), TypeOfFile::Pdf => Some(Some(Self::check_broken_pdf(file_entry))), TypeOfFile::Video => Self::check_broken_video(file_entry, stop_flag).map(Some), TypeOfFile::Unknown => { error!("Unknown file type of: {file_entry:?}"); Some(None) } } } #[fun_time(message = "look_for_broken_files", level = "debug")] pub(crate) fn look_for_broken_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.files_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::BrokenFilesChecking, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|item| item.size).sum::(), ); let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::>(); debug!("look_for_broken_files - started finding for broken files"); let mut vec_file_entry: Vec = non_cached_files_to_check .into_par_iter() .with_max_len(3) .map(|(_, file_entry)| { if check_if_stop_received(stop_flag) { return None; } let size = file_entry.size; let res = Self::check_file(file_entry, stop_flag); progress_handler.increase_items(1); progress_handler.increase_size(size); res }) .while_some() .flatten() .collect::>(); debug!("look_for_broken_files - ended finding for broken files"); progress_handler.join_thread(); // Just connect loaded results with already calculated vec_file_entry.extend(records_already_cached.into_values()); self.save_to_cache(&vec_file_entry, loaded_hash_map); self.broken_files = vec_file_entry.into_iter().filter_map(|f| if f.error_string.is_empty() { None } else { Some(f) }).collect(); self.information.number_of_broken_files = self.broken_files.len(); debug!("Found {} broken files.", self.information.number_of_broken_files); // Clean unused data self.files_to_check = Default::default(); WorkContinueStatus::Continue } } #[expect(clippy::string_slice)] // Valid, because we address up to the dot, which is known ascii character fn check_extension_availability(full_name: &Path) -> TypeOfFile { let Some(file_name) = full_name.file_name() else { error!("Missing file name in file - \"{}\"", full_name.to_string_lossy()); debug_assert!(false, "Missing file name in file - \"{}\"", full_name.to_string_lossy()); return TypeOfFile::Unknown; }; // Faster manual conversion than using Path::extension() let Some(file_name_str) = file_name.to_str() else { return TypeOfFile::Unknown }; let Some(extension_idx) = file_name_str.rfind('.') else { return TypeOfFile::Unknown }; let extension_str = &file_name_str[extension_idx + 1..]; let extension_lowercase = extension_str.to_ascii_lowercase(); if IMAGE_RS_BROKEN_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) { TypeOfFile::Image } else if ZIP_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) { TypeOfFile::ArchiveZip } else if PDF_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) { TypeOfFile::Pdf } else if AUDIO_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) { TypeOfFile::Audio } else if VIDEO_FILES_EXTENSIONS.contains(&extension_lowercase.as_str()) { TypeOfFile::Video } else { error!("File with unknown extension: \"{}\" - {extension_lowercase}", full_name.to_string_lossy()); debug_assert!(false, "File with unknown extension - \"{}\" - {extension_lowercase}", full_name.to_string_lossy()); TypeOfFile::Unknown } } pub fn get_broken_files_cache_file() -> String { format!("cache_broken_files_{CACHE_BROKEN_FILES_VERSION}.bin") } ================================================ FILE: czkawka_core/src/tools/broken_files/mod.rs ================================================ use bitflags::bitflags; pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct BrokenEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub error_string: String, } impl ResultEntry for BrokenEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_broken_entry(self) -> BrokenEntry { BrokenEntry { size: self.size, path: self.path, modified_date: self.modified_date, error_string: String::new(), } } } #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum TypeOfFile { Unknown = -1, Image = 0, ArchiveZip, Audio, Pdf, Video, } bitflags! { #[derive(PartialEq, Copy, Clone, Debug)] pub struct CheckedTypes : u32 { const NONE = 0; const PDF = 0b1; const AUDIO = 0b10; const IMAGE = 0b100; const ARCHIVE = 0b1000; const VIDEO = 0b10000; } } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_broken_files: usize, pub scanning_time: Duration, } #[derive(Clone)] pub struct BrokenFilesParameters { pub checked_types: CheckedTypes, } impl BrokenFilesParameters { pub fn new(checked_types: CheckedTypes) -> Self { Self { checked_types } } } pub struct BrokenFiles { common_data: CommonToolData, information: Info, files_to_check: BTreeMap, broken_files: Vec, params: BrokenFilesParameters, } impl BrokenFiles { pub const fn get_broken_files(&self) -> &Vec { &self.broken_files } pub(crate) fn get_params(&self) -> &BrokenFilesParameters { &self.params } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/broken_files/tests.rs ================================================ use std::fs; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes}; fn get_test_resources_path() -> PathBuf { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_resources"); assert!(path.exists(), "Test resources not found at \"{}\"", path.to_string_lossy()); path } fn corrupt_file(source: &PathBuf, dest: &PathBuf, bytes_to_corrupt: usize) { let mut content = fs::read(source).unwrap(); for byte in content.iter_mut().take(bytes_to_corrupt) { *byte = 0x11; } fs::write(dest, content).unwrap(); } #[test] fn test_find_broken_image() { let temp_dir = TempDir::new().unwrap(); let test_resources = get_test_resources_path(); let source_image = test_resources.join("images").join("normal.jpg"); let broken_image = temp_dir.path().join("broken.jpg"); corrupt_file(&source_image, &broken_image, 10); let params = BrokenFilesParameters::new(CheckedTypes::IMAGE); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); assert_eq!(broken_files.len(), 1, "Should find 1 broken image file"); assert!(!broken_files[0].error_string.is_empty(), "Error string should not be empty"); } #[test] fn test_valid_image() { let temp_dir = TempDir::new().unwrap(); let test_resources = get_test_resources_path(); let source_image = test_resources.join("images").join("normal.jpg"); let valid_image = temp_dir.path().join("valid.jpg"); fs::copy(&source_image, &valid_image).unwrap(); let params = BrokenFilesParameters::new(CheckedTypes::IMAGE); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); assert_eq!(broken_files.len(), 0, "Should find no broken image files"); } #[test] fn test_broken_audio() { let temp_dir = TempDir::new().unwrap(); let test_resources = get_test_resources_path(); let source_audio = test_resources.join("audio").join("base.mp3"); let broken_audio = temp_dir.path().join("broken.mp3"); let file_len = fs::metadata(&source_audio).unwrap().len(); corrupt_file(&source_audio, &broken_audio, file_len as usize); let good_audio = temp_dir.path().join("good.mp3"); fs::copy(&source_audio, &good_audio).unwrap(); let params = BrokenFilesParameters::new(CheckedTypes::AUDIO); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); assert_eq!(broken_files.len(), 1, "Should find 1 broken audio file"); assert!(!broken_files[0].error_string.is_empty(), "Error string should not be empty"); } #[test] fn test_mixed_valid_and_broken_images() { let temp_dir = TempDir::new().unwrap(); let test_resources = get_test_resources_path(); let source_image1 = test_resources.join("images").join("normal.jpg"); fs::copy(&source_image1, temp_dir.path().join("valid.jpg")).unwrap(); let source_image2 = test_resources.join("images").join("normal2.jpg"); corrupt_file(&source_image2, &temp_dir.path().join("broken.jpg"), 10); let params = BrokenFilesParameters::new(CheckedTypes::IMAGE); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); let info = finder.get_information(); assert_eq!(broken_files.len(), 1, "Should find only 1 broken file out of 2 total"); assert_eq!(info.number_of_broken_files, 1, "Info should report 1 broken file"); } #[test] fn test_multiple_file_types() { let temp_dir = TempDir::new().unwrap(); let test_resources = get_test_resources_path(); let source_image = test_resources.join("images").join("normal.jpg"); corrupt_file(&source_image, &temp_dir.path().join("broken.jpg"), 10); let source_audio = test_resources.join("audio").join("base.mp3"); let file_len = fs::metadata(&source_audio).unwrap().len(); corrupt_file(&source_audio, &temp_dir.path().join("broken.mp3"), file_len as usize); let params = BrokenFilesParameters::new(CheckedTypes::IMAGE | CheckedTypes::AUDIO); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); assert_eq!(broken_files.len(), 2, "Should find 2 broken files"); } #[test] fn test_empty_directory() { let temp_dir = TempDir::new().unwrap(); let params = BrokenFilesParameters::new(CheckedTypes::IMAGE); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); assert_eq!(broken_files.len(), 0, "Should find no broken files in empty directory"); } #[test] fn test_no_file_types_selected() { let temp_dir = TempDir::new().unwrap(); let test_resources = get_test_resources_path(); let source_image = test_resources.join("images").join("normal.jpg"); corrupt_file(&source_image, &temp_dir.path().join("broken.jpg"), 10); let params = BrokenFilesParameters::new(CheckedTypes::NONE); let mut finder = BrokenFiles::new(params); finder.set_included_paths(vec![temp_dir.path().to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let broken_files = finder.get_broken_files(); assert_eq!(broken_files.len(), 0, "Should find no files when no types are selected"); } ================================================ FILE: czkawka_core/src/tools/broken_files/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::consts::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_BROKEN_FILES_EXTENSIONS, PDF_FILES_EXTENSIONS, VIDEO_FILES_EXTENSIONS, ZIP_FILES_EXTENSIONS}; use crate::common::ffmpeg_utils::check_if_ffprobe_ffmpeg_exists; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::flc; use crate::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes, Info}; impl AllTraits for BrokenFiles {} impl Search for BrokenFiles { #[fun_time(message = "find_broken_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.params.checked_types.contains(CheckedTypes::VIDEO) && !check_if_ffprobe_ffmpeg_exists() { self.common_data.text_messages.critical = Some(flc!("core_ffmpeg_not_found")); #[cfg(target_os = "windows")] self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found_windows")); return; } let extension_types = [ (CheckedTypes::PDF, PDF_FILES_EXTENSIONS), (CheckedTypes::AUDIO, AUDIO_FILES_EXTENSIONS), (CheckedTypes::ARCHIVE, ZIP_FILES_EXTENSIONS), (CheckedTypes::IMAGE, IMAGE_RS_BROKEN_FILES_EXTENSIONS), (CheckedTypes::VIDEO, VIDEO_FILES_EXTENSIONS), ]; let extensions = extension_types .into_iter() .filter(|(checked_type, _)| self.get_params().checked_types.contains(*checked_type)) .flat_map(|(_, exts)| exts.to_vec()) .collect::>(); if extensions.is_empty() { self.common_data.text_messages.critical = Some(flc!("core_needs_to_set_at_least_one_broken_option")); return; } if self.prepare_items(Some(&extensions)).is_err() { return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.look_for_broken_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for BrokenFiles { fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } self.debug_print_common(); } } impl PrintResults for BrokenFiles { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.broken_files.is_empty() { writeln!(writer, "Found {} broken files.", self.information.number_of_broken_files)?; for file_entry in &self.broken_files { writeln!(writer, "\"{}\" - {}", file_entry.path.to_string_lossy(), file_entry.error_string)?; } } else { write!(writer, "Not found any broken files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.broken_files, pretty_print) } } impl DeletingItems for BrokenFiles { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.broken_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl CommonData for BrokenFiles { type Info = Info; type Parameters = BrokenFilesParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_broken_files > 0 } } ================================================ FILE: czkawka_core/src/tools/duplicate/core.rs ================================================ use std::collections::BTreeMap; use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use std::{mem, thread}; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use indexmap::IndexMap; use log::debug; use rayon::prelude::*; use crate::common::cache::{CACHE_DUPLICATE_VERSION, load_cache_from_file_generalized_by_size, save_cache_to_file_generalized}; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{CheckingMethod, FileEntry, HashType, ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::tools::duplicate::{ DuplicateEntry, DuplicateFinder, DuplicateFinderParameters, Info, PREHASHING_BUFFER_SIZE, THREAD_BUFFER, filter_hard_links, hash_calculation, hash_calculation_limit, }; impl DuplicateFinder { pub fn new(params: DuplicateFinderParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::Duplicate), information: Info::default(), files_with_identical_names: Default::default(), files_with_identical_size: Default::default(), files_with_identical_size_names: Default::default(), files_with_identical_hashes: Default::default(), files_with_identical_names_referenced: Default::default(), files_with_identical_size_names_referenced: Default::default(), files_with_identical_size_referenced: Default::default(), files_with_identical_hashes_referenced: Default::default(), params, } } #[fun_time(message = "check_files_name", level = "debug")] pub(crate) fn check_files_name(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let group_by_func = if self.get_params().case_sensitive_name_comparison { |fe: &FileEntry| { fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\" (cannot panic, because it is always normal file)", fe.path.to_string_lossy())) .to_string_lossy() .to_string() } } else { |fe: &FileEntry| { fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\" (cannot panic, because it is always normal file)", fe.path.to_string_lossy())) .to_string_lossy() .to_lowercase() } }; let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(group_by_func) .stop_flag(stop_flag) .progress_sender(progress_sender) .checking_method(CheckingMethod::Name) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.common_data.text_messages.warnings.extend(warnings); // Create new BTreeMap without single size entries(files have not duplicates) self.files_with_identical_names = grouped_file_entries .into_iter() .filter_map(|(name, vector)| { if vector.len() > 1 { Some((name, vector.into_iter().map(FileEntry::into_duplicate_entry).collect())) } else { None } }) .collect(); // Reference - only use in size, because later hash will be counted differently if self.common_data.use_reference_folders { let vec = mem::take(&mut self.files_with_identical_names) .into_iter() .filter_map(|(_name, vec_file_entry)| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); for (fe, vec_fe) in vec { self.files_with_identical_names_referenced.insert(fe.path.to_string_lossy().to_string(), (fe, vec_fe)); } } self.calculate_name_stats(); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn calculate_name_stats(&mut self) { if self.common_data.use_reference_folders { for (_fe, vector) in self.files_with_identical_names_referenced.values() { self.information.number_of_duplicated_files_by_name += vector.len(); self.information.number_of_groups_by_name += 1; } } else { for vector in self.files_with_identical_names.values() { self.information.number_of_duplicated_files_by_name += vector.len() - 1; self.information.number_of_groups_by_name += 1; } } } #[fun_time(message = "check_files_size_name", level = "debug")] pub(crate) fn check_files_size_name(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let group_by_func = if self.get_params().case_sensitive_name_comparison { |fe: &FileEntry| { ( fe.size, fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\" (cannot panic, because it is always normal file)", fe.path.to_string_lossy())) .to_string_lossy() .to_string(), ) } } else { |fe: &FileEntry| { ( fe.size, fe.path .file_name() .unwrap_or_else(|| panic!("Found invalid file_name \"{}\" (cannot panic, because it is always normal file)", fe.path.to_string_lossy())) .to_string_lossy() .to_lowercase(), ) } }; let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(group_by_func) .stop_flag(stop_flag) .progress_sender(progress_sender) .checking_method(CheckingMethod::SizeName) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.common_data.text_messages.warnings.extend(warnings); self.files_with_identical_size_names = grouped_file_entries .into_iter() .filter_map(|(size_name, vector)| { if vector.len() > 1 { Some((size_name, vector.into_iter().map(FileEntry::into_duplicate_entry).collect())) } else { None } }) .collect(); // Reference - only use in size, because later hash will be counted differently if self.common_data.use_reference_folders { let vec = mem::take(&mut self.files_with_identical_size_names) .into_iter() .filter_map(|(_size, vec_file_entry)| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); for (fe, vec_fe) in vec { self.files_with_identical_size_names_referenced .insert((fe.size, fe.path.to_string_lossy().to_string()), (fe, vec_fe)); } } self.calculate_size_name_stats(); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn calculate_size_name_stats(&mut self) { if self.common_data.use_reference_folders { for ((size, _name), (_fe, vector)) in &self.files_with_identical_size_names_referenced { self.information.number_of_duplicated_files_by_size_name += vector.len(); self.information.number_of_groups_by_size_name += 1; self.information.lost_space_by_size += (vector.len() as u64) * size; } } else { for ((size, _name), vector) in &self.files_with_identical_size_names { self.information.number_of_duplicated_files_by_size_name += vector.len() - 1; self.information.number_of_groups_by_size_name += 1; self.information.lost_space_by_size += (vector.len() as u64 - 1) * size; } } } #[fun_time(message = "check_files_size", level = "debug")] pub(crate) fn check_files_size(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|fe| fe.size) .stop_flag(stop_flag) .progress_sender(progress_sender) .checking_method(self.get_params().check_method) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.common_data.text_messages.warnings.extend(warnings); let grouped_file_entries: Vec<(u64, Vec)> = grouped_file_entries.into_iter().collect(); let rayon_max_len = if self.get_hide_hard_links() { 3 } else { 100 }; let start_time = Instant::now(); // We only gather files with more than 1 entry, because only this will be later used let initial_size = grouped_file_entries .iter() .map(|(_size, vec)| if vec.len() > 1 { vec.len() as u64 } else { 0 }) .sum::(); self.files_with_identical_size = grouped_file_entries .into_par_iter() .with_max_len(rayon_max_len) .filter_map(|(size, vec)| { if vec.len() <= 1 { return None; } let vector = if self.get_hide_hard_links() { filter_hard_links(vec) } else { vec }; if vector.len() > 1 { Some((size, vector.into_iter().map(FileEntry::into_duplicate_entry).collect())) } else { None } }) .collect(); let filtered_size = self.files_with_identical_size.values().map(|v| v.len() as u64).sum::(); debug!( "check_file_size - filtered hard links in {:?}, removed {} hardlinks ({} -> {})", start_time.elapsed(), initial_size - filtered_size, initial_size, filtered_size ); self.filter_reference_folders_by_size(); self.calculate_size_stats(); debug!( "check_file_size - after calculating size stats/duplicates, found in {} groups, {} files with same size | referenced {} groups, {} files", self.files_with_identical_size.len(), self.files_with_identical_size.values().map(Vec::len).sum::(), self.files_with_identical_size_referenced.len(), self.files_with_identical_size_referenced.values().map(|(_fe, vec)| vec.len()).sum::() ); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn calculate_size_stats(&mut self) { if self.common_data.use_reference_folders { for (size, (_fe, vector)) in &self.files_with_identical_size_referenced { self.information.number_of_duplicated_files_by_size += vector.len(); self.information.number_of_groups_by_size += 1; self.information.lost_space_by_size += (vector.len() as u64) * size; } } else { for (size, vector) in &self.files_with_identical_size { self.information.number_of_duplicated_files_by_size += vector.len() - 1; self.information.number_of_groups_by_size += 1; self.information.lost_space_by_size += (vector.len() as u64 - 1) * size; } } } #[fun_time(message = "filter_reference_folders_by_size", level = "debug")] fn filter_reference_folders_by_size(&mut self) { if self.common_data.use_reference_folders && self.get_params().check_method == CheckingMethod::Size { let vec = mem::take(&mut self.files_with_identical_size) .into_iter() .filter_map(|(_size, vec_file_entry)| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); for (fe, vec_fe) in vec { self.files_with_identical_size_referenced.insert(fe.size, (fe, vec_fe)); } } } #[fun_time(message = "prehash_load_cache_at_start", level = "debug")] fn prehash_load_cache_at_start(&mut self) -> (BTreeMap>, BTreeMap>, BTreeMap>) { // Cache algorithm // - Load data from cache // - Convert from BT> to BT // - Save to proper values let loaded_hash_map; let mut records_already_cached: BTreeMap> = Default::default(); let mut non_cached_files_to_check: BTreeMap> = Default::default(); if self.get_params().use_prehash_cache { let (messages, loaded_items) = load_cache_from_file_generalized_by_size::( &get_duplicate_cache_file(self.get_params().hash_type, true), self.get_delete_outdated_cache(), &self.files_with_identical_size, ); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); Self::diff_loaded_and_prechecked_files( "prehash_load_cache_at_start", mem::take(&mut self.files_with_identical_size), &loaded_hash_map, &mut records_already_cached, &mut non_cached_files_to_check, ); } else { loaded_hash_map = Default::default(); mem::swap(&mut self.files_with_identical_size, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } #[fun_time(message = "prehash_save_cache_at_exit", level = "debug")] fn prehash_save_cache_at_exit( &mut self, loaded_hash_map: BTreeMap>, pre_hash_results: Vec<(u64, BTreeMap>, Vec)>, ) { if self.get_params().use_prehash_cache { // All results = records already cached + computed results let mut save_cache_to_hashmap: BTreeMap = Default::default(); for (size, vec_file_entry) in loaded_hash_map { if size >= self.get_params().minimal_prehash_cache_file_size { for file_entry in vec_file_entry { save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } } } for (size, hash_map, _errors) in pre_hash_results { if size >= self.get_params().minimal_prehash_cache_file_size { for vec_file_entry in hash_map.into_values() { for file_entry in vec_file_entry { save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } } } } let messages = save_cache_to_file_generalized( &get_duplicate_cache_file(self.get_params().hash_type, true), &save_cache_to_hashmap, self.common_data.save_also_as_json, self.get_params().minimal_prehash_cache_file_size, ); self.get_text_messages_mut().extend_with_another_messages(messages); } } #[fun_time(message = "prehashing", level = "debug")] fn prehashing( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, pre_checked_map: &mut BTreeMap>, ) -> WorkContinueStatus { if self.files_with_identical_size.is_empty() { return WorkContinueStatus::Continue; } let check_type = self.get_params().hash_type; let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicatePreHashCacheLoading, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.prehash_load_cache_at_start(); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::DuplicatePreHashing, non_cached_files_to_check.values().map(Vec::len).sum(), self.get_test_type(), non_cached_files_to_check .iter() .map(|(size, items)| items.len() as u64 * PREHASHING_BUFFER_SIZE.min(*size)) .sum::(), ); // Convert to vector to be able to use with_max_len method from rayon let non_cached_files_to_check: Vec<(u64, Vec)> = non_cached_files_to_check.into_iter().collect(); debug!("Starting calculating prehash"); #[expect(clippy::type_complexity)] let pre_hash_results: Vec<(u64, BTreeMap>, Vec)> = non_cached_files_to_check .into_par_iter() .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 .map(|(size, vec_file_entry)| { let mut hashmap_with_hash: BTreeMap> = Default::default(); let mut errors: Vec = Vec::new(); THREAD_BUFFER.with_borrow_mut(|buffer| { for mut file_entry in vec_file_entry { if check_if_stop_received(stop_flag) { return None; } match hash_calculation_limit(buffer, &file_entry, check_type, PREHASHING_BUFFER_SIZE, progress_handler.size_counter()) { Ok(hash_string) => { file_entry.hash = hash_string.clone(); hashmap_with_hash.entry(hash_string).or_default().push(file_entry); } Err(s) => errors.push(s), } progress_handler.increase_items(1); } Some(()) })?; Some((size, hashmap_with_hash, errors)) }) .while_some() .collect(); debug!("Completed calculating prehash"); progress_handler.join_thread(); // Saving into cache let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicatePreHashCacheSaving, 0, self.get_test_type(), 0); // Add data from cache for (size, mut vec_file_entry) in records_already_cached { pre_checked_map.entry(size).or_default().append(&mut vec_file_entry); } // Check results for (size, hash_map, errors) in &pre_hash_results { if !errors.is_empty() { self.common_data.text_messages.warnings.append(&mut errors.clone()); } for vec_file_entry in hash_map.values() { if vec_file_entry.len() > 1 { pre_checked_map.entry(*size).or_default().append(&mut vec_file_entry.clone()); } } } self.prehash_save_cache_at_exit(loaded_hash_map, pre_hash_results); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } fn diff_loaded_and_prechecked_files( function_name: &str, used_map: BTreeMap>, loaded_hash_map: &BTreeMap>, records_already_cached: &mut BTreeMap>, non_cached_files_to_check: &mut BTreeMap>, ) { debug!("{function_name} - started diff between loaded and prechecked files"); for (size, mut vec_file_entry) in used_map { if let Some(cached_vec_file_entry) = loaded_hash_map.get(&size) { // TODO maybe hashmap is not needed when using < 4 elements let mut cached_path_entries: IndexMap<&Path, DuplicateEntry> = IndexMap::new(); for file_entry in cached_vec_file_entry { cached_path_entries.insert(&file_entry.path, file_entry.clone()); } for file_entry in vec_file_entry { if let Some(cached_file_entry) = cached_path_entries.swap_remove(file_entry.path.as_path()) { records_already_cached.entry(size).or_default().push(cached_file_entry); } else { non_cached_files_to_check.entry(size).or_default().push(file_entry); } } } else { non_cached_files_to_check.entry(size).or_default().append(&mut vec_file_entry); } } debug!( "{function_name} - completed diff between loaded and prechecked files - {}({}) non cached, {}({}) already cached", non_cached_files_to_check.len(), format_size(non_cached_files_to_check.values().map(|v| v.iter().map(|e| e.size).sum::()).sum::(), BINARY), records_already_cached.len(), format_size(records_already_cached.values().map(|v| v.iter().map(|e| e.size).sum::()).sum::(), BINARY), ); } #[fun_time(message = "full_hashing_load_cache_at_start", level = "debug")] fn full_hashing_load_cache_at_start( &mut self, mut pre_checked_map: BTreeMap>, ) -> (BTreeMap>, BTreeMap>, BTreeMap>) { let loaded_hash_map; let mut records_already_cached: BTreeMap> = Default::default(); let mut non_cached_files_to_check: BTreeMap> = Default::default(); if self.common_data.use_cache { debug!("full_hashing_load_cache_at_start - using cache"); let (messages, loaded_items) = load_cache_from_file_generalized_by_size::( &get_duplicate_cache_file(self.get_params().hash_type, false), self.get_delete_outdated_cache(), &pre_checked_map, ); self.get_text_messages_mut().extend_with_another_messages(messages); loaded_hash_map = loaded_items.unwrap_or_default(); Self::diff_loaded_and_prechecked_files( "full_hashing_load_cache_at_start", pre_checked_map, &loaded_hash_map, &mut records_already_cached, &mut non_cached_files_to_check, ); } else { debug!("full_hashing_load_cache_at_start - not using cache"); loaded_hash_map = Default::default(); mem::swap(&mut pre_checked_map, &mut non_cached_files_to_check); } (loaded_hash_map, records_already_cached, non_cached_files_to_check) } #[fun_time(message = "full_hashing_save_cache_at_exit", level = "debug")] fn full_hashing_save_cache_at_exit( &mut self, records_already_cached: BTreeMap>, full_hash_results: &mut Vec<(u64, BTreeMap>, Vec)>, loaded_hash_map: BTreeMap>, ) { if !self.common_data.use_cache { return; } 'main: for (size, vec_file_entry) in records_already_cached { // 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 for (full_size, full_hashmap, _errors) in &mut (*full_hash_results) { if size == *full_size { for file_entry in vec_file_entry { full_hashmap.entry(file_entry.hash.clone()).or_default().push(file_entry); } continue 'main; } } // Size doesn't exists add results to files let mut temp_hashmap: BTreeMap> = Default::default(); for file_entry in vec_file_entry { temp_hashmap.entry(file_entry.hash.clone()).or_default().push(file_entry); } full_hash_results.push((size, temp_hashmap, Vec::new())); } // Must save all results to file, old loaded from file with all currently counted results let mut all_results: BTreeMap = Default::default(); for (_size, vec_file_entry) in loaded_hash_map { for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } } for (_size, hashmap, _errors) in full_hash_results { for vec_file_entry in hashmap.values() { for file_entry in vec_file_entry { all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone()); } } } let messages = save_cache_to_file_generalized( &get_duplicate_cache_file(self.get_params().hash_type, false), &all_results, self.common_data.save_also_as_json, self.get_params().minimal_cache_file_size, ); self.get_text_messages_mut().extend_with_another_messages(messages); } #[fun_time(message = "full_hashing", level = "debug")] fn full_hashing( &mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, pre_checked_map: BTreeMap>, ) -> WorkContinueStatus { if pre_checked_map.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicateCacheLoading, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.full_hashing_load_cache_at_start(pre_checked_map); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::DuplicateFullHashing, non_cached_files_to_check.values().map(Vec::len).sum(), self.get_test_type(), non_cached_files_to_check.iter().map(|(size, items)| (*size) * items.len() as u64).sum::(), ); let non_cached_files_to_check: Vec<(u64, Vec)> = non_cached_files_to_check.into_iter().collect(); let check_type = self.get_params().hash_type; debug!( "Starting full hashing of {} files", non_cached_files_to_check.iter().map(|(_size, v)| v.len() as u64).sum::() ); let mut full_hash_results: Vec<(u64, BTreeMap>, Vec)> = non_cached_files_to_check .into_par_iter() .with_max_len(3) .map(|(size, vec_file_entry)| { let mut hashmap_with_hash: BTreeMap> = Default::default(); let mut errors: Vec = Vec::new(); THREAD_BUFFER.with_borrow_mut(|buffer| { for mut file_entry in vec_file_entry { if check_if_stop_received(stop_flag) { return None; } match hash_calculation(buffer, &file_entry, check_type, progress_handler.size_counter(), stop_flag) { Ok(hash_string) => { if let Some(hash_string) = hash_string { file_entry.hash = hash_string.clone(); hashmap_with_hash.entry(hash_string).or_default().push(file_entry); } else { return None; } } Err(s) => errors.push(s), } progress_handler.increase_items(1); } Some(()) })?; Some((size, hashmap_with_hash, errors)) }) .while_some() .collect(); debug!("Finished full hashing"); // Even if clicked stop, save items to cache and show results progress_handler.join_thread(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::DuplicateCacheSaving, 0, self.get_test_type(), 0); self.full_hashing_save_cache_at_exit(records_already_cached, &mut full_hash_results, loaded_hash_map); progress_handler.join_thread(); for (size, hash_map, mut errors) in full_hash_results { self.common_data.text_messages.warnings.append(&mut errors); for (_hash, vec_file_entry) in hash_map { if vec_file_entry.len() > 1 { self.files_with_identical_hashes.entry(size).or_default().push(vec_file_entry); } } } WorkContinueStatus::Continue } #[fun_time(message = "hash_reference_folders", level = "debug")] fn hash_reference_folders(&mut self) { // Reference - only use in size, because later hash will be counted differently if self.common_data.use_reference_folders { let vec = mem::take(&mut self.files_with_identical_hashes) .into_iter() .filter_map(|(_size, vec_vec_file_entry)| { let mut all_results_with_same_size = Vec::new(); for vec_file_entry in vec_vec_file_entry { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { continue; } if let Some(file) = files_from_referenced_folders.pop() { all_results_with_same_size.push((file, normal_files)); } } if all_results_with_same_size.is_empty() { None } else { Some(all_results_with_same_size) } }) .collect::)>>>(); #[expect(clippy::indexing_slicing)] // Safe, because here, empty vectors cannot exist for vec_of_vec in vec { self.files_with_identical_hashes_referenced.insert(vec_of_vec[0].0.size, vec_of_vec); } } if self.common_data.use_reference_folders { for (size, vector_vectors) in &self.files_with_identical_hashes_referenced { for (_fe, vector) in vector_vectors { self.information.number_of_duplicated_files_by_hash += vector.len(); self.information.number_of_groups_by_hash += 1; self.information.lost_space_by_hash += (vector.len() as u64) * size; } } } else { for (size, vector_vectors) in &self.files_with_identical_hashes { for vector in vector_vectors { self.information.number_of_duplicated_files_by_hash += vector.len() - 1; self.information.number_of_groups_by_hash += 1; self.information.lost_space_by_hash += (vector.len() as u64 - 1) * size; } } } } #[fun_time(message = "check_files_hash", level = "debug")] pub(crate) fn check_files_hash(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { assert_eq!(self.get_params().check_method, CheckingMethod::Hash); let mut pre_checked_map: BTreeMap> = Default::default(); if self.prehashing(stop_flag, progress_sender, &mut pre_checked_map) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } if self.full_hashing(stop_flag, progress_sender, pre_checked_map) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } self.hash_reference_folders(); // Clean unused data let files_with_identical_size = mem::take(&mut self.files_with_identical_size); thread::spawn(move || drop(files_with_identical_size)); WorkContinueStatus::Continue } } pub fn get_duplicate_cache_file(type_of_hash: HashType, is_prehash: bool) -> String { let prehash_str = if is_prehash { "_prehash" } else { "" }; format!("cache_duplicates_{type_of_hash:?}{prehash_str}_{CACHE_DUPLICATE_VERSION}.bin") } ================================================ FILE: czkawka_core/src/tools/duplicate/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::cell::RefCell; use std::collections::BTreeMap; use std::fmt::Debug; #[cfg(target_family = "unix")] use std::fs; use std::fs::File; use std::hash::Hasher; use std::io::prelude::*; #[cfg(target_family = "unix")] use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::Duration; use indexmap::IndexSet; use serde::{Deserialize, Serialize}; use static_assertions::const_assert; use xxhash_rust::xxh3::Xxh3; use crate::common::model::{CheckingMethod, FileEntry, HashType}; use crate::common::progress_stop_handler::check_if_stop_received; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; use crate::flc; pub const PREHASHING_BUFFER_SIZE: u64 = 4 * 1024; pub const THREAD_BUFFER_SIZE: usize = 2 * 1024 * 1024; thread_local! { static THREAD_BUFFER: RefCell> = RefCell::new(vec![0u8; THREAD_BUFFER_SIZE]); } #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct DuplicateEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, pub hash: String, } impl ResultEntry for DuplicateEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_duplicate_entry(self) -> DuplicateEntry { DuplicateEntry { size: self.size, path: self.path, modified_date: self.modified_date, hash: String::new(), } } } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_groups_by_size: usize, pub number_of_duplicated_files_by_size: usize, pub number_of_groups_by_hash: usize, pub number_of_duplicated_files_by_hash: usize, pub number_of_groups_by_name: usize, pub number_of_duplicated_files_by_name: usize, pub number_of_groups_by_size_name: usize, pub number_of_duplicated_files_by_size_name: usize, pub lost_space_by_size: u64, pub lost_space_by_hash: u64, pub scanning_time: Duration, } #[derive(Clone)] pub struct DuplicateFinderParameters { pub check_method: CheckingMethod, pub hash_type: HashType, pub use_prehash_cache: bool, pub minimal_cache_file_size: u64, pub minimal_prehash_cache_file_size: u64, pub case_sensitive_name_comparison: bool, } impl DuplicateFinderParameters { pub fn new( check_method: CheckingMethod, hash_type: HashType, use_prehash_cache: bool, minimal_cache_file_size: u64, minimal_prehash_cache_file_size: u64, case_sensitive_name_comparison: bool, ) -> Self { Self { check_method, hash_type, use_prehash_cache, minimal_cache_file_size, minimal_prehash_cache_file_size, case_sensitive_name_comparison, } } } pub struct DuplicateFinder { common_data: CommonToolData, information: Info, // File Size, File Entry files_with_identical_names: BTreeMap>, // File (Size, Name), File Entry files_with_identical_size_names: BTreeMap<(u64, String), Vec>, // File Size, File Entry files_with_identical_size: BTreeMap>, // File Size, next grouped by file size, next grouped by hash files_with_identical_hashes: BTreeMap>>, // File Size, File Entry files_with_identical_names_referenced: BTreeMap)>, // File (Size, Name), File Entry files_with_identical_size_names_referenced: BTreeMap<(u64, String), (DuplicateEntry, Vec)>, // File Size, File Entry files_with_identical_size_referenced: BTreeMap)>, // File Size, next grouped by file size, next grouped by hash files_with_identical_hashes_referenced: BTreeMap)>>, params: DuplicateFinderParameters, } #[cfg(target_family = "windows")] fn filter_hard_links(vec_file_entry: Vec) -> Vec { let mut inodes: IndexSet = IndexSet::with_capacity(vec_file_entry.len()); let mut identical: Vec = Vec::with_capacity(vec_file_entry.len()); for f in vec_file_entry { if let Ok(meta) = file_id::get_high_res_file_id(&f.path) { if let file_id::FileId::HighRes { file_id, .. } = meta { if !inodes.insert(file_id) { continue; } } } identical.push(f); } identical } #[cfg(target_family = "unix")] fn filter_hard_links(vec_file_entry: Vec) -> Vec { let mut inodes: IndexSet = IndexSet::with_capacity(vec_file_entry.len()); let mut identical: Vec = Vec::with_capacity(vec_file_entry.len()); for f in vec_file_entry { if let Ok(meta) = fs::metadata(&f.path) && !inodes.insert(meta.ino()) { continue; } identical.push(f); } identical } pub trait MyHasher { fn update(&mut self, bytes: &[u8]); fn finalize(&self) -> String; } impl DuplicateFinder { pub fn get_params(&self) -> &DuplicateFinderParameters { &self.params } pub const fn get_files_sorted_by_names(&self) -> &BTreeMap> { &self.files_with_identical_names } pub const fn get_files_sorted_by_size(&self) -> &BTreeMap> { &self.files_with_identical_size } pub const fn get_files_sorted_by_size_name(&self) -> &BTreeMap<(u64, String), Vec> { &self.files_with_identical_size_names } pub const fn get_files_sorted_by_hash(&self) -> &BTreeMap>> { &self.files_with_identical_hashes } pub const fn get_information(&self) -> Info { self.information } pub fn set_dry_run(&mut self, dry_run: bool) { self.common_data.dry_run = dry_run; } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } pub fn get_files_with_identical_hashes_referenced(&self) -> &BTreeMap)>> { &self.files_with_identical_hashes_referenced } pub fn get_files_with_identical_name_referenced(&self) -> &BTreeMap)> { &self.files_with_identical_names_referenced } pub fn get_files_with_identical_size_referenced(&self) -> &BTreeMap)> { &self.files_with_identical_size_referenced } pub fn get_files_with_identical_size_names_referenced(&self) -> &BTreeMap<(u64, String), (DuplicateEntry, Vec)> { &self.files_with_identical_size_names_referenced } } pub(crate) fn hash_calculation_limit(buffer: &mut [u8], file_entry: &DuplicateEntry, hash_type: HashType, limit: u64, size_counter: &Arc) -> Result { // This function is used only to calculate hash of file with limit // We must ensure that buffer is big enough to store all data // We don't need to check that each time const_assert!(PREHASHING_BUFFER_SIZE <= THREAD_BUFFER_SIZE as u64); let mut file_handler = match File::open(&file_entry.path) { Ok(t) => t, Err(e) => { size_counter.fetch_add(limit, Ordering::Relaxed); return Err(flc!( "core_unable_check_hash_of_file", file = file_entry.path.to_string_lossy().to_string(), reason = e.to_string() )); } }; let hasher = &mut *hash_type.hasher(); #[expect(clippy::indexing_slicing)] // Safe, because limit is always <= buffer size let n = match file_handler.read(&mut buffer[..limit as usize]) { Ok(t) => t, Err(e) => return Err(flc!("core_error_checking_hash_of_file", file = file_entry.path.to_string_lossy(), reason = e.to_string())), }; #[expect(clippy::indexing_slicing)] // Safe, because we read only n bytes, which is always <= limit <= buffer size hasher.update(&buffer[..n]); size_counter.fetch_add(n as u64, Ordering::Relaxed); Ok(hasher.finalize()) } pub fn hash_calculation( buffer: &mut [u8], file_entry: &DuplicateEntry, hash_type: HashType, size_counter: &Arc, stop_flag: &Arc, ) -> Result, String> { let mut file_handler = match File::open(&file_entry.path) { Ok(t) => t, Err(e) => { size_counter.fetch_add(file_entry.size, Ordering::Relaxed); return Err(flc!("core_unable_check_hash_of_file", file = file_entry.path.to_string_lossy(), reason = e.to_string())); } }; let hasher = &mut *hash_type.hasher(); loop { let n = match file_handler.read(buffer) { Ok(0) => break, Ok(t) => t, Err(e) => return Err(flc!("core_error_checking_hash_of_file", file = file_entry.path.to_string_lossy(), reason = e.to_string())), }; #[expect(clippy::indexing_slicing)] // Safe, because we read only n bytes, which is always <= buffer size hasher.update(&buffer[..n]); size_counter.fetch_add(n as u64, Ordering::Relaxed); if check_if_stop_received(stop_flag) { return Ok(None); } } Ok(Some(hasher.finalize())) } impl MyHasher for blake3::Hasher { fn update(&mut self, bytes: &[u8]) { self.update(bytes); } fn finalize(&self) -> String { self.finalize().to_hex().to_string() } } impl MyHasher for crc32fast::Hasher { fn update(&mut self, bytes: &[u8]) { self.write(bytes); } fn finalize(&self) -> String { self.finish().to_string() } } impl MyHasher for Xxh3 { fn update(&mut self, bytes: &[u8]) { self.write(bytes); } fn finalize(&self) -> String { self.finish().to_string() } } #[cfg(test)] mod tests2 { use std::fs::File; use std::io; use super::*; use crate::common::model::FileEntry; use crate::tools::duplicate::filter_hard_links; #[test] fn test_filter_hard_links_empty() { let expected: Vec = Default::default(); assert_eq!(expected, filter_hard_links(Vec::new())); } #[cfg(target_family = "unix")] #[test] fn test_filter_hard_links() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&src)?; fs::hard_link(src.clone(), dst.clone())?; let e1 = FileEntry { path: src, ..Default::default() }; let e2 = FileEntry { path: dst, ..Default::default() }; let actual = filter_hard_links(vec![e1.clone(), e2]); assert_eq!(vec![e1], actual); Ok(()) } #[test] fn test_filter_hard_links_regular_files() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let (src, dst) = (dir.path().join("a"), dir.path().join("b")); File::create(&src)?; File::create(&dst)?; let e1 = FileEntry { path: src, ..Default::default() }; let e2 = FileEntry { path: dst, ..Default::default() }; let actual = filter_hard_links(vec![e1.clone(), e2.clone()]); assert_eq!(vec![e1, e2], actual); Ok(()) } #[test] fn test_hash_calculation() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let mut buf = [0u8; 1 << 10]; let src = dir.path().join("a"); let mut file = File::create(&src)?; file.write_all(b"aaAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFFFFFGGGGGGGGG")?; let e = DuplicateEntry { path: src, ..Default::default() }; let size_counter = Arc::new(AtomicU64::new(0)); let r = hash_calculation(&mut buf, &e, HashType::Blake3, &size_counter, &Arc::default()) .expect("hash_calculation failed") .expect("hash_calculation returned None"); assert!(!r.is_empty()); assert_eq!(size_counter.load(Ordering::Relaxed), 45); Ok(()) } #[test] fn test_hash_calculation_limit() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let mut buf = [0u8; 1000]; let src = dir.path().join("a"); let mut file = File::create(&src)?; file.write_all(b"aa")?; let e = DuplicateEntry { path: src, ..Default::default() }; let size_counter_1 = Arc::new(AtomicU64::new(0)); let size_counter_2 = Arc::new(AtomicU64::new(0)); let size_counter_3 = Arc::new(AtomicU64::new(0)); let r1 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 1, &size_counter_1).expect("hash_calculation failed"); let r2 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 2, &size_counter_2).expect("hash_calculation failed"); let r3 = hash_calculation_limit(&mut buf, &e, HashType::Blake3, 1000, &size_counter_3).expect("hash_calculation failed"); assert_ne!(r1, r2); assert_eq!(r2, r3); assert_eq!(1, size_counter_1.load(Ordering::Relaxed)); assert_eq!(2, size_counter_2.load(Ordering::Relaxed)); assert_eq!(2, size_counter_3.load(Ordering::Relaxed)); Ok(()) } #[test] fn test_hash_calculation_invalid_file() -> io::Result<()> { let dir = tempfile::Builder::new().tempdir()?; let mut buf = [0u8; 1 << 10]; let src = dir.path().join("a"); let e = DuplicateEntry { path: src, ..Default::default() }; let r = hash_calculation(&mut buf, &e, HashType::Blake3, &Arc::default(), &Arc::default()).expect_err("hash_calculation succeeded"); assert!(!r.is_empty()); Ok(()) } } ================================================ FILE: czkawka_core/src/tools/duplicate/tests.rs ================================================ use std::fs; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::model::{CheckingMethod, HashType}; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters}; #[test] fn test_find_duplicates_by_hash() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create duplicate files with same content fs::write(path.join("file1.txt"), b"duplicate content").unwrap(); fs::write(path.join("file2.txt"), b"duplicate content").unwrap(); fs::write(path.join("unique.txt"), b"unique content").unwrap(); let params = DuplicateFinderParameters::new(CheckingMethod::Hash, HashType::Blake3, false, 0, 0, true); let mut finder = DuplicateFinder::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_minimal_file_size(0); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_groups_by_hash, 1, "Should find 1 group of duplicates"); assert_eq!(info.number_of_duplicated_files_by_hash, 1, "Should find 1 duplicate file"); } #[test] fn test_find_duplicates_by_size() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create files with same size fs::write(path.join("file1.txt"), b"12345").unwrap(); fs::write(path.join("file2.txt"), b"abcde").unwrap(); fs::write(path.join("unique.txt"), b"123").unwrap(); let params = DuplicateFinderParameters::new(CheckingMethod::Size, HashType::Blake3, false, 0, 0, true); let mut finder = DuplicateFinder::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); finder.set_minimal_file_size(0); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_groups_by_size, 1, "Should find 1 group by size"); assert_eq!(info.number_of_duplicated_files_by_size, 1, "Should find 1 duplicate by size"); } #[test] fn test_find_duplicates_by_name() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let dir1 = path.join("dir1"); let dir2 = path.join("dir2"); fs::create_dir(&dir1).unwrap(); fs::create_dir(&dir2).unwrap(); // Create files with same name in different directories fs::write(dir1.join("duplicate.txt"), b"content1").unwrap(); fs::write(dir2.join("duplicate.txt"), b"content2").unwrap(); fs::write(dir1.join("unique.txt"), b"unique").unwrap(); let params = DuplicateFinderParameters::new(CheckingMethod::Name, HashType::Blake3, false, 0, 0, true); let mut finder = DuplicateFinder::new(params); finder.set_recursive_search(true); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_minimal_file_size(0); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_groups_by_name, 1, "Should find 1 group by name"); assert_eq!(info.number_of_duplicated_files_by_name, 1, "Should find 1 duplicate by name"); } #[test] fn test_no_duplicates_found() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create unique files fs::write(path.join("file1.txt"), b"content1").unwrap(); fs::write(path.join("file2.txt"), b"content2").unwrap(); let params = DuplicateFinderParameters::new(CheckingMethod::Hash, HashType::Blake3, false, 0, 0, true); let mut finder = DuplicateFinder::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); finder.set_minimal_file_size(0); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_groups_by_hash, 0, "Should find no duplicate groups"); assert_eq!(info.lost_space_by_hash, 0, "Should have no lost space"); } #[test] fn test_lost_space_calculation() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create 3 files with 100 bytes each, all duplicates let content = vec![b'A'; 100]; fs::write(path.join("file1.txt"), &content).unwrap(); fs::write(path.join("file2.txt"), &content).unwrap(); fs::write(path.join("file3.txt"), &content).unwrap(); let params = DuplicateFinderParameters::new(CheckingMethod::Hash, HashType::Blake3, false, 0, 0, true); let mut finder = DuplicateFinder::new(params); finder.set_minimal_file_size(0); finder.set_use_cache(false); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.lost_space_by_hash, 200, "Should calculate 200 bytes lost space (2 duplicate files * 100 bytes)"); } ================================================ FILE: czkawka_core/src/tools/duplicate/traits.rs ================================================ use std::io::prelude::*; use std::io::{self}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::model::{CheckingMethod, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters, Info}; impl AllTraits for DuplicateFinder {} impl DeletingItems for DuplicateFinder { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.common_data.delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = match self.get_params().check_method { CheckingMethod::Name => self.files_with_identical_names.values().cloned().collect::>(), CheckingMethod::SizeName => self.files_with_identical_size_names.values().cloned().collect::>(), CheckingMethod::Hash => self.files_with_identical_hashes.values().flatten().cloned().collect::>(), CheckingMethod::Size => self.files_with_identical_size.values().cloned().collect::>(), _ => panic!(), }; self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } impl Search for DuplicateFinder { #[fun_time(message = "find_duplicates", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty(); match self.get_params().check_method { CheckingMethod::Name => { self.common_data.stopped_search = self.check_files_name(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } CheckingMethod::SizeName => { self.common_data.stopped_search = self.check_files_size_name(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } CheckingMethod::Size => { self.common_data.stopped_search = self.check_files_size(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } CheckingMethod::Hash => { self.common_data.stopped_search = self.check_files_size(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } self.common_data.stopped_search = self.check_files_hash(stop_flag, progress_sender) == WorkContinueStatus::Stop; if self.common_data.stopped_search { return; } } _ => panic!(), } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); self.debug_print(); } } impl DebugPrint for DuplicateFinder { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); println!( "Number of duplicated files by size(in groups) - {} ({})", self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size ); println!( "Number of duplicated files by hash(in groups) - {} ({})", self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash ); println!( "Number of duplicated files by name(in groups) - {} ({})", self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name ); println!( "Lost space by size - {} ({} bytes)", format_size(self.information.lost_space_by_size, BINARY), self.information.lost_space_by_size ); println!( "Lost space by hash - {} ({} bytes)", format_size(self.information.lost_space_by_hash, BINARY), self.information.lost_space_by_hash ); println!("### Other"); println!("Files list size - {}", self.files_with_identical_size.len()); println!("Hashed files list size - {}", self.files_with_identical_hashes.len()); println!("Files with identical names - {}", self.files_with_identical_names.len()); println!("Files with identical size names - {}", self.files_with_identical_size_names.len()); println!("Files with identical names referenced - {}", self.files_with_identical_names_referenced.len()); println!("Files with identical size names referenced - {}", self.files_with_identical_size_names_referenced.len()); println!("Files with identical size referenced - {}", self.files_with_identical_size_referenced.len()); println!("Files with identical hashes referenced - {}", self.files_with_identical_hashes_referenced.len()); println!("Checking Method - {:?}", self.get_params().check_method); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for DuplicateFinder { fn write_results(&self, writer: &mut T) -> io::Result<()> { self.write_base_search_paths(writer)?; match self.get_params().check_method { CheckingMethod::Name => { if !self.files_with_identical_names.is_empty() { writeln!( writer, "-------------------------------------------------Files with same names-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same name(may have different content)", self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name, )?; for (name, vector) in self.files_with_identical_names.iter().rev() { writeln!(writer, "Name - {} - {} files ", name, vector.len())?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else if !self.files_with_identical_names_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same names in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same name(may have different content)", self.information.number_of_duplicated_files_by_name, self.information.number_of_groups_by_name, )?; for (name, (file_entry, vector)) in self.files_with_identical_names_referenced.iter().rev() { writeln!(writer, "Name - {} - {} files ", name, vector.len())?; writeln!(writer, "Reference file - \"{}\"", file_entry.path.to_string_lossy())?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else { write!(writer, "Not found any files with same names.")?; } } CheckingMethod::SizeName => { if !self.files_with_identical_names.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size and names-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same size and name(may have different content)", self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name, )?; for ((size, name), vector) in self.files_with_identical_size_names.iter().rev() { writeln!(writer, "Name - {}, {} - {} files ", name, format_size(*size, BINARY), vector.len())?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else if !self.files_with_identical_names_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size and names in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} files in {} groups with same size and name(may have different content)", self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name, )?; for ((size, name), (file_entry, vector)) in self.files_with_identical_size_names_referenced.iter().rev() { writeln!(writer, "Name - {}, {} - {} files ", name, format_size(*size, BINARY), vector.len())?; writeln!(writer, "Reference file - \"{}\"", file_entry.path.to_string_lossy())?; for j in vector { writeln!(writer, "\"{}\"", j.path.to_string_lossy())?; } writeln!(writer)?; } } else { write!(writer, "Not found any files with same size and names.")?; } } CheckingMethod::Size => { if !self.files_with_identical_size.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size, format_size(self.information.lost_space_by_size, BINARY) )?; for (size, vector) in self.files_with_identical_size.iter().rev() { write!(writer, "\n---- Size {} ({}) - {} files \n", format_size(*size, BINARY), size, vector.len())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } else if !self.files_with_identical_size_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same size in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_size, self.information.number_of_groups_by_size, format_size(self.information.lost_space_by_size, BINARY) )?; for (size, (file_entry, vector)) in self.files_with_identical_size_referenced.iter().rev() { writeln!(writer, "\n---- Size {} ({}) - {} files", format_size(*size, BINARY), size, vector.len())?; writeln!(writer, "Reference file - \"{}\"", file_entry.path.to_string_lossy())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } else { write!(writer, "Not found any duplicates.")?; } } CheckingMethod::Hash => { if !self.files_with_identical_hashes.is_empty() { writeln!( writer, "-------------------------------------------------Files with same hashes-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash, format_size(self.information.lost_space_by_hash, BINARY) )?; for (size, vectors_vector) in self.files_with_identical_hashes.iter().rev() { for vector in vectors_vector { writeln!(writer, "\n---- Size {} ({}) - {} files", format_size(*size, BINARY), size, vector.len())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } } else if !self.files_with_identical_hashes_referenced.is_empty() { writeln!( writer, "-------------------------------------------------Files with same hashes in referenced folders-------------------------------------------------" )?; writeln!( writer, "Found {} duplicated files which in {} groups which takes {}.", self.information.number_of_duplicated_files_by_hash, self.information.number_of_groups_by_hash, format_size(self.information.lost_space_by_hash, BINARY) )?; for (size, vectors_vector) in self.files_with_identical_hashes_referenced.iter().rev() { for (file_entry, vector) in vectors_vector { writeln!(writer, "\n---- Size {} ({}) - {} files", format_size(*size, BINARY), size, vector.len())?; writeln!(writer, "Reference file - \"{}\"", file_entry.path.to_string_lossy())?; for file_entry in vector { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } } } else { write!(writer, "Not found any duplicates.")?; } } _ => panic!(), } Ok(()) } // TODO - check if is possible to save also data in header about size and name in SizeName mode - https://github.com/qarmin/czkawka/issues/1137 fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> io::Result<()> { if self.get_use_reference() { match self.get_params().check_method { CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names_referenced, pretty_print), CheckingMethod::SizeName => { self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names_referenced.values().collect::>(), pretty_print) } CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_referenced, pretty_print), CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes_referenced, pretty_print), _ => panic!(), } } else { match self.get_params().check_method { CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names, pretty_print), CheckingMethod::SizeName => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names.values().collect::>(), pretty_print), CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size, pretty_print), CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes, pretty_print), _ => panic!(), } } } } impl CommonData for DuplicateFinder { type Info = Info; type Parameters = DuplicateFinderParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn get_check_method(&self) -> CheckingMethod { self.get_params().check_method } fn found_any_items(&self) -> bool { self.get_information().number_of_duplicated_files_by_hash > 0 || self.get_information().number_of_duplicated_files_by_name > 0 || self.get_information().number_of_duplicated_files_by_size > 0 || self.get_information().number_of_duplicated_files_by_size_name > 0 } } ================================================ FILE: czkawka_core/src/tools/empty_files/core.rs ================================================ use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::CommonToolData; use crate::tools::empty_files::{EmptyFiles, Info}; impl EmptyFiles { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::EmptyFiles), information: Info::default(), empty_files: Vec::new(), } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .minimal_file_size(0) .maximal_file_size(0) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.empty_files = grouped_file_entries.into_values().flatten().collect(); self.information.number_of_empty_files = self.empty_files.len(); self.common_data.text_messages.warnings.extend(warnings); debug!("Found {} empty files.", self.information.number_of_empty_files); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } } ================================================ FILE: czkawka_core/src/tools/empty_files/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::time::Duration; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_empty_files: usize, pub scanning_time: Duration, } pub struct EmptyFiles { common_data: CommonToolData, information: Info, empty_files: Vec, } impl Default for EmptyFiles { fn default() -> Self { Self::new() } } impl EmptyFiles { pub const fn get_empty_files(&self) -> &Vec { &self.empty_files } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/empty_files/tests.rs ================================================ use std::fs; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::empty_files::EmptyFiles; #[test] fn test_find_empty_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create empty files fs::write(path.join("empty1.txt"), b"").unwrap(); fs::write(path.join("empty2.txt"), b"").unwrap(); fs::write(path.join("not_empty.txt"), b"content").unwrap(); let mut finder = EmptyFiles::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_empty_files, 2, "Should find 2 empty files"); assert_eq!(finder.get_empty_files().len(), 2, "Empty files list should contain 2 files"); } #[test] fn test_no_empty_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create only non-empty files fs::write(path.join("file1.txt"), b"content1").unwrap(); fs::write(path.join("file2.txt"), b"content2").unwrap(); let mut finder = EmptyFiles::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_empty_files, 0, "Should find no empty files"); assert!(finder.get_empty_files().is_empty(), "Empty files list should be empty"); } #[test] fn test_recursive_search_empty_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let subdir = path.join("subdir"); fs::create_dir(&subdir).unwrap(); // Create empty files in different directories fs::write(path.join("empty1.txt"), b"").unwrap(); fs::write(subdir.join("empty2.txt"), b"").unwrap(); let mut finder = EmptyFiles::new(); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_empty_files, 2, "Should find empty files in subdirectories"); } ================================================ FILE: czkawka_core/src/tools/empty_files/traits.rs ================================================ use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::empty_files::{EmptyFiles, Info}; impl AllTraits for EmptyFiles {} impl Search for EmptyFiles { #[fun_time(message = "find_empty_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for EmptyFiles { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); println!("Empty list size - {}", self.empty_files.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for EmptyFiles { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.empty_files.is_empty() { writeln!(writer, "Found {} empty files.", self.information.number_of_empty_files)?; for file_entry in &self.empty_files { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } } else { write!(writer, "Not found any empty files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.empty_files, pretty_print) } } impl CommonData for EmptyFiles { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_empty_files > 0 } } impl DeletingItems for EmptyFiles { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.empty_files.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } ================================================ FILE: czkawka_core/src/tools/empty_folder/core.rs ================================================ use std::fs::DirEntry; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use indexmap::IndexMap; use log::debug; use rayon::prelude::*; use crate::common::dir_traversal::{common_get_entry_data, common_get_metadata_dir, common_read_dir, get_modified_time}; use crate::common::directories::Directories; use crate::common::items::ExcludedItems; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::empty_folder::{EmptyFolder, FolderEmptiness, FolderEntry, Info}; impl EmptyFolder { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::EmptyFolders), information: Default::default(), empty_folder_list: Default::default(), } } pub const fn get_empty_folder_list(&self) -> &IndexMap { &self.empty_folder_list } pub const fn get_information(&self) -> Info { self.information } pub(crate) fn optimize_folders(&mut self) { let mut new_directory_folders: IndexMap = Default::default(); for (name, folder_entry) in &self.empty_folder_list { match &folder_entry.parent_path { Some(t) => { if !self.empty_folder_list.contains_key(t) { new_directory_folders.insert(name.clone(), folder_entry.clone()); } } None => { new_directory_folders.insert(name.clone(), folder_entry.clone()); } } } self.empty_folder_list = new_directory_folders; self.information.number_of_empty_folders = self.empty_folder_list.len(); } #[fun_time(message = "check_for_empty_folders", level = "debug")] pub(crate) fn check_for_empty_folders(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let mut folders_to_check: Vec = self.common_data.directories.included_directories.clone(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::CollectingFiles, 0, self.get_test_type(), 0); let excluded_items = self.common_data.excluded_items.clone(); let directories = self.common_data.directories.clone(); let mut non_empty_folders: Vec = Vec::new(); let mut start_folder_entries = Vec::with_capacity(folders_to_check.len()); let mut new_folder_entries_list = Vec::new(); for dir in &folders_to_check { start_folder_entries.push(FolderEntry { path: dir.clone(), parent_path: None, is_empty: FolderEmptiness::Maybe, modified_date: 0, }); } while !folders_to_check.is_empty() { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } let segments: Vec<_> = folders_to_check .into_par_iter() .map(|current_folder| { let mut dir_result = Vec::new(); let mut warnings = Vec::new(); let mut non_empty_folder = None; let mut folder_entries_list = Vec::new(); let current_folder_as_string = current_folder.to_string_lossy().to_string(); let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return (dir_result, warnings, Some(current_folder_as_string), folder_entries_list); }; let mut counter = 0; // Check every sub folder/file/link etc. for entry in read_dir { let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, ¤t_folder) else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue }; if file_type.is_dir() { counter += 1; Self::process_dir_in_dir_mode( ¤t_folder, ¤t_folder_as_string, entry_data, &directories, &mut dir_result, &mut warnings, &excluded_items, &mut non_empty_folder, &mut folder_entries_list, ); } else if non_empty_folder.is_none() { non_empty_folder = Some(current_folder_as_string.clone()); } } if counter > 0 { // Increase counter in batch, because usually it may be slow to add multiple times atomic value progress_handler.increase_items(counter); } (dir_result, warnings, non_empty_folder, folder_entries_list) }) .collect(); let required_size = segments.iter().map(|(segment, _, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, non_empty_folder, fe_list) in segments { folders_to_check.extend(segment); if !warnings.is_empty() { self.common_data.text_messages.warnings.extend(warnings); } if let Some(non_empty_folder) = non_empty_folder { non_empty_folders.push(non_empty_folder); } new_folder_entries_list.push(fe_list); } } let mut folder_entries: IndexMap = IndexMap::with_capacity(start_folder_entries.len() + new_folder_entries_list.iter().map(Vec::len).sum::()); for fe in start_folder_entries { folder_entries.insert(fe.path.to_string_lossy().to_string(), fe); } for fe_list in new_folder_entries_list { for fe in fe_list { folder_entries.insert(fe.path.to_string_lossy().to_string(), fe); } } for current_folder in non_empty_folders.into_iter().rev() { Self::set_as_not_empty_folder(&mut folder_entries, ¤t_folder); } for (name, folder_entry) in folder_entries { if folder_entry.is_empty != FolderEmptiness::No { self.empty_folder_list.insert(name, folder_entry); } } debug!("Found {} empty folders.", self.empty_folder_list.len()); progress_handler.join_thread(); WorkContinueStatus::Continue } pub(crate) fn set_as_not_empty_folder(folder_entries: &mut IndexMap, current_folder: &str) { let mut d = folder_entries .get_mut(current_folder) .unwrap_or_else(|| panic!("Folder {current_folder} not found in folder_entries (cannot panic, because we first added parent folders)")); if d.is_empty == FolderEmptiness::No { return; // Already set as non empty by one of its child } // Loop to recursively set as non empty this and all its parent folders loop { d.is_empty = FolderEmptiness::No; if let Some(parent_path) = &d.parent_path { let cf = parent_path.clone(); d = folder_entries .get_mut(&cf) .unwrap_or_else(|| panic!("Folder {cf} not found in folder_entries (cannot panic, because we first added parent folders)")); if d.is_empty == FolderEmptiness::No { break; // Already set as non empty, so one of child already set it to non empty } } else { break; } } } fn process_dir_in_dir_mode( current_folder: &Path, current_folder_as_str: &str, entry_data: &DirEntry, directories: &Directories, dir_result: &mut Vec, warnings: &mut Vec, excluded_items: &ExcludedItems, non_empty_folder: &mut Option, folder_entries_list: &mut Vec, ) { let next_folder = entry_data.path(); if excluded_items.is_excluded(&next_folder) || directories.is_excluded_dir(&next_folder) { if non_empty_folder.is_none() { *non_empty_folder = Some(current_folder_as_str.to_string()); } return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&next_folder) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &next_folder) else { if non_empty_folder.is_none() { *non_empty_folder = Some(current_folder_as_str.to_string()); } return; }; dir_result.push(next_folder.clone()); folder_entries_list.push(FolderEntry { path: next_folder, parent_path: Some(current_folder_as_str.to_string()), is_empty: FolderEmptiness::Maybe, modified_date: get_modified_time(&metadata, warnings, current_folder, true), }); } } ================================================ FILE: czkawka_core/src/tools/empty_folder/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::path::{Path, PathBuf}; use std::time::Duration; use indexmap::IndexMap; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; #[derive(Clone, Debug)] pub struct FolderEntry { pub path: PathBuf, pub(crate) parent_path: Option, // Usable only when finding pub(crate) is_empty: FolderEmptiness, pub modified_date: u64, } impl ResultEntry for FolderEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { 0 } } pub struct EmptyFolder { common_data: CommonToolData, information: Info, empty_folder_list: IndexMap, // Path, FolderEntry } /// Enum with values which show if folder is empty. /// In function "`optimize_folders`" automatically "Maybe" is changed to "Yes", so it is not necessary to put it here #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub(crate) enum FolderEmptiness { No, Maybe, } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_empty_folders: usize, pub scanning_time: Duration, } impl Default for EmptyFolder { fn default() -> Self { Self::new() } } ================================================ FILE: czkawka_core/src/tools/empty_folder/tests.rs ================================================ use std::fs; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::empty_folder::EmptyFolder; #[test] fn test_find_empty_folders() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create empty directories fs::create_dir(path.join("empty1")).unwrap(); fs::create_dir(path.join("empty2")).unwrap(); // Create non-empty directory let non_empty = path.join("non_empty"); fs::create_dir(&non_empty).unwrap(); fs::write(non_empty.join("file.txt"), b"content").unwrap(); let mut finder = EmptyFolder::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_empty_folders, 2, "Should find 2 empty folders"); } #[test] fn test_nested_empty_folders() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create nested empty directories let parent = path.join("parent"); let child = parent.join("child"); fs::create_dir(&parent).unwrap(); fs::create_dir(&child).unwrap(); let mut finder = EmptyFolder::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); // After optimization, only the deepest empty folder should be counted let info = finder.get_information(); assert!(info.number_of_empty_folders > 0, "Should find empty folders"); } #[test] fn test_no_empty_folders() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create directory with file let dir = path.join("dir"); fs::create_dir(&dir).unwrap(); fs::write(dir.join("file.txt"), b"content").unwrap(); let mut finder = EmptyFolder::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_empty_folders, 0, "Should find no empty folders"); } #[test] fn test_folder_with_only_empty_subfolders() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); // Create parent with only empty subdirectories let parent = path.join("parent"); fs::create_dir(&parent).unwrap(); fs::create_dir(parent.join("empty_child1")).unwrap(); fs::create_dir(parent.join("empty_child2")).unwrap(); let mut finder = EmptyFolder::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); // Parent and children are all empty assert!( info.number_of_empty_folders == 1, "Should find 1 empty folder (the parent) - which contains only empty subfolders" ); } ================================================ FILE: czkawka_core/src/tools/empty_folder/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use rayon::prelude::*; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::empty_folder::{EmptyFolder, Info}; impl AllTraits for EmptyFolder {} impl Search for EmptyFolder { #[fun_time(message = "find_empty_folders", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } if self.check_for_empty_folders(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } self.optimize_folders(); if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for EmptyFolder { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); println!("Number of empty folders - {}", self.information.number_of_empty_folders); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for EmptyFolder { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.empty_folder_list.is_empty() { writeln!(writer, "--------------------------Empty folder list--------------------------")?; writeln!(writer, "Found {} empty folders", self.information.number_of_empty_folders)?; let mut empty_folder_list = self.empty_folder_list.keys().collect::>(); empty_folder_list.par_sort_unstable(); for name in empty_folder_list { writeln!(writer, "{name}")?; } } else { write!(writer, "Not found any empty folders.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.empty_folder_list.keys().collect::>(), pretty_print) } } impl CommonData for EmptyFolder { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_empty_folders > 0 } } impl DeletingItems for EmptyFolder { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages( stop_flag, progress_sender, DeleteItemType::DeletingFolders(self.empty_folder_list.values().cloned().collect::>()), ), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } ================================================ FILE: czkawka_core/src/tools/exif_remover/core.rs ================================================ use std::collections::BTreeMap; use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{fs, mem, panic}; use crossbeam_channel::Sender; use fun_time::fun_time; use little_exif::filetype::FileExtension; use little_exif::ifd::ExifTagGroup; use little_exif::metadata::Metadata; use log::{debug, error}; use rayon::prelude::*; use crate::common::cache::{CACHE_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path}; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::flc; use crate::tools::exif_remover::{ExifEntry, ExifRemover, ExifRemoverParameters, ExifTagInfo, ExifTagsFixerParams, Info}; impl ExifRemover { pub fn new(params: ExifRemoverParameters) -> Self { let mut additional_excluded_tags = BTreeMap::new(); let tiff_disabled_tags = vec![ "ImageWidth", "ImageHeight", "BitsPerSample", "Compression", "PhotometricInterpretation", "StripOffsets", "SamplesPerPixel", "RowsPerStrip", "StripByteCounts", "PlanarConfiguration", ]; for i in ["tif", "tiff"] { additional_excluded_tags.insert(i, tiff_disabled_tags.clone()); } Self { common_data: CommonToolData::new(ToolType::ExifRemover), information: Info::default(), exif_files: Vec::new(), files_to_check: Default::default(), params, additional_excluded_tags, } } #[fun_time(message = "find_exif_files", level = "debug")] pub(crate) fn find_exif_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.files_to_check = grouped_file_entries .into_values() .flatten() .map(|fe| { let exif_entry = ExifEntry { path: fe.path.clone(), size: fe.size, modified_date: fe.modified_date, exif_tags: Vec::new(), error: None, }; (fe.path.to_string_lossy().to_string(), exif_entry) }) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("find_exif_files - Found {} files to check.", self.files_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "load_cache", level = "debug")] fn load_cache( &mut self, _stop_flag: &Arc, progress_sender: Option<&Sender>, ) -> (BTreeMap, BTreeMap, BTreeMap) { let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::ExifRemoverCacheLoading, 0, self.get_test_type(), 0); let res = load_and_split_cache_generalized_by_path(&get_exif_remover_cache_file(), mem::take(&mut self.files_to_check), self); progress_handler.join_thread(); res } #[fun_time(message = "save_to_cache", level = "debug")] fn save_to_cache( &mut self, vec_file_entry: &[ExifEntry], loaded_hash_map: BTreeMap, _stop_flag: &Arc, progress_sender: Option<&Sender>, ) { let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::ExifRemoverCacheSaving, 0, self.get_test_type(), 0); save_and_connect_cache_generalized_by_path(&get_exif_remover_cache_file(), vec_file_entry, loaded_hash_map, self); progress_handler.join_thread(); } #[fun_time(message = "check_exif_in_files", level = "debug")] pub(crate) fn check_exif_in_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.files_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(stop_flag, progress_sender); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::ExifRemoverExtractingTags, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|item| item.size).sum::(), ); let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::>(); debug!("check_exif_in_files - started extracting EXIF data"); let mut vec_file_entry: Vec = non_cached_files_to_check .into_par_iter() .map(|(_, mut file_entry)| { if check_if_stop_received(stop_flag) { return None; } let size = file_entry.size; let res = extract_exif_tags(&file_entry.path); progress_handler.increase_items(1); progress_handler.increase_size(size); match res { Ok(tags) => { file_entry.exif_tags = tags.into_iter().map(|(name, code, group)| ExifTagInfo { name, code, group }).collect(); } Err(e) => { file_entry.error = Some(format!("Failed to extract Exif data for file \"{}\": {}", file_entry.path.to_string_lossy(), e)); } } Some(file_entry) }) .while_some() .collect(); debug!("check_exif_in_files - finished extracting EXIF data"); progress_handler.join_thread(); vec_file_entry.extend(records_already_cached.into_values()); self.save_to_cache(&vec_file_entry, loaded_hash_map, stop_flag, progress_sender); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } // After saving to cache, remove ignored tags - because in cache we need to store full info about tags for entry in &mut vec_file_entry { let extension = entry.path.extension().and_then(|ext| ext.to_str()).unwrap_or("").to_lowercase(); if let Some(additional_ignored_tags) = self.additional_excluded_tags.get(&extension.as_str()) { entry.exif_tags.retain(|tag_item| !additional_ignored_tags.contains(&tag_item.name.as_str())); } if self.params.ignored_tags.is_empty() { continue; } entry.exif_tags.retain(|tag_item| !self.params.ignored_tags.contains(&tag_item.name)); } self.exif_files = vec_file_entry.into_iter().filter(|f| f.error.is_none() && !f.exif_tags.is_empty()).collect(); self.exif_files.iter_mut().for_each(|file| file.exif_tags.sort_unstable_by(|a, b| a.name.cmp(&b.name))); self.information.number_of_files_with_exif = self.exif_files.len(); debug!("Found {} files with EXIF data.", self.information.number_of_files_with_exif); self.files_to_check = Default::default(); WorkContinueStatus::Continue } #[fun_time(message = "fix_files", level = "debug")] pub(crate) fn fix_files(&mut self, stop_flag: &Arc, _progress_sender: Option<&Sender>, fix_params: ExifTagsFixerParams) { let warnings: Vec<_> = mem::take(&mut self.exif_files) .into_par_iter() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } let exif_data_to_remove: Vec<(u16, String)> = entry.exif_tags.iter().map(|item_tag| (item_tag.code, item_tag.group.clone())).collect(); match clean_exif_tags(&entry.path.to_string_lossy(), &exif_data_to_remove, fix_params.override_file) { Ok(_number_removed_tags) => Some(None), Err(e) => Some(Some(format!("Failed to clean EXIF tags for file \"{}\": {}", entry.path.to_string_lossy(), e))), } }) .while_some() .flatten() .collect(); self.common_data.text_messages.warnings.extend(warnings); } } pub fn clean_exif_tags(file_path: &str, tags_to_remove: &[(u16, String)], override_file: bool) -> Result { panic::catch_unwind(|| { let file_path = Path::new(file_path); let mut file_data = fs::read(file_path).map_err(|e| e.to_string())?; let mut cursor = std::io::Cursor::new(&file_data); let ext = FileExtension::auto_detect(&mut cursor).ok_or_else(|| "Failed to detect file type".to_string())?; let metadata = Metadata::new_from_vec(&file_data, ext).map_err(|e| format!("Failed to read EXIF: {e}"))?; let mut new_metadata = metadata; let mut tags_removed: u32 = 0; for (tag_u16, tag_group) in tags_to_remove { let Ok(tag_group) = string_to_exif_tag_group(tag_group) else { error!("Unknown EXIF tag group string: {tag_group}, skipping tag removal."); continue; }; new_metadata.remove_tag_by_hex_group(*tag_u16, tag_group); tags_removed += 1; } new_metadata.write_to_vec(&mut file_data, ext).map_err(|e| e.to_string())?; if override_file { fs::write(file_path, file_data).map_err(|e| e.to_string())?; } else { let extension = file_path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); let new_file_path = file_path.with_extension(format!("czkawka_cleaned_exif.{extension}")); fs::write(new_file_path, file_data).map_err(|e| e.to_string())?; } Ok(tags_removed) }) .map_err(|e| format!("Panic occurred while reading EXIF: {e:?}"))? .map_err(|e: String| format!("Failed to remove EXIF from file {file_path} - {e}")) } pub fn extract_exif_tags_public(path: &Path) -> Result, String> { let tags = extract_exif_tags(path)?; Ok(tags.into_iter().map(|(_, code, group)| (code, group)).collect()) } fn extract_exif_tags(path: &Path) -> Result, String> { panic::catch_unwind(|| { let file_path = Path::new(path); let data = fs::read(file_path).map_err(|e| e.to_string())?; let mut cursor = std::io::Cursor::new(&data); let ext = FileExtension::auto_detect(&mut cursor).ok_or_else(|| "Failed to detect file type".to_string())?; let metadata = Metadata::new_from_vec(&data, ext).map_err(|e| format!("Failed to read EXIF: {e}"))?; let mut tags = Vec::new(); for tag in &metadata { let tag_name = format!("{tag:?}"); let tag_u16 = tag.as_u16(); let tag_group = exif_tag_group_to_string(tag.get_group()); if let Some(pos) = tag_name.find('(') { #[expect(clippy::string_slice)] // Safe, because pos is from find tags.push((tag_name[..pos].to_string(), tag_u16, tag_group)); } else { tags.push((tag_name, tag_u16, tag_group)); } } Ok(tags) }) .map_err(|e| format!("Panic occurred while reading \"{}\" - EXIF: {e:?}", path.to_string_lossy()))? } pub fn file_extension_to_string(extension: FileExtension) -> &'static str { match extension { FileExtension::PNG { .. } => "PNG", FileExtension::JPEG => "JPEG", FileExtension::TIFF => "TIFF", FileExtension::WEBP => "WEBP", FileExtension::NAKED_JXL => "NAKED_JXL", FileExtension::JXL => "JXL", FileExtension::HEIF => "HEIF", } } pub fn string_to_file_extension(s: &str) -> FileExtension { match s { "PNG" => FileExtension::PNG { as_zTXt_chunk: true }, "JPEG" => FileExtension::JPEG, "TIFF" => FileExtension::TIFF, "WEBP" => FileExtension::WEBP, "NAKED_JXL" => FileExtension::NAKED_JXL, "JXL" => FileExtension::JXL, "HEIF" => FileExtension::HEIF, _ => { error!("Unknown file extension string: {s}, defaulting to JPEG"); FileExtension::JPEG } // Default to JPEG } } // Nom-exif implementation // Probably will use this version in future // fn extract_exif_tags2(path: &Path) -> Result, String> { // let res = panic::catch_unwind(|| { // let mut parser = nom_exif::MediaParser::new(); // let ms = nom_exif::MediaSource::file_path(path).map_err(|e| format!("Failed to open file: {e}"))?; // let mut results = Vec::new(); // if !ms.has_exif() { // return Ok(results); // } // let exif_iter: nom_exif::ExifIter = parser.parse(ms).map_err(|e| format!("Failed to parse EXIF data: {e}"))?; // for exif_entry in exif_iter { // results.push(exif_entry.tag().map_or_else(|| "Unknown".to_string(), |t| format!("{t:?}"))); // } // // Ok(results) // }); // // res.unwrap_or_else(|_| { // let message = crate::common::create_crash_message("nom-exif", path.to_string_lossy().as_ref(), "https://github.com/mindeng/nom-exif"); // error!("{message}"); // Err("Panic in get_rotation_from_exif".to_string()) // }) // } pub fn string_to_exif_tag_group(tag: &str) -> Result { match tag { "EXIF" => Ok(ExifTagGroup::EXIF), "INTEROP" => Ok(ExifTagGroup::INTEROP), "GPS" => Ok(ExifTagGroup::GPS), "GENERIC" => Ok(ExifTagGroup::GENERIC), _ => Err(flc!("core_unknown_exif_tag_group", tag = tag)), } } pub fn exif_tag_group_to_string(tag_group: ExifTagGroup) -> String { match tag_group { ExifTagGroup::EXIF => "EXIF".to_string(), ExifTagGroup::INTEROP => "INTEROP".to_string(), ExifTagGroup::GPS => "GPS".to_string(), ExifTagGroup::GENERIC => "GENERIC".to_string(), } } pub fn get_exif_remover_cache_file() -> String { format!("cache_exif_remover_{CACHE_VERSION}.bin") } ================================================ FILE: czkawka_core/src/tools/exif_remover/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::collections::BTreeMap; use std::path::PathBuf; use std::time::Duration; use serde::{Deserialize, Serialize}; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; #[derive(Debug, Default, Clone, Copy)] pub struct Info { pub number_of_files_with_exif: usize, pub scanning_time: Duration, } #[derive(Clone, Default)] pub struct ExifRemoverParameters { pub ignored_tags: Vec, } impl ExifRemoverParameters { pub fn new(ignored_tags: Vec) -> Self { Self { ignored_tags } } } #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ExifEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub exif_tags: Vec, pub error: Option, } #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ExifTagInfo { pub name: String, pub code: u16, pub group: String, } #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ExifTagsFixerParams { pub override_file: bool, } impl ResultEntry for ExifEntry { fn get_path(&self) -> &std::path::Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } pub struct ExifRemover { common_data: CommonToolData, information: Info, exif_files: Vec, files_to_check: BTreeMap, params: ExifRemoverParameters, additional_excluded_tags: BTreeMap<&'static str, Vec<&'static str>>, } impl ExifRemover { pub const fn get_exif_files(&self) -> &Vec { &self.exif_files } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/exif_remover/tests.rs ================================================ use std::fs; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::exif_remover::{ExifRemover, ExifRemoverParameters}; fn get_test_resources_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_resources").join("images") } #[test] fn test_find_exif_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let source_image = get_test_resources_path().join("normal.jpg"); let dest_image = path.join("test.jpg"); fs::copy(&source_image, &dest_image).unwrap(); let mut finder = ExifRemover::new(ExifRemoverParameters::default()); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_files_with_exif, 1, "Should find at least one file with EXIF data"); let exif_files = finder.get_exif_files(); assert_eq!(exif_files.len(), 1, "Should find exactly one file with EXIF"); assert!(!exif_files[0].exif_tags.is_empty(), "EXIF tags should not be empty"); } #[test] fn test_empty_directory() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let mut finder = ExifRemover::new(ExifRemoverParameters::default()); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let exif_files = finder.get_exif_files(); assert_eq!(exif_files.len(), 0, "Should find no files with EXIF in empty directory"); } #[test] fn test_non_image_files() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); fs::write(path.join("test.txt"), b"This is not an image").unwrap(); let mut finder = ExifRemover::new(ExifRemoverParameters::default()); finder.set_included_paths(vec![path.to_path_buf()]); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let exif_files = finder.get_exif_files(); assert_eq!(exif_files.len(), 0, "Should not find EXIF in non-image files"); } ================================================ FILE: czkawka_core/src/tools/exif_remover/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::BINARY; use crate::common::consts::EXIF_FILES_EXTENSIONS; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search}; use crate::tools::exif_remover::{ExifEntry, ExifRemover, ExifRemoverParameters, ExifTagsFixerParams, Info}; impl AllTraits for ExifRemover {} impl DeletingItems for ExifRemover { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => { let files_to_delete: Vec = self.exif_files.clone(); self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(files_to_delete)) } DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } impl FixingItems for ExifRemover { type FixParams = ExifTagsFixerParams; #[fun_time(message = "fix_items", level = "debug")] fn fix_items(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, fix_params: Self::FixParams) { self.fix_files(stop_flag, progress_sender, fix_params); } } impl DebugPrint for ExifRemover { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("### INDIVIDUAL DEBUG PRINT ###"); println!("Info: {:?}", self.information); println!("Number of files with EXIF: {}", self.information.number_of_files_with_exif); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for ExifRemover { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if self.information.number_of_files_with_exif != 0 { writeln!(writer, "Found {} files with EXIF data.\n", self.information.number_of_files_with_exif)?; for exif_entry in &self.exif_files { writeln!( writer, "\nFile: \"{}\" - {} - {} - {:?}", exif_entry.path.to_string_lossy(), humansize::format_size(exif_entry.size, BINARY), exif_entry.modified_date, exif_entry.exif_tags.iter().map(|item_tag| item_tag.name.clone()).collect::>() )?; } } else { writeln!(writer, "Not found any files with EXIF data.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.exif_files, pretty_print) } } impl Search for ExifRemover { #[fun_time(message = "find_exif_data", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(Some(EXIF_FILES_EXTENSIONS)).is_err() { return; } if self.find_exif_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.check_exif_in_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl CommonData for ExifRemover { type Info = Info; type Parameters = ExifRemoverParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_files_with_exif > 0 } } ================================================ FILE: czkawka_core/src/tools/invalid_symlinks/core.rs ================================================ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::debug; use crate::common::dir_traversal::{Collect, DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::CommonToolData; use crate::tools::invalid_symlinks::{ErrorType, Info, InvalidSymlinks, MAX_NUMBER_OF_SYMLINK_JUMPS, SymlinkInfo}; impl InvalidSymlinks { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::InvalidSymlinks), information: Info::default(), invalid_symlinks: Vec::new(), } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .common_data(&self.common_data) .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .collect(Collect::InvalidSymlinks) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.invalid_symlinks = grouped_file_entries .into_values() .flatten() .filter_map(|e| { let (destination_path, type_of_error) = Self::check_invalid_symlinks(&e.path)?; Some(e.into_symlinks_entry(SymlinkInfo { destination_path, type_of_error })) }) .collect(); self.information.number_of_invalid_symlinks = self.invalid_symlinks.len(); self.common_data.text_messages.warnings.extend(warnings); debug!("Found {} invalid symlinks.", self.information.number_of_invalid_symlinks); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn check_invalid_symlinks(current_file_name: &Path) -> Option<(PathBuf, ErrorType)> { let mut destination_path = PathBuf::new(); let type_of_error; match current_file_name.read_link() { Ok(t) => { destination_path.push(t); let mut loop_count = 0; let mut current_path = current_file_name.to_path_buf(); loop { if loop_count == 0 && !current_path.exists() { type_of_error = ErrorType::NonExistentFile; break; } if loop_count == MAX_NUMBER_OF_SYMLINK_JUMPS { type_of_error = ErrorType::InfiniteRecursion; break; } current_path = match current_path.read_link() { Ok(t) => t, Err(_inspected) => { // Looks that some next symlinks are broken, but we do nothing with it - TODO why they are broken return None; } }; loop_count += 1; } } Err(_inspected) => { // Failed to load info about it type_of_error = ErrorType::NonExistentFile; } } Some((destination_path, type_of_error)) } } ================================================ FILE: czkawka_core/src/tools/invalid_symlinks/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::fmt::Display; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; use crate::flc; #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_invalid_symlinks: usize, pub scanning_time: Duration, } const MAX_NUMBER_OF_SYMLINK_JUMPS: i32 = 20; #[derive(Clone, Debug, PartialEq, Eq, Copy, Deserialize, Serialize)] pub enum ErrorType { InfiniteRecursion, NonExistentFile, } impl ErrorType { pub fn translate(self) -> String { match self { Self::InfiniteRecursion => flc!("core_invalid_symlink_infinite_recursion"), Self::NonExistentFile => flc!("core_invalid_symlink_non_existent_destination"), } } } impl Display for ErrorType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InfiniteRecursion => write!(f, "Infinite recursion"), Self::NonExistentFile => write!(f, "Non existent file"), } } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct SymlinkInfo { pub destination_path: PathBuf, pub type_of_error: ErrorType, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SymlinksFileEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub symlink_info: SymlinkInfo, } impl ResultEntry for SymlinksFileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_symlinks_entry(self, symlink_info: SymlinkInfo) -> SymlinksFileEntry { SymlinksFileEntry { size: self.size, path: self.path, modified_date: self.modified_date, symlink_info, } } } pub struct InvalidSymlinks { common_data: CommonToolData, information: Info, invalid_symlinks: Vec, } impl Default for InvalidSymlinks { fn default() -> Self { Self::new() } } impl InvalidSymlinks { pub const fn get_invalid_symlinks(&self) -> &Vec { &self.invalid_symlinks } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/invalid_symlinks/tests.rs ================================================ #[cfg(target_family = "unix")] use std::fs; #[cfg(target_family = "unix")] use std::sync::Arc; #[cfg(target_family = "unix")] use std::sync::atomic::AtomicBool; #[cfg(target_family = "unix")] use tempfile::TempDir; #[cfg(target_family = "unix")] use crate::common::tool_data::CommonData; #[cfg(target_family = "unix")] use crate::common::traits::Search; #[cfg(target_family = "unix")] use crate::tools::invalid_symlinks::InvalidSymlinks; #[test] #[cfg(target_family = "unix")] fn test_find_invalid_symlinks() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let valid_target = path.join("valid_target.txt"); fs::write(&valid_target, b"content").unwrap(); let valid_link = path.join("valid_link"); std::os::unix::fs::symlink(&valid_target, &valid_link).unwrap(); let invalid_link = path.join("invalid_link"); std::os::unix::fs::symlink(path.join("non_existent.txt"), &invalid_link).unwrap(); let mut finder = InvalidSymlinks::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_invalid_symlinks, 1, "Should find 1 invalid symlink"); } #[test] #[cfg(target_family = "unix")] fn test_no_invalid_symlinks() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let target = path.join("target.txt"); fs::write(&target, b"content").unwrap(); let link = path.join("link"); std::os::unix::fs::symlink(&target, &link).unwrap(); let mut finder = InvalidSymlinks::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_invalid_symlinks, 0, "Should find no invalid symlinks"); } #[test] #[cfg(target_family = "unix")] fn test_deleted_target_creates_invalid_symlink() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let target = path.join("target.txt"); fs::write(&target, b"content").unwrap(); let link = path.join("link"); std::os::unix::fs::symlink(&target, &link).unwrap(); fs::remove_file(&target).unwrap(); let mut finder = InvalidSymlinks::new(); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_invalid_symlinks, 1, "Should find the broken symlink"); } ================================================ FILE: czkawka_core/src/tools/invalid_symlinks/traits.rs ================================================ use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::invalid_symlinks::{ErrorType, Info, InvalidSymlinks}; impl AllTraits for InvalidSymlinks {} impl Search for InvalidSymlinks { #[fun_time(message = "find_invalid_links", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for InvalidSymlinks { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); println!("Invalid symlinks list size - {}", self.invalid_symlinks.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for InvalidSymlinks { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.invalid_symlinks.is_empty() { writeln!(writer, "Found {} invalid symlinks.", self.information.number_of_invalid_symlinks)?; for file_entry in &self.invalid_symlinks { writeln!( writer, "\"{}\"\t\t\"{}\"\t\t{}", file_entry.path.to_string_lossy(), file_entry.symlink_info.destination_path.to_string_lossy(), match file_entry.symlink_info.type_of_error { ErrorType::InfiniteRecursion => "Infinite Recursion", ErrorType::NonExistentFile => "Non Existent File", } )?; } } else { write!(writer, "Not found any invalid symlinks.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.invalid_symlinks, pretty_print) } } impl CommonData for InvalidSymlinks { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_invalid_symlinks > 0 } } impl DeletingItems for InvalidSymlinks { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.common_data.delete_method { DeleteMethod::Delete => self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(self.invalid_symlinks.clone())), DeleteMethod::None => WorkContinueStatus::Continue, _ => unreachable!(), } } } ================================================ FILE: czkawka_core/src/tools/mod.rs ================================================ pub mod bad_extensions; pub mod bad_names; pub mod big_file; pub mod broken_files; pub mod duplicate; pub mod empty_files; pub mod empty_folder; pub mod exif_remover; pub mod invalid_symlinks; pub mod same_music; pub mod similar_images; pub mod similar_videos; pub mod temporary; pub mod video_optimizer; ================================================ FILE: czkawka_core/src/tools/same_music/core.rs ================================================ use std::collections::BTreeMap; use std::fs::File; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::{mem, panic}; use crossbeam_channel::Sender; use fun_time::fun_time; use indexmap::IndexSet; use lofty::file::{AudioFile, TaggedFileExt}; use lofty::prelude::*; use lofty::read_from; use log::{debug, error}; use rayon::prelude::*; use rusty_chromaprint::{Configuration, Fingerprinter, match_fingerprints}; use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions}; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use crate::common::cache::{CACHE_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path}; use crate::common::create_crash_message; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::flc; use crate::tools::same_music::{GroupedFilesToCheck, Info, MusicEntry, MusicSimilarity, SameMusic, SameMusicParameters}; impl SameMusic { pub fn new(params: SameMusicParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::SameMusic), information: Info::default(), music_entries: Vec::with_capacity(2048), duplicated_music_entries: Vec::new(), music_to_check: Default::default(), duplicated_music_entries_referenced: Vec::new(), hash_preset_config: Configuration::preset_test1(), // TODO allow to change this and move to parameters params, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .checking_method(self.params.check_type) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.music_to_check = grouped_file_entries .into_values() .flatten() .map(|fe| (fe.path.to_string_lossy().to_string(), fe.into_music_entry())) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} music files.", self.music_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "load_cache", level = "debug")] fn load_cache(&mut self, checking_tags: bool) -> (BTreeMap, BTreeMap, BTreeMap) { load_and_split_cache_generalized_by_path(&get_similar_music_cache_file(checking_tags), mem::take(&mut self.music_to_check), self) } #[fun_time(message = "save_cache", level = "debug")] fn save_cache(&mut self, vec_file_entry: &[MusicEntry], loaded_hash_map: BTreeMap, checking_tags: bool) { save_and_connect_cache_generalized_by_path(&get_similar_music_cache_file(checking_tags), vec_file_entry, loaded_hash_map, self); } #[fun_time(message = "calculate_fingerprint", level = "debug")] pub(crate) fn calculate_fingerprint(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_entries.is_empty() { return WorkContinueStatus::Continue; } // We only calculate fingerprints, for files with similar titles // This saves a lot of time, because we don't need to calculate and later compare fingerprints for files with different titles if self.params.compare_fingerprints_only_with_similar_titles { let grouped_by_title: BTreeMap> = Self::get_entries_grouped_by_title(mem::take(&mut self.music_entries)); self.music_to_check = grouped_by_title .into_iter() .filter_map(|(_title, entries)| if entries.len() >= 2 { Some(entries) } else { None }) .flatten() .map(|e| (e.path.to_string_lossy().to_string(), e)) .collect(); } else { self.music_to_check = mem::take(&mut self.music_entries).into_iter().map(|e| (e.path.to_string_lossy().to_string(), e)).collect(); } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheLoadingFingerprints, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(false); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SameMusicCalculatingFingerprints, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|e| e.size).sum::(), ); let configuration = &self.hash_preset_config; let non_cached_files_to_check = non_cached_files_to_check.into_iter().collect::>(); debug!("calculate_fingerprint - starting fingerprinting"); let mut vec_file_entry = non_cached_files_to_check .into_par_iter() .with_max_len(2) .map(|(path, mut music_entry)| { if check_if_stop_received(stop_flag) { return None; } let res = calc_fingerprint_helper(path, configuration); progress_handler.increase_size(music_entry.size); progress_handler.increase_items(1); let Ok(fingerprint) = res else { return Some(None); }; music_entry.fingerprint = fingerprint; Some(Some(music_entry)) }) .while_some() .flatten() .collect::>(); debug!("calculate_fingerprint - ended fingerprinting"); progress_handler.join_thread(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheSavingFingerprints, 0, self.get_test_type(), 0); vec_file_entry.extend(records_already_cached.into_values()); self.save_cache(&vec_file_entry, loaded_hash_map, false); self.music_entries = vec_file_entry; progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "read_tags", level = "debug")] pub(crate) fn read_tags(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_to_check.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheLoadingTags, 0, self.get_test_type(), 0); let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache(true); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SameMusicReadingTags, non_cached_files_to_check.len(), self.get_test_type(), 0, ); debug!("read_tags - starting reading tags"); // Clean for duplicate files let mut vec_file_entry = non_cached_files_to_check .into_par_iter() .map(|(path, music_entry)| { if check_if_stop_received(stop_flag) { return None; } let res = read_single_file_tags(&path, music_entry); progress_handler.increase_items(1); Some(res) }) .while_some() .flatten() .collect::>(); debug!("read_tags - ended reading tags"); progress_handler.join_thread(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicCacheSavingTags, 0, self.get_test_type(), 0); vec_file_entry.extend(records_already_cached.into_values()); self.save_cache(&vec_file_entry, loaded_hash_map, true); self.music_entries = vec_file_entry; progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "check_for_duplicate_tags", level = "debug")] pub(crate) fn check_for_duplicate_tags(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_entries.is_empty() { return WorkContinueStatus::Continue; } let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicComparingTags, self.music_entries.len(), self.get_test_type(), 0); let mut old_duplicates: Vec> = vec![self.music_entries.clone()]; let mut new_duplicates: Vec> = Vec::new(); if (self.params.music_similarity & MusicSimilarity::TRACK_TITLE) == MusicSimilarity::TRACK_TITLE { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item( old_duplicates, progress_handler.items_counter(), |fe| fe.track_title.clone(), self.params.approximate_comparison, ); } if (self.params.music_similarity & MusicSimilarity::TRACK_ARTIST) == MusicSimilarity::TRACK_ARTIST { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item( old_duplicates, progress_handler.items_counter(), |fe| fe.track_artist.clone(), self.params.approximate_comparison, ); } if (self.params.music_similarity & MusicSimilarity::YEAR) == MusicSimilarity::YEAR { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| fe.year.clone(), false); } if (self.params.music_similarity & MusicSimilarity::LENGTH) == MusicSimilarity::LENGTH { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| format_audio_duration(fe.length), false); } if (self.params.music_similarity & MusicSimilarity::GENRE) == MusicSimilarity::GENRE { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } old_duplicates = self.check_music_item(old_duplicates, progress_handler.items_counter(), |fe| fe.genre.clone(), false); } if (self.params.music_similarity & MusicSimilarity::BITRATE) == MusicSimilarity::BITRATE { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } let old_duplicates_len = old_duplicates.len(); for vec_file_entry in old_duplicates { let mut hash_map: BTreeMap> = Default::default(); for file_entry in vec_file_entry { if file_entry.bitrate != 0 { let thing = file_entry.bitrate.to_string(); hash_map.entry(thing).or_default().push(file_entry); } } for (_title, vec_file_entry) in hash_map { if vec_file_entry.len() > 1 { new_duplicates.push(vec_file_entry); } } } progress_handler.increase_items(old_duplicates_len); old_duplicates = new_duplicates; } progress_handler.join_thread(); self.duplicated_music_entries = old_duplicates; if self.common_data.use_reference_folders { self.duplicated_music_entries_referenced = self.common_data.directories.filter_reference_folders(mem::take(&mut self.duplicated_music_entries)); } if self.common_data.use_reference_folders { for (_fe, vector) in &self.duplicated_music_entries_referenced { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.duplicated_music_entries { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clear unused data self.music_entries.clear(); WorkContinueStatus::Continue } fn split_fingerprints_to_base_and_files_to_compare(&self, music_data: Vec) -> (Vec, Vec) { if self.common_data.use_reference_folders { music_data.into_iter().partition(|f| self.common_data.directories.is_in_referenced_directory(f.get_path())) } else { (music_data.clone(), music_data) } } fn get_entries_grouped_by_title(music_data: Vec) -> BTreeMap> { let mut entries_grouped_by_title: BTreeMap> = BTreeMap::new(); for entry in music_data { let simplified_track_title = get_simplified_name(&entry.track_title); // TODO maybe add as option to check for empty titles? if simplified_track_title.is_empty() { continue; } entries_grouped_by_title.entry(simplified_track_title).or_default().push(entry); } entries_grouped_by_title } fn split_fingerprints_to_check(&mut self) -> Vec { if self.params.compare_fingerprints_only_with_similar_titles { let entries_grouped_by_title: BTreeMap> = Self::get_entries_grouped_by_title(mem::take(&mut self.music_entries)); entries_grouped_by_title .into_iter() .filter_map(|(_title, entries)| { let (base_files, files_to_compare) = self.split_fingerprints_to_base_and_files_to_compare(entries); // When there is 0 files in base files or files to compare there will be no comparison, so removing it from the list // 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 #[expect(clippy::indexing_slicing)] // Validated that base_files/files_to_compare are not empty if base_files.is_empty() || files_to_compare.is_empty() || (base_files.len() == 1 && files_to_compare.len() == 1 && (base_files[0].path == files_to_compare[0].path)) { return None; } Some(GroupedFilesToCheck { base_files, files_to_compare }) }) .collect() } else { let entries = mem::take(&mut self.music_entries); let (base_files, files_to_compare) = self.split_fingerprints_to_base_and_files_to_compare(entries); vec![GroupedFilesToCheck { base_files, files_to_compare }] } } fn compare_fingerprints( &mut self, stop_flag: &Arc, items_counter: &Arc, base_files: Vec, files_to_compare: &[MusicEntry], ) -> Option>> { let mut used_paths: IndexSet = Default::default(); let configuration = &self.hash_preset_config; let minimum_segment_duration = self.params.minimum_segment_duration; let maximum_difference = self.params.maximum_difference; let mut duplicated_music_entries = Vec::new(); for f_entry in base_files { items_counter.fetch_add(1, Ordering::Relaxed); if check_if_stop_received(stop_flag) { return None; } let f_string = f_entry.path.to_string_lossy().to_string(); if used_paths.contains(&f_string) { continue; } let (mut collected_similar_items, errors): (Vec<_>, Vec<_>) = files_to_compare .par_iter() .map(|e_entry| { let e_string = e_entry.path.to_string_lossy().to_string(); if used_paths.contains(&e_string) || e_string == f_string { return None; } let mut segments = match match_fingerprints(&f_entry.fingerprint, &e_entry.fingerprint, configuration) { Ok(segments) => segments, Err(e) => return Some(Err(flc!("core_error_comparing_fingerprints", reason = e.to_string()))), }; segments.retain(|s| s.duration(configuration) > minimum_segment_duration && s.score < maximum_difference); if segments.is_empty() { None } else { Some(Ok((e_string, e_entry))) } }) .flatten() .partition_map(|res| match res { Ok(entry) => itertools::Either::Left(entry), Err(err) => itertools::Either::Right(err), }); self.common_data.text_messages.errors.extend(errors); collected_similar_items.retain(|(path, _entry)| !used_paths.contains(path)); if !collected_similar_items.is_empty() { let mut music_entries = Vec::new(); for (path, entry) in collected_similar_items { used_paths.insert(path); music_entries.push(entry.clone()); } used_paths.insert(f_string); music_entries.push(f_entry); duplicated_music_entries.push(music_entries); } } Some(duplicated_music_entries) } #[fun_time(message = "check_for_duplicate_fingerprints", level = "debug")] pub(crate) fn check_for_duplicate_fingerprints(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.music_entries.is_empty() { return WorkContinueStatus::Continue; } let grouped_files_to_check = self.split_fingerprints_to_check(); let base_files_number = grouped_files_to_check.iter().map(|g| g.base_files.len()).sum::(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SameMusicComparingFingerprints, base_files_number, self.get_test_type(), 0); let mut duplicated_music_entries = Vec::new(); for group in grouped_files_to_check { let GroupedFilesToCheck { base_files, files_to_compare } = group; let Some(temp_music_entries) = self.compare_fingerprints(stop_flag, progress_handler.items_counter(), base_files, &files_to_compare) else { progress_handler.join_thread(); return WorkContinueStatus::Stop; }; duplicated_music_entries.extend(temp_music_entries); } progress_handler.join_thread(); self.duplicated_music_entries = duplicated_music_entries; if self.common_data.use_reference_folders { self.duplicated_music_entries_referenced = self.common_data.directories.filter_reference_folders(mem::take(&mut self.duplicated_music_entries)); } if self.common_data.use_reference_folders { for (_fe, vector) in &self.duplicated_music_entries_referenced { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.duplicated_music_entries { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clear unused data self.music_entries.clear(); WorkContinueStatus::Continue } #[fun_time(message = "check_music_item", level = "debug")] fn check_music_item( &self, old_duplicates: Vec>, items_counter: &Arc, get_item: fn(&MusicEntry) -> String, approximate_comparison: bool, ) -> Vec> { let mut new_duplicates: Vec<_> = Default::default(); let old_duplicates_len = old_duplicates.len(); for vec_file_entry in old_duplicates { let mut hash_map: BTreeMap> = Default::default(); for file_entry in vec_file_entry { let mut thing = get_item(&file_entry).trim().to_lowercase(); if approximate_comparison { thing = get_simplified_name(&thing); } if !thing.is_empty() { hash_map.entry(thing).or_default().push(file_entry); } } for (_title, vec_file_entry) in hash_map { if vec_file_entry.len() > 1 { new_duplicates.push(vec_file_entry); } } } items_counter.fetch_add(old_duplicates_len, Ordering::Relaxed); new_duplicates } } // TODO this should be taken from rusty-chromaprint repo, not reimplemented here fn calc_fingerprint_helper>(path: P, config: &Configuration) -> Result, String> { let path = path.as_ref().to_path_buf(); panic::catch_unwind(|| { let path = &path; let src = File::open(path).map_err(|_| "failed to open file".to_string())?; let mss = MediaSourceStream::new(Box::new(src), Default::default()); let mut hint = Hint::new(); if let Some(ext) = path.extension().and_then(std::ffi::OsStr::to_str) { hint.with_extension(ext); } let meta_opts: MetadataOptions = Default::default(); let fmt_opts: FormatOptions = Default::default(); let probed = symphonia::default::get_probe() .format(&hint, mss, &fmt_opts, &meta_opts) .map_err(|_| "unsupported format".to_string())?; let mut format = probed.format; let track = format .tracks() .iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .ok_or_else(|| "no supported audio tracks".to_string())?; let dec_opts: DecoderOptions = Default::default(); let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &dec_opts) .map_err(|_| "unsupported codec".to_string())?; let track_id = track.id; let mut printer = Fingerprinter::new(config); let sample_rate = track.codec_params.sample_rate.ok_or_else(|| "missing sample rate".to_string())?; let channels = track.codec_params.channels.ok_or_else(|| "missing audio channels".to_string())?.count() as u32; printer.start(sample_rate, channels).map_err(|_| "initializing fingerprinter".to_string())?; let mut sample_buf = None; loop { let Ok(packet) = format.next_packet() else { break; }; if packet.track_id() != track_id { continue; } match decoder.decode(&packet) { Ok(audio_buf) => { if sample_buf.is_none() { let spec = *audio_buf.spec(); let duration = audio_buf.capacity() as u64; sample_buf = Some(SampleBuffer::::new(duration, spec)); } if let Some(buf) = &mut sample_buf { buf.copy_interleaved_ref(audio_buf); printer.consume(buf.samples()); } } Err(symphonia::core::errors::Error::DecodeError(_)) => (), Err(_) => break, } } printer.finish(); Ok(printer.fingerprint().to_vec()) }) .unwrap_or_else(|_| { let message = create_crash_message("Symphonia", &path.to_string_lossy(), "https://github.com/pdeljanov/Symphonia"); error!("{message}"); Err(message) }) } fn read_single_file_tags(path: &str, mut music_entry: MusicEntry) -> Option { let Ok(mut file) = File::open(path) else { return None; }; let Ok(possible_tagged_file) = panic::catch_unwind(move || read_from(&mut file).ok()) else { let message = create_crash_message("Lofty", path, "https://github.com/Serial-ATA/lofty-rs"); error!("{message}"); return None; }; let Some(tagged_file) = possible_tagged_file else { return Some(music_entry) }; let properties = tagged_file.properties(); let mut track_title = String::new(); let mut track_artist = String::new(); let mut year = String::new(); let mut genre = String::new(); let bitrate = properties.audio_bitrate().unwrap_or(0); if let Some(tag) = tagged_file.primary_tag() { track_title = tag.get_string(ItemKey::TrackTitle).unwrap_or_default().to_string(); track_artist = tag.get_string(ItemKey::TrackArtist).unwrap_or_default().to_string(); year = tag.get_string(ItemKey::Year).unwrap_or_default().to_string(); genre = tag.get_string(ItemKey::Genre).unwrap_or_default().to_string(); } for tag in tagged_file.tags() { if track_title.is_empty() && let Some(tag_value) = tag.get_string(ItemKey::TrackTitle) { track_title = tag_value.to_string(); } if track_artist.is_empty() && let Some(tag_value) = tag.get_string(ItemKey::TrackArtist) { track_artist = tag_value.to_string(); } if year.is_empty() && let Some(tag_value) = tag.get_string(ItemKey::Year) { year = tag_value.to_string(); } if genre.is_empty() && let Some(tag_value) = tag.get_string(ItemKey::Genre) { genre = tag_value.to_string(); } } let length_milliseconds = properties.duration().as_millis(); let length_in_seconds = if length_milliseconds == 0 { 0 } else { let secs = properties.duration().as_secs() as u32; if secs == 0 { 1 } else { secs } }; music_entry.track_title = track_title; music_entry.track_artist = track_artist; music_entry.year = year; music_entry.length = length_in_seconds; music_entry.genre = genre; music_entry.bitrate = bitrate; Some(music_entry) } pub fn format_audio_duration(duration: u32) -> String { let hours = duration / 3600; let minutes = (duration % 3600) / 60; let seconds = duration % 60; if hours > 0 { format!("{hours}:{minutes:02}:{seconds:02}") } else { format!("{minutes}:{seconds:02}") } } fn get_simplified_name_internal(what: &str, ignore_numbers: bool) -> String { let mut new_what = String::with_capacity(what.len()); let mut tab_number = 0; let mut space_before = true; for character in what.chars().map(|e| if e.is_whitespace() { ' ' } else { e }) { match character { '(' | '[' => { tab_number += 1; } ')' | ']' => { if tab_number == 0 { // Nothing to do, not even save it to output } else { tab_number -= 1; } } ' ' => { if !space_before { new_what.push(' '); space_before = true; } } ch => { if tab_number == 0 { if ch.is_ascii_alphabetic() || (!ignore_numbers && ch.is_numeric()) { space_before = false; new_what.push(ch); } else { let new_items = deunicode::deunicode_char(character).map_or_else(|| vec![character; 1], |e| e.trim().to_string().chars().collect::>()); // If is equal, then we're trying to deunicode e.g. dot, comma etc. // We just ignore char, because it is mostly useless, but we add space instead it if it wasn't added already if new_items.first() == Some(&character) { if !space_before { new_what.push(' '); space_before = true; } } else { new_what.extend(new_items.into_iter()); space_before = false; } } } } } } if new_what.ends_with(' ') { new_what.pop(); } new_what } fn get_simplified_name(what: &str) -> String { let new_what = get_simplified_name_internal(what, true); if !new_what.is_empty() { return new_what; } let new_what = get_simplified_name_internal(what, false); if !new_what.is_empty() { return new_what; } let simplified_unicode = deunicode::deunicode(what).trim().to_string(); if !simplified_unicode.is_empty() { return simplified_unicode; } // If everything failed, we return original string // this is more useful than returning empty string, which is ignored by other functions what.trim().to_string() } pub fn get_similar_music_cache_file(checking_tags: bool) -> String { if checking_tags { format!("cache_same_music_tags_{CACHE_VERSION}.bin") } else { format!("cache_same_music_fingerprints_{CACHE_VERSION}.bin") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_simplified_names() { let cases = [ ("roman ( ziemniak ) ", "roman"), (" HH) ", "HH"), (" fsf.f. ", "fsf f"), (" śśśśćććć ", "sssscccc"), ("rr\t", "rr"), ("Kekistan (feat. roman) [Mix on Mix]", "Kekistan"), ("23", "23"), ("23 (random)", "23"), ("(23)", "(23)"), ]; for (input, expected) in cases { let res = get_simplified_name(input); assert_eq!(res, expected, "Input: {input}, Expected: {expected}, Got: {res}"); } } } ================================================ FILE: czkawka_core/src/tools/same_music/mod.rs ================================================ use bitflags::bitflags; pub mod core; pub mod traits; #[cfg(test)] mod tests; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::time::Duration; use rusty_chromaprint::Configuration; use serde::{Deserialize, Serialize}; use crate::common::model::{CheckingMethod, FileEntry}; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; bitflags! { #[derive(PartialEq, Copy, Clone, Debug)] pub struct MusicSimilarity : u32 { const NONE = 0; const TRACK_TITLE = 0b1; const TRACK_ARTIST = 0b10; const YEAR = 0b100; const LENGTH = 0b1000; const GENRE = 0b10000; const BITRATE = 0b10_0000; } } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct MusicEntry { pub size: u64, pub path: PathBuf, pub modified_date: u64, pub fingerprint: Vec, pub track_title: String, pub track_artist: String, pub year: String, pub length: u32, pub genre: String, pub bitrate: u32, } impl ResultEntry for MusicEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_music_entry(self) -> MusicEntry { MusicEntry { size: self.size, path: self.path, modified_date: self.modified_date, fingerprint: Vec::new(), track_title: String::new(), track_artist: String::new(), year: String::new(), length: 0, genre: String::new(), bitrate: 0, } } } struct GroupedFilesToCheck { pub base_files: Vec, pub files_to_compare: Vec, } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_duplicates: usize, pub number_of_groups: usize, pub scanning_time: Duration, } #[derive(Clone)] pub struct SameMusicParameters { pub music_similarity: MusicSimilarity, pub approximate_comparison: bool, pub check_type: CheckingMethod, pub minimum_segment_duration: f32, pub maximum_difference: f64, pub compare_fingerprints_only_with_similar_titles: bool, } impl SameMusicParameters { pub fn new( music_similarity: MusicSimilarity, approximate_comparison: bool, check_type: CheckingMethod, minimum_segment_duration: f32, maximum_difference: f64, compare_fingerprints_only_with_similar_titles: bool, ) -> Self { assert!(!music_similarity.is_empty()); assert!([CheckingMethod::AudioTags, CheckingMethod::AudioContent].contains(&check_type)); Self { music_similarity, approximate_comparison, check_type, minimum_segment_duration, maximum_difference, compare_fingerprints_only_with_similar_titles, } } } pub struct SameMusic { common_data: CommonToolData, information: Info, music_to_check: BTreeMap, music_entries: Vec, duplicated_music_entries: Vec>, duplicated_music_entries_referenced: Vec<(MusicEntry, Vec)>, hash_preset_config: Configuration, params: SameMusicParameters, } impl SameMusic { pub const fn get_duplicated_music_entries(&self) -> &Vec> { &self.duplicated_music_entries } pub fn get_params(&self) -> &SameMusicParameters { &self.params } pub const fn get_information(&self) -> Info { self.information } pub fn get_similar_music_referenced(&self) -> &Vec<(MusicEntry, Vec)> { &self.duplicated_music_entries_referenced } pub fn get_number_of_base_duplicated_files(&self) -> usize { if self.common_data.use_reference_folders { self.duplicated_music_entries_referenced.len() } else { self.duplicated_music_entries.len() } } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } } ================================================ FILE: czkawka_core/src/tools/same_music/tests.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crate::common::model::CheckingMethod; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::same_music::{MusicSimilarity, SameMusic, SameMusicParameters}; fn get_test_resources_path() -> PathBuf { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_resources").join("audio"); assert!(path.exists(), "Test resources not found at \"{}\"", path.to_string_lossy()); path } #[test] fn test_same_music_by_content_high_similarity() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioContent, 10.0, 0.2, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 1); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates.iter().map(|e| e.len()).sum::(), 2); } #[test] fn test_same_music_by_content_medium_similarity() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioContent, 10.0, 0.5, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 1); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates.iter().map(|e| e.len()).sum::(), 2); } #[test] fn test_same_music_by_content_low_similarity() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioContent, 10.0, 0.8, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 3); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates.iter().map(|e| e.len()).sum::(), 4); } #[test] fn test_same_music_by_tags_title_artist() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new( MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST, false, CheckingMethod::AudioTags, 10.0, 0.2, false, ); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 4); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates[0].len(), 5); } #[test] fn test_same_music_by_tags_year() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new(MusicSimilarity::YEAR, false, CheckingMethod::AudioTags, 10.0, 0.2, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 0); assert_eq!(info.number_of_groups, 0); assert_eq!(duplicates.len(), 0); } #[test] fn test_same_music_by_tags_genre() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new(MusicSimilarity::GENRE, false, CheckingMethod::AudioTags, 10.0, 0.2, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 4); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates[0].len(), 5); } #[test] fn test_same_music_by_tags_bitrate() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new(MusicSimilarity::BITRATE, false, CheckingMethod::AudioTags, 10.0, 0.2, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 2); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates.iter().map(|e| e.len()).sum::(), 3); } #[test] fn test_same_music_by_tags_all_criteria() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new( MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST | MusicSimilarity::YEAR | MusicSimilarity::GENRE, false, CheckingMethod::AudioTags, 10.0, 0.2, false, ); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 0); assert_eq!(info.number_of_groups, 0); assert_eq!(duplicates.len(), 0); } #[test] fn test_same_music_approximate_comparison() { let test_path = get_test_resources_path(); let params = SameMusicParameters::new( MusicSimilarity::TRACK_TITLE | MusicSimilarity::TRACK_ARTIST, true, CheckingMethod::AudioTags, 10.0, 0.2, false, ); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 4); assert_eq!(info.number_of_groups, 1); assert_eq!(duplicates.len(), 1); assert_eq!(duplicates[0].len(), 5); } #[test] fn test_same_music_empty_directory() { use tempfile::TempDir; let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let params = SameMusicParameters::new(MusicSimilarity::TRACK_TITLE, false, CheckingMethod::AudioTags, 10.0, 0.2, false); let mut finder = SameMusic::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let duplicates = finder.get_duplicated_music_entries(); assert_eq!(info.number_of_duplicates, 0); assert_eq!(info.number_of_groups, 0); assert_eq!(duplicates.len(), 0); } ================================================ FILE: czkawka_core/src/tools/same_music/traits.rs ================================================ use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::consts::AUDIO_FILES_EXTENSIONS; use crate::common::model::{CheckingMethod, WorkContinueStatus}; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::flc; use crate::tools::same_music::core::format_audio_duration; use crate::tools::same_music::{Info, MusicEntry, MusicSimilarity, SameMusic, SameMusicParameters}; impl AllTraits for SameMusic {} impl Search for SameMusic { #[fun_time(message = "find_same_music", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(Some(AUDIO_FILES_EXTENSIONS)).is_err() { return; } self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty(); if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } match self.params.check_type { CheckingMethod::AudioTags => { if self.params.music_similarity == MusicSimilarity::NONE { self.common_data.text_messages.critical = flc!("core_no_similarity_method_selected").into(); return; } if self.read_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.check_for_duplicate_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } } CheckingMethod::AudioContent => { if self.read_tags(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.calculate_fingerprint(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.check_for_duplicate_fingerprints(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } } _ => panic!(), } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for SameMusic { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); println!("Found files music - {}", self.music_entries.len()); println!("Found duplicated files music - {}", self.duplicated_music_entries.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for SameMusic { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.duplicated_music_entries.is_empty() { writeln!(writer, "{} music files which have similar friends\n\n.", self.duplicated_music_entries.len())?; for vec_file_entry in &self.duplicated_music_entries { writeln!(writer, "Found {} music files which have similar friends", vec_file_entry.len())?; for file_entry in vec_file_entry { write_music_entry(writer, file_entry)?; } writeln!(writer)?; } } else if !self.duplicated_music_entries_referenced.is_empty() { writeln!(writer, "{} music files which have similar friends\n\n.", self.duplicated_music_entries_referenced.len())?; for (file_entry, vec_file_entry) in &self.duplicated_music_entries_referenced { writeln!(writer, "Found {} music files which have similar friends", vec_file_entry.len())?; writeln!(writer)?; write_music_entry(writer, file_entry)?; for file_entry in vec_file_entry { write_music_entry(writer, file_entry)?; } writeln!(writer)?; } } else { write!(writer, "Not found any similar music files.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { if self.get_use_reference() { self.save_results_to_file_as_json_internal(file_name, &self.duplicated_music_entries_referenced, pretty_print) } else { self.save_results_to_file_as_json_internal(file_name, &self.duplicated_music_entries, pretty_print) } } } fn write_music_entry(writer: &mut T, file_entry: &MusicEntry) -> std::io::Result<()> { writeln!( writer, "TT: {} - TA: {} - Y: {} - L: {} - G: {} - B: {} - P: \"{}\"", file_entry.track_title, file_entry.track_artist, file_entry.year, format_audio_duration(file_entry.length), file_entry.genre, file_entry.bitrate, file_entry.path.to_string_lossy() ) } impl CommonData for SameMusic { type Info = Info; type Parameters = SameMusicParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn get_check_method(&self) -> CheckingMethod { self.get_params().check_type } fn found_any_items(&self) -> bool { self.information.number_of_duplicates > 0 } } impl DeletingItems for SameMusic { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.duplicated_music_entries.clone(); self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } ================================================ FILE: czkawka_core/src/tools/similar_images/core.rs ================================================ use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::{mem, panic}; use bk_tree::BKTree; use crossbeam_channel::Sender; use fun_time::fun_time; use image::GenericImageView; use image_hasher::{FilterType, HashAlg, HasherConfig}; use indexmap::{IndexMap, IndexSet}; use log::{debug, error}; use rayon::prelude::*; use crate::common::cache::{CACHE_IMAGE_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path}; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult, inode, take_1_per_inode}; use crate::common::image::get_dynamic_image_from_path; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::flc; use crate::tools::similar_images::{Hamming, ImHash, ImagesEntry, SIMILAR_VALUES, SimilarImages, SimilarImagesParameters, SimilarityPreset}; impl SimilarImages { pub fn new(params: SimilarImagesParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::SimilarImages), information: Default::default(), bktree: BKTree::new(Hamming), similar_vectors: Vec::new(), similar_referenced_vectors: Vec::new(), params, images_to_check: Default::default(), image_hashes: Default::default(), } } #[fun_time(message = "check_for_similar_images", level = "debug")] pub(crate) fn check_for_similar_images(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(inode) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.images_to_check = grouped_file_entries .into_par_iter() .flat_map(if self.get_hide_hard_links() { |(_, fes)| fes } else { take_1_per_inode }) .map(|fe| { let fe_str = fe.path.to_string_lossy().to_string(); let image_entry = fe.into_images_entry(); (fe_str, image_entry) }) .collect(); self.information.initial_found_files = self.images_to_check.len(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} image files.", self.images_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "hash_images_load_cache", level = "debug")] fn hash_images_load_cache(&mut self) -> (BTreeMap, BTreeMap, BTreeMap) { load_and_split_cache_generalized_by_path( &get_similar_images_cache_file(self.get_params().hash_size, self.get_params().hash_alg, self.get_params().image_filter), mem::take(&mut self.images_to_check), self, ) } #[fun_time(message = "save_to_cache", level = "debug")] fn save_to_cache(&mut self, vec_file_entry: &[ImagesEntry], loaded_hash_map: BTreeMap) { save_and_connect_cache_generalized_by_path( &get_similar_images_cache_file(self.get_params().hash_size, self.get_params().hash_alg, self.get_params().image_filter), vec_file_entry, loaded_hash_map, self, ); } #[fun_time(message = "hash_images", level = "debug")] pub(crate) fn hash_images(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.images_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.hash_images_load_cache(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SimilarImagesCalculatingHashes, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|entry| entry.size).sum(), ); debug!("hash_images - start hashing images"); let (mut vec_file_entry, errors): (Vec, Vec) = non_cached_files_to_check .into_par_iter() .map(|(_s, file_entry)| { if check_if_stop_received(stop_flag) { return None; } let size = file_entry.size; let res = self.collect_image_file_entry(file_entry); progress_handler.increase_items(1); progress_handler.increase_size(size); Some(res) }) .while_some() .partition_map(|res| match res { Ok(entry) => itertools::Either::Left(entry), Err(err) => itertools::Either::Right(err), }); self.common_data.text_messages.errors.extend(errors); debug!("hash_images - end hashing {} images", vec_file_entry.len()); progress_handler.join_thread(); vec_file_entry.extend(records_already_cached.into_values()); self.save_to_cache(&vec_file_entry, loaded_hash_map); // All valid entries are used to create bktree used to check for hash similarity for file_entry in vec_file_entry { // 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) if !(file_entry.hash.is_empty() || file_entry.hash.iter().all(|e| *e == 0) || file_entry.hash.iter().all(|e| *e == 255)) { self.image_hashes.entry(file_entry.hash.clone()).or_default().push(file_entry); } } // Break if stop was clicked after saving to cache if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } fn collect_image_file_entry(&self, mut file_entry: ImagesEntry) -> Result { let img = get_dynamic_image_from_path(&file_entry.path.to_string_lossy(), None)?.image; let dimensions = img.dimensions(); file_entry.width = dimensions.0; file_entry.height = dimensions.1; let hasher_config = HasherConfig::new() .hash_size(self.get_params().hash_size as u32, self.get_params().hash_size as u32) .hash_alg(self.get_params().hash_alg) .resize_filter(self.get_params().image_filter); let hasher = hasher_config.to_hasher(); let hash = hasher.hash_image(&img); file_entry.hash = hash.as_bytes().to_vec(); Ok(file_entry) } // Split hashes at 2 parts, base hashes and hashes to compare, 3 argument is set of hashes with multiple images #[fun_time(message = "split_hashes", level = "debug")] fn split_hashes(&mut self, all_hashed_images: &IndexMap>) -> (Vec, IndexSet) { let hashes_with_multiple_images: IndexSet = all_hashed_images .iter() .filter_map(|(hash, vec_file_entry)| { if vec_file_entry.len() >= 2 { return Some(hash.clone()); } None }) .collect(); let mut base_hashes = Vec::new(); // Initial hashes if self.common_data.use_reference_folders { let mut files_from_referenced_folders: IndexMap> = IndexMap::new(); let mut normal_files: IndexMap> = IndexMap::new(); all_hashed_images.clone().into_iter().for_each(|(hash, vec_file_entry)| { for file_entry in vec_file_entry { if is_in_reference_folder(&self.common_data.directories.reference_directories, &file_entry.path) { files_from_referenced_folders.entry(hash.clone()).or_default().push(file_entry); } else { normal_files.entry(hash.clone()).or_default().push(file_entry); } } }); for hash in normal_files.into_keys() { self.bktree.add(hash); } for hash in files_from_referenced_folders.into_keys() { base_hashes.push(hash); } } else { for original_hash in all_hashed_images.keys() { self.bktree.add(original_hash.clone()); } base_hashes = all_hashed_images.keys().cloned().collect::>(); } (base_hashes, hashes_with_multiple_images) } #[fun_time(message = "collect_hash_compare_result", level = "debug")] fn collect_hash_compare_result( &self, hashes_parents: IndexMap, hashes_with_multiple_images: &IndexSet, all_hashed_images: &IndexMap>, collected_similar_images: &mut IndexMap>, hashes_similarity: IndexMap, ) { // Collecting results to vector for (parent_hash, child_number) in hashes_parents { // If hash contains other hasher OR multiple images are available for checked hash if child_number > 0 || hashes_with_multiple_images.contains(&parent_hash) { let vec_fe = all_hashed_images[&parent_hash].clone(); collected_similar_images.insert(parent_hash.clone(), vec_fe); } } for (child_hash, (parent_hash, similarity)) in hashes_similarity { let mut vec_fe = all_hashed_images[&child_hash].clone(); for fe in &mut vec_fe { fe.difference = similarity; } collected_similar_images .get_mut(&parent_hash) .expect("Cannot find parent hash - this should be added in previous step") .append(&mut vec_fe); } } #[fun_time(message = "compare_hashes_with_non_zero_tolerance", level = "debug")] fn compare_hashes_with_non_zero_tolerance( &mut self, all_hashed_images: &IndexMap>, collected_similar_images: &mut IndexMap>, progress_sender: Option<&Sender>, stop_flag: &Arc, tolerance: u32, ) -> WorkContinueStatus { // Don't use hashes with multiple images in bktree, because they will always be master of group and cannot be find by other hashes let (base_hashes, hashes_with_multiple_images) = self.split_hashes(all_hashed_images); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::SimilarImagesComparingHashes, base_hashes.len(), self.get_test_type(), 0); let mut hashes_parents: IndexMap = Default::default(); // Hashes used as parent (hash, children_number_of_hash) let mut hashes_similarity: IndexMap = Default::default(); // Hashes used as child, (parent_hash, similarity) // Check them in chunks, to decrease number of used memory // Without chunks, every single hash would be compared to every other hash and generate really big amount of results // With chunks we can save results to variables and later use such variables, to skip ones with too big difference // Not really helpful, when not finding almost any duplicates, but with bigger amount of them, this should help a lot let base_hashes_chunks = base_hashes.chunks(1000); for chunk in base_hashes_chunks { let partial_results = chunk .into_par_iter() .map(|hash_to_check| { progress_handler.increase_items(1); if check_if_stop_received(stop_flag) { return None; } let mut found_items = self .bktree .find(hash_to_check, tolerance) .filter(|(similarity, compared_hash)| { *similarity != 0 && !hashes_parents.contains_key(*compared_hash) && !hashes_with_multiple_images.contains(*compared_hash) }) .filter(|(similarity, compared_hash)| { if let Some((_, other_similarity_with_parent)) = hashes_similarity.get(*compared_hash) { // If current hash is more similar to other hash than to current parent hash, then skip check earlier // Because there is no way to be more similar to other hash than to current parent hash if *similarity >= *other_similarity_with_parent { return false; } } true }) .collect::>(); // Sort by tolerance found_items.sort_unstable_by_key(|f| f.0); Some((hash_to_check, found_items)) }) .while_some() // TODO - this filter move to into_par_iter above .filter(|(original_hash, vec_similar_hashes)| !vec_similar_hashes.is_empty() || hashes_with_multiple_images.contains(*original_hash)) .collect::>(); if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } SimilarImages::connect_results_simplified(partial_results, &mut hashes_parents, &mut hashes_similarity, &hashes_with_multiple_images); } // To avoid situations in simplified connector we don't add such hashes to results for multiple_image_hash in &hashes_with_multiple_images { if !hashes_parents.contains_key(multiple_image_hash) { hashes_parents.insert(multiple_image_hash.clone(), 0); } } progress_handler.join_thread(); debug_check_for_duplicated_things(self.common_data.use_reference_folders, &hashes_parents, &hashes_similarity, all_hashed_images, "LATTER"); self.collect_hash_compare_result(hashes_parents, &hashes_with_multiple_images, all_hashed_images, collected_similar_images, hashes_similarity); WorkContinueStatus::Continue } fn connect_results_simplified<'a>( partial_results: Vec<(&'a ImHash, Vec<(u32, &'a ImHash)>)>, hashes_parents: &mut IndexMap, hashes_similarity: &mut IndexMap, hashes_with_multiple_images: &IndexSet, ) { // To simplify later logic, we sort all results by similarity // 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) // 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. let mut flattened_partial_results: Vec<(&'a ImHash, (u32, &'a ImHash))> = partial_results .into_iter() .filter_map(|(parent, similar)| { if similar.is_empty() { assert!(hashes_with_multiple_images.contains(parent)); // We expect, that only hashes with multiple images can have no similar hashes 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 None } else { Some(similar.into_iter().map(move |sim| (parent, sim))) } }) .flatten() .collect::>(); flattened_partial_results.sort_by_key(|(_parent, (similarity, _compared_hash))| *similarity); // Original hash means, that we check this hash and we can easily find this hash a new parent // Compared hash cannot be changed if it is already parent to different hash, because it would be too complex to handle this properly for (original_hash, (similarity, compared_hash)) in flattened_partial_results { // If compared hash already is parent to different hash, skip it // This may be not optimal, because we may miss better parent for such hash, but I have no idea how to properly reparent it // This would be hard, because we would need to track all similar hashes for reparented childrens, to find them better parents if hashes_parents.contains_key(compared_hash) { continue; } let compared_hash_parent = if let Some((other_parent_hash, other_similarity)) = hashes_similarity.get(compared_hash) { if *other_similarity > similarity { Some(other_parent_hash.clone()) } else { // Have parent, but with lower similarity, so skipping this one continue; } } else { None }; // If current checked hash, have parent, first we must check if similarity between them is lower than checked item if let Some((current_parent_hash, current_similarity_with_parent)) = hashes_similarity.get(original_hash) { if *current_similarity_with_parent <= similarity { // Have more similar parent, so skip this one continue; } let children_count = hashes_parents.get_mut(current_parent_hash).expect("Cannot find parent hash"); *children_count -= 1; let left_any_children = *children_count != 0; // We can remove entirely previous parent from hashes_parents if it will not have any other children // Of course, only if hash applies to single image, because hashes with multiple images must stay in parents list if !left_any_children && !hashes_with_multiple_images.contains(current_parent_hash) { hashes_parents.swap_remove(current_parent_hash); } hashes_similarity .swap_remove(original_hash) .expect("This should never fail, because we are iterating over this hash"); let parent = hashes_parents.insert((*original_hash).clone(), 1); assert!(parent.is_none(), "Parent hash should not exist here"); } else { *hashes_parents.entry(original_hash.clone()).or_insert(0) += 1; } // This overwrites parent hash if there was any // or just adds new record if there was no parent hashes_similarity.insert(compared_hash.clone(), (original_hash.clone(), similarity)); if let Some(compared_hash_parent) = compared_hash_parent { *hashes_parents.get_mut(&compared_hash_parent).expect("Cannot find parent hash") -= 1; } } } #[fun_time(message = "find_similar_hashes", level = "debug")] pub(crate) fn find_similar_hashes(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.image_hashes.is_empty() { return WorkContinueStatus::Continue; } let tolerance = self.get_params().max_difference; // Results let mut collected_similar_images: IndexMap> = Default::default(); let all_hashed_images = mem::take(&mut self.image_hashes); // Checking entries with tolerance 0 is really easy and fast, because only entries with same hashes needs to be checked if tolerance == 0 { for (hash, vec_file_entry) in all_hashed_images { if vec_file_entry.len() >= 2 { collected_similar_images.insert(hash, vec_file_entry); } } } else if self.compare_hashes_with_non_zero_tolerance(&all_hashed_images, &mut collected_similar_images, progress_sender, stop_flag, tolerance) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } Self::verify_duplicated_items(&collected_similar_images); // Info about hashes is not needed anymore, so we drop this info self.similar_vectors = collected_similar_images.into_values().collect(); self.exclude_items_with_same_size(); self.remove_multiple_records_from_reference_folders(); if self.common_data.use_reference_folders { for (_fe, vector) in &self.similar_referenced_vectors { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.similar_vectors { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clean unused data to save ram self.image_hashes = Default::default(); self.images_to_check = Default::default(); self.bktree = BKTree::new(Hamming); WorkContinueStatus::Continue } #[fun_time(message = "exclude_items_with_same_size", level = "debug")] fn exclude_items_with_same_size(&mut self) { if self.get_params().exclude_images_with_same_size { for vec_file_entry in mem::take(&mut self.similar_vectors) { let mut bt_sizes: BTreeSet = Default::default(); let mut vec_values = Vec::new(); for file_entry in vec_file_entry { if bt_sizes.insert(file_entry.size) { vec_values.push(file_entry); } } if vec_values.len() > 1 { self.similar_vectors.push(vec_values); } } } } #[fun_time(message = "remove_multiple_records_from_reference_folders", level = "debug")] fn remove_multiple_records_from_reference_folders(&mut self) { if self.common_data.use_reference_folders { self.similar_referenced_vectors = mem::take(&mut self.similar_vectors) .into_iter() .filter_map(|vec_file_entry| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); } } // TODO this probably not works good when reference folders are used pub(crate) fn verify_duplicated_items(collected_similar_images: &IndexMap>) { if !cfg!(debug_assertions) { return; } // Validating if group contains duplicated results let mut result_hashset: IndexSet = Default::default(); let mut found = false; for vec_file_entry in collected_similar_images.values() { if vec_file_entry.is_empty() { error!("Found empty group"); found = true; continue; } if vec_file_entry.len() == 1 { error!("Found simple element {vec_file_entry:?}"); found = true; continue; } for file_entry in vec_file_entry { let st = file_entry.path.to_string_lossy().to_string(); if result_hashset.contains(&st) { found = true; error!("Duplicated Element {st}"); } else { result_hashset.insert(st); } } } assert!(!found, "Found Invalid entries, verify errors before"); } } fn is_in_reference_folder(reference_directories: &[PathBuf], path: &Path) -> bool { reference_directories.iter().any(|e| path.starts_with(e)) } #[expect(clippy::indexing_slicing)] // Because hash size is validated before pub fn get_string_from_similarity(similarity: u32, hash_size: u8) -> String { let index_preset = match hash_size { 8 => 0, 16 => 1, 32 => 2, 64 => 3, _ => panic!("Invalid hash size {hash_size} (caller is responsible for validating this)"), }; if similarity == 0 { flc!("core_similarity_original") } else if similarity <= SIMILAR_VALUES[index_preset][0] { flc!("core_similarity_very_high") } else if similarity <= SIMILAR_VALUES[index_preset][1] { flc!("core_similarity_high") } else if similarity <= SIMILAR_VALUES[index_preset][2] { flc!("core_similarity_medium") } else if similarity <= SIMILAR_VALUES[index_preset][3] { flc!("core_similarity_small") } else if similarity <= SIMILAR_VALUES[index_preset][4] { flc!("core_similarity_very_small") } else if similarity <= SIMILAR_VALUES[index_preset][5] { flc!("core_similarity_minimal") } else { panic!("Invalid similarity value {similarity} for hash size {hash_size} (index {index_preset}) (caller is responsible for validating this)"); } } #[expect(clippy::indexing_slicing)] // Because hash size is validated before pub fn return_similarity_from_similarity_preset(similarity_preset: SimilarityPreset, hash_size: u8) -> u32 { let index_preset = match hash_size { 8 => 0, 16 => 1, 32 => 2, 64 => 3, _ => panic!("Invalid hash size {hash_size} (caller is responsible for validating this)"), }; match similarity_preset { SimilarityPreset::Original => 0, SimilarityPreset::VeryHigh => SIMILAR_VALUES[index_preset][0], SimilarityPreset::High => SIMILAR_VALUES[index_preset][1], SimilarityPreset::Medium => SIMILAR_VALUES[index_preset][2], SimilarityPreset::Small => SIMILAR_VALUES[index_preset][3], SimilarityPreset::VerySmall => SIMILAR_VALUES[index_preset][4], SimilarityPreset::Minimal => SIMILAR_VALUES[index_preset][5], SimilarityPreset::None => panic!("Invalid similarity preset None (caller is responsible for validating this)"), } } pub(crate) fn convert_filters_to_string(image_filter: FilterType) -> String { match image_filter { FilterType::Lanczos3 => "Lanczos3", FilterType::Nearest => "Nearest", FilterType::Triangle => "Triangle", FilterType::Gaussian => "Gaussian", FilterType::CatmullRom => "CatmullRom", } .to_string() } pub(crate) fn convert_algorithm_to_string(hash_alg: HashAlg) -> String { match hash_alg { HashAlg::Mean => "Mean", HashAlg::Gradient => "Gradient", HashAlg::Blockhash => "Blockhash", HashAlg::VertGradient => "VertGradient", HashAlg::DoubleGradient => "DoubleGradient", HashAlg::Median => "Median", } .to_string() } #[allow(clippy::allow_attributes)] #[allow(unfulfilled_lint_expectations)] // Happens only on release build #[expect(dead_code)] #[expect(unreachable_code)] #[expect(unused_variables)] // Function to validate if after first check there are any duplicated entries // E.g. /a.jpg is used also as master and similar image which is forbidden, because may // cause accidentally delete more pictures that user wanted fn debug_check_for_duplicated_things( use_reference_folders: bool, hashes_parents: &IndexMap, hashes_similarity: &IndexMap, all_hashed_images: &IndexMap>, numm: &str, ) { if !cfg!(debug_assertions) { return; } if use_reference_folders { return; } let mut found_broken_thing = false; let mut hashmap_hashes: IndexSet<_> = Default::default(); let mut hashmap_names: IndexSet<_> = Default::default(); for (hash, number_of_children) in hashes_parents { if *number_of_children > 0 { if hashmap_hashes.contains(hash) { debug!("------1--HASH--{} {:?}", numm, all_hashed_images[hash]); found_broken_thing = true; } hashmap_hashes.insert((*hash).clone()); for i in &all_hashed_images[hash] { let name = i.path.to_string_lossy().to_string(); if hashmap_names.contains(&name) { debug!("------1--NAME--{numm} {name:?}"); found_broken_thing = true; } hashmap_names.insert(name); } } } for hash in hashes_similarity.keys() { if hashmap_hashes.contains(hash) { debug!("------2--HASH--{} {:?}", numm, all_hashed_images[hash]); found_broken_thing = true; } hashmap_hashes.insert((*hash).clone()); for i in &all_hashed_images[hash] { let name = i.path.to_string_lossy().to_string(); if hashmap_names.contains(&name) { debug!("------2--NAME--{numm} {name:?}"); found_broken_thing = true; } hashmap_names.insert(name); } } assert!(!found_broken_thing); } pub fn get_similar_images_cache_file(hash_size: u8, hash_alg: HashAlg, image_filter: FilterType) -> String { format!( "cache_similar_images_{hash_size}_{}_{}_{CACHE_IMAGE_VERSION}.bin", convert_algorithm_to_string(hash_alg), convert_filters_to_string(image_filter), ) } #[cfg(test)] mod tests { use std::path::PathBuf; use bk_tree::BKTree; use image::imageops::FilterType; use image_hasher::HashAlg; use indexmap::IndexMap; use super::*; use crate::common::tool_data::CommonData; use crate::tools::similar_images::{Hamming, ImHash, ImagesEntry, SimilarImages, SimilarImagesParameters}; fn get_default_parameters() -> SimilarImagesParameters { SimilarImagesParameters { hash_alg: HashAlg::Gradient, hash_size: 8, max_difference: 0, image_filter: FilterType::Lanczos3, exclude_images_with_same_size: false, } } // Just to debug changes to algorithms // #[test] // fn test_fuzzer() { // for _ in 0..100 { // let mut parameters = get_default_parameters(); // parameters.similarity = rand::random::() % 40; // let mut similar_images = SimilarImages::new(parameters); // // for i in 0..(rand::random::() % 2000) { // let mut entry = vec![1u8; 8]; // entry[1] = rand::random::(); // if rand::random::() { // entry[2] = rand::random::(); // } // if rand::random::() { // entry[3] = rand::random::(); // } // if rand::random::() { // entry[4] = rand::random::(); // } // let fe = create_random_file_entry(entry, &format!("file_{i}.txt")); // add_hashes(&mut similar_images.image_hashes, vec![fe]); // } // // similar_images.find_similar_hashes(&Arc::default(), None); // } // } #[test] fn test_compare_no_images() { use crate::common::traits::Search; for _ in 0..100 { let mut similar_images = SimilarImages::new(get_default_parameters()); similar_images.search(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 0); } } #[test] fn test_compare_tolerance_0_normal_mode() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 0; let mut similar_images = SimilarImages::new(parameters); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "cde.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "rrt.txt"); let fe5 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "bld.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1.clone(), fe2.clone(), fe3.clone(), fe4.clone(), fe5.clone()]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 2); let first_group = similar_images.get_similar_images()[0].iter().map(|e| &e.path).collect::>(); let second_group = similar_images.get_similar_images()[1].iter().map(|e| &e.path).collect::>(); // Initial order is not guaranteed, so we need to check both options if similar_images.get_similar_images()[0][0].hash == fe1.hash { assert_eq!(first_group, vec![&fe1.path, &fe2.path]); assert_eq!(second_group, vec![&fe3.path, &fe4.path, &fe5.path]); } else { assert_eq!(first_group, vec![&fe3.path, &fe4.path, &fe5.path]); assert_eq!(second_group, vec![&fe1.path, &fe2.path]); } } } #[test] fn test_simple_normal_one_group() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 1; let mut similar_images = SimilarImages::new(parameters); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 1); } } #[test] fn test_2000_hashes() { let mut parameters = get_default_parameters(); parameters.max_difference = 10; let mut similar_images = SimilarImages::new(parameters); for i in 0..2000 { let mut entry = vec![1u8; 8]; entry[7] = (i as u32 % 256) as u8; entry[6] = (i as u32 / 256 % 256) as u8; entry[5] = (i as u32 / 256 / 256 % 256) as u8; let fe = create_random_file_entry(entry, &format!("file_{i}.txt")); add_hashes(&mut similar_images.image_hashes, vec![fe]); } similar_images.find_similar_hashes(&Arc::default(), None); assert!(!similar_images.get_similar_images().is_empty()); } #[test] fn test_simple_normal_one_group_extended() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 2; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 2], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 1); assert_eq!(similar_images.get_similar_images()[0].len(), 3); } } #[test] fn test_simple_normal_one_group_extended2() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 222222; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![59, 41, 53, 27, 19, 143, 228, 228], "abc.txt"); let fe2 = create_random_file_entry(vec![57, 41, 60, 155, 51, 173, 204, 228], "bcd.txt"); let fe3 = create_random_file_entry(vec![28, 222, 206, 192, 203, 157, 25, 24], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 1); assert_eq!(similar_images.get_similar_images()[0].len(), 3); } } #[test] fn test_simple_referenced_same_group() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 0; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images().len(), 0); } } #[test] fn test_simple_referenced_group_extended() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 0; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); assert_eq!(similar_images.get_similar_images_referenced().len(), 1); assert_eq!(similar_images.get_similar_images_referenced()[0].1.len(), 1); } } #[test] fn test_simple_referenced_group_extended2() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 0; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc2.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd2.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 2); assert!(res[0].1.iter().all(|e| e.path.starts_with("/home/kk/"))); } } #[test] fn test_simple_normal_too_small_similarity() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b00001], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b00100], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b10000], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images(); assert!(res.is_empty()); } } #[test] fn test_simple_normal_union_of_similarity() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 4; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(false); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0001], "abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_1111], "bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0111_1111], "rrd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images(); assert_eq!(res.len(), 1); let mut path = res[0].iter().map(|e| e.path.to_string_lossy().to_string()).collect::>(); path.sort(); if res[0].len() == 3 { assert_eq!(path, vec!["abc.txt".to_string(), "bcd.txt".to_string(), "rrd.txt".to_string()]); } else if res[0].len() == 2 { assert!(path == vec!["abc.txt".to_string(), "bcd.txt".to_string()] || path == vec!["bcd.txt".to_string(), "rrd.txt".to_string()]); } else { panic!("Invalid number of items"); } } } #[test] fn test_reference_similarity_only_one() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0011], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 1); assert_eq!(res[0].0.path, PathBuf::from("/home/rr/abc.txt")); assert_eq!(res[0].1[0].path, PathBuf::from("/home/kk/bcd.txt")); } } #[test] fn test_reference_too_small_similarity() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0010], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 0); } } #[test] fn test_reference_minimal() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0011], "/home/kk/bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0100], "/home/kk/bcd2.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1100], "/home/rr/krkr.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 2); assert_eq!(res[0].1.len(), 1); assert_eq!(res[1].1.len(), 1); #[allow(clippy::allow_attributes)] #[allow(clippy::cmp_owned)] // TODO Bug in nightly if res[0].1[0].path == PathBuf::from("/home/kk/bcd.txt") { assert_eq!(res[0].0.path, PathBuf::from("/home/rr/abc.txt")); assert_eq!(res[1].0.path, PathBuf::from("/home/rr/krkr.txt")); } else if res[0].1[0].path == PathBuf::from("/home/kk/bcd2.txt") { assert_eq!(res[0].0.path, PathBuf::from("/home/rr/krkr.txt")); assert_eq!(res[1].0.path, PathBuf::from("/home/rr/abc.txt")); } } } #[test] fn test_reference_same() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 1; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 1); } } #[test] fn test_reference_union() { for _ in 0..100 { let mut parameters = get_default_parameters(); parameters.max_difference = 10; let mut similar_images = SimilarImages::new(parameters); similar_images.set_use_reference_folders(true); // Not using special method, because it validates if path exists similar_images.common_data.directories.reference_directories = vec![PathBuf::from("/home/rr/")]; let fe0 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1000], "/home/rr/abc2.txt"); let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0001], "/home/rr/abc.txt"); let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1110], "/home/kk/bcd.txt"); let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0100], "/home/kk/bcd2.txt"); let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b1100], "/home/rr/krkr.txt"); add_hashes(&mut similar_images.image_hashes, vec![fe0, fe1, fe2, fe3, fe4]); similar_images.find_similar_hashes(&Arc::default(), None); let res = similar_images.get_similar_images_referenced(); assert_eq!(res.len(), 1); assert_eq!(res[0].1.len(), 2); assert_eq!(res[0].0.path, PathBuf::from("/home/rr/krkr.txt")); } } #[test] fn test_tolerance() { // This test not really tests anything, but shows that current hamming distance works // in bits instead of bytes // I tried to make it work in bytes, but it was terrible, so Hamming should be really Ok let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 1]; let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 2]; let mut bktree = BKTree::new(Hamming); bktree.add(fe1); let (similarity, _hash) = bktree.find(&fe2, 100).next().expect("No similar images found"); assert_eq!(similarity, 2); let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 1]; let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 3]; let mut bktree = BKTree::new(Hamming); bktree.add(fe1); let (similarity, _hash) = bktree.find(&fe2, 100).next().expect("No similar images found"); assert_eq!(similarity, 1); let fe1 = vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0000]; let fe2 = vec![1, 1, 1, 1, 1, 1, 1, 0b0000_1000]; let mut bktree = BKTree::new(Hamming); bktree.add(fe1); let (similarity, _hash) = bktree.find(&fe2, 100).next().expect("No similar images found"); assert_eq!(similarity, 1); } fn add_hashes(hashmap: &mut IndexMap>, file_entries: Vec) { for fe in file_entries { hashmap.entry(fe.hash.clone()).or_default().push(fe); } } fn create_random_file_entry(hash: Vec, name: &str) -> ImagesEntry { ImagesEntry { path: PathBuf::from(name.to_string()), size: 0, width: 100, height: 100, modified_date: 0, hash, difference: 0, } } } #[cfg(test)] mod connect_results_tests { use image_hasher::{FilterType, HashAlg}; use indexmap::{IndexMap, IndexSet}; use super::*; #[test] fn test_connect_results_real_case() { let params = SimilarImagesParameters::new(10, 8, HashAlg::Gradient, FilterType::Lanczos3, false); let _finder = SimilarImages::new(params); let hash1: ImHash = vec![59, 41, 53, 27, 19, 143, 228, 228]; let hash2: ImHash = vec![57, 41, 60, 155, 51, 173, 204, 228]; let hash3: ImHash = vec![28, 222, 206, 192, 203, 157, 25, 24]; let partial_results = vec![ (&hash1, vec![(9, &hash2), (43, &hash3)]), (&hash2, vec![(9, &hash1), (38, &hash3)]), (&hash3, vec![(38, &hash2), (43, &hash1)]), ]; let mut hashes_parents: IndexMap = IndexMap::new(); let mut hashes_similarity: IndexMap = IndexMap::new(); let hashes_with_multiple_images: IndexSet = IndexSet::new(); assert_eq!(hashes_parents.len(), 0); assert_eq!(hashes_similarity.len(), 0); SimilarImages::connect_results_simplified(partial_results, &mut hashes_parents, &mut hashes_similarity, &hashes_with_multiple_images); assert_eq!(hashes_parents.len(), 1); assert_eq!(hashes_similarity.len(), 2); } } ================================================ FILE: czkawka_core/src/tools/similar_images/mod.rs ================================================ pub mod core; pub mod traits; pub use core::return_similarity_from_similarity_preset; #[cfg(test)] mod tests; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::time::Duration; use bk_tree::BKTree; use hamming_bitwise_fast::hamming_bitwise_fast; use image_hasher::{FilterType, HashAlg}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; type ImHash = Vec; // 40 is a little useless in 8 similarity - but this value is kept to simplify harder Krokiet max value calculations pub const SIMILAR_VALUES: [[u32; 6]; 4] = [ [1, 2, 5, 7, 14, 40], // 8 [2, 5, 15, 30, 40, 40], // 16 [4, 10, 20, 40, 40, 40], // 32 [6, 20, 40, 40, 40, 40], // 64 ]; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ImagesEntry { pub path: PathBuf, pub size: u64, pub width: u32, pub height: u32, pub modified_date: u64, pub hash: ImHash, pub difference: u32, } impl ResultEntry for ImagesEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_images_entry(self) -> ImagesEntry { ImagesEntry { size: self.size, path: self.path, modified_date: self.modified_date, width: 0, height: 0, hash: Vec::new(), difference: 0, } } } #[derive(Clone, Debug, Copy)] pub enum SimilarityPreset { Original, VeryHigh, High, Medium, Small, VerySmall, Minimal, None, } struct Hamming; impl bk_tree::Metric for Hamming { fn distance(&self, a: &ImHash, b: &ImHash) -> u32 { hamming_bitwise_fast(a, b) } fn threshold_distance(&self, a: &ImHash, b: &ImHash, _threshold: u32) -> Option { Some(self.distance(a, b)) } } #[derive(Clone)] pub struct SimilarImagesParameters { pub max_difference: u32, pub hash_size: u8, pub hash_alg: HashAlg, pub image_filter: FilterType, pub exclude_images_with_same_size: bool, } impl SimilarImagesParameters { pub fn new(max_difference: u32, hash_size: u8, hash_alg: HashAlg, image_filter: FilterType, exclude_images_with_same_size: bool) -> Self { assert!([8, 16, 32, 64].contains(&hash_size)); Self { max_difference, hash_size, hash_alg, image_filter, exclude_images_with_same_size, } } } pub struct SimilarImages { common_data: CommonToolData, information: Info, bktree: BKTree, similar_vectors: Vec>, similar_referenced_vectors: Vec<(ImagesEntry, Vec)>, // Hashmap with image hashes and Vector with names of files image_hashes: IndexMap>, images_to_check: BTreeMap, params: SimilarImagesParameters, } #[derive(Default, Clone, Copy)] pub struct Info { pub initial_found_files: usize, pub number_of_duplicates: usize, pub number_of_groups: usize, pub scanning_time: Duration, } impl SimilarImages { pub fn get_params(&self) -> &SimilarImagesParameters { &self.params } pub const fn get_similar_images(&self) -> &Vec> { &self.similar_vectors } pub fn get_similar_images_referenced(&self) -> &Vec<(ImagesEntry, Vec)> { &self.similar_referenced_vectors } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/similar_images/tests.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use image_hasher::{FilterType, HashAlg}; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::similar_images::{SimilarImages, SimilarImagesParameters}; fn get_test_resources_path() -> PathBuf { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_resources").join("images"); assert!(path.exists(), "Test resources not found at \"{}\"", path.to_string_lossy()); path } #[test] fn test_similar_images() { let test_path = get_test_resources_path(); let algo_filter_hash_sim_found = [ (HashAlg::Gradient, FilterType::Lanczos3, 8, 222240, 2, 1, 3), (HashAlg::Gradient, FilterType::Lanczos3, 8, 15, 1, 1, 2), (HashAlg::Gradient, FilterType::Lanczos3, 8, 8, 0, 0, 0), (HashAlg::Blockhash, FilterType::Lanczos3, 8, 40, 2, 1, 3), (HashAlg::Blockhash, FilterType::Lanczos3, 8, 15, 1, 1, 2), (HashAlg::Blockhash, FilterType::Lanczos3, 8, 2, 0, 0, 0), (HashAlg::Mean, FilterType::Lanczos3, 8, 40, 2, 1, 3), (HashAlg::Mean, FilterType::Lanczos3, 8, 15, 1, 1, 2), (HashAlg::Mean, FilterType::Lanczos3, 8, 2, 0, 0, 0), (HashAlg::DoubleGradient, FilterType::Lanczos3, 8, 40, 2, 1, 3), (HashAlg::DoubleGradient, FilterType::Lanczos3, 8, 15, 1, 1, 2), (HashAlg::DoubleGradient, FilterType::Lanczos3, 8, 2, 0, 0, 0), (HashAlg::VertGradient, FilterType::Lanczos3, 8, 40, 2, 1, 3), (HashAlg::VertGradient, FilterType::Lanczos3, 8, 15, 1, 1, 2), (HashAlg::VertGradient, FilterType::Lanczos3, 8, 2, 0, 0, 0), (HashAlg::Gradient, FilterType::Gaussian, 16, 15, 0, 0, 0), (HashAlg::Gradient, FilterType::Gaussian, 16, 32, 1, 1, 2), (HashAlg::VertGradient, FilterType::Nearest, 16, 32, 1, 1, 2), ]; for (idx, (hash_alg, filter_type, hash_size, similarity, duplicates, groups, all_in_similar)) in algo_filter_hash_sim_found.into_iter().enumerate() { let params = SimilarImagesParameters::new(similarity, hash_size, hash_alg, filter_type, false); let mut finder = SimilarImages::new(params); finder.set_included_paths(vec![test_path.clone()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let similar_images = finder.get_similar_images(); let msg = format!("Failed for algo/filter/hash/similarity set {idx}: {hash_alg:?}/{filter_type:?}/{hash_size}/{similarity}"); assert_eq!(info.initial_found_files, 3, "{msg}"); assert_eq!(info.number_of_duplicates, duplicates, "{msg}"); assert_eq!(info.number_of_groups, groups, "{msg}"); assert_eq!(similar_images.len(), groups, "{msg}"); assert_eq!(similar_images.iter().map(|e| e.len()).sum::(), all_in_similar, "{msg}"); } } #[test] fn test_similar_images_exclude_same_size() { let test_path = get_test_resources_path(); let params = SimilarImagesParameters::new(10, 8, HashAlg::Gradient, FilterType::Lanczos3, true); let mut finder = SimilarImages::new(params); finder.set_included_paths(vec![test_path]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let similar_images = finder.get_similar_images(); let info = finder.get_information(); assert!(info.number_of_groups > 0); for group in similar_images { if group.len() > 1 { let first_size = group[0].size; let all_same_size = group.iter().all(|img| img.size == first_size); assert!(!all_same_size); } } } #[test] fn test_similar_images_empty_directory() { use tempfile::TempDir; let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let params = SimilarImagesParameters::new(10, 8, HashAlg::Gradient, FilterType::Lanczos3, false); let mut finder = SimilarImages::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); let similar_images = finder.get_similar_images(); assert_eq!(info.number_of_duplicates, 0); assert_eq!(info.number_of_groups, 0); assert_eq!(similar_images.len(), 0); } ================================================ FILE: czkawka_core/src/tools/similar_images/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS}; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::similar_images::core::get_string_from_similarity; use crate::tools::similar_images::{Info, SimilarImages, SimilarImagesParameters}; impl AllTraits for SimilarImages {} impl Search for SimilarImages { #[fun_time(message = "find_similar_images", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { let extensions = if cfg!(feature = "heif") { [IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat() } else { [IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat() }; if self.prepare_items(Some(&extensions)).is_err() { return; } self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty(); if self.check_for_similar_images(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.hash_images(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.find_similar_hashes(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DebugPrint for SimilarImages { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for SimilarImages { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; if !self.similar_vectors.is_empty() { write!(writer, "{} images which have similar friends\n\n", self.similar_vectors.len())?; for struct_similar in &self.similar_vectors { writeln!(writer, "Found {} images which have similar friends", struct_similar.len())?; for file_entry in struct_similar { writeln!( writer, "\"{}\" - {}x{} - {} - {}", file_entry.path.to_string_lossy(), file_entry.width, file_entry.height, format_size(file_entry.size, BINARY), get_string_from_similarity(file_entry.difference, self.get_params().hash_size) )?; } writeln!(writer)?; } } else if !self.similar_referenced_vectors.is_empty() { writeln!(writer, "{} images which have similar friends\n\n", self.similar_referenced_vectors.len())?; for (file_entry, vec_file_entry) in &self.similar_referenced_vectors { writeln!(writer, "Found {} images which have similar friends", vec_file_entry.len())?; writeln!(writer)?; writeln!( writer, "\"{}\" - {}x{} - {} - {}", file_entry.path.to_string_lossy(), file_entry.width, file_entry.height, format_size(file_entry.size, BINARY), get_string_from_similarity(file_entry.difference, self.get_params().hash_size) )?; for file_entry in vec_file_entry { writeln!( writer, "\"{}\" - {}x{} - {} - {}", file_entry.path.to_string_lossy(), file_entry.width, file_entry.height, format_size(file_entry.size, BINARY), get_string_from_similarity(file_entry.difference, self.get_params().hash_size) )?; } writeln!(writer)?; } } else { write!(writer, "Not found any similar images.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { if self.get_use_reference() { self.save_results_to_file_as_json_internal(file_name, &self.similar_referenced_vectors, pretty_print) } else { self.save_results_to_file_as_json_internal(file_name, &self.similar_vectors, pretty_print) } } } impl CommonData for SimilarImages { type Info = Info; type Parameters = SimilarImagesParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_duplicates > 0 } } impl DeletingItems for SimilarImages { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.similar_vectors.clone(); self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } ================================================ FILE: czkawka_core/src/tools/similar_videos/core.rs ================================================ use std::collections::{BTreeMap, BTreeSet}; use std::mem; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use indexmap::IndexMap; use log::debug; use rayon::prelude::*; use vid_dup_finder_lib::{CreationOptions, Cropdetect, VideoHash, VideoHashBuilder}; use crate::common::cache::{CACHE_VIDEO_VERSION, load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path}; use crate::common::config_cache_path::get_config_cache_path; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult, inode, take_1_per_inode}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::ResultEntry; use crate::common::video_utils::{VIDEO_THUMBNAILS_SUBFOLDER, VideoMetadata, generate_thumbnail}; use crate::tools::similar_videos::{SimilarVideos, SimilarVideosParameters, VideosEntry}; impl SimilarVideos { pub fn new(params: SimilarVideosParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::SimilarVideos), information: Default::default(), similar_vectors: Vec::new(), videos_hashes: Default::default(), videos_to_check: Default::default(), similar_referenced_vectors: Vec::new(), params, } } #[fun_time(message = "check_for_similar_videos", level = "debug")] pub(crate) fn check_for_similar_videos(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(inode) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { self.videos_to_check = grouped_file_entries .into_par_iter() .flat_map(if self.get_hide_hard_links() { |(_, fes)| fes } else { take_1_per_inode }) .map(|fe| (fe.path.to_string_lossy().to_string(), fe.into_videos_entry())) .collect(); self.common_data.text_messages.warnings.extend(warnings); debug!("check_files - Found {} video files.", self.videos_to_check.len()); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } fn check_video_file_entry(&self, mut file_entry: VideosEntry) -> VideosEntry { let creation_options = CreationOptions { skip_forward_amount: self.params.skip_forward_amount as f64, duration: self.params.duration as f64, cropdetect: self.params.crop_detect, }; let vhash = match VideoHashBuilder::from_options(creation_options).hash(file_entry.path.clone()) { Ok(t) => t, Err(e) => { let path = file_entry.path.to_string_lossy(); file_entry.error = format!("Failed to hash file \"{path}\": reason {e}"); return file_entry; } }; file_entry.vhash = vhash; file_entry } fn read_video_properties(mut file_entry: VideosEntry) -> VideosEntry { match VideoMetadata::from_path(&file_entry.path) { Ok(metadata) => { file_entry.fps = metadata.fps; file_entry.codec = metadata.codec; file_entry.bitrate = metadata.bitrate; file_entry.width = metadata.width; file_entry.height = metadata.height; file_entry.duration = metadata.duration; } Err(e) => { let path = file_entry.path.to_string_lossy(); file_entry.error = format!("Failed to read properties for file \"{path}\": reason {e}"); } } file_entry } #[fun_time(message = "sort_videos", level = "debug")] pub(crate) fn sort_videos(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.videos_to_check.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache_at_start(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SimilarVideosCalculatingHashes, non_cached_files_to_check.len(), self.get_test_type(), 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 ); let non_cached_files_to_check: Vec<_> = non_cached_files_to_check.into_iter().map(|f| f.1).collect(); let mut vec_file_entry: Vec = non_cached_files_to_check .into_par_iter() .with_max_len(2) .map(|file_entry| { if check_if_stop_received(stop_flag) { return None; } // Currently size is not too much relevant // let size = file_entry.size; let res = self.check_video_file_entry(file_entry); let res = Self::read_video_properties(res); progress_handler.increase_items(1); // progress_handler.increase_size(size); Some(res) }) .while_some() .collect::>(); progress_handler.join_thread(); // Just connect loaded results with already calculated hashes vec_file_entry.extend(records_already_cached.into_values()); self.save_cache(&vec_file_entry, loaded_hash_map); let mut hashmap_with_file_entries: IndexMap = Default::default(); let mut vector_of_hashes: Vec = Vec::new(); for file_entry in vec_file_entry { if file_entry.error.is_empty() { vector_of_hashes.push(file_entry.vhash.clone()); hashmap_with_file_entries.insert(file_entry.vhash.src_path().to_string_lossy().to_string(), file_entry); } else { self.common_data.text_messages.warnings.push(file_entry.error); } } // Break if stop was clicked after saving to cache if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } self.match_groups_of_videos(vector_of_hashes, &hashmap_with_file_entries); if self.create_thumbnails(progress_sender, stop_flag) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } self.remove_from_reference_folders(); if self.common_data.use_reference_folders { for (_fe, vector) in &self.similar_referenced_vectors { self.information.number_of_duplicates += vector.len(); self.information.number_of_groups += 1; } } else { for vector in &self.similar_vectors { self.information.number_of_duplicates += vector.len() - 1; self.information.number_of_groups += 1; } } // Clean unused data self.videos_hashes = Default::default(); self.videos_to_check = Default::default(); WorkContinueStatus::Continue } #[fun_time(message = "create_thumbnails", level = "debug")] fn create_thumbnails(&mut self, progress_sender: Option<&Sender>, stop_flag: &Arc) -> WorkContinueStatus { let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::SimilarVideosCreatingThumbnails, self.similar_vectors.iter().map(|e| e.len()).sum::(), self.get_test_type(), 0, ); let Some(config_cache_path) = get_config_cache_path() else { return WorkContinueStatus::Continue; }; let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER); if let Err(e) = std::fs::create_dir_all(&thumbnails_dir) { debug!("Failed to create thumbnails directory: {e}"); return WorkContinueStatus::Continue; } let thumbnail_video_percentage_from_start = self.params.thumbnail_video_percentage_from_start; let generate_grid_instead_of_single = self.params.generate_thumbnail_grid_instead_of_single; let thumbnail_grid_tiles_per_side = self.params.thumbnail_grid_tiles_per_side; let errors = self .similar_vectors .par_iter_mut() .with_max_len(2) .map(|vec_file_entry| { let mut errs = Vec::new(); for file_entry in vec_file_entry { if check_if_stop_received(stop_flag) { return errs; } match generate_thumbnail( stop_flag, &file_entry.path, file_entry.size, file_entry.modified_date, file_entry.duration, &thumbnails_dir, thumbnail_video_percentage_from_start, generate_grid_instead_of_single, thumbnail_grid_tiles_per_side, self.params.generate_thumbnails, ) { Ok(Some(thumbnail_path)) => { file_entry.thumbnail_path = Some(thumbnail_path); } Ok(None) => {} Err(e) => errs.push(e), } progress_handler.increase_items(1); } errs }) .flatten() .collect::>(); self.common_data.text_messages.warnings.extend(errors); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "save_cache", level = "debug")] fn save_cache(&mut self, vec_file_entry: &[VideosEntry], loaded_hash_map: BTreeMap) { save_and_connect_cache_generalized_by_path( &get_similar_videos_cache_file(self.params.skip_forward_amount, self.params.duration, self.params.crop_detect), vec_file_entry, loaded_hash_map, self, ); } #[fun_time(message = "load_cache_at_start", level = "debug")] fn load_cache_at_start(&mut self) -> (BTreeMap, BTreeMap, BTreeMap) { load_and_split_cache_generalized_by_path( &get_similar_videos_cache_file(self.params.skip_forward_amount, self.params.duration, self.params.crop_detect), mem::take(&mut self.videos_to_check), self, ) } #[fun_time(message = "match_groups_of_videos", level = "debug")] fn match_groups_of_videos(&mut self, vector_of_hashes: Vec, hashmap_with_file_entries: &IndexMap) { // Tolerance in library is a value between 0 and 1 // Tolerance in this app is a value between 0 and 20 // Default tolerance in library is 0.30 // We need to allow to set value in range 0 - 0.5 let match_group = vid_dup_finder_lib::search(vector_of_hashes, self.get_params().tolerance as f64 / 40.0f64); let mut collected_similar_videos: Vec> = Default::default(); for i in match_group { let mut temp_vector: Vec = Vec::new(); let mut bt_size: BTreeSet = Default::default(); for j in i.duplicates() { let file_entry = &hashmap_with_file_entries[&j.to_string_lossy().to_string()]; if self.get_params().exclude_videos_with_same_size { if bt_size.insert(file_entry.size) { temp_vector.push(file_entry.clone()); } } else { temp_vector.push(file_entry.clone()); } } if temp_vector.len() > 1 { collected_similar_videos.push(temp_vector); } } self.similar_vectors = collected_similar_videos; } #[fun_time(message = "remove_from_reference_folders", level = "debug")] fn remove_from_reference_folders(&mut self) { if self.common_data.use_reference_folders { self.similar_referenced_vectors = mem::take(&mut self.similar_vectors) .into_iter() .filter_map(|vec_file_entry| { let (mut files_from_referenced_folders, normal_files): (Vec<_>, Vec<_>) = vec_file_entry .into_iter() .partition(|e| self.common_data.directories.is_in_referenced_directory(e.get_path())); if normal_files.is_empty() { None } else { files_from_referenced_folders.pop().map(|file| (file, normal_files)) } }) .collect::)>>(); } } } pub fn get_similar_videos_cache_file(skip_forward_amount: u32, duration: u32, crop_detect: Cropdetect) -> String { let crop_detect_str = match crop_detect { Cropdetect::None => "none", Cropdetect::Letterbox => "letterbox", Cropdetect::Motion => "motion", }; format!("cache_similar_videos_{CACHE_VIDEO_VERSION}__skip_{skip_forward_amount}__dur_{duration}__cd_{crop_detect_str}.bin") } pub fn format_bitrate_opt(bitrate: Option) -> String { match bitrate { Some(b) => { if b >= 1_000_000 { format!("{:.1} Mbps", b as f64 / 1_000_000.0) } else if b >= 1000 { format!("{:.0} kbps", b as f64 / 1000.0) } else { format!("{b} bps") } } None => String::from(""), } } pub fn format_duration_opt(duration: Option) -> String { duration .map(|d| { let hours = (d / 3600.0) as u32; let minutes = ((d % 3600.0) / 60.0) as u32; let seconds = (d % 60.0) as u32; if hours > 0 { format!("{hours:02}:{minutes:02}:{seconds:02}") } else { format!("{minutes:02}:{seconds:02}") } }) .unwrap_or_default() } ================================================ FILE: czkawka_core/src/tools/similar_videos/mod.rs ================================================ pub mod core; pub mod traits; #[cfg(test)] mod tests; use std::collections::BTreeMap; use std::ops::RangeInclusive; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::{Deserialize, Serialize}; use vid_dup_finder_lib::{Cropdetect, VideoHash}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; pub const MAX_TOLERANCE: i32 = 20; pub const DEFAULT_CROP_DETECT: Cropdetect = Cropdetect::Letterbox; pub const ALLOWED_SKIP_FORWARD_AMOUNT: RangeInclusive = 0..=300; pub const DEFAULT_SKIP_FORWARD_AMOUNT: u32 = 15; pub const ALLOWED_VID_HASH_DURATION: RangeInclusive = 2..=60; pub const DEFAULT_VID_HASH_DURATION: u32 = 10; pub const DEFAULT_VIDEO_PERCENTAGE_FOR_THUMBNAIL: u8 = 10; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VideosEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub vhash: VideoHash, pub error: String, // Properties extracted from video pub fps: Option, pub codec: Option, pub bitrate: Option, pub width: Option, pub height: Option, pub duration: Option, #[serde(skip)] // Saving it to cache is bad idea, because cache can be moved to another locations pub thumbnail_path: Option, } impl ResultEntry for VideosEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_videos_entry(self) -> VideosEntry { VideosEntry { size: self.size, path: self.path, modified_date: self.modified_date, vhash: Default::default(), error: String::new(), fps: None, codec: None, bitrate: None, width: None, height: None, duration: None, thumbnail_path: None, } } } #[derive(Clone, Debug)] pub struct SimilarVideosParameters { pub tolerance: i32, pub exclude_videos_with_same_size: bool, pub skip_forward_amount: u32, pub duration: u32, pub crop_detect: Cropdetect, pub generate_thumbnails: bool, pub thumbnail_video_percentage_from_start: u8, pub generate_thumbnail_grid_instead_of_single: bool, pub thumbnail_grid_tiles_per_side: u8, } pub fn crop_detect_from_str_opt(s: &str) -> Option { match s.to_lowercase().as_str() { "none" => Some(Cropdetect::None), "letterbox" => Some(Cropdetect::Letterbox), "motion" => Some(Cropdetect::Motion), _ => None, } } impl SimilarVideosParameters { pub fn new( tolerance: i32, exclude_videos_with_same_size: bool, skip_forward_amount: u32, duration: u32, crop_detect: Cropdetect, generate_thumbnails: bool, thumbnail_video_percentage_from_start: u8, generate_thumbnail_grid_instead_of_single: bool, thumbnail_grid_tiles_per_side: u8, ) -> Self { assert!((0..=MAX_TOLERANCE).contains(&tolerance)); assert!(ALLOWED_SKIP_FORWARD_AMOUNT.contains(&skip_forward_amount)); assert!(ALLOWED_VID_HASH_DURATION.contains(&duration)); Self { tolerance, exclude_videos_with_same_size, skip_forward_amount, duration, crop_detect, generate_thumbnails, thumbnail_video_percentage_from_start, generate_thumbnail_grid_instead_of_single, thumbnail_grid_tiles_per_side, } } } pub struct SimilarVideos { common_data: CommonToolData, information: Info, similar_vectors: Vec>, similar_referenced_vectors: Vec<(VideosEntry, Vec)>, videos_hashes: BTreeMap, Vec>, videos_to_check: BTreeMap, params: SimilarVideosParameters, } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_duplicates: usize, pub number_of_groups: usize, pub scanning_time: Duration, } impl SimilarVideos { pub fn get_params(&self) -> &SimilarVideosParameters { &self.params } pub const fn get_similar_videos(&self) -> &Vec> { &self.similar_vectors } pub const fn get_information(&self) -> Info { self.information } pub fn get_similar_videos_referenced(&self) -> &Vec<(VideosEntry, Vec)> { &self.similar_referenced_vectors } pub fn get_number_of_base_duplicated_files(&self) -> usize { if self.common_data.use_reference_folders { self.similar_referenced_vectors.len() } else { self.similar_vectors.len() } } pub fn get_use_reference(&self) -> bool { self.common_data.use_reference_folders } } ================================================ FILE: czkawka_core/src/tools/similar_videos/tests.rs ================================================ use std::sync::Arc; use std::sync::atomic::AtomicBool; use tempfile::TempDir; use vid_dup_finder_lib::Cropdetect; use crate::common::tool_data::CommonData; use crate::common::traits::Search; use crate::tools::similar_videos::{SimilarVideos, SimilarVideosParameters}; // Tests are quite limited here, due to the needing of external ffmpeg libraries and video files. // Just tested is that searching in an empty directory works as expected - no found similar videos #[test] fn test_similar_videos_empty_directory() { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path(); let params = SimilarVideosParameters::new(10, false, 15, 10, Cropdetect::Letterbox, false, 0, false, 2); let mut finder = SimilarVideos::new(params); finder.set_included_paths(vec![path.to_path_buf()]); finder.set_recursive_search(true); finder.set_use_cache(false); let stop_flag = Arc::new(AtomicBool::new(false)); finder.search(&stop_flag, None); let info = finder.get_information(); assert_eq!(info.number_of_duplicates, 0, "Should find no duplicates in empty directory"); assert_eq!(info.number_of_groups, 0, "Should find no groups in empty directory"); } ================================================ FILE: czkawka_core/src/tools/similar_videos/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::consts::VIDEO_FILES_EXTENSIONS; use crate::common::ffmpeg_utils::check_if_ffprobe_ffmpeg_exists; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::flc; use crate::tools::similar_videos::core::{format_bitrate_opt, format_duration_opt}; use crate::tools::similar_videos::{Info, SimilarVideos, SimilarVideosParameters}; impl AllTraits for SimilarVideos {} impl Search for SimilarVideos { #[fun_time(message = "find_similar_videos", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if !check_if_ffprobe_ffmpeg_exists() { self.common_data.text_messages.critical = Some(flc!("core_ffmpeg_not_found")); #[cfg(target_os = "windows")] self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found_windows")); return; } if self.prepare_items(Some(VIDEO_FILES_EXTENSIONS)).is_err() { return; } self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty() || !self.common_data.directories.reference_files.is_empty(); if self.check_for_similar_videos(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.sort_videos(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DeletingItems for SimilarVideos { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.similar_vectors.clone(); self.delete_advanced_elements_and_add_to_messages(stop_flag, progress_sender, files_to_delete) } } impl DebugPrint for SimilarVideos { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("---------------DEBUG PRINT---------------"); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for SimilarVideos { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; fn write_video_entry(writer: &mut T, file_entry: &crate::tools::similar_videos::VideosEntry) -> std::io::Result<()> { let bitrate = format_bitrate_opt(file_entry.bitrate); let fps = file_entry.fps.map(|e| format!("{e:.2}")).unwrap_or_default(); let codec = file_entry.codec.clone().unwrap_or_default(); let dimensions = if let (Some(w), Some(h)) = (file_entry.width, file_entry.height) { format!("{w}x{h}") } else { "".to_string() }; let duration = format_duration_opt(file_entry.duration); writeln!( writer, "\"{}\" - {} - {} - {} - {} - {} - {}", file_entry.path.to_string_lossy(), format_size(file_entry.size, BINARY), bitrate, fps, codec, dimensions, duration ) } if !self.similar_vectors.is_empty() { write!(writer, "{} videos which have similar friends\n\n", self.similar_vectors.len())?; for struct_similar in &self.similar_vectors { writeln!( writer, "Found {} videos which have similar friends (path, size, bitrate, fps, codec, dimensions, duration)", struct_similar.len() )?; for file_entry in struct_similar { write_video_entry(writer, file_entry)?; } writeln!(writer)?; } } else if !self.similar_referenced_vectors.is_empty() { write!( writer, "{} videos which have similar friends (path, size, bitrate, fps, codec, dimensions, duration)\n\n", self.similar_referenced_vectors.len() )?; for (fe, struct_similar) in &self.similar_referenced_vectors { writeln!(writer, "Found {} videos which have similar friends", struct_similar.len())?; writeln!(writer)?; write_video_entry(writer, fe)?; for file_entry in struct_similar { write_video_entry(writer, file_entry)?; } writeln!(writer)?; } } else { write!(writer, "Not found any similar videos.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { if self.get_use_reference() { self.save_results_to_file_as_json_internal(file_name, &self.similar_referenced_vectors, pretty_print) } else { self.save_results_to_file_as_json_internal(file_name, &self.similar_vectors, pretty_print) } } } impl CommonData for SimilarVideos { type Info = Info; type Parameters = SimilarVideosParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_duplicates > 0 } } ================================================ FILE: czkawka_core/src/tools/temporary/core.rs ================================================ use std::fs::DirEntry; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crossbeam_channel::Sender; use fun_time::fun_time; use rayon::prelude::*; use crate::common::dir_traversal::{common_read_dir, get_modified_time}; use crate::common::directories::Directories; use crate::common::items::ExcludedItems; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::tools::temporary::{Info, TEMP_EXTENSIONS, Temporary, TemporaryFileEntry}; impl Temporary { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::TemporaryFiles), information: Info::default(), temporary_files: Vec::new(), } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let mut folders_to_check: Vec = self.common_data.directories.included_directories.clone(); let progress_handler = prepare_thread_handler_common(progress_sender, CurrentStage::CollectingFiles, 0, self.get_test_type(), 0); while !folders_to_check.is_empty() { if check_if_stop_received(stop_flag) { progress_handler.join_thread(); return WorkContinueStatus::Stop; } let segments: Vec<_> = folders_to_check .into_par_iter() .map(|current_folder| { let mut dir_result = Vec::new(); let mut warnings = Vec::new(); let mut fe_result = Vec::new(); let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return (dir_result, warnings, fe_result); }; // Check every sub folder/file/link etc. for entry in read_dir { let Ok(entry_data) = entry else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue; }; if file_type.is_dir() { check_folder_children( &mut dir_result, &mut warnings, &entry_data, self.common_data.recursive_search, &self.common_data.directories, &self.common_data.excluded_items, ); } else if file_type.is_file() && let Some(file_entry) = self.get_file_entry(progress_handler.items_counter(), &entry_data, &mut warnings) { fe_result.push(file_entry); } } (dir_result, warnings, fe_result) }) .collect(); let required_size = segments.iter().map(|(segment, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, fe_result) in segments { folders_to_check.extend(segment); self.common_data.text_messages.warnings.extend(warnings); for fe in fe_result { self.temporary_files.push(fe); } } } progress_handler.join_thread(); self.information.number_of_temporary_files = self.temporary_files.len(); WorkContinueStatus::Continue } pub(crate) fn get_file_entry(&self, items_counter: &Arc, entry_data: &DirEntry, warnings: &mut Vec) -> Option { items_counter.fetch_add(1, Ordering::Relaxed); let current_file_name = entry_data.path(); if self.common_data.excluded_items.is_excluded(¤t_file_name) { return None; } let file_name = entry_data.file_name(); let file_name_ascii_lowercase = file_name.to_ascii_lowercase(); let file_name_lowercase = file_name_ascii_lowercase.to_string_lossy(); if !TEMP_EXTENSIONS.iter().any(|f| file_name_lowercase.ends_with(f)) { return None; } let Ok(metadata) = entry_data.metadata() else { return None; }; // Creating new file entry Some(TemporaryFileEntry { modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), size: metadata.len(), path: current_file_name, }) } } pub(crate) fn check_folder_children( dir_result: &mut Vec, warnings: &mut Vec, entry_data: &DirEntry, recursive_search: bool, directories: &Directories, excluded_items: &ExcludedItems, ) { if !recursive_search { return; } let next_item = entry_data.path(); if directories.is_excluded_dir(&next_item) { return; } if excluded_items.is_excluded(&next_item) { return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&next_item) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } #[cfg(target_family = "windows")] let _ = warnings; // Silence unused variable warning on Windows dir_result.push(next_item); } ================================================ FILE: czkawka_core/src/tools/temporary/mod.rs ================================================ pub mod core; pub mod traits; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::Serialize; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; const TEMP_EXTENSIONS: &[&str] = &[ "#", "thumbs.db", ".bak", "~", ".tmp", ".temp", ".ds_store", ".crdownload", ".part", ".cache", ".dmp", ".download", ".partial", ]; #[derive(Clone, Serialize, Debug)] pub struct TemporaryFileEntry { pub path: PathBuf, pub modified_date: u64, pub size: u64, } impl ResultEntry for TemporaryFileEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } #[derive(Default, Clone, Copy)] pub struct Info { pub number_of_temporary_files: usize, pub scanning_time: Duration, } pub struct Temporary { common_data: CommonToolData, information: Info, temporary_files: Vec, } impl Default for Temporary { fn default() -> Self { Self::new() } } impl Temporary { pub const fn get_temporary_files(&self) -> &Vec { &self.temporary_files } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/temporary/traits.rs ================================================ use std::io::prelude::*; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData, DeleteItemType, DeleteMethod}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, PrintResults, Search}; use crate::tools::temporary::{Info, Temporary}; impl AllTraits for Temporary {} impl Search for Temporary { #[fun_time(message = "find_temporary_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if self.prepare_items(None).is_err() { return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.delete_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl DeletingItems for Temporary { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.get_cd().delete_method == DeleteMethod::None { return WorkContinueStatus::Continue; } let files_to_delete = self.temporary_files.clone(); self.delete_simple_elements_and_add_to_messages(stop_flag, progress_sender, DeleteItemType::DeletingFiles(files_to_delete)) } } impl PrintResults for Temporary { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; writeln!(writer, "Found {} temporary files.\n", self.information.number_of_temporary_files)?; for file_entry in &self.temporary_files { writeln!(writer, "\"{}\"", file_entry.path.to_string_lossy())?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.temporary_files, pretty_print) } } impl CommonData for Temporary { type Info = Info; type Parameters = (); fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters {} fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { self.information.number_of_temporary_files > 0 } } impl DebugPrint for Temporary { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("### Information's"); println!("Temporary list size - {}", self.temporary_files.len()); self.debug_print_common(); } } ================================================ FILE: czkawka_core/src/tools/video_optimizer/core/video_converter.rs ================================================ use std::fs; use std::path::Path; use std::process::Command; use std::sync::Arc; use std::sync::atomic::AtomicBool; use log::error; use crate::common::process_utils::run_command_interruptible; use crate::common::video_utils::VideoMetadata; use crate::flc; use crate::tools::video_optimizer::{VideoTranscodeEntry, VideoTranscodeFixParams}; pub fn check_video(mut entry: VideoTranscodeEntry) -> VideoTranscodeEntry { let metadata = match VideoMetadata::from_path(&entry.path) { Ok(metadata) => metadata, Err(e) => { entry.error = Some(flc!("core_failed_to_get_video_metadata", file = entry.path.to_string_lossy(), reason = e)); return entry; } }; let Some(current_codec) = metadata.codec.clone() else { entry.error = Some(flc!("core_failed_to_get_video_codec", file = entry.path.to_string_lossy())); return entry; }; let Some(duration) = metadata.duration else { entry.error = Some(flc!("core_failed_to_get_video_duration", file = entry.path.to_string_lossy())); return entry; }; entry.codec = current_codec; entry.duration = duration; match (metadata.width, metadata.height) { (Some(width), Some(height)) => { entry.width = width; entry.height = height; } _ => { entry.error = Some(flc!("core_failed_to_get_video_dimensions", file = entry.path.to_string_lossy())); return entry; } } entry } pub fn process_video(stop_flag: &Arc, video_path: &str, original_size: u64, params: VideoTranscodeFixParams) -> Result<(), String> { let temp_output = Path::new(video_path).with_extension("czkawka_optimized.mp4"); let mut command = Command::new("ffmpeg"); command .arg("-i") .arg(video_path) .arg("-nostdin") .arg("-c:v") .arg(params.codec.as_str()) .arg("-crf") .arg(params.quality.to_string()); if params.limit_video_size { let scale_filter = format!("scale='min({},iw):min({},ih):force_original_aspect_ratio=decrease'", params.max_width, params.max_height); command.arg("-vf").arg(scale_filter); } command.arg("-c:a").arg("copy").arg("-y").arg(&temp_output); match run_command_interruptible(command, stop_flag) { None => { let _ = fs::remove_file(&temp_output); return Err(flc!("core_video_processing_stopped_by_user")); } Some(Err(e)) => { let _ = fs::remove_file(&temp_output); return Err(flc!("core_failed_to_process_video", file = video_path, reason = e)); } Some(Ok(output)) => { if !output.status.success() { let connected = format!("{} - {}", output.stdout, output.stderr); if connected.to_lowercase().contains("unknown encoder") { return Err(flc!("core_ffmpeg_unknown_encoder", file = video_path, encoder = params.codec.as_ffprobe_codec_name())); } error!( "FFmpeg failed to transcode video \"{}\" with status {}. Stdout: {}, Stderr: {}", video_path, output.status, output.stdout, output.stderr ); return Err(flc!("core_ffmpeg_error", file = video_path, code = output.status.to_string(), reason = output.stderr)); } } } let metadata = fs::metadata(&temp_output).map_err(|e| { let _ = fs::remove_file(&temp_output); flc!( "core_failed_to_get_metadata_of_optimized_file", file = temp_output.to_string_lossy(), reason = e.to_string() ) })?; let new_size = metadata.len(); if params.fail_if_not_smaller && new_size >= original_size { let _ = fs::remove_file(&temp_output); return Err(flc!( "core_optimized_file_larger", optimized = temp_output.to_string_lossy(), new_size = new_size, original = video_path, original_size = original_size )); } if params.overwrite_original { fs::rename(&temp_output, video_path).map_err(|e| { let _ = fs::remove_file(&temp_output); flc!("core_failed_to_replace_with_optimized", file = video_path, reason = e.to_string()) })?; return Ok(()); } Ok(()) } ================================================ FILE: czkawka_core/src/tools/video_optimizer/core/video_cropper.rs ================================================ use std::path::Path; use std::process::Command; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use image::RgbImage; use log::error; use crate::common::consts::VIDEO_RESOLUTION_LIMIT; use crate::common::process_utils::run_command_interruptible; use crate::common::video_utils::{VideoMetadata, extract_frame_ffmpeg}; use crate::flc; use crate::tools::video_optimizer::{VideoCropEntry, VideoCropParams, VideoCropSingleFixParams, VideoCroppingMechanism}; const MIN_SAMPLES: usize = 3; const MIN_SAMPLE_INTERVAL: f32 = 0.1; #[derive(Debug, Clone, Copy, PartialEq)] struct Rectangle { top: u32, bottom: u32, left: u32, right: u32, } impl Rectangle { fn new(top: u32, bottom: u32, left: u32, right: u32) -> Self { let s = Self { top, bottom, left, right }; s.validate(); s } fn union(&self, other: &Self) -> Self { let s = Self { top: self.top.min(other.top), bottom: self.bottom.max(other.bottom), left: self.left.min(other.left), right: self.right.max(other.right), }; s.validate(); s } fn validate(&self) { assert!( self.left <= self.right && self.top <= self.bottom, "Invalid rectangle coordinates: top={}, bottom={}, left={}, right={}. Expected: left <= right && top <= bottom (critical algorithm error, please report an issue)", self.top, self.bottom, self.left, self.right ); } fn validate_image_size(&self, width: u32, height: u32) { assert!( self.right <= width && self.bottom <= height, "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)", width, height, self.right, self.bottom ); } fn is_cropping_needed(&self, width: u32, height: u32, min_crop_size: u32) -> bool { let right_margin = width - self.right; let bottom_margin = height - self.bottom; self.left > min_crop_size || right_margin > min_crop_size || self.top > min_crop_size || bottom_margin > min_crop_size } } fn is_pixel_black(img: &image::RgbImage, x: u32, y: u32, black_pixel_threshold: u8) -> bool { let pixel = img.get_pixel(x, y); pixel.0.iter().all(|&channel| channel <= black_pixel_threshold) } #[derive(Debug)] enum BlackBarResult { NoBlackBars, BlackBarsDetected(Rectangle), FullBlackImage, } fn detect_black_bars(rgb_img: &RgbImage, params: &VideoCropParams) -> BlackBarResult { let (width, height) = rgb_img.dimensions(); let min_percentage = params.black_bar_min_percentage as f32 / 100.0; let mut left_crop = 0u32; for x in 0..width { let black_pixels = (0..height).filter(|&y| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count(); if (black_pixels as f32 / height as f32) < min_percentage { break; } left_crop = x + 1; } let mut right_pos = width; for x in (0..width).rev() { let black_pixels = (0..height).filter(|&y| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count(); if (black_pixels as f32 / height as f32) < min_percentage { right_pos = x + 1; break; } } if left_crop >= right_pos { return BlackBarResult::FullBlackImage; } let mut top_crop = 0u32; for y in 0..height { let black_pixels = (0..width).filter(|&x| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count(); if (black_pixels as f32 / width as f32) < min_percentage { break; } top_crop = y + 1; } let mut bottom_pos = height; for y in (0..height).rev() { let black_pixels = (0..width).filter(|&x| is_pixel_black(rgb_img, x, y, params.black_pixel_threshold)).count(); if (black_pixels as f32 / width as f32) < min_percentage { bottom_pos = y + 1; break; } } if top_crop >= bottom_pos { return BlackBarResult::FullBlackImage; } let rect = Rectangle::new(top_crop, bottom_pos, left_crop, right_pos); if rect.is_cropping_needed(width, height, params.min_crop_size) { BlackBarResult::BlackBarsDetected(rect) } else { BlackBarResult::NoBlackBars } } fn analyze_black_bars( duration: f32, get_frame: &F, stop_flag: &Arc, first_frame: &RgbImage, params: &VideoCropParams, path: &Path, ) -> Option, String>> where F: Fn(f32) -> Result, { if stop_flag.load(Ordering::Relaxed) { return None; } let mut rectangle = match detect_black_bars(first_frame, params) { BlackBarResult::BlackBarsDetected(rect) => Some(rect), BlackBarResult::NoBlackBars => { return Some(Ok(None)); } BlackBarResult::FullBlackImage => None, }; let num_samples = ((duration / MIN_SAMPLE_INTERVAL).floor() as usize).clamp(MIN_SAMPLES, params.max_samples); for i in 1..num_samples { if stop_flag.load(Ordering::Relaxed) { return None; } let timestamp = (i as f32 / num_samples as f32) * duration; let tmp_frame = match get_frame(timestamp) { Ok(frame) => frame, Err(e) => { return Some(Err(flc!( "core_failed_get_frame_at_timestamp", file = path.to_string_lossy().to_string(), timestamp = timestamp, reason = e ))); } }; if tmp_frame.dimensions() != first_frame.dimensions() { return Some(Err(flc!( "core_frame_dimensions_mismatch", timestamp = timestamp, first_w = first_frame.width(), first_h = first_frame.height() ))); } match detect_black_bars(&tmp_frame, params) { BlackBarResult::BlackBarsDetected(tmp_rect) => { rectangle = match rectangle { Some(current_rect) => Some(current_rect.union(&tmp_rect)), None => Some(tmp_rect), }; } BlackBarResult::NoBlackBars => { return Some(Ok(None)); } BlackBarResult::FullBlackImage => { // Do nothing - leave the current rectangle as is } } } if let Some(rectangle) = rectangle { rectangle.validate(); // Rectangle may extend step by step to full image size, so that is why previous checks are not enough if !rectangle.is_cropping_needed(first_frame.width(), first_frame.height(), params.min_crop_size) { return Some(Ok(None)); } Some(Ok(Some(rectangle))) } else { Some(Ok(None)) // All frames were fully black } } fn diff_between_dynamic_images(img_original: &RgbImage, mut consumed_temp_img: RgbImage) -> RgbImage { assert_eq!( img_original.dimensions(), consumed_temp_img.dimensions(), "Image dimensions do not match for diffing (critical algorithm error, please report an issue)" ); img_original.pixels().zip(consumed_temp_img.pixels_mut()).for_each(|(img_original_pixel, consumed_pixel)| { consumed_pixel .0 .iter_mut() .zip(img_original_pixel.0.iter()) .for_each(|(consumed_channel, &original_channel)| { *consumed_channel = original_channel.abs_diff(*consumed_channel); }); }); consumed_temp_img } fn analyze_static_image_parts( duration: f32, get_frame: &F, stop_flag: &Arc, first_frame: &RgbImage, params: &VideoCropParams, path: &Path, ) -> Option, String>> where F: Fn(f32) -> Result, { if stop_flag.load(Ordering::Relaxed) { return None; } // Initial rectangle is empty, because with only one frame we cannot determine static parts let mut rectangle: Option = None; let num_samples = ((duration / MIN_SAMPLE_INTERVAL).floor() as usize).clamp(MIN_SAMPLES, params.max_samples); for i in 1..num_samples { if stop_flag.load(Ordering::Relaxed) { return None; } let timestamp = (i as f32 / num_samples as f32) * duration; let tmp_frame = match get_frame(timestamp) { Ok(frame) => frame, Err(e) => { return Some(Err(flc!( "core_failed_get_frame_from_file", file = path.to_string_lossy().to_string(), timestamp = timestamp, reason = e ))); } }; if tmp_frame.dimensions() != first_frame.dimensions() { return Some(Err(flc!( "core_frame_dimensions_mismatch", timestamp = timestamp, first_w = first_frame.width(), first_h = first_frame.height() ))); } let dynamic_image_diff: RgbImage = diff_between_dynamic_images(first_frame, tmp_frame); match detect_black_bars(&dynamic_image_diff, params) { BlackBarResult::FullBlackImage => { // Do nothing - leave the current rectangle as is } BlackBarResult::NoBlackBars => { return Some(Ok(None)); } BlackBarResult::BlackBarsDetected(tmp_rect) => { rectangle = match rectangle { Some(current_rect) => Some(current_rect.union(&tmp_rect)), None => Some(tmp_rect), }; } } } if let Some(rectangle) = rectangle { rectangle.validate(); // Rectangle may extend step by step to full image size, so that is why previous checks are not enough if !rectangle.is_cropping_needed(first_frame.width(), first_frame.height(), params.min_crop_size) { return Some(Ok(None)); } Some(Ok(Some(rectangle))) } else { Some(Ok(None)) // All frames were fully static } } fn extract_video_metadata_for_crop(entry: &mut VideoCropEntry) -> Result<(u32, u32, f64, f64), ()> { let metadata = match VideoMetadata::from_path(&entry.path) { Ok(metadata) => metadata, Err(e) => { entry.error = Some(format!("Failed to get video metadata for file \"{}\": {}", entry.path.to_string_lossy(), e)); return Err(()); } }; let Some(current_codec) = metadata.codec.clone() else { entry.error = Some(format!("Failed to get video codec from metadata for file \"{}\"", entry.path.to_string_lossy())); return Err(()); }; entry.codec = current_codec; let (width, height) = match (metadata.width, metadata.height) { (Some(width), Some(height)) => { entry.width = width; entry.height = height; (width, height) } _ => { entry.error = Some(format!("Failed to get video dimensions from metadata for file \"{}\"", entry.path.to_string_lossy())); return Err(()); } }; let Some(duration) = metadata.duration else { entry.error = Some(format!("Failed to get video duration from metadata, for file \"{}\"", entry.path.to_string_lossy())); return Err(()); }; entry.duration = duration; let fps = metadata.fps.unwrap_or(25.0); Ok((width, height, duration, fps)) } pub fn check_video_crop(mut entry: VideoCropEntry, params: &VideoCropParams, stop_flag: &Arc) -> Option { let Ok((_width, _height, duration, _fps)) = extract_video_metadata_for_crop(&mut entry) else { return Some(entry); }; let video_path = entry.path.clone(); let get_frame = |timestamp: f32| -> Result { extract_frame_ffmpeg(&video_path, timestamp, None) }; // TODO - metadata are broken? Not proper? // Metadata shows different dimensions than actual frames extracted - quite strange, probably rotated data - let first_frame = match get_frame(0.0) { Ok(frame) => frame, Err(e) => { entry.error = Some(format!("Failed to extract first frame for video \"{}\": {}", entry.path.to_string_lossy(), e)); return Some(entry); } }; let (width, height) = first_frame.dimensions(); entry.height = height; entry.width = width; if entry.width > VIDEO_RESOLUTION_LIMIT || entry.height > VIDEO_RESOLUTION_LIMIT { entry.error = Some(format!( "Image dimensions for video \"{}\" exceed the limit: {}x{} > {}x{}", entry.path.to_string_lossy(), entry.width, entry.height, VIDEO_RESOLUTION_LIMIT, VIDEO_RESOLUTION_LIMIT )); return Some(entry); } match params.crop_detect { VideoCroppingMechanism::BlackBars => match analyze_black_bars(duration as f32, &get_frame, stop_flag, &first_frame, params, &entry.path) { Some(Ok(Some(rectangle))) => { rectangle.validate_image_size(width, height); entry.new_image_dimensions = (rectangle.left, rectangle.top, rectangle.right, rectangle.bottom); } Some(Ok(None)) => { // No black bars } Some(Err(e)) => { entry.error = Some(e); return Some(entry); } None => return None, }, VideoCroppingMechanism::StaticContent => match analyze_static_image_parts(duration as f32, &get_frame, stop_flag, &first_frame, params, &entry.path) { Some(Ok(Some(rectangle))) => { rectangle.validate_image_size(width, height); entry.new_image_dimensions = (rectangle.left, rectangle.top, rectangle.right, rectangle.bottom); } Some(Ok(None)) => {} Some(Err(e)) => { entry.error = Some(e); return Some(entry); } None => return None, }, } Some(entry) } pub fn fix_video_crop(video_path: &Path, params: &VideoCropSingleFixParams, stop_flag: &Arc, current_codec: &str) -> Result<(), String> { if stop_flag.load(Ordering::Relaxed) { return Err("Video processing was stopped by user".to_string()); } let (left, top, right, bottom) = params.crop_rectangle; if left >= right || top >= bottom { return Err(flc!("core_invalid_crop_rectangle", left = left, top = top, right = right, bottom = bottom)); } let crop_width = right - left; let crop_height = bottom - top; let crop_type_suffix = match params.crop_mechanism { VideoCroppingMechanism::BlackBars => "blackbars", VideoCroppingMechanism::StaticContent => "staticcontent", }; let extension = video_path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); let temp_output = video_path.with_extension(format!("czkawka_cropped_{crop_type_suffix}.{extension}")); let mut command = Command::new("ffmpeg"); command.arg("-i").arg(video_path).arg("-vf").arg(format!("crop={crop_width}:{crop_height}:{left}:{top}")); match (params.target_codec, params.quality) { (None, None) => { // Do nothing, do not convert video to different codec } (Some(target_codec), Some(quality)) => { command.arg("-c:v").arg(target_codec.as_str()).arg("-crf").arg(quality.to_string()); } _ => { return Err("Both target_codec and quality must be specified together".to_string()); } } command.arg("-c:a").arg("copy"); command.arg("-y").arg(&temp_output); match run_command_interruptible(command, stop_flag) { None => { let _ = std::fs::remove_file(&temp_output); return Err(String::from("Video cropping was stopped by user")); } Some(Err(e)) => { let _ = std::fs::remove_file(&temp_output); return Err(flc!("core_failed_to_crop_video_file", file = video_path.to_string_lossy(), reason = e)); } Some(Ok(output)) => { if !output.status.success() { let connected = format!("{} - {}", output.stdout, output.stderr); if connected.to_lowercase().contains("unknown encoder") { let missing_codec = match params.target_codec { Some(target_codec) => target_codec.as_ffprobe_codec_name(), None => current_codec, }; return Err(flc!("core_ffmpeg_unknown_encoder", file = video_path.to_string_lossy(), encoder = missing_codec)); } error!( "FFmpeg failed to crop video \"{}\" with status {}. Stdout: {}, Stderr: {}", video_path.to_string_lossy(), output.status, output.stdout, output.stderr ); return Err(flc!( "core_ffmpeg_error", file = video_path.to_string_lossy(), code = output.status.to_string(), reason = output.stderr )); } } } if !temp_output.exists() { error!("Cropped video file was not created: {temp_output:?}"); return Err(flc!("core_cropped_video_not_created", temp = format!("{:?}", temp_output))); } if params.overwrite_original { std::fs::rename(&temp_output, video_path).map_err(|e| format!("Failed to replace original file: {e}"))?; } Ok(()) } #[cfg(test)] mod tests { use std::sync::Arc; use std::sync::atomic::AtomicBool; use image::RgbImage; use super::*; fn default_test_params() -> VideoCropParams { VideoCropParams { crop_detect: VideoCroppingMechanism::BlackBars, black_pixel_threshold: 20, black_bar_min_percentage: 90, max_samples: 60, min_crop_size: 5, generate_thumbnails: false, thumbnail_video_percentage_from_start: 0, generate_thumbnail_grid_instead_of_single: false, thumbnail_grid_tiles_per_side: 2, } } fn create_colored_frame(width: u32, height: u32, r: u8, g: u8, b: u8) -> RgbImage { let mut img = RgbImage::new(width, height); for pixel in img.pixels_mut() { *pixel = image::Rgb([r, g, b]); } img } fn create_frame_with_black_bars(width: u32, height: u32, bar_size: u32) -> RgbImage { let mut img = RgbImage::new(width, height); for (x, y, pixel) in img.enumerate_pixels_mut() { if x < bar_size || x >= width - bar_size || y < bar_size || y >= height - bar_size { *pixel = image::Rgb([0, 0, 0]); } else { *pixel = image::Rgb([100, 150, 200]); } } img } #[test] fn test_is_pixel_black() { let params = default_test_params(); let black_img = RgbImage::from_pixel(10, 10, image::Rgb([0, 0, 0])); assert!(is_pixel_black(&black_img, 5, 5, params.black_pixel_threshold)); let light_gray_img = RgbImage::from_pixel(10, 10, image::Rgb([20, 20, 20])); assert!(is_pixel_black(&light_gray_img, 5, 5, params.black_pixel_threshold)); let dark_gray_img = RgbImage::from_pixel(10, 10, image::Rgb([21, 21, 21])); assert!(!is_pixel_black(&dark_gray_img, 5, 5, params.black_pixel_threshold)); let white_img = RgbImage::from_pixel(10, 10, image::Rgb([255, 255, 255])); assert!(!is_pixel_black(&white_img, 5, 5, params.black_pixel_threshold)); } #[test] fn test_detect_black_bars_no_bars() { let params = default_test_params(); let img = create_colored_frame(100, 100, 100, 150, 200); let result = detect_black_bars(&img, ¶ms); assert!(matches!(result, BlackBarResult::NoBlackBars)); } #[test] fn test_detect_black_bars_with_bars() { let params = default_test_params(); let img = create_frame_with_black_bars(200, 200, 20); let result = detect_black_bars(&img, ¶ms); if let BlackBarResult::BlackBarsDetected(rect) = result { assert!(rect.left >= 15 && rect.left <= 25, "Left crop: {}", rect.left); assert!(rect.top >= 15 && rect.top <= 25, "Top crop: {}", rect.top); assert!(rect.right >= 175 && rect.right <= 185, "Right position: {}", rect.right); assert!(rect.bottom >= 175 && rect.bottom <= 185, "Bottom position: {}", rect.bottom); } else { panic!("Expected BlackBarsDetected, got {result:?}"); } } #[test] fn test_detect_black_bars_small_bars() { let params = default_test_params(); let img = create_frame_with_black_bars(200, 200, 3); let result = detect_black_bars(&img, ¶ms); assert!(matches!(result, BlackBarResult::NoBlackBars)); } #[test] fn test_rectangle_union() { let rect1 = Rectangle::new(10, 10, 10, 10); let rect2 = Rectangle::new(5, 15, 8, 12); let union = rect1.union(&rect2); assert_eq!(union.top, 5); assert_eq!(union.bottom, 15); assert_eq!(union.left, 8); assert_eq!(union.right, 12); } #[test] fn test_rectangle_is_cropping_needed() { let params = default_test_params(); // Image 100x100, cropped to (10, 10) -> (90, 90), so 10px margin on each side let cropping_needed = Rectangle::new(10, 90, 10, 90); assert!(cropping_needed.is_cropping_needed(100, 100, params.min_crop_size)); // Image 100x100, no cropping: (0, 0) -> (100, 100) let no_cropping_needed = Rectangle::new(0, 100, 0, 100); assert!(!no_cropping_needed.is_cropping_needed(100, 100, params.min_crop_size)); // Image 100x100, small crop (3px on each side) - below threshold let small_crop = Rectangle::new(3, 97, 3, 97); assert!(!small_crop.is_cropping_needed(100, 100, params.min_crop_size)); } #[test] fn test_analyze_black_bars_consistent_bars() { let params = default_test_params(); let stop_flag = Arc::new(AtomicBool::new(false)); let duration = 10.0; let get_frame = |_timestamp: f32| -> Result { Ok(create_frame_with_black_bars(200, 200, 20)) }; let result = analyze_black_bars( duration, &get_frame, &stop_flag, &create_frame_with_black_bars(200, 200, 20), ¶ms, Path::new("text.txt"), ); assert!(result.expect("Expected Result").unwrap().is_some()); } #[test] fn test_analyze_black_bars_no_bars() { let params = default_test_params(); let stop_flag = Arc::new(AtomicBool::new(false)); let duration = 10.0; let get_frame = |_timestamp: f32| -> Result { Ok(create_colored_frame(200, 200, 100, 150, 200)) }; let result = analyze_black_bars( duration, &get_frame, &stop_flag, &create_colored_frame(200, 200, 100, 150, 200), ¶ms, Path::new("text.txt"), ); assert!(result.expect("Expected Result").unwrap().is_none()); } #[test] fn test_analyze_black_bars_inconsistent_bars() { let params = default_test_params(); let stop_flag = Arc::new(AtomicBool::new(false)); let duration = 10.0; let get_frame = |timestamp: f32| -> Result { if timestamp < 5.0 { Ok(create_frame_with_black_bars(200, 200, 20)) } else { Ok(create_colored_frame(200, 200, 100, 150, 200)) } }; let result = analyze_black_bars( duration, &get_frame, &stop_flag, &create_frame_with_black_bars(200, 200, 20), ¶ms, Path::new("text.txt"), ); assert!(result.expect("Expected Result").unwrap().is_none()); } #[test] fn test_analyze_black_bars_variable_rectangles() { let params = default_test_params(); let stop_flag = Arc::new(AtomicBool::new(false)); let duration = 10.0; let get_frame = |timestamp: f32| -> Result { if timestamp < 3.0 { Ok(create_frame_with_black_bars(200, 200, 20)) } else if timestamp < 7.0 { Ok(create_frame_with_black_bars(200, 200, 18)) } else { Ok(create_frame_with_black_bars(200, 200, 22)) } }; let result = analyze_black_bars( duration, &get_frame, &stop_flag, &create_frame_with_black_bars(200, 200, 20), ¶ms, Path::new("text.txt"), ); let rect = result.expect("Expected Result").unwrap().unwrap(); assert_eq!(rect.left, 18); assert_eq!(rect.top, 18); assert_eq!(rect.right, 200 - 18); assert_eq!(rect.bottom, 200 - 18); } #[test] fn test_detect_black_bars_fuzzer() { let params = default_test_params(); let test_cases = vec![ (1, 1, "1x1 image"), (1, 100, "1 pixel wide"), (100, 1, "1 pixel tall"), (2, 2, "2x2 minimum"), (10, 10, "10x10 small"), (100, 100, "100x100 medium"), (1920, 1080, "1920x1080 Full HD"), (3840, 2160, "3840x2160 4K"), ]; for (width, height, desc) in test_cases { // Test 1: All black image let mut all_black = RgbImage::new(width, height); for pixel in all_black.pixels_mut() { *pixel = image::Rgb([0, 0, 0]); } let result = detect_black_bars(&all_black, ¶ms); assert!(matches!(result, BlackBarResult::FullBlackImage), "All black image should return FullBlackImage for {desc}"); // Test 2: All white image let mut all_white = RgbImage::new(width, height); for pixel in all_white.pixels_mut() { *pixel = image::Rgb([255, 255, 255]); } let result = detect_black_bars(&all_white, ¶ms); assert!(matches!(result, BlackBarResult::NoBlackBars), "All white image should return NoBlackBars for {desc}"); // Test 4: Checkerboard pattern (no black bars) if width > 4 && height > 4 { let mut checkerboard = RgbImage::new(width, height); for (x, y, pixel) in checkerboard.enumerate_pixels_mut() { let color = if (x + y) % 2 == 0 { 255 } else { 0 }; *pixel = image::Rgb([color, color, color]); } let result = detect_black_bars(&checkerboard, ¶ms); assert!(matches!(result, BlackBarResult::NoBlackBars), "Checkerboard should return NoBlackBars for {desc}"); } } } } ================================================ FILE: czkawka_core/src/tools/video_optimizer/core.rs ================================================ use std::collections::BTreeMap; use std::mem; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crossbeam_channel::Sender; use fun_time::fun_time; use log::{debug, info}; use rayon::prelude::*; use crate::common::cache::{load_and_split_cache_generalized_by_path, save_and_connect_cache_generalized_by_path}; use crate::common::config_cache_path::get_config_cache_path; use crate::common::dir_traversal::{DirTraversalBuilder, DirTraversalResult}; use crate::common::model::{ToolType, WorkContinueStatus}; use crate::common::progress_data::{CurrentStage, ProgressData}; use crate::common::progress_stop_handler::{check_if_stop_received, prepare_thread_handler_common}; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::video_utils::{VIDEO_THUMBNAILS_SUBFOLDER, generate_thumbnail}; use crate::tools::video_optimizer::{ Info, VideoCropEntry, VideoCropParams, VideoCropSingleFixParams, VideoOptimizer, VideoOptimizerFixParams, VideoOptimizerParameters, VideoTranscodeEntry, VideoTranscodeParams, }; mod video_converter; mod video_cropper; pub use video_converter::process_video; pub use video_cropper::fix_video_crop; use crate::common::cache::CACHE_VIDEO_OPTIMIZE_VERSION; use crate::common::traits::ResultEntry; use crate::flc; impl VideoOptimizer { pub fn new(params: VideoOptimizerParameters) -> Self { Self { common_data: CommonToolData::new(ToolType::VideoOptimizer), information: Info::default(), video_transcode_test_entries: Default::default(), video_crop_test_entries: Default::default(), video_transcode_result_entries: Vec::new(), video_crop_result_entries: Vec::new(), params, } } #[fun_time(message = "scan_files", level = "debug")] pub(crate) fn scan_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { let result = DirTraversalBuilder::new() .group_by(|_fe| ()) .stop_flag(stop_flag) .progress_sender(progress_sender) .common_data(&self.common_data) .build() .run(); match result { DirTraversalResult::SuccessFiles { grouped_file_entries, warnings } => { match &self.params { VideoOptimizerParameters::VideoTranscode(_) => { self.video_transcode_test_entries = grouped_file_entries .into_values() .flatten() .map(|fe| (fe.get_path().to_string_lossy().to_string(), fe.into_video_transcode_entry())) .collect(); info!("Found {} files to check", self.video_transcode_test_entries.len()); } VideoOptimizerParameters::VideoCrop(_) => { self.video_crop_test_entries = grouped_file_entries .into_values() .flatten() .map(|fe| (fe.get_path().to_string_lossy().to_string(), fe.into_video_crop_entry())) .collect(); info!("Found {} files to check", self.video_crop_test_entries.len()); } } self.common_data.text_messages.warnings.extend(warnings); WorkContinueStatus::Continue } DirTraversalResult::Stopped => WorkContinueStatus::Stop, } } #[fun_time(message = "check_files", level = "debug")] pub(crate) fn check_files(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { match self.params.clone() { VideoOptimizerParameters::VideoTranscode(params) => self.process_video_transcode(stop_flag, progress_sender, params), VideoOptimizerParameters::VideoCrop(_) => self.process_video_crop(stop_flag, progress_sender), } } #[fun_time(message = "process_video_transcode", level = "debug")] fn process_video_transcode(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, params: VideoTranscodeParams) -> WorkContinueStatus { if self.video_transcode_test_entries.is_empty() { return WorkContinueStatus::Continue; } let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_video_transcode_cache(); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::VideoOptimizerProcessingVideos, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|entry| entry.size).sum(), ); let mut entries: Vec = non_cached_files_to_check .into_par_iter() .map(|(_path, entry)| { if check_if_stop_received(stop_flag) { return None; } let size = entry.size; let res = video_converter::check_video(entry); progress_handler.increase_items(1); progress_handler.increase_size(size); Some(res) }) .while_some() .collect(); self.common_data.text_messages.warnings.extend(entries.iter().filter_map(|e| e.error.as_ref()).cloned()); entries.extend(records_already_cached.into_values()); progress_handler.join_thread(); self.save_video_transcode_cache(&entries, loaded_hash_map); entries.retain(|e| e.error.is_none() && !params.excluded_codecs.contains(&e.codec)); self.video_transcode_result_entries = entries; self.information.number_of_videos_to_transcode = self.video_transcode_result_entries.len(); if self.create_transcode_thumbnails(progress_sender, stop_flag, ¶ms) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "process_video_crop", level = "debug")] fn process_video_crop(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) -> WorkContinueStatus { if self.video_crop_test_entries.is_empty() { return WorkContinueStatus::Continue; } let VideoOptimizerParameters::VideoCrop(params) = self.params.clone() else { unreachable!("process_video_crop called with non VideoCrop parameters, caller is responsible for that"); }; let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_video_crop_cache(¶ms); let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::VideoOptimizerProcessingVideos, non_cached_files_to_check.len(), self.get_test_type(), non_cached_files_to_check.values().map(|entry| entry.size).sum(), ); let mut vec_file_entry: Vec = non_cached_files_to_check .into_par_iter() .map(|(_path, entry)| { if check_if_stop_received(stop_flag) { return None; } let size = entry.size; let res = video_cropper::check_video_crop(entry, ¶ms, stop_flag); progress_handler.increase_items(1); progress_handler.increase_size(size); res }) .while_some() .collect(); self.common_data .text_messages .warnings .extend(vec_file_entry.iter().filter_map(|e| e.error.as_ref()).cloned()); vec_file_entry.extend(records_already_cached.into_values()); progress_handler.join_thread(); self.save_video_crop_cache(&vec_file_entry, ¶ms, loaded_hash_map); vec_file_entry.retain(|e| e.error.is_none() && e.new_image_dimensions != (0, 0, 0, 0)); self.video_crop_result_entries = vec_file_entry; self.information.number_of_videos_to_crop = self.video_crop_result_entries.len(); if self.create_crop_thumbnails(progress_sender, stop_flag, ¶ms) == WorkContinueStatus::Stop { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "create_transcode_thumbnails", level = "debug")] fn create_transcode_thumbnails(&mut self, progress_sender: Option<&Sender>, stop_flag: &Arc, params: &VideoTranscodeParams) -> WorkContinueStatus { let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::VideoOptimizerCreatingThumbnails, self.video_transcode_result_entries.len(), self.get_test_type(), 0, ); let Some(config_cache_path) = get_config_cache_path() else { return WorkContinueStatus::Continue; }; let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER); if let Err(e) = std::fs::create_dir_all(&thumbnails_dir) { debug!("Failed to create thumbnails directory: {e}"); return WorkContinueStatus::Continue; } let thumbnail_video_percentage_from_start = params.thumbnail_video_percentage_from_start; let generate_grid_instead_of_single = params.generate_thumbnail_grid_instead_of_single; let thumbnail_grid_tiles_per_side = params.thumbnail_grid_tiles_per_side; let errors = self .video_transcode_result_entries .par_iter_mut() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } match generate_thumbnail( stop_flag, &entry.path, entry.size, entry.modified_date, Some(entry.duration), &thumbnails_dir, thumbnail_video_percentage_from_start, generate_grid_instead_of_single, thumbnail_grid_tiles_per_side, params.generate_thumbnails, ) { Ok(Some(thumbnail_path)) => { entry.thumbnail_path = Some(thumbnail_path); progress_handler.increase_items(1); Some(None) } Ok(None) => { progress_handler.increase_items(1); Some(None) } Err(e) => { progress_handler.increase_items(1); Some(Some(e)) } } }) .while_some() .flatten() .collect::>(); self.common_data.text_messages.warnings.extend(errors); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "create_crop_thumbnails", level = "debug")] fn create_crop_thumbnails(&mut self, progress_sender: Option<&Sender>, stop_flag: &Arc, params: &VideoCropParams) -> WorkContinueStatus { let progress_handler = prepare_thread_handler_common( progress_sender, CurrentStage::VideoOptimizerCreatingThumbnails, self.video_crop_result_entries.len(), self.get_test_type(), self.video_crop_result_entries.iter().map(|e| e.size).sum(), ); let Some(config_cache_path) = get_config_cache_path() else { return WorkContinueStatus::Continue; }; let thumbnails_dir = config_cache_path.cache_folder.join(VIDEO_THUMBNAILS_SUBFOLDER); if let Err(e) = std::fs::create_dir_all(&thumbnails_dir) { debug!("Failed to create thumbnails directory: {e}"); return WorkContinueStatus::Continue; } let thumbnail_video_percentage_from_start = params.thumbnail_video_percentage_from_start; let generate_grid_instead_of_single = params.generate_thumbnail_grid_instead_of_single; let thumbnail_grid_tiles_per_side = params.thumbnail_grid_tiles_per_side; let errors = self .video_crop_result_entries .par_iter_mut() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } let result = generate_thumbnail( stop_flag, &entry.path, entry.size, entry.modified_date, Some(entry.duration), &thumbnails_dir, thumbnail_video_percentage_from_start, generate_grid_instead_of_single, thumbnail_grid_tiles_per_side, params.generate_thumbnails, ); match result { Ok(Some(thumbnail_path)) => { entry.thumbnail_path = Some(thumbnail_path); progress_handler.increase_items(1); Some(None) } Ok(None) => { progress_handler.increase_items(1); Some(None) } Err(e) => { progress_handler.increase_items(1); Some(Some(e)) } } }) .while_some() .flatten() .collect::>(); self.common_data.text_messages.warnings.extend(errors); progress_handler.join_thread(); if check_if_stop_received(stop_flag) { return WorkContinueStatus::Stop; } WorkContinueStatus::Continue } #[fun_time(message = "load_video_transcode_cache", level = "debug")] fn load_video_transcode_cache( &mut self, ) -> ( BTreeMap, BTreeMap, BTreeMap, ) { load_and_split_cache_generalized_by_path(&get_video_transcode_cache_file(), mem::take(&mut self.video_transcode_test_entries), self) } #[fun_time(message = "load_video_crop_cache", level = "debug")] fn load_video_crop_cache(&mut self, params: &VideoCropParams) -> (BTreeMap, BTreeMap, BTreeMap) { load_and_split_cache_generalized_by_path(&get_video_crop_cache_file(params), mem::take(&mut self.video_crop_test_entries), self) } #[fun_time(message = "save_video_transcode_cache", level = "debug")] fn save_video_transcode_cache(&mut self, vec_file_entry: &[VideoTranscodeEntry], loaded_hash_map: BTreeMap) { save_and_connect_cache_generalized_by_path(&get_video_transcode_cache_file(), vec_file_entry, loaded_hash_map, self); } #[fun_time(message = "save_video_crop_cache", level = "debug")] fn save_video_crop_cache(&mut self, vec_file_entry: &[VideoCropEntry], params: &VideoCropParams, loaded_hash_map: BTreeMap) { save_and_connect_cache_generalized_by_path(&get_video_crop_cache_file(params), vec_file_entry, loaded_hash_map, self); } #[fun_time(message = "fix_files", level = "debug")] pub(crate) fn fix_files(&mut self, stop_flag: &Arc, _progress_sender: Option<&Sender>, fix_params: VideoOptimizerFixParams) { match self.params.clone() { VideoOptimizerParameters::VideoTranscode(_) => { let VideoOptimizerFixParams::VideoTranscode(video_transcode_params) = fix_params else { unreachable!("VideoTranscode mode should have VideoTranscode fix_params(caller is responsible for that)"); }; let transcode_warnings: Vec<_> = mem::take(&mut self.video_transcode_result_entries) .into_par_iter() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } match process_video(stop_flag, &entry.path.to_string_lossy(), entry.size, video_transcode_params) { Ok(_new_size) => Some(None), Err(e) => Some(Some(flc!("core_failed_to_optimize_video", file = entry.path.to_string_lossy(), reason = e))), } }) .while_some() .flatten() .collect(); self.common_data.text_messages.warnings.extend(transcode_warnings); } VideoOptimizerParameters::VideoCrop(_) => { let VideoOptimizerFixParams::VideoCrop(video_crop_params) = fix_params else { unreachable!("VideoCrop mode should have VideoCrop fix_params(caller is responsible for that)"); }; let crop_warnings: Vec<_> = mem::take(&mut self.video_crop_result_entries) .into_par_iter() .map(|entry| { if check_if_stop_received(stop_flag) { return None; } let (left, top, right, bottom) = entry.new_image_dimensions; let entry_crop_params = VideoCropSingleFixParams { overwrite_original: video_crop_params.overwrite_original, target_codec: video_crop_params.target_codec, quality: video_crop_params.quality, crop_rectangle: (left, top, right, bottom), crop_mechanism: video_crop_params.crop_mechanism, }; match fix_video_crop(&entry.path, &entry_crop_params, stop_flag, &entry.codec) { Ok(()) => Some(None), Err(e) => Some(Some(flc!("core_failed_to_crop_video", file = entry.path.to_string_lossy(), reason = e))), } }) .while_some() .flatten() .collect(); self.common_data.text_messages.warnings.extend(crop_warnings); } } } } pub fn get_video_transcode_cache_file() -> String { format!("cache_video_transcode_{CACHE_VIDEO_OPTIMIZE_VERSION}.bin") } pub fn get_video_crop_cache_file(params: &VideoCropParams) -> String { format!( "cache_video_crop_{CACHE_VIDEO_OPTIMIZE_VERSION}_{:?}_t{}_p{}_s{}_c{}.bin", params.crop_detect, params.black_pixel_threshold, params.black_bar_min_percentage, params.max_samples, params.min_crop_size ) } ================================================ FILE: czkawka_core/src/tools/video_optimizer/mod.rs ================================================ pub mod core; #[cfg(test)] mod tests; pub mod traits; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::time::Duration; use serde::{Deserialize, Serialize}; use crate::common::model::FileEntry; use crate::common::tool_data::CommonToolData; use crate::common::traits::ResultEntry; use crate::flc; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum VideoCodec { H264, H265, Av1, Vp9, } impl VideoCodec { pub const fn as_str(&self) -> &str { match self { Self::H264 => "libx264", Self::H265 => "libx265", Self::Av1 => "libaom-av1", Self::Vp9 => "libvpx-vp9", } } pub const fn as_ffprobe_codec_name(self) -> &'static str { match self { Self::H264 => "h264", Self::H265 => "h265", Self::Av1 => "av1", Self::Vp9 => "vp9", } } } impl std::str::FromStr for VideoCodec { type Err = String; fn from_str(codec: &str) -> Result { match codec.to_lowercase().as_str() { "h264" | "libx264" => Ok(Self::H264), "h265" | "hevc" | "libx265" => Ok(Self::H265), "av1" | "libaom-av1" => Ok(Self::Av1), "vp9" | "libvpx-vp9" => Ok(Self::Vp9), _ => Err(flc!("core_unknown_codec", codec = codec)), } } } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum VideoCroppingMechanism { BlackBars, StaticContent, } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum VideoOptimizerMode { VideoTranscode, VideoCrop, } impl std::str::FromStr for VideoOptimizerMode { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "transcode" | "videotranscode" => Ok(Self::VideoTranscode), "crop" | "videocrop" => Ok(Self::VideoCrop), _ => Err(flc!("core_invalid_video_optimizer_mode", mode = s)), } } } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum VideoOptimizerFixParams { VideoTranscode(VideoTranscodeFixParams), VideoCrop(VideoCropFixParams), } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct VideoTranscodeFixParams { pub codec: VideoCodec, pub quality: u32, pub fail_if_not_smaller: bool, pub overwrite_original: bool, pub limit_video_size: bool, pub max_width: u32, pub max_height: u32, } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct VideoCropSingleFixParams { pub overwrite_original: bool, pub target_codec: Option, pub quality: Option, pub crop_rectangle: (u32, u32, u32, u32), pub crop_mechanism: VideoCroppingMechanism, } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct VideoCropFixParams { pub overwrite_original: bool, pub target_codec: Option, pub quality: Option, pub crop_mechanism: VideoCroppingMechanism, } #[derive(Debug, Default, Clone, Copy)] pub struct Info { pub scanning_time: Duration, pub number_of_videos_to_transcode: usize, pub number_of_videos_to_crop: usize, } #[derive(Clone, PartialEq, Debug)] pub enum VideoOptimizerParameters { VideoTranscode(VideoTranscodeParams), VideoCrop(VideoCropParams), } impl VideoOptimizerParameters { pub fn get_generate_number_of_items_in_thumbnail_grid(&self) -> u8 { let (generate_thumbnail_grid_instead_of_single, thumbnail_grid_tiles_per_side) = match self { Self::VideoTranscode(params) => (params.generate_thumbnail_grid_instead_of_single, params.thumbnail_grid_tiles_per_side), Self::VideoCrop(params) => (params.generate_thumbnail_grid_instead_of_single, params.thumbnail_grid_tiles_per_side), }; if generate_thumbnail_grid_instead_of_single { thumbnail_grid_tiles_per_side } else { 1 } } } #[derive(Clone, Eq, PartialEq, Debug)] pub struct VideoTranscodeParams { pub(crate) excluded_codecs: Vec, pub(crate) generate_thumbnails: bool, pub(crate) thumbnail_video_percentage_from_start: u8, pub(crate) generate_thumbnail_grid_instead_of_single: bool, pub(crate) thumbnail_grid_tiles_per_side: u8, } #[derive(Clone, PartialEq, Debug)] pub struct VideoCropParams { pub(crate) crop_detect: VideoCroppingMechanism, pub(crate) black_pixel_threshold: u8, pub(crate) black_bar_min_percentage: u8, pub(crate) max_samples: usize, pub(crate) min_crop_size: u32, pub(crate) generate_thumbnails: bool, pub(crate) thumbnail_video_percentage_from_start: u8, pub(crate) generate_thumbnail_grid_instead_of_single: bool, pub(crate) thumbnail_grid_tiles_per_side: u8, } impl VideoTranscodeParams { pub fn new( excluded_codecs: Vec, generate_thumbnails: bool, thumbnail_video_percentage_from_start: u8, generate_thumbnail_grid_instead_of_single: bool, thumbnail_grid_tiles_per_side: u8, ) -> Self { Self { excluded_codecs, generate_thumbnails, thumbnail_video_percentage_from_start, generate_thumbnail_grid_instead_of_single, thumbnail_grid_tiles_per_side, } } } impl Default for VideoTranscodeParams { fn default() -> Self { Self { excluded_codecs: vec!["hevc".to_string(), "h265".to_string(), "av1".to_string(), "vp9".to_string()], generate_thumbnails: false, thumbnail_video_percentage_from_start: 10, generate_thumbnail_grid_instead_of_single: false, thumbnail_grid_tiles_per_side: 2, } } } impl VideoCropParams { pub fn with_custom_params( crop_detect: VideoCroppingMechanism, black_pixel_threshold: u8, black_bar_min_percentage: u8, max_samples: usize, min_crop_size: u32, generate_thumbnails: bool, thumbnail_video_percentage_from_start: u8, generate_thumbnail_grid_instead_of_single: bool, thumbnail_grid_tiles_per_side: u8, ) -> Self { assert!(black_pixel_threshold <= 128, "black_pixel_threshold must be 0-128, got {black_pixel_threshold}"); assert!( (50..=100).contains(&black_bar_min_percentage), "black_bar_min_percentage must be 50-100, got {black_bar_min_percentage}" ); assert!((5..=1000).contains(&max_samples), "max_samples must be 5-1000, got {max_samples}"); assert!((1..=1000).contains(&min_crop_size), "min_crop_size must be 1-1000, got {min_crop_size}"); Self { crop_detect, black_pixel_threshold, black_bar_min_percentage, max_samples, min_crop_size, generate_thumbnails, thumbnail_video_percentage_from_start, generate_thumbnail_grid_instead_of_single, thumbnail_grid_tiles_per_side, } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VideoTranscodeEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub error: Option, pub codec: String, pub width: u32, pub height: u32, pub duration: f64, #[serde(skip)] // Saving it to cache is bad idea, because cache can be moved to another locations pub thumbnail_path: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VideoCropEntry { pub path: PathBuf, pub size: u64, pub modified_date: u64, pub error: Option, pub codec: String, pub width: u32, pub height: u32, pub new_image_dimensions: (u32, u32, u32, u32), pub duration: f64, #[serde(skip)] // Saving it to cache is bad idea, because cache can be moved to another locations pub thumbnail_path: Option, } impl ResultEntry for VideoTranscodeEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl ResultEntry for VideoCropEntry { fn get_path(&self) -> &Path { &self.path } fn get_modified_date(&self) -> u64 { self.modified_date } fn get_size(&self) -> u64 { self.size } } impl FileEntry { fn into_video_transcode_entry(self) -> VideoTranscodeEntry { VideoTranscodeEntry { size: self.size, path: self.path, modified_date: self.modified_date, error: None, codec: String::new(), width: 0, height: 0, duration: 0.0, thumbnail_path: None, } } fn into_video_crop_entry(self) -> VideoCropEntry { VideoCropEntry { size: self.size, path: self.path, modified_date: self.modified_date, error: None, codec: String::new(), width: 0, height: 0, new_image_dimensions: (0, 0, 0, 0), duration: 0.0, thumbnail_path: None, } } } pub enum VideoOptimizerEntry { VideoTranscode(VideoTranscodeEntry), VideoCrop(VideoCropEntry), } pub struct VideoOptimizer { common_data: CommonToolData, information: Info, video_transcode_test_entries: BTreeMap, video_crop_test_entries: BTreeMap, video_transcode_result_entries: Vec, video_crop_result_entries: Vec, params: VideoOptimizerParameters, } impl VideoOptimizer { pub const fn get_video_transcode_entries(&self) -> &Vec { &self.video_transcode_result_entries } pub const fn get_video_crop_entries(&self) -> &Vec { &self.video_crop_result_entries } pub const fn get_params(&self) -> &VideoOptimizerParameters { &self.params } pub const fn get_information(&self) -> Info { self.information } } ================================================ FILE: czkawka_core/src/tools/video_optimizer/tests.rs ================================================ ================================================ FILE: czkawka_core/src/tools/video_optimizer/traits.rs ================================================ use std::io::Write; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Instant; use crossbeam_channel::Sender; use fun_time::fun_time; use humansize::{BINARY, format_size}; use crate::common::consts::VIDEO_FILES_EXTENSIONS; use crate::common::ffmpeg_utils::check_if_ffprobe_ffmpeg_exists; use crate::common::model::WorkContinueStatus; use crate::common::progress_data::ProgressData; use crate::common::tool_data::{CommonData, CommonToolData}; use crate::common::traits::{AllTraits, DebugPrint, DeletingItems, FixingItems, PrintResults, Search}; use crate::flc; use crate::tools::video_optimizer::{Info, VideoOptimizer, VideoOptimizerFixParams, VideoOptimizerParameters}; impl AllTraits for VideoOptimizer {} impl DeletingItems for VideoOptimizer { #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self, _stop_flag: &Arc, _progress_sender: Option<&Sender>) -> WorkContinueStatus { unreachable!("VideoOptimizer does not support deleting files"); } } impl FixingItems for VideoOptimizer { type FixParams = VideoOptimizerFixParams; #[fun_time(message = "fix_items", level = "debug")] fn fix_items(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>, fix_params: Self::FixParams) { self.fix_files(stop_flag, progress_sender, fix_params); } } impl DebugPrint for VideoOptimizer { #[expect(clippy::print_stdout)] fn debug_print(&self) { if !cfg!(debug_assertions) || cfg!(test) { return; } println!("### INDIVIDUAL DEBUG PRINT ###"); println!("Info: {:?}", self.information); println!("Mode: {:?}", self.params); println!("Video transcode entries: {}", self.video_transcode_result_entries.len()); println!("Video crop entries: {}", self.video_crop_result_entries.len()); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for VideoOptimizer { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { self.write_base_search_paths(writer)?; match self.params.clone() { VideoOptimizerParameters::VideoTranscode(_) => { writeln!(writer)?; let total_entries = self.video_transcode_result_entries.len(); let entries_needing_optimization = self.video_transcode_result_entries.iter().filter(|e| !e.codec.is_empty() && e.error.is_none()).count(); let failed_entries = self.video_transcode_result_entries.iter().filter(|e| e.error.is_some()).count(); writeln!(writer, "Total files found: {total_entries}")?; writeln!(writer, "Files needing optimization: {entries_needing_optimization}")?; writeln!(writer, "Failed to analyze: {failed_entries}")?; writeln!(writer)?; for entry in &self.video_transcode_result_entries { if !entry.codec.is_empty() { writeln!( writer, "\"{}\" - Codec: {} - Dimensions: {}x{} - Size: {}", entry.path.to_string_lossy(), entry.codec, entry.width, entry.height, format_size(entry.size, BINARY) )?; } } } VideoOptimizerParameters::VideoCrop(_) => { writeln!(writer)?; let total_entries = self.video_crop_result_entries.len(); let entries_with_crop_info = self.video_crop_result_entries.iter().filter(|e| !e.codec.is_empty() && e.error.is_none()).count(); let failed_entries = self.video_crop_result_entries.iter().filter(|e| e.error.is_some()).count(); writeln!(writer, "Total files found: {total_entries}")?; writeln!(writer, "Files with crop information: {entries_with_crop_info}")?; writeln!(writer, "Failed to analyze: {failed_entries}")?; writeln!(writer)?; for entry in &self.video_crop_result_entries { if !entry.codec.is_empty() { let (lt, rt, rb, lb) = entry.new_image_dimensions; let new_image_dimensions = format!(" New dimensions: LT:{lt}, RT:{rt}, RB:{rb}, LB:{lb}"); writeln!( writer, "\"{}\" - Codec: {} - Dimensions: {}x{} - Size: {}{new_image_dimensions}", entry.path.to_string_lossy(), entry.codec, entry.width, entry.height, format_size(entry.size, BINARY) )?; } } } } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { match &self.params { VideoOptimizerParameters::VideoTranscode(_) => self.save_results_to_file_as_json_internal(file_name, &self.video_transcode_result_entries, pretty_print), VideoOptimizerParameters::VideoCrop(_) => self.save_results_to_file_as_json_internal(file_name, &self.video_crop_result_entries, pretty_print), } } } impl Search for VideoOptimizer { #[fun_time(message = "scan_media_files", level = "info")] fn search(&mut self, stop_flag: &Arc, progress_sender: Option<&Sender>) { let start_time = Instant::now(); let () = (|| { if !check_if_ffprobe_ffmpeg_exists() { self.common_data.text_messages.critical = Some(flc!("core_ffmpeg_not_found")); #[cfg(target_os = "windows")] self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found_windows")); return; } if self.prepare_items(Some(VIDEO_FILES_EXTENSIONS)).is_err() { return; } if self.scan_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; return; } if self.check_files(stop_flag, progress_sender) == WorkContinueStatus::Stop { self.common_data.stopped_search = true; } })(); self.information.scanning_time = start_time.elapsed(); if !self.common_data.stopped_search { self.debug_print(); } } } impl CommonData for VideoOptimizer { type Info = Info; type Parameters = VideoOptimizerParameters; fn get_information(&self) -> Self::Info { self.information } fn get_params(&self) -> Self::Parameters { self.params.clone() } fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } fn found_any_items(&self) -> bool { match &self.params { VideoOptimizerParameters::VideoTranscode(_) => self.information.number_of_videos_to_transcode > 0, VideoOptimizerParameters::VideoCrop(_) => self.information.number_of_videos_to_crop > 0, } } } ================================================ FILE: czkawka_gui/Cargo.toml ================================================ [package] name = "czkawka_gui" version = "11.0.1" authors = ["Rafał Mikrut "] edition = "2024" rust-version = "1.92.0" description = "GTK frontend of Czkawka" license = "MIT" homepage = "https://github.com/qarmin/czkawka" repository = "https://github.com/qarmin/czkawka" [dependencies] gdk4 = { version = "0.11.0", default-features = false, features = ["v4_6"] } glib = "0.22.0" gtk4 = { version = "0.11.0", default-features = false, features = ["v4_6"] } humansize = "2.1" chrono = "0.4.38" crossbeam-channel = "0.5" directories-next = "2.0" open = "5.3" image = "0.25" regex = "1.11" fs_extra = "1.3" dunce = "1.0.5" i18n-embed = { version = "0.16", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.10" rust-embed = { version = "8.5", features = ["debug-embed"] } once_cell = "1.20" log = "0.4.22" fun_time = { version = "0.3", features = ["log"] } rayon = "1.10" czkawka_core = { path = "../czkawka_core", version = "11.0.1", features = [] } resvg = { version = "0.47.0", default-features = false } serde_json = "1.0.142" serde = { version = "1.0.219", features = ["derive"] } itertools = "0.14.0" rand = "0.10.0" [dev-dependencies] [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["combaseapi", "objbase", "shobjidl_core", "windef", "winerror", "wtypesbase", "winuser"] } [features] default = [] heif = ["czkawka_core/heif"] libraw = ["czkawka_core/libraw"] libavif = ["czkawka_core/libavif"] # Allows to use trash on Linux when using xdg-portal, needed by e.g. flatpak where normal trash access always fails # No-op on other OSes, it is slower and provides less helpful error messages xdg_portal_trash = ["czkawka_core/xdg_portal_trash"] [lints] workspace = true ================================================ FILE: czkawka_gui/LICENSE_CC_BY_4_ICONS ================================================ All icons, in this project are licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). Copyright (c) 2020 [jannuary](https://github.com/jannuary) - icons/icon_about.png - icons/icon.ico Copyright (c) 2020-2026 Rafał Mikrut - icons/*.svg License: CC-BY-4.0 Creative Commons Attribution 4.0 International Public License . By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. . Section 1 -- Definitions. . a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. . b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. . c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. . d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. . e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. . f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. . g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. . h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. . i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. . j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. . k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. . Section 2 -- Scope. . a. License grant. . 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: . a. reproduce and Share the Licensed Material, in whole or in part; and . b. produce, reproduce, and Share Adapted Material. . 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. . 3. Term. The term of this Public License is specified in Section 6(a). . 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. . 5. Downstream recipients. . a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. . b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. . 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). . b. Other rights. . 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. . 2. Patent and trademark rights are not licensed under this Public License. . 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. . Section 3 -- License Conditions. . Your exercise of the Licensed Rights is expressly made subject to the following conditions. . a. Attribution. . 1. If You Share the Licensed Material (including in modified form), You must: . a. retain the following if it is supplied by the Licensor with the Licensed Material: . i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); . ii. a copyright notice; . iii. a notice that refers to this Public License; . iv. a notice that refers to the disclaimer of warranties; . v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; . b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and . c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. . 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. . 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. . 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. . Section 4 -- Sui Generis Database Rights. . Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: . a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; . b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and . c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. . For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. . Section 5 -- Disclaimer of Warranties and Limitation of Liability. . a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. . b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. . c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. . Section 6 -- Term and Termination. . a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. . b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: . 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or . 2. upon express reinstatement by the Licensor. . For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. . c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. . d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. . Section 7 -- Other Terms and Conditions. . a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. . b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. . Section 8 -- Interpretation. . a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. . b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. . c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. . d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ================================================ FILE: czkawka_gui/LICENSE_MIT_APP_CODE ================================================ MIT License Copyright (c) 2020-2026 Rafał Mikrut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: czkawka_gui/LICENSE_MIT_WINDOWS_THEME ================================================ (Used only in prebuild-binaries) MIT License Copyright (c) 2019-2020 Nick Rhodes Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: czkawka_gui/README.md ================================================ ![czkawka_logo](https://user-images.githubusercontent.com/41945903/102616149-66490400-4137-11eb-9cd6-813b2b070834.png) Czkawka GUI is a graphical user interface for Czkawka Core, built with GTK 4. ![Screenshot from 2023-11-26 12-43-32](https://github.com/qarmin/czkawka/assets/41945903/722ed490-0be1-4dac-bcfc-182a4d0787dc) ## Maintenance Mode Czkawka GTK is currently in maintenance mode. This 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. Active development is now focused on the Krokiet GUI. ## Requirements Requirements depend on your platform. Prebuilt binaries are available here: https://github.com/qarmin/czkawka/releases/ Additional features such as HEIF, libraw, and libavif require extra libraries to be installed, which may increase the number of dependencies. ### Linux #### Prebuilt binaries / Self-compiled Ubuntu: `sudo apt install libgtk-4-bin libheif1 libraw-bin ffmpeg -y` ### Mac ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install gtk4 ffmpeg librsvg libheif libraw dav1d ``` ### Windows #### Prebuilt binaries All 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). ## Installation ### Prebuilt binaries (All OS) After installing the required dependencies, download the prebuilt binaries for your platform from the [releases page](https://github.com/qarmin/czkawka/releases). ### Linux #### Flatpak ``` flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install flathub com.github.qarmin.czkawka ``` #### Debian package (Unofficial) Requires Debian 13 (or derivatives) or later. ``` sudo apt install czkawka_gui ``` #### PPA (Unofficial) - Debian-based distributions (Ubuntu, Linux Mint, etc.) ``` sudo add-apt-repository ppa:xtradeb/apps sudo apt update sudo apt install czkawka ``` [PPA page](https://launchpad.net/~xtradeb/+archive/ubuntu/apps) ### Mac #### Homebrew (Unofficial) ``` brew install czkawka ``` [Formula page](https://formulae.brew.sh/formula/czkawka) ### Windows #### MSYS2 (Unofficial) ``` pacman -S mingw-w64-x86_64-czkawka-gui ``` [Package link](https://packages.msys2.org/base/mingw-w64-czkawka) The file should be installed to `C:\msys64\mingw64\bin\czkawka_gui.exe` and can be run from there. This version is likely the most feature-complete on Windows, as it is compiled with optional features enabled. ## Compilation Compiling 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. ### Requirements | Program | Minimal version | |:-------:|:---------------:| | Rust | 1.92.0 | | GTK | 4.6 | The Rust version corresponds to the latest rustc available in Debian Sid: https://packages.debian.org/sid/rustc ### Linux (Ubuntu; similar steps apply to other distributions) ```shell sudo apt install libgtk-4-dev -y # Base sudo apt install libgtk-4-dev libheif-dev libraw-dev libavif-dev libdav1d-dev -y # With features cargo run --release --bin czkawka_gui # Or with support for heif, libraw, libavif cargo run --release --bin czkawka_gui --features "heif,libraw,libavif" ``` ### Mac ```shell /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install rustup gtk4 adwaita-icon-theme ffmpeg librsvg libheif libraw dav1d pkg-config rustup-init cargo run --release --bin czkawka_gui # Or with support for heif, libraw, libavif cargo run --release --bin czkawka_gui --features "heif,libraw,libavif" ``` ### Windows Currently, there are no instructions for compiling the app natively on Windows.
You 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)
There 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 ## Limitations Not all features and components are implemented here. The main limitations are: - The Windows version does not support HEIF and WebP files with prebuilt binaries (the MSYS2 version supports them). - On Windows, text may appear very small on high-resolution displays. You can manually change DPI scaling for this app: - [Recommended fix](https://github.com/qarmin/czkawka/issues/787#issuecomment-1292253437) (modify gtk.css) - [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). ## License The code is distributed under the MIT license. The icon was created by [jannuary](https://github.com/jannuary) and is licensed under CC-BY-4.0. The Windows dark theme is from the [WhiteSur](https://github.com/slypy/whitesur-gtk4-theme) project, licensed under MIT. The program is completely free to use. "Gratis to uczciwa cena" - "Free is a fair price" ## Name Czkawka is a Polish word meaning _hiccup_. I chose this name because I wanted to hear people speaking other languages pronounce it, so feel free to say it however you like. This 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. At 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. ================================================ FILE: czkawka_gui/i18n/ar/czkawka_gui.ftl ================================================ # Window titles window_settings_title = الإعدادات window_main_title = Czkawka window_progress_title = المسح window_compare_images = مقارنة الصور # General general_ok_button = حسناً general_close_button = أغلق # Krokiet info dialog krokiet_info_title = تقديم Krokiet - نسخة جديدة من Czkawka krokiet_info_message = كروكيت هو الإصدار الجديد والمحسّن والأسرع والأكثر موثوقية لـ Czkawka GTK GUI! إنه أسهل في التشغيل وأكثر مقاومة للتغييرات في النظام، لأنه يعتمد فقط على المكتبات الأساسية المتاحة افتراضيًا على معظم الأنظمة. كروكيت أيضًا يقدم ميزات يفتقر إليها Czkawka، بما في ذلك الصور المصغرة في وضع مقارنة الفيديو، ومسحّف EXIF، وخيارات تقدم نقل/نسخ/حذف الملفات أو ترتيب موسع. جربه بنفسك وشاهد الفرق! ستواصل Czkawka تلقي إصلاحات الأخطاء والتحديثات الصغيرة مني، ولكن جميع الميزات الجديدة ستتم تطويرها حصريًا لكروكيت، وأي شخص حر في المساهمة بميزات جديدة أو إضافة أوضاع مفقودة أو توسيع Czkawka بشكل أكبر. ملاحظة: يجب أن يظهر هذا الرسالة مرة واحدة فقط. إذا ظهر مرة أخرى، قم بتعيين متغير البيئة CZKAWKA_DONT_ANNOY_ME إلى أي قيمة غير فارغة. # Main window music_title_checkbox = العنوان music_artist_checkbox = الفنان music_year_checkbox = السنة music_bitrate_checkbox = معدل music_genre_checkbox = النوع music_length_checkbox = طول music_comparison_checkbox = مقارنة تقريبية music_checking_by_tags = الوسوم music_checking_by_content = محتوى same_music_seconds_label = الحد الأدنى من مدة التجزئة الثانية same_music_similarity_label = الفرق الأقصى music_compare_only_in_title_group = مقارنة داخل مجموعات من العناوين المتشابهة music_compare_only_in_title_group_tooltip = عند تمكينه، يتم تجميع الملفات حسب العنوان ومن ثم مقارنتها ببعضها البعض. بمليون ملف من أصل عشرة آلاف ملف، بدلاً من حوالي مليارية مقارنات عادةً ستكون حول 20000 مقارنة. same_music_tooltip = يمكن تكوين البحث عن ملفات موسيقية مشابهة بواسطة محتواها عن طريق الإعداد: - الحد الأدنى لوقت الشظايا الذي يمكن بعدها تحديد ملفات الموسيقى على أنها - الحد الأقصى للفارق بين جزأين تم اختبارهما والمفتاح إلى النتائج الجيدة هو العثور على مجموعات معقولة من هذه المعلمات، عن تقديمه. تحديد الحد الأدنى من الوقت إلى 5 ثوان والحد الأقصى للفرق إلى 1.0، سيبحث عن أجزاء متطابقة تقريبا في الملفات. وقت 20 ثانية وفارق أقصى قدره 6.0، من ناحية أخرى، يعمل بشكل جيد من أجل العثور على تعديلات أو إصدارات حية وما إلى ذلك. بشكل افتراضي، يتم مقارنة كل ملف موسيقي بآخر وقد يستغرق ذلك الكثير من الوقت عند اختبار العديد من الملفات، لذلك من الأفضل عادة استخدام المجلدات المرجعية وتحديد الملفات التي يجب مقارنتها مع بعضها البعض (مع نفس كمية الملفات)، مقارنة بصمات الأصابع ستكون أسرع من 4 × على الأقل من دون مجلدات مرجعية). music_comparison_checkbox_tooltip = يبحث عن ملفات الموسيقى المشابهة باستخدام الذكاء الاصطناعي، الذي يستخدم التعلم الآلي لإزالة الأقواس من الجملة. على سبيل المثال، مع تمكين هذا الخيار، سيتم النظر في الملفات محل النقاش كتردّدات: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = حالة حساسة duplicate_case_sensitive_name_tooltip = عند تمكين هذا الخيار، قم بتجميع السجلات فقط عندما يكون لديها اسمًا متطابقًا تمامًا مثل: Żołd <-> Żołd تعطيل هذا الخيار سيرتبب الأسماء دون التحقق من كون كل حرف بنفس الحجم مثل: żoŁD <-> Żołd duplicate_mode_size_name_combo_box = الحجم والاسم duplicate_mode_name_combo_box = الاسم duplicate_mode_size_combo_box = الحجم duplicate_mode_hash_combo_box = التجزئة duplicate_hash_type_tooltip = يقدم Czkawka 3 أنواع من التجزئة: Blake3 - دالة التجزئة المشفرة. هذا هو الافتراضي لأنه سريع جدا. CRC32 - دالة التجزئة البسيطة. وينبغي أن يكون هذا أسرع من بليك 3، ولكن نادرا ما تحدث بعض الاصطدام. XXH3 - مشابهة جدا في الأداء وجودة التجزئة للـ Blake3 (ولكن غير مشفرة). لذلك يمكن بسهولة تبادل مثل هذه الأوضاع. duplicate_check_method_tooltip = في الوقت الحالي، تقدم Czkawka ثلاثة أنواع من الطرق للعثور على التكرارات: Name - Finds الملفات التي تحمل نفس الاسم. الحجم - العثور على الملفات التي لها نفس الحجم. Hash - العثور على الملفات التي لها نفس المحتوى. هذا الوضع يقوم بتجزئة الملف ثم يقارن هذا التجزئة للعثور على التكرار. هذا الوضع هو أكثر الطرق أماناً للعثور على التكرار. يستخدم التطبيق بكثافة ذاكرة التخزين المؤقت، لذا يجب أن تكون المسح الثاني والمزيد لنفس البيانات أسرع بكثير من الأول. image_hash_size_tooltip = كل صورة تم فحصها تنتج تجزئة خاصة يمكن مقارنتها مع بعضها البعض، والاختلاف الصغير بينهما يعني أن هذه الصور متشابهة. 8 حجم التجزئة جيد جدا للعثور على صور تشبه قليلا فقط الصور الأصلية. مع مجموعة أكبر من الصور (>1000)، هذا سوف ينتج كمية كبيرة من الإيجابيات الكاذبة، لذا أوصي باستخدام حجم تجزئة أكبر في هذه الحالة. 16 هو حجم التجزئة الافتراضي الذي يمثل حلاً وسطاً جيداً بين العثور على صور مشابهة قليلاً فقط وبين حدوث عدد صغير من تصادم التجزئة. 32 و64 تجزئة لا تجد سوى صور مشابهة جداً، ولكن ينبغي ألا يكون لها تقريباً إيجابيات كاذبة (ربما باستثناء بعض الصور مع قناة ألفا). image_resize_filter_tooltip = لحساب تشفير الصورة، يجب أولاً أن تقوم المكتبة بإعادة حجمها. تعتمد على الخوارزمية المختارة، ستحظى الصورة الناتجة المستخدمة لحساب التشفير بمظهر قليلاً ما يختلف. الخوارزمية الأسرع للاستخدام، ولكن أيضًا تلك التي تعطي أسوأ النتائج، هي Nearest. يتم تمكينها افتراضيًا، لأن مع حجم التشفير 16x16، فإن الجودة المنخفضة غير مرئية حقاً. مع حجم التشفير 8x8، يُنصح باستخدام خوارزمية مختلفة عن Nearest لتحسين مجموعات الصور. image_hash_alg_tooltip = يمكن للمستخدمين الاختيار من واحدة من خوارزميات عديدة لحساب التجزئة. لكل منها نقاط قوية وأضعف وسوف تعطي أحيانا نتائج أفضل وأحيانا أسوأ لصور مختلفة. لذلك ، لتحديد أفضل واحد لك، يتطلب الاختبار اليدوي. big_files_mode_combobox_tooltip = يسمح بالبحث عن ملفات أصغر/أكبر big_files_mode_label = الملفات المحددة big_files_mode_smallest_combo_box = الأصغر حجماً big_files_mode_biggest_combo_box = الاكبر main_notebook_duplicates = الملفات المكررة main_notebook_empty_directories = دلائل فارغة main_notebook_big_files = الملفات الكبيرة main_notebook_empty_files = الملفات الفارغة main_notebook_temporary = ملفات مؤقتة main_notebook_similar_images = صور مشابهة main_notebook_similar_videos = مقاطع فيديو مماثلة main_notebook_same_music = مكرر الموسيقى main_notebook_symlinks = الروابط الرمزية غير صالحة main_notebook_broken_files = الملفات المكسورة main_notebook_bad_extensions = ملحقات سيئة main_tree_view_column_file_name = اسم الملف main_tree_view_column_folder_name = اسم المجلد main_tree_view_column_path = المسار main_tree_view_column_modification = تاريخ التعديل main_tree_view_column_size = الحجم main_tree_view_column_similarity = تماثل main_tree_view_column_dimensions = الأبعاد main_tree_view_column_title = العنوان main_tree_view_column_artist = الفنان main_tree_view_column_year = السنة main_tree_view_column_bitrate = معدل main_tree_view_column_length = طول main_tree_view_column_genre = النوع main_tree_view_column_symlink_file_name = اسم ملف الرابط الرمزي main_tree_view_column_symlink_folder = مجلد الرابط الرمزي main_tree_view_column_destination_path = مسار الوجهة main_tree_view_column_type_of_error = نوع الخطأ main_tree_view_column_current_extension = التمديد الحالي main_tree_view_column_proper_extensions = التمديد الصحيح main_tree_view_column_fps = FPS main_tree_view_column_codec = ترميز main_label_check_method = طريقة التحقق main_label_hash_type = نوع التجزئة main_label_hash_size = حجم التجزئة main_label_size_bytes = الحجم (بايت) main_label_min_size = الحد الأدنى main_label_max_size = الحد الأقصى main_label_shown_files = عدد الملفات المعروضة main_label_resize_algorithm = تغيير حجم الخوارزمية main_label_similarity = مشابهة{ " " } main_check_box_broken_files_audio = الصوت main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = أرشيف main_check_box_broken_files_image = صورة main_check_box_broken_files_video = فيديو main_check_box_broken_files_video_tooltip = يستخدم ffmpeg/ffprobe للتحقق من صحة ملفات الفيديو. بطيء جداً وقد يكتشف الأخطاء الضوئية حتى لو كان الملف يعمل بشكل جيد. check_button_general_same_size = تجاهل نفس الحجم check_button_general_same_size_tooltip = تجاهل الملفات ذات الحجم المتطابق في النتائج - عادة ما تكون هذه المكررة 1:1 main_label_size_bytes_tooltip = حجم الملفات التي سيتم استخدامها في المسح # Upper window upper_tree_view_included_folder_column_title = مجلدات للبحث upper_tree_view_included_reference_column_title = المجلدات المرجعية upper_recursive_button = متكرر upper_recursive_button_tooltip = إذا تم تحديده، ابحث أيضا عن الملفات التي لم توضع مباشرة تحت المجلدات المختارة. upper_manual_add_included_button = إضافة يدوي upper_add_included_button = إضافة upper_remove_included_button = إزالة upper_manual_add_excluded_button = إضافة يدوي upper_add_excluded_button = إضافة upper_remove_excluded_button = إزالة upper_manual_add_included_button_tooltip = إضافة اسم الدليل للبحث باليد. لإضافة مسارات متعددة في وقت واحد، قم بفصلها بواسطة ؛ /home/rozkaz سيضيف دليلين /home/rozkaz و /home/rozkaz upper_add_included_button_tooltip = إضافة دليل جديد للبحث. upper_remove_included_button_tooltip = حذف الدليل من البحث. upper_manual_add_excluded_button_tooltip = إضافة اسم الدليل المستبعد يدوياً. لإضافة مسارات متعددة في وقت واحد، قم بفصلها بواسطة ؛ /home/roman;/home/krokiet سيضيف دليلين / home/roman و /home/keokiet upper_add_excluded_button_tooltip = إضافة دليل ليتم استبعاده في البحث. upper_remove_excluded_button_tooltip = حذف الدليل من المستبعد. upper_notebook_items_configuration = تكوين العناصر upper_notebook_excluded_directories = المسارات المستبعدة upper_notebook_included_directories = المسارات المضمنة upper_allowed_extensions_tooltip = يجب أن تكون الملحقات المسموح بها مفصولة بفواصل (بشكل افتراضي كلها متاحة). أجهزة الماكرو التالية، التي تضيف ملحقات متعددة في وقت واحد، متاحة أيضا: IMAGE، VIDEO، MUSIC، TEXT. مثال استخدام ".exe, IMAGE, VIDEO, .rar, 7z" - وهذا يعني أن الصور (e. .jpg, png) الفيديوهات (مثلاً: avi, mp4) و ex, rar و 7z سيتم مسح الملفات. upper_excluded_extensions_tooltip = قائمة الملفات المعطلة التي سيتم تجاهلها في المسح. عند استخدام الملحقات المسموح بها والمعطلة على حد سواء، هذه واحدة لها أولوية أعلى، لذلك لن يتم تحديد الملف. upper_excluded_items_tooltip = يجب أن تتضمن العناصر المستبعدة * ويفصل بينها الفواصل. هذا أبطأ من المسارات المستبعدة، لذا استخدمه بحذر. upper_excluded_items = البنود المستثناة: upper_allowed_extensions = الإضافات المسموح بها: upper_excluded_extensions = الملحقات المعطّلة: # Popovers popover_select_all = حدد الكل popover_unselect_all = إلغاء تحديد الكل popover_reverse = الاختيار العكسي popover_select_all_except_shortest_path = حدد الكل باستثناء أقصر مسار popover_select_all_except_longest_path = حدد الكل باستثناء أطول مسار popover_select_all_except_oldest = حدد الكل باستثناء الأقدم popover_select_all_except_newest = حدد الكل باستثناء الأحدث popover_select_one_oldest = حدد أقدم واحد popover_select_one_newest = حدد واحد أحدث popover_select_custom = تحديد مخصص popover_unselect_custom = إلغاء تحديد مخصص popover_select_all_images_except_biggest = حدد الكل باستثناء أكبر popover_select_all_images_except_smallest = حدد الكل باستثناء الأصغر popover_custom_path_check_button_entry_tooltip = اختر السجلات بواسطة المسار. 예시 استخدام: /home/pimpek/rzecz.txt يمكن العثور عليه باستخدام /home/pim* popover_custom_name_check_button_entry_tooltip = حدد السجلات حسب أسماء الملفات. استخدام مثال: /usr/ping/pong.txt يمكن العثور عليه مع *ong* popover_custom_regex_check_button_entry_tooltip = حدد السجلات بواسطة Regex. مع هذا الوضع، النص الذي تم البحث عنه هو المسار بالاسم. مثال الاستخدام: /usr/bin/ziemniak. يمكن العثور على xt مع /ziem[a-z]+ يستخدم هذا التطبيق الافتراضي Rust regex . يمكنك قراءة المزيد عنه هنا: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = تمكين الكشف الحساس لحالة الأحرف. عند تعطيل / المنزل/* يجد كلا من /HoMe/roman و /home/roman. popover_custom_not_all_check_button_tooltip = تمنع اختيار جميع السجلات في المجموعة. هذا مفعل بالطبيعة، لأن في معظم الحالات لا تريد حذف كلاً من الملفات الأصلية والمكررة، ولكنك ترغب في ترك على الأقل ملف واحد. تحذير: هذه الإعداد لا يعمل إذا كنت قد اخترت يدويًا جميع النتائج في مجموعة محددة بالفعل. popover_custom_regex_path_label = المسار popover_custom_regex_name_label = الاسم popover_custom_regex_regex_label = مسار Regex + اسم popover_custom_case_sensitive_check_button = حساسية الحالة popover_custom_all_in_group_label = عدم تحديد جميع السجلات في المجموعة popover_custom_mode_unselect = إلغاء تحديد مخصص popover_custom_mode_select = تحديد مخصص popover_sort_file_name = اسم الملف popover_sort_folder_name = اسم المجلد popover_sort_full_name = الاسم الكامل popover_sort_size = الحجم popover_sort_selection = التحديد popover_invalid_regex = Regex غير صحيح popover_valid_regex = Regex صالح # Bottom buttons bottom_search_button = البحث bottom_select_button = حدد bottom_delete_button = حذف bottom_save_button = حفظ bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = نقل bottom_sort_button = فرز bottom_compare_button = قارن bottom_search_button_tooltip = بدء البحث bottom_select_button_tooltip = حدد السجلات. يمكن معالجة الملفات/المجلدات المحددة في وقت لاحق. bottom_delete_button_tooltip = حذف الملفات/المجلدات المحددة. bottom_save_button_tooltip = حفظ البيانات حول البحث في الملف bottom_symlink_button_tooltip = إنشاء روابط رمزية. يعمل فقط عندما يتم تحديد نتيجتين على الأقل في المجموعة. أولا لم يتغير و الثاني و اللاحق مرتبطين بالأول. bottom_hardlink_button_tooltip = إنشاء روابط صلبة. يعمل فقط عندما يتم تحديد نتيجتين على الأقل في المجموعة. أولا لم يتغير و الثاني و اللاحق متصلين بالأول. bottom_hardlink_button_not_available_tooltip = قم بخلق روابط صعبة. الزر معدم، لأن روابط صعبة لا يمكن إنشاؤها. تworks فقط مع صلاحيات مدير في ويندوز، لذا تأكد من تشغيل التطبيق كمدير. إذا كان التطبيق يعمل بالفعل بصلاحية مثل هذه، فقم بفحص مشاكل مماثلة على جيت هاب. bottom_move_button_tooltip = ينقل الملفات إلى الدليل المختار. ينسخ جميع الملفات إلى الدليل دون الحفاظ على شجرة الدليل. عند محاولة نقل ملفين مع نفس الاسم إلى مجلد، سيتم فشل الثانية وإظهار الخطأ. bottom_sort_button_tooltip = ترتيب الملفات/المجلدات وفقا للطريقة المحددة. bottom_compare_button_tooltip = قارن الصور في المجموعة. bottom_show_errors_tooltip = إظهار/إخفاء لوحة النص السفلية. bottom_show_upper_notebook_tooltip = إظهار/إخفاء لوحة دفتر الملاحظات العلوية. # Progress Window progress_stop_button = توقف progress_stop_additional_message = إيقاف الطلب # About Window about_repository_button_tooltip = رابط لصفحة المستودع مع رمز المصدر. about_donation_button_tooltip = رابط لصفحة التبرع. about_instruction_button_tooltip = رابط لصفحة التعليمات. about_translation_button_tooltip = رابط إلى صفحة كراودِن مع ترجمة التطبيق. يتم دعم البولندية الرسمية والإنجليزية. about_repository_button = المستودع about_donation_button = تبرع about_instruction_button = تعليمات about_translation_button = الترجمة # Header header_setting_button_tooltip = فتح مربع حوار الإعدادات. header_about_button_tooltip = فتح مربع الحوار مع معلومات حول التطبيق. # Settings ## General settings_number_of_threads = عدد المواضيع المستخدمة settings_number_of_threads_tooltip = عدد المواضيع المستخدمة، 0 يعني أن جميع المواضيع المتاحة سيتم استخدامها. settings_use_rust_preview = استخدام المكتبات الخارجية بدلاً من gtk لتحميل المعاينات settings_use_rust_preview_tooltip = وفي بعض الأحيان سيكون استخدام معاينات gtk أسرع ويدعم صيغا أكثر، ولكن في بعض الأحيان قد يكون الأمر على العكس تماما. إذا كان لديك مشاكل في تحميل المعاينات، فيمكنك محاولة تغيير هذا الإعداد. على أنظمة غير لينوكس، يوصى باستخدام هذا الخيار، لأن gtk-pixbuf غير متوفر دائمًا هناك لذلك فإن تعطيل هذا الخيار لن يقوم بتحميل المعاينات لبعض الصور. settings_label_restart = تحتاج إلى إعادة تشغيل التطبيق لتطبيق الإعدادات! settings_ignore_other_filesystems = تجاهل نظم الملفات الأخرى (Linux) settings_ignore_other_filesystems_tooltip = يتجاهل الملفات التي ليست في نفس نظام الملفات مثل الدلائل التي تم بحثها. يعمل مثل خيار -xdev في العثور على أمر على Linux settings_save_at_exit_button_tooltip = حفظ التكوين إلى الملف عند إغلاق التطبيق. settings_load_at_start_button_tooltip = تحميل التكوين من الملف عند فتح التطبيق. إذا لم يتم تمكينه، سيتم استخدام الإعدادات الافتراضية. settings_confirm_deletion_button_tooltip = إظهار مربع حوار التأكيد عند النقر على زر الحذف. settings_confirm_link_button_tooltip = إظهار مربع حوار التأكيد عند النقر على زر الارتباط الصلب/الرمزي. settings_confirm_group_deletion_button_tooltip = إظهار مربع حوار التحذير عند محاولة حذف جميع السجلات من المجموعة. settings_show_text_view_button_tooltip = إظهار لوحة النص في أسفل واجهة المستخدم. settings_use_cache_button_tooltip = استخدام ذاكرة التخزين المؤقت للملف. settings_save_also_as_json_button_tooltip = حفظ ذاكرة التخزين المؤقت إلى تنسيق JSON (قابل للقراءة البشرية). من الممكن تعديل محتواه. الذاكرة المؤقتة من هذا الملف سيتم قراءتها تلقائيًا بواسطة التطبيق إذا كان مخبأ تنسيق ثنائي (مع امتداد بن ) مفقود. settings_use_trash_button_tooltip = نقل الملفات إلى سلة المهملات بدلاً من حذفها بشكل دائم. settings_language_label_tooltip = لغة واجهة المستخدم. settings_save_at_exit_button = حفظ التكوين عند إغلاق التطبيق settings_load_at_start_button = تحميل التكوين عند فتح التطبيق settings_confirm_deletion_button = إظهار تأكيد مربع الحوار عند حذف أي ملفات settings_confirm_link_button = إظهار مربع حوار تأكيد عند ربط أي ملفات بصعوبة/رموز settings_confirm_group_deletion_button = إظهار تأكيد مربع الحوار عند حذف جميع الملفات في المجموعة settings_show_text_view_button = إظهار لوحة النص السفلي settings_use_cache_button = استخدام ذاكرة التخزين المؤقت settings_save_also_as_json_button = حفظ ذاكرة التخزين المؤقت أيضا كملف JSON settings_use_trash_button = نقل الملفات المحذوفة إلى سلة المهملات settings_language_label = اللغة settings_multiple_delete_outdated_cache_checkbutton = حذف إدخالات ذاكرة التخزين المؤقت القديمة تلقائياً settings_multiple_delete_outdated_cache_checkbutton_tooltip = حذف نتائج التخزين المؤقت القديمة التي تشير إلى ملفات غير موجودة. عند تمكينها، تقوم التطبيق بضمان أن جميع السجلات تشير إلى ملفات صالحة عند تحميل السجلات (يتم تجاهل تلك المعطوبة). تعطيل هذا الخيار سيساعد في فحص الملفات على الأقراص الخارجية، بحيث لن يتم مسح دخول التخزين المؤقت المتعلقة بها في الفحص التالي. في حالة وجود مئات الآلاف من السجلات في التخزين المؤقت، يُنصح بتمكين هذا الخيار، مما سيسرع تحميل/حفظ التخزين المؤقت في بداية/نهاية الفحص. settings_notebook_general = عمومي settings_notebook_duplicates = مكرر settings_notebook_images = صور مشابهة settings_notebook_videos = فيديو مشابه ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = عرض المعاينة على الجانب الأيمن (عند تحديد ملف صورة). settings_multiple_image_preview_checkbutton = عرض معاينة الصورة settings_multiple_clear_cache_button_tooltip = قم بإزالة ذاكرة التخزين المؤقت يدويًا للعناصر القديمة. يجب استخدام هذا فقط إذا تم تعطيل الإزالة التلقائية. settings_multiple_clear_cache_button = إزالة النتائج القديمة من ذاكرة التخزين المؤقت. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = يختبئ جميع الملفات باستثناء واحد، إذا أشار كل منها إلى نفس البيانات (وهو متصل بشكل صلب). مثال: في حالة وجود سبع ملفات على дисك مترابطة ببيانات معينة وملف مختلف يحتوي على نفس البيانات ولكن inode مختلف,则继续翻译剩下的部分: ملف inode، ثم في مستكشف الملفات المكرر، سيتم عرض只有一个唯一文件和一个来自硬链接的文件。. settings_duplicates_minimal_size_entry_tooltip = 설정할 최소 파일 크기를 캐시에 저장할 것입니다. 작은 값을 선택하면 더 많은 기록이 생성됩니다. 이는 검색 속도가 빨라질 것이지만 캐시 로드/저장은 느려질 수 있습니다. settings_duplicates_prehash_checkbutton_tooltip = تمكين التخزين المؤقت للتجزئة (تجزئة محسوبة من جزء صغير من الملف) مما يسمح برفض النتائج غير المكررة في وقت سابق. يتم تعطيله بشكل افتراضي لأنه يمكن أن يتسبب في تباطؤ في بعض الحالات. يوصى بشدة باستخدامها عند مسح مئات الألوف أو الملايين من الملفات، لأنه يمكن تسريع البحث عدة مرات. settings_duplicates_prehash_minimal_entry_tooltip = الحجم الأدنى للإدخال المخبئ. settings_duplicates_hide_hard_link_button = إخفاء الروابط الصلبة settings_duplicates_prehash_checkbutton = استخدام ذاكرة التخزين المؤقت settings_duplicates_minimal_size_cache_label = الحجم الأدنى للملفات (بالبايت) المحفوظة إلى ذاكرة التخزين المؤقت settings_duplicates_minimal_size_cache_prehash_label = الحجم الأدنى للملفات (بالبايت) المحفوظة في ذاكرة التخزين المؤقت ## Saving/Loading settings settings_saving_button_tooltip = حفظ الإعدادات الحالية إلى الملف. settings_loading_button_tooltip = تحميل الإعدادات من الملف واستبدل الإعدادات الحالية بها. settings_reset_button_tooltip = إعادة تعيين الإعدادات الحالية إلى الإعدادات الافتراضية. settings_saving_button = حفظ التكوين settings_loading_button = تحميل التكوين settings_reset_button = إعادة ضبط الإعدادات ## Opening cache/config folders settings_folder_cache_open_tooltip = يفتح المجلد الذي تخزن فيه ملفات الكاش النصية. يمكن أن يؤدي تعديل ملفات الكاش إلى ظهور نتائج غير صالحة. ومع ذلك، يمكن أن يوفر تغيير المسار الوقت عند تحريك عدد كبير من الملفات إلى موقع مختلف. في حالة وجود مشاكل مع الكاش، يمكن إزالة هذه الملفات. التطبيق سيعيد إنشاءها تلقائيًا. يمكنك نسخ هذه الملفات بين الحواسيب للاستفادة من توفير الوقت في عملية المسح مرة أخرى للملفات (بالطبع إذا كانت لديهم هيكلة مجلدات مشابهة). settings_folder_settings_open_tooltip = يفتح المجلد الذي يحتوي على إعدادات Czkawka. تحذير: تعديل الإعدادات يدويًا قد يتعكر دفق العمل الخاص بك. settings_folder_cache_open = فتح مجلد التخزين المؤقت settings_folder_settings_open = فتح مجلد الإعدادات # Compute results compute_stopped_by_user = تم إيقاف البحث من قبل المستخدم compute_found_duplicates_hash_size = تم العثور على { $number_files } مكررة في { $number_groups } مجموعات أخذت { $size } في { $time } compute_found_duplicates_name = تم العثور على { $number_files } مكررة في { $number_groups } مجموعات في { $time } compute_found_empty_folders = تم العثور على { $number_files } مجلدات فارغة في { $time } compute_found_empty_files = تم العثور على { $number_files } ملفات فارغة في { $time } compute_found_big_files = تم العثور على { $number_files } ملفات كبيرة في { $time } compute_found_temporary_files = تم العثور على { $number_files } ملفات مؤقتة في { $time } compute_found_images = تم العثور على { $number_files } صور مماثلة في { $number_groups } مجموعات في { $time } compute_found_videos = تم العثور على { $number_files } مقاطع فيديو مماثلة في { $number_groups } مجموعات في { $time } compute_found_music = تم العثور على { $number_files } ملفات موسيقية مماثلة في { $number_groups } مجموعات في { $time } compute_found_invalid_symlinks = تم العثور على { $number_files } روابط رموز غير صالحة في { $time } compute_found_broken_files = تم العثور على { $number_files } ملفات مكسورة في { $time } compute_found_bad_extensions = تم العثور على { $number_files } ملفات ذات ملحقات غير صالحة في { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] تم فحص ملف { $file_number } *[other] تم فحص { $file_number } ملفًا } progress_scanning_extension_of_files = تم التحقق من ملحق من ملف { $file_checked }/{ $all_files } progress_scanning_broken_files = تم التحقق من الملف { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_video = تم تجزئة فيديو { $file_checked }/{ $all_files } progress_creating_video_thumbnails = تم إنشاء مصغرات للفيديو { $file_checked }/{ $all_files } progress_scanning_image = تجزئة من { $file_checked }/{ $all_files } صورة ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = مقارنة { $file_checked }/{ $all_files } هاش الصورة progress_scanning_music_tags_end = مقارنة العلامات { $file_checked }/{ $all_files } ملف الموسيقى progress_scanning_music_tags = قراءة العلامات { $file_checked }/{ $all_files } ملف الموسيقى progress_scanning_music_content_end = مقارنة بصمة الإصبع من { $file_checked }/{ $all_files } ملف موسيقي progress_scanning_music_content = تم حساب بصمة الإصبع { $file_checked }/{ $all_files } ملف موسيقي ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] تم فحص مجلد { $folder_number } *[other] تم فحص { $folder_number } مجلدًا } progress_scanning_size = حجم ملف { $file_number } المسح الضوئي progress_scanning_size_name = اسم وحجم الملف { $file_number } الذي تم فحصه progress_scanning_name = تم فحص اسم الملف { $file_number } progress_analyzed_partial_hash = تم تحليل التجزئة الجزئية ل { $file_checked }/{ $all_files } ملفات ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = تم تحليل التجزئة الكاملة من ملفات { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = تحميل ذاكرة التخزين المؤقت progress_prehash_cache_saving = حفظ ذاكرة التخزين المؤقت progress_hash_cache_loading = تحميل ذاكرة التخزين المؤقت للتجزئة progress_hash_cache_saving = حفظ ذاكرة التخزين المؤقت progress_cache_loading = تحميل ذاكرة التخزين المؤقت progress_cache_saving = حفظ ذاكرة التخزين المؤقت progress_current_stage = المرحلة الحالية:{ "" } progress_all_stages = جميع المراحل:{ " " } # Saving loading saving_loading_saving_success = حفظ التكوين إلى ملف { $name }. saving_loading_saving_failure = فشل في حفظ بيانات التكوين إلى الملف { $name }، السبب { $reason }. saving_loading_reset_configuration = تم مسح التكوين الحالي. saving_loading_loading_success = تم تحميل إعدادات التطبيق بشكل صحيح. saving_loading_failed_to_create_config_file = فشل في إنشاء ملف الإعداد"{ $path }"، السبب"{ $reason }". saving_loading_failed_to_read_config_file = لا يمكن تحميل التكوين من "{ $path }" لأنه غير موجود أو ليس ملفا. saving_loading_failed_to_read_data_from_file = لا يمكن قراءة البيانات من الملف"{ $path }"، السبب"{ $reason }". # Other selected_all_reference_folders = لا يمكن بدء البحث، عندما يتم تعيين جميع الدلائل كمجلدات مرجعية searching_for_data = البحث عن البيانات، قد يستغرق بعض الوقت، يرجى الانتظار... text_view_messages = الرسائل text_view_warnings = التحذيرات text_view_errors = أخطاء about_window_motto = هذا البرنامج حر في الاستخدام وسوف يكون دائماً. krokiet_new_app = Czkawka في وضع الصيانة، مما يعني أنه سيتم إصلاح الأخطاء الحرجة فقط ولن يتم إضافة أي ميزات جديدة. للحصول على ميزات جديدة، يرجى التحقق من تطبيق كروكييت الجديد، الذي أكثر استقراراً وأداء ولا يزال قيد التطوير النشط. # Various dialog dialogs_ask_next_time = اسأل المرة القادمة symlink_failed = فشل الربط التكافلي { $name } إلى { $target }، السبب { $reason } delete_title_dialog = تأكيد حذف delete_question_label = هل أنت متأكد من أنك تريد حذف الملفات؟ delete_all_files_in_group_title = تأكيد حذف جميع الملفات في المجموعة delete_all_files_in_group_label1 = ويتم اختيار جميع السجلات في بعض المجموعات. delete_all_files_in_group_label2 = هل أنت متأكد من أنك تريد حذفهم؟ delete_items_label = { $items } سيتم حذف الملفات. delete_items_groups_label = { $items } ملفات من { $groups } سيتم حذف المجموعات. hardlink_failed = فشل الربط { $name } إلى { $target }، السبب { $reason } hard_sym_invalid_selection_title_dialog = إختيار غير صالح مع بعض المجموعات hard_sym_invalid_selection_label_1 = في بعض المجموعات هناك رقم قياسي واحد تم اختياره وسيتم تجاهله. hard_sym_invalid_selection_label_2 = لتتمكن من صلابة / ربط هذه الملفات، يجب اختيار نتيجتين على الأقل في المجموعة. hard_sym_invalid_selection_label_3 = الأول في المجموعة معترف به على أنه أصلي ولا يتغير ولكن الثاني ثم يتم تعديله. hard_sym_link_title_dialog = تأكيد الرابط hard_sym_link_label = هل أنت متأكد من أنك تريد ربط هذه الملفات؟ move_folder_failed = فشل في نقل المجلد { $name }، السبب { $reason } move_file_failed = فشل نقل الملف { $name }، السبب { $reason } move_files_title_dialog = اختر مجلد تريد نقل الملفات المكررة إليه move_files_choose_more_than_1_path = يمكن تحديد مسار واحد فقط لتكون قادرة على نسخ الملفات المكررة، المحددة { $path_number }. move_stats = نقل بشكل صحيح { $num_files }/{ $all_files } عناصر save_results_to_file = حفظت النتائج إلى ملفات txt و json في "{ $name }" مجلد. search_not_choosing_any_music = خطأ: يجب عليك تحديد مربع اختيار واحد على الأقل مع أنواع البحث عن الموسيقى. search_not_choosing_any_broken_files = خطأ: يجب عليك تحديد مربع اختيار واحد على الأقل مع نوع الملفات المحددة المكسورة. include_folders_dialog_title = مجلدات لتضمينها exclude_folders_dialog_title = مجلدات للاستبعاد include_manually_directories_dialog_title = إضافة دليل يدوياً cache_properly_cleared = مسح ذاكرة التخزين المؤقت بشكل صحيح cache_clear_duplicates_title = مسح ذاكرة التخزين المؤقت التكراري cache_clear_similar_images_title = مسح ذاكرة التخزين المؤقت مشابهة للصور cache_clear_similar_videos_title = مسح ذاكرة التخزين المؤقت المماثلة للفيديوهات cache_clear_message_label_1 = هل تريد مسح ذاكرة التخزين المؤقت للإدخالات العتيقة؟ cache_clear_message_label_2 = هذه العملية ستزيل جميع إدخالات ذاكرة التخزين المؤقت التي تشير إلى ملفات غير صالحة. cache_clear_message_label_3 = قد يؤدي هذا إلى تسريع التحميل/الحفظ إلى ذاكرة التخزين المؤقت. cache_clear_message_label_4 = تحذير: العملية ستزيل جميع البيانات المخزنة مؤقتاً من الأقراص الخارجية الغير موصولة. لذلك سوف تحتاج كل تجزئة إلى التجديد. # Show preview preview_image_resize_failure = فشل تغيير حجم الصورة { $name }. preview_image_opening_failure = فشل في فتح الصورة { $name }، السبب { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = المجموعة { $current_group }/{ $all_groups } ({ $images_in_group } صورة) compare_move_left_button = ل compare_move_right_button = ر ================================================ FILE: czkawka_gui/i18n/bg/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Настройки window_main_title = Czkawka (Хълцук) window_progress_title = Сканиране window_compare_images = Сравни изображения # General general_ok_button = Ок general_close_button = Затвори # Krokiet info dialog krokiet_info_title = Представяме ви Krokiet - Нова версия на Czkawka krokiet_info_message = Krokiet е новата, подобрена, по-бърза и по-надеждна версия на Czkawka GTK GUI! По-лесно се изпълнява и е по-устойчив на системни промени, тъй като разчита само на основни библиотеки, налични по подразбиране на повечето системи. Krokiet също така предлага функции, които Czkawka няма, включително миниатюри в режим на сравнение на видео, EXIF почистване, прогрес при преместване/копиране/изтриване на файлове или разширени опции за сортиране. Опитайте го и вижте разликата! Czkawka ще продължи да получава поправки на грешки и малки актуализации от мен, но всички нови функции ще бъдат разработени изключително за Krokiet, а всеки е свободен да допринася с нови функции, да добавя липсващи режими или да разширява Czkawka допълнително. ПС: Това съобщение трябва да се появи само веднъж. Ако се появи отново, задайте променливата на средата CZKAWKA_DONT_ANNOY_ME на всяка непразна стойност. # Main window music_title_checkbox = Заглавие music_artist_checkbox = Изпълнител music_year_checkbox = Година music_bitrate_checkbox = Битрейт music_genre_checkbox = Жанр music_length_checkbox = Продължителност music_comparison_checkbox = Приблизително сравнение music_checking_by_tags = Етикети music_checking_by_content = Съдържание same_music_seconds_label = Минимална продължителност на фрагмента в секунди same_music_similarity_label = Максимална разлика music_compare_only_in_title_group = Сравни в групи от подобни заглавия music_compare_only_in_title_group_tooltip = Когато е включено, файловете са групирани по заглавие и след това се сравняват едно с друго. С 10000 файла, вместо почти 100 милиона сравнения, обикновено ще има около 20000 сравнения. same_music_tooltip = Търсенето на подобни музикални файлове по съдържание може да се конфигурира чрез настройка: - Минималното време на фрагмента, след което музикалните файлове могат да бъдат идентифицирани като подобни - Максимална разлика между два тествани фрагмента Ключът към добрите резултати е да се намерят разумни комбинации от тези параметри, например. Ако зададете минималното време на 5 s, а максималната разлика на 1,0, ще търсите почти идентични фрагменти във файловете. От друга страна, време от 20 s и максимална разлика от 6,0 работят добре за намиране на ремикси/живи версии и т. н. По подразбиране всеки музикален файл се сравнява един с друг и това може да отнеме много време при тестване на много файлове, така че обикновено е по-добре да се използват референтни папки и да се укаже кои файлове да се сравняват един с друг(при същото количество файлове сравняването на отпечатъци ще бъде по-бързо поне 4 пъти, отколкото без референтни папки). music_comparison_checkbox_tooltip = Програмата търси подобни музикални файлове с помощта на изкуствен интелект, който използва машинно обучение за премахване на скоби от фраза. Например, при активирана тази опция въпросните файлове ще се считат за дубликати: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Чувствително изписване duplicate_case_sensitive_name_tooltip = Когато е разрешено, групата записва само записи с едно и също име, напр. Żołd <-> Żołd При деактивиране на тази опция имената ще се групират, без да се проверява дали всяка буква е с еднакъв размер, напр. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Размер и име duplicate_mode_name_combo_box = Име duplicate_mode_size_combo_box = Размер duplicate_mode_hash_combo_box = Хеш duplicate_hash_type_tooltip = Czkawka предлага 3 вида хешове: Blake3 - криптографска хеш функция. Тя е избрана по подразбиране, тъй като е много бърза. CRC32 - проста хеш функция. Тя би трябвало да е по-бърза от Blake3, но много рядко може да има някои колизии. XXH3 - много подобна по производителност и качество на хеширане на Blake3 (но некриптографска). Така че тези режими могат лесно да се сменят. duplicate_check_method_tooltip = Засега Czkawka предлага три вида методи за намиране на дубликати чрез: Име - Намира файлове с еднакво име. Размер - Намира файлове с еднакъв размер. Hash - Намира файлове с еднакво съдържание. Този режим хешира файла и по-късно сравнява този хеш, за да намери дубликати. Този режим е най-сигурният начин за намиране на дубликати. Приложението използва силно кеша, така че второто и следващите сканирания на едни и същи данни би трябвало да са много по-бързи от първото. image_hash_size_tooltip = Всяко сравнено изображение дава специален хеш, който може да бъде сравнен с другите и малка разлика между тях означава че изображенията са близки. Размер 8 хеш е сравнително добър за намиране на изображения, които са близки до оригинала. С по-голям набор изображения (>1000), това ще доведе до голяма бройка фалшиви позитивни, така че препоръчвам да се ползва по-голям размер на хеша в този случай. 16 е размер по подразбиране, който е сравнително добър компромис между намиране на малки разлики в изображенията и имайки малко хеш колизии. 32 и 64 хешове намират само много сходни изображения, но ще имат почти никакви фалшиви позитивни (с изключение на някой изображения с алфа канал). image_resize_filter_tooltip = За да изчисли хеша на изображението, библиотеката трябва първо да го оразмери. В зависимост от избрания алгоритъм, крайното изображение използвано за изчисляване на хеша може да изглежда леко различно. Най-бързият алгоритъм, но и даващ най-лоши резултати е Най-Близък. Използва се по-подразбиране, защото хеш с размер 16х16 с ниско качество не е толкова видимо. С 8х8 хеш, се препоръчва да се използва различен алгоритъм от Най-Близък за да има по-добри групи изображения. image_hash_alg_tooltip = Потребителите могат да изберат един от многото алгоритми за изчисляване на хеша. Всеки от тях има както силни, така и слаби страни и понякога дава по-добри, а понякога по-лоши резултати за различни изображения. Затова, за да определите най-добрия за вас, е необходимо ръчно тестване. big_files_mode_combobox_tooltip = Позволява търсене на най-малките/най-големите файлове big_files_mode_label = Проверени файлове big_files_mode_smallest_combo_box = Най-малкия big_files_mode_biggest_combo_box = Най-големия main_notebook_duplicates = Повтарящи се файлове main_notebook_empty_directories = Празни директории main_notebook_big_files = Големи файлове main_notebook_empty_files = Празни файлове main_notebook_temporary = Временни файлове main_notebook_similar_images = Подобни изображения main_notebook_similar_videos = Подобни видеа main_notebook_same_music = Музикални дубликати main_notebook_symlinks = Невалидни симлинкове main_notebook_broken_files = Повредени файлове main_notebook_bad_extensions = Повредени разширения main_tree_view_column_file_name = Име на файла main_tree_view_column_folder_name = Име на папката main_tree_view_column_path = Път main_tree_view_column_modification = Дата на промяна main_tree_view_column_size = Размер main_tree_view_column_similarity = Прилика main_tree_view_column_dimensions = Размери main_tree_view_column_title = Заглавие main_tree_view_column_artist = Изпълнител main_tree_view_column_year = Година main_tree_view_column_bitrate = Битрейт main_tree_view_column_length = Дължина main_tree_view_column_genre = Жанр main_tree_view_column_symlink_file_name = Име на файла на Symlink main_tree_view_column_symlink_folder = Symlink папка main_tree_view_column_destination_path = Път за местоположение main_tree_view_column_type_of_error = Тип на грешка main_tree_view_column_current_extension = Избрано разширение main_tree_view_column_proper_extensions = Правилно разширение main_tree_view_column_fps = БПС main_tree_view_column_codec = Кодек main_label_check_method = Провери метод main_label_hash_type = Хеш тип main_label_hash_size = Хеш размер main_label_size_bytes = Размер (байтове) main_label_min_size = Мин main_label_max_size = Макс main_label_shown_files = Брой на показани файлове main_label_resize_algorithm = Преоразмери алгоритъма main_label_similarity = Сходство{ " " } main_check_box_broken_files_audio = Аудио main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = Архив main_check_box_broken_files_image = Изображение main_check_box_broken_files_video = Видео main_check_box_broken_files_video_tooltip = Използва ffmpeg/ffprobe за валидиране на видео файлове. Доста бавно и може да открие педантични грешки дори ако файлът се възпроизвежда добре. check_button_general_same_size = Игнорирай еднакъв размер check_button_general_same_size_tooltip = Игнорирай файлове с идентичен размер в резултата - обикновено това са 1:1 дубликати main_label_size_bytes_tooltip = Размер на файловете, които ще се използват при сканиране # Upper window upper_tree_view_included_folder_column_title = Папки за търсене upper_tree_view_included_reference_column_title = Папки за справка upper_recursive_button = Рекурсивен upper_recursive_button_tooltip = Ако е избрано, се търсят и файлове, които не са поставени директно в избраните папки. upper_manual_add_included_button = Ръчно добавяне upper_add_included_button = Добави upper_remove_included_button = Премахни upper_manual_add_excluded_button = Ръчно добавяне upper_add_excluded_button = Добави upper_remove_excluded_button = Премахни upper_manual_add_included_button_tooltip = Добавяне на име на директория за ръчно търсене. За да добавите няколко пътища наведнъж, разделете ги с ; /home/roman;/home/rozkaz ще добави две директории /home/roman и /home/rozkaz upper_add_included_button_tooltip = Добавяне на нова директория за търсене. upper_remove_included_button_tooltip = Изтриване на директорията от търсенето. upper_manual_add_excluded_button_tooltip = Добавете името на изключената директория на ръка. За да добавите няколко пътя наведнъж, разделете ги с ; /home/roman;/home/krokiet ще добави две директории /home/roman и /home/keokiet upper_add_excluded_button_tooltip = Добавяне на директория, която да бъде изключена при търсене. upper_remove_excluded_button_tooltip = Изтриване на директория от изключените. upper_notebook_items_configuration = Конфигурация на елементите upper_notebook_excluded_directories = Изключени пътища upper_notebook_included_directories = Включени пътища upper_allowed_extensions_tooltip = Разрешените разширения трябва да бъдат разделени със запетаи (по подразбиране са налични всички). Налични са и следните макроси, които добавят няколко разширения наведнъж: ИЗОБРАЖЕНИЕ, ВИДЕО, МУЗИКА, ТЕКСТ. Пример за използване ".exe, IMAGE, VIDEO, .rar, 7z" - това означава, че ще бъдат сканирани изображения (напр. jpg, png), видеоклипове (напр. avi, mp4), файлове exe, rar и 7z. upper_excluded_extensions_tooltip = Списък с изключени от търсенето файлове. Когато се ползват едновременно включени и изключени разширения, този тук има по-голям приоритет и файла няма да бъде проверен. upper_excluded_items_tooltip = Изключените елементи трябва да съдържат * wildcard и да бъдат разделени с ками. Това е по-бавно от Excluded Paths, така че използвайте внимателно. upper_excluded_items = Изключени елементи: upper_allowed_extensions = Разрешени разширения: upper_excluded_extensions = Изключени разширения: # Popovers popover_select_all = Избери всички popover_unselect_all = Размаркирайте всички popover_reverse = Избери обратното popover_select_all_except_shortest_path = Изберете всички, освен най-краткия път popover_select_all_except_longest_path = Изберете всички, освен най-дълъг път popover_select_all_except_oldest = Избери всички освен най-старото popover_select_all_except_newest = Избери всички освен най-новото popover_select_one_oldest = Избери най-старото popover_select_one_newest = Избери най-новото popover_select_custom = Избери по избор popover_unselect_custom = Размаркирай по избор popover_select_all_images_except_biggest = Избери всички освен най-големия popover_select_all_images_except_smallest = Избери всички освен най-малкия popover_custom_path_check_button_entry_tooltip = Изберете записи по път. Пример за използване: /home/pimpek/rzecz.txt може да бъде намерен с /home/pim* popover_custom_name_check_button_entry_tooltip = Изберете записи по имена на файлове. Пример за използване: /usr/ping/pong.txt може да бъде намерен с *ong* popover_custom_regex_check_button_entry_tooltip = Избиране на записи по зададен Regex. В този режим търсеният текст е Path with Name. Пример за използване: /usr/bin/ziemniak.txt може да бъде намерен с /ziem[a-z]+ В този случай се използва имплементацията на regex по подразбиране на Rust. Можете да прочетете повече за нея тук: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Активира откриването с отчитане на големи и малки букви. Когато е изключено, /home/* намира както /HoMe/roman, така и /home/roman. popover_custom_not_all_check_button_tooltip = Предотвратява избирането на всички записи в групата. Това е разрешено по подразбиране, тъй като в повечето ситуации не искате да изтривате и оригиналните, и дублираните файлове, а искате да оставите поне един файл. ПРЕДУПРЕЖДЕНИЕ: Тази настройка не работи, ако вече сте избрали ръчно всички резултати в групата. popover_custom_regex_path_label = Път popover_custom_regex_name_label = Име popover_custom_regex_regex_label = Regex Път + Име popover_custom_case_sensitive_check_button = Чувствителност на буквите popover_custom_all_in_group_label = Да не се избират всички записи в групата popover_custom_mode_unselect = Премахване на избора по избор popover_custom_mode_select = Избери по избор popover_sort_file_name = Име на файла popover_sort_folder_name = Име на папката popover_sort_full_name = Пълно име popover_sort_size = Размер popover_sort_selection = Избор popover_invalid_regex = Regex е невалиден popover_valid_regex = Regex е валиден # Bottom buttons bottom_search_button = Търсене bottom_select_button = Избери bottom_delete_button = Изтрий bottom_save_button = Запази bottom_symlink_button = Симлинк bottom_hardlink_button = Хардлинк bottom_move_button = Премести bottom_sort_button = Сортирай bottom_compare_button = Сравни bottom_search_button_tooltip = Започни търсене bottom_select_button_tooltip = Изберете записи. Само избраните файлове/папки могат да бъдат обработени по-късно. bottom_delete_button_tooltip = Изтрий избрани файлове/папки. bottom_save_button_tooltip = Записване на данни за търсенето във файл bottom_symlink_button_tooltip = Създаване на символни връзки. Работи само когато са избрани поне два резултата в група. Първият е непроменен, а вторият и по-късните са символни връзки към първия. bottom_hardlink_button_tooltip = Създаване на твърди връзки. Работи само когато са избрани поне два резултата в група. Първият е непроменен, а вторият и по-късните са свързани с първия. bottom_hardlink_button_not_available_tooltip = Създаване на твърди връзки. Бутонът е деактивиран, тъй като не могат да се създават твърди връзки. Хардлинковете работят само с администраторски права в Windows, затова не забравяйте да стартирате приложението като администратор. Ако приложението вече работи с такива привилегии, проверете за подобни проблеми в Github. bottom_move_button_tooltip = Премества файлове в избрана директория. Той копира всички файлове в директорията, без да запазва дървото на директориите. При опит за преместване на два файла с еднакво име в папка, вторият ще се провали и ще покаже грешка. bottom_sort_button_tooltip = Сортира файловете/папките според избрания метод. bottom_compare_button_tooltip = Сравни изображенията в групата. bottom_show_errors_tooltip = Показване/скриване на долния текстов панел. bottom_show_upper_notebook_tooltip = Показване/скриване на горния панел на бележника. # Progress Window progress_stop_button = Спри progress_stop_additional_message = Спри избраните # About Window about_repository_button_tooltip = Връзка към страницата на хранилището с изходния код. about_donation_button_tooltip = Връзка към страницата за дарения. about_instruction_button_tooltip = Връзка към страницата с инструкции. about_translation_button_tooltip = Връзка към страницата на Crowdin с преводи на приложения. Официално се поддържат полски и английски език. about_repository_button = Хранилище about_donation_button = Дарение about_instruction_button = Инструкции about_translation_button = Преводи # Header header_setting_button_tooltip = Отваря диалогов прозорец за настройки. header_about_button_tooltip = Отваря диалогов прозорец с информация за приложението. # Settings ## General settings_number_of_threads = Брой използвани нишки settings_number_of_threads_tooltip = Брой използвани нишки, 0 означава, че ще бъдат използвани всички налични нишки. settings_use_rust_preview = Използвай външна библиотека вместо GTK да зареди визуализацията settings_use_rust_preview_tooltip = Използвайки GTK визуализации, понякога ще е по-бързо и ще поддържа повече формати, но понякога това може да е точно обратното. Ако имате проблеми със зареждането на визуализации, можете да пробвате да промените тази настройка. На не Linux-ови системи е препоръчително да ползвате тази опция, защото gtk-pixbuf не винаги е налично, затова изключването на тази опция може да спре зареждането на визуализациите на някои изображения. settings_label_restart = Трябва да рестартирате приложението, за да приложите настройките! settings_ignore_other_filesystems = Игнориране на други файлови системи (само за Linux) settings_ignore_other_filesystems_tooltip = игнорира файлове, които не са в същата файлова система като търсените директории. Работи по същия начин като опцията -xdev в командата find в Linux settings_save_at_exit_button_tooltip = Записване на конфигурацията във файл при затваряне на приложението. settings_load_at_start_button_tooltip = Зареждане на конфигурацията от файл при отваряне на приложението. Ако не е разрешено, ще се използват настройките по подразбиране. settings_confirm_deletion_button_tooltip = Показване на диалогов прозорец за потвърждение при натискане на бутона за изтриване. settings_confirm_link_button_tooltip = Показване на диалогов прозорец за потвърждение, когато щракнете върху бутона за твърда/симултанна връзка. settings_confirm_group_deletion_button_tooltip = Показване на диалогов прозорец с предупреждение при опит за изтриване на всички записи от групата. settings_show_text_view_button_tooltip = Показване на текстовия панел в долната част на потребителския интерфейс. settings_use_cache_button_tooltip = Използвайте кеш за файлове. settings_save_also_as_json_button_tooltip = Записване на кеша в (разбираем за човека) формат JSON. Възможно е да променяте съдържанието му. Кешът от този файл ще бъде прочетен автоматично от приложението, ако липсва кеш в двоичен формат (с разширение bin). settings_use_trash_button_tooltip = Премества файловете в кошчето, вместо да ги изтрие окончателно. settings_language_label_tooltip = Език за потребителски интерфейс. settings_save_at_exit_button = Запазване на конфигурацията при затваряне на приложението settings_load_at_start_button = Зареждане на конфигурацията при отваряне на приложението settings_confirm_deletion_button = Показване на диалогов прозорец за потвърждение при изтриване на файлове settings_confirm_link_button = Показване на диалогов прозорец за потвърждение при твърди/симетрични връзки на файлове settings_confirm_group_deletion_button = Показване на диалогов прозорец за потвърждение при изтриване на всички файлове в групата settings_show_text_view_button = Показване на долния текстов панел settings_use_cache_button = Използвай кеш settings_save_also_as_json_button = Също запази леша като JSON файл settings_use_trash_button = Премести изтритите файлове в кошчето settings_language_label = Език settings_multiple_delete_outdated_cache_checkbutton = Автоматично изтриване на остарелите записи в кеша settings_multiple_delete_outdated_cache_checkbutton_tooltip = Изтриване на остарелите резултати от кеша, които сочат към несъществуващи файлове. Когато е разрешено, приложението се уверява, че при зареждане на записи всички записи сочат към валидни файлове (повредените се игнорират). Деактивирането на тази функция ще помогне при сканиране на файлове на външни дискове, тъй като записите от кеша за тях няма да бъдат изчистени при следващото сканиране. В случай че имате стотици хиляди записи в кеша, предлагаме да включите тази опция, което ще ускори зареждането/спасяването на кеша в началото/края на сканирането. settings_notebook_general = Общи settings_notebook_duplicates = Дубликати settings_notebook_images = Сходни изображения settings_notebook_videos = Сходни видеа ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Показва предварителен преглед от дясната страна (при избиране на файл с изображение). settings_multiple_image_preview_checkbutton = Показване на предварителен преглед на изображението settings_multiple_clear_cache_button_tooltip = Изчистете ръчно кеша от остарели записи. Това трябва да се използва само ако автоматичното изчистване е деактивирано. settings_multiple_clear_cache_button = Премахни остарели резултати от кеша. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Скрива всички файлове с изключение на един, ако всички сочат към едни и същи данни (са твърдо свързани). Пример: В случай, че на диска има седем файла, които са свързани с определени данни, и един различен файл със същите данни, но с различен inode, тогава в търсачката за дубликати ще бъдат показани само един уникален файл и един файл от свързаните. settings_duplicates_minimal_size_entry_tooltip = Задаване на минималния размер на файла, който ще се кешира. Ако изберете по-малка стойност, ще се генерират повече записи. Това ще ускори търсенето, но ще забави зареждането/запазването на кеша. settings_duplicates_prehash_checkbutton_tooltip = Позволява кеширане на prehash (хеш, изчислен от малка част от файла), което позволява по-ранно отхвърляне на недублирани резултати. По подразбиране е забранено, тъй като в някои ситуации може да доведе до забавяне на работата. Силно се препоръчва да се използва при сканиране на стотици хиляди или милиони файлове, защото може да ускори търсенето многократно. settings_duplicates_prehash_minimal_entry_tooltip = Минимален размер на записа в кеша. settings_duplicates_hide_hard_link_button = Скрий твърди връзки settings_duplicates_prehash_checkbutton = Използване на предварителен кеш settings_duplicates_minimal_size_cache_label = Минимален размер на файловете (в байтове), записани в кеша settings_duplicates_minimal_size_cache_prehash_label = Минимален размер на файловете (в байтове), които се записват в предварителния кеш ## Saving/Loading settings settings_saving_button_tooltip = Записване на текущата конфигурация на настройките във файл. settings_loading_button_tooltip = Зареждане на настройките от файл и заместване на текущата конфигурация с тях. settings_reset_button_tooltip = Възстановяване на текущата конфигурация до тази по подразбиране. settings_saving_button = Запазване на конфигурацията settings_loading_button = Конфигурация за зареждане settings_reset_button = Нулиране на конфигурацията ## Opening cache/config folders settings_folder_cache_open_tooltip = Отваря папката, в която се съхраняват кеш txt файловете. Промяната на кеш файловете може да доведе до показване на невалидни резултати. Промяната на пътя обаче може да спести време при преместване на голямо количество файлове на друго място. Можете да копирате тези файлове между компютрите, за да спестите време за повторно сканиране на файловете (разбира се, ако те имат сходна структура на директориите). В случай на проблеми с кеша тези файлове могат да бъдат премахнати. Приложението автоматично ще ги възстанови. settings_folder_settings_open_tooltip = Отваря папката, в която се съхранява конфигурацията на Czkawka. ПРЕДУПРЕЖДЕНИЕ: Ръчното модифициране на конфигурацията може да наруши работния ви процес. settings_folder_cache_open = Отворете папката с кеш settings_folder_settings_open = Отваряне на папката с настройки # Compute results compute_stopped_by_user = Търсенето е спряно от потребител compute_found_duplicates_hash_size = Намерени са { $number_files } дубликати в { $number_groups } групи, които заемат { $size } за { $time } compute_found_duplicates_name = Намерих { $number_files } дублики в { $number_groups } групи за { $time } compute_found_empty_folders = Найдени са { $number_files } празни папки във { $time } compute_found_empty_files = Найдени са { $number_files } празни файлова обекта в { $time } compute_found_big_files = Намерих { $number_files } големи файла в { $time } compute_found_temporary_files = Найдени са { $number_files } временни файлъв в { $time } compute_found_images = Найшли се { $number_files } подобни изображения в { $number_groups } групи за { $time } compute_found_videos = Намерил { $number_files } подобни видео файла в { $number_groups } групи за { $time } compute_found_music = Найдено { $number_files } подобни музикални файлове в { $number_groups } групи за { $time } compute_found_invalid_symlinks = Намерени { $number_files } невалидни символни връзки в { $time } compute_found_broken_files = Намерих { $number_files } повредени файла в { $time } compute_found_bad_extensions = Намерени са { $number_files } файла с невалидни разширения за { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Сканиран { $file_number } файл *[other] Сканирани { $file_number } файлове } progress_scanning_extension_of_files = Проверено разширение на { $file_checked }/{ $all_files } файла progress_scanning_broken_files = Проверени { $file_checked }/{ $all_files } файла от ({ $data_checked }/{ $all_data }) progress_scanning_video = Хеширани { $file_checked }/{ $all_files } видеа progress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video progress_scanning_image = Хеширани { $file_checked }/{ $all_files } изображения ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Сравнени { $file_checked }/{ $all_files } хешове на изображения progress_scanning_music_tags_end = Сравнени тагове на { $file_checked }/{ $all_files } музикални файла progress_scanning_music_tags = Прочетени { $file_checked }/{ $all_files } тага на музикални файла progress_scanning_music_content_end = Сравнени { $file_checked }/{ $all_files } отпечатъка на музикални файла progress_scanning_music_content = Изчислени { $file_checked }/{ $all_files } отпечатъка на музикални файла ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Сканирана { $folder_number } папка *[other] Сканирани { $folder_number } папки } progress_scanning_size = Сканиран размер на { $file_number } файла progress_scanning_size_name = Сканиран име и размер на { $file_number } файла progress_scanning_name = Сканиран име на { $file_number } файла progress_analyzed_partial_hash = Анализиран частичен хеш на { $file_checked }/{ $all_files } файла ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Анализиран пълен хеш на { $file_checked }/{ $all_files } файла ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Зареждане на prehash кеш progress_prehash_cache_saving = Запис на prehash кеш progress_hash_cache_loading = Зареждане на hash кеш progress_hash_cache_saving = Запис на hash кеш progress_cache_loading = Зарежда кеш progress_cache_saving = Запазва кеш progress_current_stage = Текущ етап:{ " " } progress_all_stages = Всички етапи:{ " " } # Saving loading saving_loading_saving_success = Запазване на конфигурацията във файл { $name }. saving_loading_saving_failure = Неуспешно спъжаване на конфигурационните данни в файл { $name }, причина { $reason }. saving_loading_reset_configuration = Текущата конфигурация е изтрита. saving_loading_loading_success = Правилно заредена конфигурация на приложението. saving_loading_failed_to_create_config_file = Неуспешно създаване на конфигурационен файл "{ $path }", причина "{ $reason }". saving_loading_failed_to_read_config_file = Не може да се зареди конфигурация от "{ $path }", защото тя не съществува или не е файл. saving_loading_failed_to_read_data_from_file = Не може да се прочетат данни от файл "{ $path }", причина "{ $reason }". # Other selected_all_reference_folders = Не може да се стартира търсене, когато всички директории са зададени като референтни папки searching_for_data = Търсене на данни, може да отнеме известно време, моля, изчакайте... text_view_messages = СЪОБЩЕНИЯ text_view_warnings = ПРЕДУПРЕЖДЕНИЯ text_view_errors = ГРЕШКИ about_window_motto = Тази програма е безплатна за използване и винаги ще бъде такава. krokiet_new_app = Цквака е в режим на поддръжка, че се приемат само критични грешки и нито еднаnova функционалност ще бъде добавена. За nova функционалност моля проверете новата апликация Крочиец, която е по-стабилна и изтеглянечна и се развива все още активно. # Various dialog dialogs_ask_next_time = Попитайте следващия път symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason } delete_title_dialog = Изтрий потвърждението delete_question_label = Сигурни ли сте, че искате да изтриете файловете? delete_all_files_in_group_title = Потвърждаване на изтриването на всички файлове в групата delete_all_files_in_group_label1 = В някои групи се избират всички записи. delete_all_files_in_group_label2 = Сигурни ли сте, че искате да ги изтриете? delete_items_label = { $items } файловете ще бъдат изтрити. delete_items_groups_label = { $items } Файловете от { $groups } групите ще бъдат изтрити. hardlink_failed = Неуспех при създаване на твърд линк за { $name } в { $target }, причина { $reason } hard_sym_invalid_selection_title_dialog = Невалидна селекция при някои групи hard_sym_invalid_selection_label_1 = В някои групи е избран само един запис и той ще бъде пренебрегнат. hard_sym_invalid_selection_label_2 = За да можете да свържете тези файлове с твърда/симетрична връзка, трябва да изберете поне два резултата в групата. hard_sym_invalid_selection_label_3 = Първият в групата се признава за оригинален и не се променя, но вторият и следващите се променят. hard_sym_link_title_dialog = Потвърждаване на връзката hard_sym_link_label = Потвърждаване на връзкатаСигурни ли сте, че искате да свържете тези файлове? move_folder_failed = Неуспешно преместване на папка { $name }, причина { $reason } move_file_failed = Неуспешно преместване на файл { $name }, причина { $reason } move_files_title_dialog = Изберете папката, в която искате да преместите дублираните файлове move_files_choose_more_than_1_path = Може да се избере само един път, за да може да се копират дублираните им файлове, selected { $path_number }. move_stats = Правилно преместени { $num_files }/{ $all_files } елементи save_results_to_file = Запазени резултати едновременно към txt и json файлове в папка "{ $name }". search_not_choosing_any_music = ГРЕШКА: Трябва да изберете поне едно квадратче за отметка с типове търсене на музика. search_not_choosing_any_broken_files = ГРЕШКА: Трябва да изберете поне едно квадратче за отметка с тип на проверените счупени файлове. include_folders_dialog_title = Папки, които да се включват exclude_folders_dialog_title = Папки, които да се изключат include_manually_directories_dialog_title = Добаеви ръчно директория cache_properly_cleared = Правилно изчистен кеш cache_clear_duplicates_title = Изчистване на кеша за дубликати cache_clear_similar_images_title = Изчистване на кеша на подобни изображения cache_clear_similar_videos_title = Изчистване на кеша на подобни видеоклипове cache_clear_message_label_1 = Искате ли да изчистите кеша от остарели записи? cache_clear_message_label_2 = Тази операция ще премахне всички записи в кеша, които сочат към невалидни файлове. cache_clear_message_label_3 = Това може леко да ускори зареждането/записването в кеша. cache_clear_message_label_4 = ПРЕДУПРЕЖДЕНИЕ: Операцията ще премахне всички кеширани данни от изключените външни дискове. Така че всеки хеш ще трябва да бъде възстановен. # Show preview preview_image_resize_failure = Неуспешно променяне на размера на изображението { $name }. preview_image_opening_failure = Неуспешно отваряне на изображение { $name }, причина { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Група { $current_group }/{ $all_groups } ({ $images_in_group } изображения) compare_move_left_button = Л compare_move_right_button = Д ================================================ FILE: czkawka_gui/i18n/cs/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Nastavení window_main_title = Czkawka (Škytavka) window_progress_title = Skenování window_compare_images = Porovnat obrázky # General general_ok_button = Ok general_close_button = Zavřít # Krokiet info dialog krokiet_info_title = Představujeme Krokiet - Nová verze Czkawka krokiet_info_message = Krokiet je nový, vylepšený, rychlejší a spolehlivější verze Czkawky GTK GUI! 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ů. 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í. Vyzkoušejte to a uvidíte rozdíl! 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. 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. # Main window music_title_checkbox = Hlava 1 – Celkem music_artist_checkbox = Umělec music_year_checkbox = Rok music_bitrate_checkbox = Přenosová rychlost music_genre_checkbox = Žánr music_length_checkbox = Délka music_comparison_checkbox = Přibližné srovnání music_checking_by_tags = Štítky music_checking_by_content = Obsah same_music_seconds_label = Minimální délka trvání druhého fragmentu same_music_similarity_label = Maximální rozdíl music_compare_only_in_title_group = Porovnat v rámci skupin podobných názvů music_compare_only_in_title_group_tooltip = Pokud je povoleno, soubory jsou seskupeny podle názvu a poté vzájemně porovnávány. S 10000 soubory, místo toho se obvykle uskuteční téměř 100 milionů srovnání kolem 20000 srovnání. same_music_tooltip = Vyhledávání podobných hudebních souborů podle jejich obsahu může být nakonfigurováno nastavením: - Minimální doba fragmentu, po které mohou být hudební soubory identifikovány jako podobné - Maximální rozdíl mezi dvěma testovanými fragmenty Klíč k dobrým výsledkům je najít rozumné kombinace těchto parametrů, pro stanovení. Nastavení minimální doby na 5 s a maximální rozdíl na 1,0 bude hledat téměř stejné fragmenty v souborech. Čas 20 s a maximální rozdíl 6,0 na druhé straně funguje dobře pro nalezení remixů/živých verzí atd. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Rozlišuje malá a velká písmena duplicate_case_sensitive_name_tooltip = Pokud je povoleno, skupiny pouze záznamy, pokud mají přesně stejný název, např.Żołd <-> Żołd Zakázání takové volby bude názvy skupin bez kontroly, zda je každé písmeno stejné velikosti, např. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Velikost a název duplicate_mode_name_combo_box = Název duplicate_mode_size_combo_box = Velikost duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka nabízí 3 typy hash: Blake3 - kryptografická hash funkce. Toto je výchozí, protože je velmi rychlý. 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. XXH3 - velmi podobné ve výkonu a kvalitě hash jako Blake3 (ale nekryptografické). Takovéto režimy mohou být snadno zaměnitelné. duplicate_check_method_tooltip = Pro tuto chvíli nabízí Czkawka tři typy metod, které vyhledávají duplicitní soubory: Název - Nalezení souborů, které mají stejný název. Velikost - Nalezí soubory, které mají stejnou velikost. 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í. image_hash_size_tooltip = 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é. 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. 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í. 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). image_resize_filter_tooltip = Pro výpočet hash obrázku musí knihovna nejprve změnit velikost. V závislosti na zvoleném algoritmu bude výsledný obrázek použitý k výpočtu hash vypadat trochu jinak. 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á. S velikostí hash 8x8 je doporučeno použít jiný algoritmus než nejbližší pro lepší skupiny obrázků. image_hash_alg_tooltip = Uživatelé si mohou vybrat z jednoho z mnoha algoritmů pro výpočet hashu. Každý má silné a slabší body a někdy přinese lepší a někdy horší výsledky pro různé obrázky. Takže k určení nejlepšího pro vás je vyžadováno ruční testování. big_files_mode_combobox_tooltip = Umožňuje vyhledávat malé / největší soubory big_files_mode_label = Zaškrtnuté soubory big_files_mode_smallest_combo_box = Nejmenší big_files_mode_biggest_combo_box = Největší main_notebook_duplicates = Duplicitní soubory main_notebook_empty_directories = Prázdné adresáře main_notebook_big_files = Velké soubory main_notebook_empty_files = Prázdné soubory main_notebook_temporary = Dočasné soubory main_notebook_similar_images = Podobné obrázky main_notebook_similar_videos = Podobná videa main_notebook_same_music = Hudební duplikáty main_notebook_symlinks = Neplatné symbolické odkazy main_notebook_broken_files = Rozbité soubory main_notebook_bad_extensions = Špatná rozšíření main_tree_view_column_file_name = Název souboru main_tree_view_column_folder_name = Název složky main_tree_view_column_path = Cesta main_tree_view_column_modification = Datum změny main_tree_view_column_size = Velikost main_tree_view_column_similarity = Podobnost main_tree_view_column_dimensions = Rozměry main_tree_view_column_title = Hlava main_tree_view_column_artist = Umělec main_tree_view_column_year = Rok main_tree_view_column_bitrate = Přenosová rychlost main_tree_view_column_length = Délka main_tree_view_column_genre = Žánr main_tree_view_column_symlink_file_name = Název souboru symbolického odkazu main_tree_view_column_symlink_folder = Složka symbolického odkazu main_tree_view_column_destination_path = Cílová cesta main_tree_view_column_type_of_error = Typ chyby main_tree_view_column_current_extension = Aktuální rozšíření main_tree_view_column_proper_extensions = Řádné rozšíření main_tree_view_column_fps = FPS main_tree_view_column_codec = Kodek main_label_check_method = Metoda kontroly main_label_hash_type = Typ Hash main_label_hash_size = Velikost hash main_label_size_bytes = Velikost (bajty) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Počet zobrazených souborů main_label_resize_algorithm = Změna velikosti algoritmu main_label_similarity = Podobnost { " " } main_check_box_broken_files_audio = Zvuk main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Archivovat main_check_box_broken_files_image = Obrázek main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignorovat stejnou velikost check_button_general_same_size_tooltip = Ignorovat soubory se stejnou velikostí ve výsledcích - obvykle se jedná o 1:1 duplicitní main_label_size_bytes_tooltip = Velikost souborů, které budou použity při skenování # Upper window upper_tree_view_included_folder_column_title = Vyhledávané složky upper_tree_view_included_reference_column_title = Referenční složky upper_recursive_button = Rekurentní upper_recursive_button_tooltip = Pokud je vybráno, hledejte také soubory, které nejsou umístěny přímo pod vybranými složkami. upper_manual_add_included_button = Ruční přidání upper_add_included_button = Přidat upper_remove_included_button = Odebrat upper_manual_add_excluded_button = Ruční přidání upper_add_excluded_button = Přidat upper_remove_excluded_button = Odebrat upper_manual_add_included_button_tooltip = Přidat název adresáře k hledání ručně. Chcete-li přidat více cest najednou, oddělte je od ; /home/roman;/home/rozkaz přidá dva adresáře /home/roman a /home/rozkaz upper_add_included_button_tooltip = Přidat nový adresář k vyhledávání. upper_remove_included_button_tooltip = Odstranit adresář z hledání. upper_manual_add_excluded_button_tooltip = Přidejte ručně název vyloučené adresáře. Chcete-li přidat více cest najednou, oddělte je od ; /home/roman;/home/krokiet přidá dva adresáře /home/roman a /home/keokiet upper_add_excluded_button_tooltip = Přidat adresář, který bude při hledání vyloučen. upper_remove_excluded_button_tooltip = Odstranit adresář z vyloučení. upper_notebook_items_configuration = Konfigurace položek upper_notebook_excluded_directories = Vyloučené cesty upper_notebook_included_directories = Zahrnuté cesty upper_allowed_extensions_tooltip = Povolené přípony musí být odděleny čárkami (ve výchozím nastavení jsou všechny k dispozici). Následující makra, která přidávají více rozšíření najednou, jsou také k dispozici: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Seznam zakázaných souborů, které budou při skenování ignorovány. Při používání povolených i zakázaných přípon, má tato vyšší prioritu, takže soubor nebude zaškrtnut. upper_excluded_items_tooltip = Vyřazené položky musí obsahovat * wildcard a měly by být odděleny čárkami. Toto je pomalejší než Excluded Paths, takže používejte opatrně. upper_excluded_items = Vyloučené položky: upper_allowed_extensions = Povolená rozšíření: upper_excluded_extensions = Zakázané rozšíření: # Popovers popover_select_all = Vybrat vše popover_unselect_all = Odznačit vše popover_reverse = Reverzní výběr popover_select_all_except_shortest_path = Vyberte vše kromě nejkratší cesty popover_select_all_except_longest_path = Vyberte vše kromě nejdelší cesty popover_select_all_except_oldest = Vybrat vše kromě nejstarších popover_select_all_except_newest = Vybrat vše kromě nejnovějších popover_select_one_oldest = Vyberte jeden nejstarší popover_select_one_newest = Vyberte jeden nejnovější popover_select_custom = Vybrat vlastní popover_unselect_custom = Zrušit výběr vlastních popover_select_all_images_except_biggest = Vybrat vše kromě největších popover_select_all_images_except_smallest = Vybrat všechny kromě nejmenších popover_custom_path_check_button_entry_tooltip = Vyberte záznamy podle cesty. Příklad použití: /home/pimpek/rzecz.txt lze nalézt pomocí /home/pim* popover_custom_name_check_button_entry_tooltip = Vyberte záznamy podle názvů souborů. Příklad použití: /usr/ping/pong.txt lze nalézt s *ong* popover_custom_regex_check_button_entry_tooltip = Vyberte záznamy podle zadaného Regexu. S tímto režimem je vyhledávaná cesta se jménem. Příklad použití: /usr/bin/ziemniak. xt lze nalézt pomocí /ziem[a-z]+ Toto používá výchozí implementaci Rust regex. Více o tom si můžete přečíst zde: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Umožňuje detekci citlivosti na malá a velká písmena. Pokud je vypnuta /doma/* nálezů jak /HoMe/roman tak /home/roman. popover_custom_not_all_check_button_tooltip = Zabraňuje výběru všech záznamů ve skupině. 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. VAROVÁNÍ: Toto nastavení nefunguje, pokud jste již ručně vybrali všechny výsledky ve skupině. popover_custom_regex_path_label = Cesta popover_custom_regex_name_label = Název popover_custom_regex_regex_label = Regex cesta + Jméno popover_custom_case_sensitive_check_button = Rozlišit malá a velká písmena popover_custom_all_in_group_label = Nesbírat všechny záznamy ve skupině popover_custom_mode_unselect = Zrušit výběr vlastních popover_custom_mode_select = Vybrat vlastní popover_sort_file_name = Název souboru popover_sort_folder_name = Název adresáře popover_sort_full_name = Jméno a příjmení popover_sort_size = Velikost popover_sort_selection = Výběr popover_invalid_regex = Regex je neplatný popover_valid_regex = Regex je platný # Bottom buttons bottom_search_button = Hledat bottom_select_button = Vybrat bottom_delete_button = Vymazat bottom_save_button = Uložit bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Přesunout bottom_sort_button = Seřadit bottom_compare_button = Porovnat bottom_search_button_tooltip = Začít hledání bottom_select_button_tooltip = Vyberte záznamy. Pouze vybrané soubory/složky mohou být později zpracovány. bottom_delete_button_tooltip = Odstranit vybrané soubory/složky. bottom_save_button_tooltip = Ukládat data o hledání do souboru bottom_symlink_button_tooltip = Vytvořit symbolické odkazy. Funguje pouze tehdy, pokud jsou vybrány alespoň dva výsledky ve skupině. Nejprve je nezměněna a druhé a později jsou souvztažné s prvními. bottom_hardlink_button_tooltip = Vytvořit hardwarové odkazy. Funguje pouze tehdy, pokud jsou vybrány alespoň dva výsledky ve skupině. Nejprve je nezměněna a druhé a později jsou těžce propojeny s prvními. bottom_hardlink_button_not_available_tooltip = Vytvořit hardwarové odkazy. Tlačítko je zakázáno, protože hardwarové odkazy nelze vytvořit. 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. Pokud aplikace s takovými oprávněními již funguje, podívejte se na podobné problémy na Githubu. bottom_move_button_tooltip = Přesune soubory do vybraného adresáře. Zkopíruje všechny soubory do adresáře bez uchování stromu adresáře. Při pokusu přesunout dva soubory se stejným názvem do složky, druhý selže a zobrazí chybu. bottom_sort_button_tooltip = Seřazuje soubory/složky podle zvolené metody. bottom_compare_button_tooltip = Porovnat obrázky ve skupině. bottom_show_errors_tooltip = Zobrazit/skrýt spodní textový panel. bottom_show_upper_notebook_tooltip = Zobrazit/skrýt horní panel sešitu. # Progress Window progress_stop_button = Zastavit progress_stop_additional_message = Zastavit požadavek # About Window about_repository_button_tooltip = Odkaz na stránku repositáře se zdrojovým kódem. about_donation_button_tooltip = Odkaz na stránku s darováním. about_instruction_button_tooltip = Odkaz na stránku instrukcí. about_translation_button_tooltip = Odkaz na stránku Crowdin s překlady aplikací. Oficiálně polština a angličtina jsou podporovány. about_repository_button = Repozitář about_donation_button = Darovat about_instruction_button = Instrukce about_translation_button = Překlad # Header header_setting_button_tooltip = Otevře dialogové okno nastavení. header_about_button_tooltip = Otevře dialog s informacemi o aplikaci. # Settings ## General settings_number_of_threads = Počet použitých vláken settings_number_of_threads_tooltip = Počet použitých vláken, 0 znamená, že budou použita všechna dostupná vlákna. settings_use_rust_preview = Místo toho použít externí knihovny gtk k načtení náhledů settings_use_rust_preview_tooltip = 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. Pokud máte problémy s načítáním náhledů, můžete zkusit toto nastavení změnit. 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ů. settings_label_restart = Pro použití nastavení je třeba restartovat aplikaci! settings_ignore_other_filesystems = Ignorovat ostatní souborové systémy (pouze Linux) settings_ignore_other_filesystems_tooltip = ignoruje soubory, které nejsou ve stejném souborovém systému jako prohledávané adresáře. Funguje stejně jako -xdev možnost najít příkaz na Linuxu settings_save_at_exit_button_tooltip = Uložit konfiguraci do souboru při zavření aplikace. settings_load_at_start_button_tooltip = Načíst konfiguraci ze souboru při otevírání aplikace. Pokud není povoleno, budou použita výchozí nastavení. settings_confirm_deletion_button_tooltip = Zobrazit potvrzovací dialogové okno při kliknutí na tlačítko mazat. settings_confirm_link_button_tooltip = Zobrazit potvrzovací dialog při kliknutí na tlačítko hard/symbolický odkaz. settings_confirm_group_deletion_button_tooltip = Zobrazit varovný dialog při pokusu o odstranění všech záznamů ze skupiny. settings_show_text_view_button_tooltip = Zobrazit textový panel v dolní části uživatelského rozhraní. settings_use_cache_button_tooltip = Použít cache souborů. settings_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). settings_use_trash_button_tooltip = Přesune soubory do koše a místo toho je trvale odstraní. settings_language_label_tooltip = Jazyk uživatelského rozhraní. settings_save_at_exit_button = Uložit konfiguraci při zavření aplikace settings_load_at_start_button = Načíst konfiguraci při otevření aplikace settings_confirm_deletion_button = Zobrazit dialogové okno potvrzení při mazání všech souborů settings_confirm_link_button = Zobrazit dialogové okno pro pevné / symbolické odkazy settings_confirm_group_deletion_button = Zobrazit dialogové okno potvrzení při mazání všech souborů ve skupině settings_show_text_view_button = Zobrazit spodní textový panel settings_use_cache_button = Použít keš settings_save_also_as_json_button = Ukládat mezipaměť také jako soubor JSON settings_use_trash_button = Přesunout smazané soubory do koše settings_language_label = Jazyk settings_multiple_delete_outdated_cache_checkbutton = Automaticky odstranit zastaralé položky v mezipaměti settings_multiple_delete_outdated_cache_checkbutton_tooltip = Odstranit zastaralé výsledky mezipaměti, které ukazují na neexistující soubory. 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). 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í. 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í. settings_notebook_general = Obecná ustanovení settings_notebook_duplicates = Duplikáty settings_notebook_images = Podobné obrázky settings_notebook_videos = Podobné video ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Zobrazí náhled na pravé straně (při výběru souboru obrázku). settings_multiple_image_preview_checkbutton = Zobrazit náhled obrázku settings_multiple_clear_cache_button_tooltip = Ručně vymazat mezipaměť zastaralých položek. Toto by mělo být použito pouze v případě, že je zakázáno automatické vymazání. settings_multiple_clear_cache_button = Odstranit zastaralé výsledky z mezipaměti. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Skryje všechny soubory kromě jedné, pokud všechny odkazují na stejná data (jsou hardlinované). 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ů. settings_duplicates_minimal_size_entry_tooltip = Nastavte minimální velikost souboru, který bude uložen do mezipaměti. 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. settings_duplicates_prehash_checkbutton_tooltip = 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ů. Ve výchozím nastavení je zakázáno, protože v některých situacích může způsobit zpomalení. 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. settings_duplicates_prehash_minimal_entry_tooltip = Minimální velikost položky v mezipaměti. settings_duplicates_hide_hard_link_button = Skrýt pevné odkazy settings_duplicates_prehash_checkbutton = Použít mezipaměť rozpoznávání settings_duplicates_minimal_size_cache_label = Minimální velikost souborů (v bajtech) uložených do mezipaměti settings_duplicates_minimal_size_cache_prehash_label = Minimální velikost souborů (v bajtech) uložených pro zachycení keše ## Saving/Loading settings settings_saving_button_tooltip = Uložit aktuální nastavení do souboru. settings_loading_button_tooltip = Načíst nastavení ze souboru a nahradit jejich aktuální konfiguraci. settings_reset_button_tooltip = Obnovit aktuální konfiguraci na výchozí. settings_saving_button = Uložit konfiguraci settings_loading_button = Načíst konfiguraci settings_reset_button = Obnovit konfiguraci ## Opening cache/config folders settings_folder_cache_open_tooltip = Otevře složku, kde jsou uloženy soubory txt v mezipaměti. Ú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í. 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). V případě problémů s mezipamětí, mohou být tyto soubory odstraněny. Aplikace je automaticky obnoví. settings_folder_settings_open_tooltip = Otevře složku, kde je uloženo nastavení Czkawka. VAROVÁNÍ: Ruční úprava konfigurace může poškodit váš pracovní postup. settings_folder_cache_open = Otevřít složku mezipaměti settings_folder_settings_open = Otevřít složku s nastavením # Compute results compute_stopped_by_user = Vyhledávání bylo zastaveno uživatelem compute_found_duplicates_hash_size = Nalezeno { $number_files } duplikátů v { $number_groups } skupinách, které trvaly { $size } v { $time } compute_found_duplicates_name = Nalezeno { $number_files } duplikátů v { $number_groups } skupinách v { $time } compute_found_empty_folders = Nalezeno { $number_files } prázdné složky v { $time } compute_found_empty_files = Nalezeno { $number_files } prázdných souborů v { $time } compute_found_big_files = Nalezeno { $number_files } velkých souborů v { $time } compute_found_temporary_files = Nalezeno { $number_files } dočasných souborů v { $time } compute_found_images = Nalezeno { $number_files } podobných obrázků v { $number_groups } skupinách v { $time } compute_found_videos = Nalezeno { $number_files } podobných videí v { $number_groups } skupinách v { $time } compute_found_music = Nalezeno { $number_files } podobných hudebních souborů v { $number_groups } skupinách v { $time } compute_found_invalid_symlinks = Nalezeno { $number_files } neplatných symbolických odkazů v { $time } compute_found_broken_files = Nalezeno { $number_files } rozbitých souborů v { $time } compute_found_bad_extensions = Nalezeno { $number_files } souborů s neplatnými příponami v { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Naskenovaný { $file_number } soubor *[other] Skenované { $file_number } soubory } progress_scanning_extension_of_files = Kontrola přípony { $file_checked }/{ $all_files } souboru progress_scanning_broken_files = Kontrola { $file_checked }/{ $all_files } soubor ({ $data_checked }/{ $all_data }) progress_scanning_video = Hashováno { $file_checked }/{ $all_files } videa progress_creating_video_thumbnails = Vytvořené náhledy { $file_checked }/{ $all_files } videa progress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Porovnáno { $file_checked }/{ $all_files } hash obrázků progress_scanning_music_tags_end = Porovnávané štítky hudebního souboru { $file_checked }/{ $all_files } progress_scanning_music_tags = Číst tagy { $file_checked }/{ $all_files } hudebního souboru progress_scanning_music_content_end = Porovnávaný otisk hudby { $file_checked }/{ $all_files } progress_scanning_music_content = Vypočítaný otisk hudby { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Naskenovaná složka { $folder_number } *[other] Naskenovaná { $folder_number } složky } progress_scanning_size = Naskenovaná velikost souboru { $file_number } progress_scanning_size_name = Naskenovaný název a velikost { $file_number } souboru progress_scanning_name = Naskenované jméno { $file_number } souboru progress_analyzed_partial_hash = Analyzováno částečné hash souborů { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Analyzováno plné hash souborů { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Načítání mezipaměti rozpoznání progress_prehash_cache_saving = Ukládání mezipaměti rozpoznání progress_hash_cache_loading = Načítání hash keše progress_hash_cache_saving = Ukládání keše hash progress_cache_loading = Načítání keše progress_cache_saving = Ukládání keše progress_current_stage = Aktuální fáze:{ " " } progress_all_stages = Všechny etapy:{ " " } # Saving loading saving_loading_saving_success = Uložena konfigurace do souboru { $name }. saving_loading_saving_failure = Nepodařilo se uložit konfigurační data do souboru { $name }, důvod { $reason }. saving_loading_reset_configuration = Aktuální konfigurace byla vymazána. saving_loading_loading_success = Správně načtená konfigurace aplikace. saving_loading_failed_to_create_config_file = Nepodařilo se vytvořit konfigurační soubor "{ $path }", důvod "{ $reason }". saving_loading_failed_to_read_config_file = Konfiguraci z "{ $path } nelze načíst, protože neexistuje nebo není soubor. saving_loading_failed_to_read_data_from_file = Nelze číst data ze souboru "{ $path }", důvod "{ $reason }". # Other selected_all_reference_folders = Hledání nelze spustit, pokud jsou všechny adresáře nastaveny jako referenční složky searching_for_data = Vyhledávání dat může chvíli trvat, prosím čekejte... text_view_messages = ZPRÁVY text_view_warnings = VAROVÁNÍ text_view_errors = CHYBA about_window_motto = Tento program je zdarma a bude vždy používán. krokiet_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. # Various dialog dialogs_ask_next_time = Příště se zeptat symlink_failed = Nepodařilo se symbolicky propojit { $name } do { $target }, důvod { $reason } delete_title_dialog = Potvrzení odstranění delete_question_label = Jste si jisti, že chcete odstranit soubory? delete_all_files_in_group_title = Potvrzení odstranění všech souborů ve skupině delete_all_files_in_group_label1 = V některých skupinách jsou vybrány všechny záznamy. delete_all_files_in_group_label2 = Jste si jisti, že je chcete odstranit? delete_items_label = { $items } soubory budou odstraněny. delete_items_groups_label = { $items } souborů z { $groups } skupin bude smazáno. hardlink_failed = Nepodařilo se propojit { $name } na { $target }, důvod { $reason } hard_sym_invalid_selection_title_dialog = Neplatný výběr s některými skupinami hard_sym_invalid_selection_label_1 = V některých skupinách je vybrán pouze jeden záznam a bude ignorován. hard_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ě. hard_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. hard_sym_link_title_dialog = Potvrzení odkazu hard_sym_link_label = Jste si jisti, že chcete propojit tyto soubory? move_folder_failed = Nepodařilo se přesunout složku { $name }, důvod { $reason } move_file_failed = Nepodařilo se přesunout soubor { $name }, důvod { $reason } move_files_title_dialog = Vyberte složku, do které chcete přesunout duplicitní soubory move_files_choose_more_than_1_path = Lze vybrat pouze jednu cestu, aby bylo možné zkopírovat jejich duplikované soubory, vybrané { $path_number }. move_stats = Správně přesunuto { $num_files }/{ $all_files } položek save_results_to_file = Uloženy výsledky do složky txt i json do složky "{ $name }". search_not_choosing_any_music = CHYBA: Musíte vybrat alespoň jedno zaškrtávací políčko s prohledáváním hudby. search_not_choosing_any_broken_files = CHYBA: Musíte vybrat alespoň jedno zaškrtávací políčko s typem zkontrolovaných poškozených souborů. include_folders_dialog_title = Složky, které chcete zahrnout exclude_folders_dialog_title = Složky k vyloučení include_manually_directories_dialog_title = Přidat adresář ručně cache_properly_cleared = Správně vymazaná mezipaměť cache_clear_duplicates_title = Vymazání cache duplicity cache_clear_similar_images_title = Vymazání cache podobných obrázků cache_clear_similar_videos_title = Vymazání cache podobných videí cache_clear_message_label_1 = Chcete vymazat mezipaměť zastaralých položek? cache_clear_message_label_2 = Tato operace odstraní všechny položky mezipaměti, které ukazují na neplatné soubory. cache_clear_message_label_3 = To může mírně urychlit načítání/ukládání do mezipaměti. cache_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. # Show preview preview_image_resize_failure = Nepodařilo se změnit velikost obrázku { $name }. preview_image_opening_failure = Nepodařilo se otevřít obrázek { $name }, důvod { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Skupina { $current_group }/{ $all_groups } ({ $images_in_group } obrázků) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/de/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Einstellungen window_main_title = Czkawka (Schluckauf) window_progress_title = Scannen window_compare_images = Bilder vergleichen # General general_ok_button = Ok general_close_button = Schließen # Krokiet info dialog krokiet_info_title = Krokiet – Neue Version von Czkawka krokiet_info_message = Krokiet ist die neue, verbesserte, schnellere und zuverlässigere Version der Czkawka GTK GUI! 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. 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. Probiere es aus und sieh den Unterschied! 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. PS: Diese Nachricht sollte nur einmal erscheinen. Wenn sie erneut angezeigt wird, setze die Umgebungsvariable CZKAWKA_DONT_ANNOY_ME auf einen nicht leeren Wert. # Main window music_title_checkbox = Titel music_artist_checkbox = Künstler music_year_checkbox = Jahr music_bitrate_checkbox = Bitraten music_genre_checkbox = Genretype music_length_checkbox = Dauer music_comparison_checkbox = Ungefährer Vergleich music_checking_by_tags = Schlagworte music_checking_by_content = Inhalt same_music_seconds_label = Minimale Dauer des Fragments, in Sekunden same_music_similarity_label = Maximaler Unterschied music_compare_only_in_title_group = Vergleiche innerhalb von Gruppen ähnlicher Titel music_compare_only_in_title_group_tooltip = Wenn aktiviert, werden Dateien nach Titel gruppiert und dann miteinander verglichen. Mit 10000 Dateien, statt fast 100 Millionen Vergleiche wird es in der Regel rund 20000 Vergleiche geben. same_music_tooltip = Die Suche nach ähnlichen Musikdateien nach dem Inhalt kann über die Einstellung konfiguriert werden: - Die minimale Fragmentzeit, nach der Musikdateien als ähnlich identifiziert werden können - Der maximale Unterschied zwischen zwei getesteten Fragmenten Der Schlüssel zu guten Ergebnissen ist die Suche nach sinnvollen Kombinationen dieser Parameter. für bereitgestellt. Wenn Sie die minimale Zeit auf 5 Sekunden und den maximalen Unterschied auf 1,0 setzen, werden fast identische Fragmente in den Dateien gesucht. Eine Zeit von 20 Sekunden und ein maximaler Unterschied von 6.0 hingegen funktioniert gut um Remix/Live-Versionen zu finden. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Gross-/Kleinschreibung beachten duplicate_case_sensitive_name_tooltip = Wenn aktiviert, gruppieren Sie nur Datensätze, wenn sie genau denselben Namen haben, z. żoŁD <-> Żołd Deaktivieren dieser Option gruppiert Namen ohne zu überprüfen, ob jeder Buchstabe die gleiche Größe wie żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Größe und Name duplicate_mode_name_combo_box = Name duplicate_mode_size_combo_box = Größe duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka bietet 3 Arten von Hashes an, die verwendet werden können: Blake3 - kryptographische Hashfunktion. Wegen ihrer Geschwindikeit wird Sie als Standard-Hash-Algorithmus verwendet. CRC32 - einfache Hash-Funktion. Sie sollte schneller sein als Blake3, könnte aber in seltenen Fällen Kollisionen haben. XXH3 - bei Geschwindikeit und Hashqualität vergleichbar mit Blake3 (aber nicht kryptographisch). Beide Modi können somit leicht miteinander ausgetauscht werden. duplicate_check_method_tooltip = Derzeit bietet Czkawka drei Methoden an, um Duplikate zu finden: Name - Findet Dateien mit gleichem Namen. Größe - Findet Dateien mit gleicher Größe. 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. image_hash_size_tooltip = 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. 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. 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. 32 und 64 Hashes finden nur sehr ähnliche Bilder, sollten aber fast keine falschen positiven Bilder haben (vielleicht mit Ausnahme einiger Bilder mit Alphakanal). image_resize_filter_tooltip = Um Hash des Bildes zu berechnen, muss die Bibliothek zuerst die Größe des Hashs verändern. Abhängig vom gewählten Algorithmus sieht das resultierende Bild, das zur Hashberechnung verwendet wird, ein wenig anders aus. 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. Mit 8x8 Hashgröße wird empfohlen, einen anderen Algorithmus als nahe zu verwenden, um bessere Gruppen von Bildern zu haben. image_hash_alg_tooltip = Benutzer können aus einem von vielen Algorithmen zur Berechnung des Hashs wählen. Jeder hat sowohl starke als auch schwächere Punkte und wird manchmal bessere und manchmal schlechtere Ergebnisse für verschiedene Bilder liefern. Um also den besten für Sie zu ermitteln, ist eine manuelle Prüfung erforderlich. big_files_mode_combobox_tooltip = Erlaubt die Suche nach kleinsten/größten Dateien big_files_mode_label = Überprüfte Dateien big_files_mode_smallest_combo_box = Die kleinsten big_files_mode_biggest_combo_box = Die größten main_notebook_duplicates = Gleiche Dateien main_notebook_empty_directories = Leere Verzeichnisse main_notebook_big_files = Große Dateien main_notebook_empty_files = Leere Dateien main_notebook_temporary = Temporäre Dateien main_notebook_similar_images = Ähnliche Bilder main_notebook_similar_videos = Ähnliche Videos main_notebook_same_music = Gleiche Musik main_notebook_symlinks = Ungültige Symlinks main_notebook_broken_files = Defekte Dateien main_notebook_bad_extensions = Falsche Erweiterungen main_tree_view_column_file_name = Dateiname main_tree_view_column_folder_name = Ordnername main_tree_view_column_path = Pfad main_tree_view_column_modification = Änderungsdatum main_tree_view_column_size = Größe main_tree_view_column_similarity = Ähnlichkeit main_tree_view_column_dimensions = Abmessungen main_tree_view_column_title = Titel main_tree_view_column_artist = Künstler main_tree_view_column_year = Jahr main_tree_view_column_bitrate = Bitrate main_tree_view_column_length = Dauer main_tree_view_column_genre = Genretype main_tree_view_column_symlink_file_name = Symlink Dateiname main_tree_view_column_symlink_folder = Symlink-Ordner main_tree_view_column_destination_path = Zielpfad main_tree_view_column_type_of_error = Fehlertyp main_tree_view_column_current_extension = Aktuelle Erweiterung main_tree_view_column_proper_extensions = Richtige Erweiterung main_tree_view_column_fps = FPS main_tree_view_column_codec = Codec main_label_check_method = Prüfmethode main_label_hash_type = Hash Typ main_label_hash_size = Hash Größe main_label_size_bytes = Größe (Bytes) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Anzahl der angezeigten Dateien main_label_resize_algorithm = Algorithmus skalieren main_label_similarity = Ähnlichkeit{ " " } main_check_box_broken_files_audio = Ton main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Archiv main_check_box_broken_files_image = Bild main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Gleiche Größe ignorieren check_button_general_same_size_tooltip = Ignoriere Dateien mit identischer Größe in den Ergebnissen - in der Regel sind es 1:1 Duplikate main_label_size_bytes_tooltip = Größe der Dateien, die beim Scannen verwendet werden # Upper window upper_tree_view_included_folder_column_title = Zu durchsuchende Ordner upper_tree_view_included_reference_column_title = Referenzordner upper_recursive_button = Rekursiv upper_recursive_button_tooltip = Falls ausgewählt, suchen Sie auch nach Dateien, die nicht direkt unter den ausgewählten Ordnern platziert werden. upper_manual_add_included_button = Manuell hinzufügen upper_add_included_button = Neu upper_remove_included_button = Entfernen upper_manual_add_excluded_button = Manuell hinzufügen upper_add_excluded_button = Neu upper_remove_excluded_button = Entfernen upper_manual_add_included_button_tooltip = Verzeichnisname zur Suche per Hand hinzufügen. Um mehrere Pfade gleichzeitig hinzuzufügen, trennen Sie sie durch ; /home/roman;/home/rozkaz fügt zwei Verzeichnisse /home/roman und /home/rozkaz hinzu upper_add_included_button_tooltip = Neues Verzeichnis zur Suche hinzufügen. upper_remove_included_button_tooltip = Verzeichnis von der Suche löschen. upper_manual_add_excluded_button_tooltip = Ausgeschlossenen Verzeichnisnamen per Hand hinzufügen. Um mehrere Pfade gleichzeitig hinzuzufügen, trennen Sie sie durch ; /home/roman;/home/krokiet wird zwei Verzeichnisse /home/roman und /home/keokiet hinzufügen upper_add_excluded_button_tooltip = Verzeichnis hinzufügen, das bei der Suche ausgeschlossen werden soll. upper_remove_excluded_button_tooltip = Ausgeschlossene Verzeichnisse löschen. upper_notebook_items_configuration = Suchbedingungen upper_notebook_excluded_directories = Ausgeschlossene Pfade upper_notebook_included_directories = Einbezogene Pfade upper_allowed_extensions_tooltip = Erlaubte Erweiterungen müssen durch Kommas getrennt werden (standardmäßig sind alle verfügbar). Folgende Makros, die mehrere Erweiterungen gleichzeitig hinzufügen, sind ebenfalls verfügbar: IMAGE, VIDEO, MUSIC, TEXT. Nutzungsbeispiel ".exe, IMAGE, VIDEO, .rar, 7z" - bedeutet, dass Bilder (jpg, png ...), Videodateien (avi, mp4 ...), exe, rar und 7z gescannt werden. upper_excluded_extensions_tooltip = Liste der deaktivierten Dateien, die beim Scannen ignoriert werden. Wenn sowohl erlaubte als auch deaktivierte Erweiterungen verwendet werden, hat diese eine höhere Priorität, so dass die Datei nicht ausgewählt wird. upper_excluded_items_tooltip = Ausgeschlossene Elemente müssen * Wildcard und durch Kommas getrennt enthalten. Dies ist langsamer als Exkludierte Pfade, also verwenden Sie es vorsichtig. upper_excluded_items = Ausgeschlossene Elemente: upper_allowed_extensions = Erlaubte Erweiterungen: upper_excluded_extensions = Deaktivierte Erweiterungen: # Popovers popover_select_all = Alles auswählen popover_unselect_all = Gesamte Auswahl aufheben popover_reverse = Auswahl umkehren popover_select_all_except_shortest_path = Wähle alle außer den kürzesten Pfad popover_select_all_except_longest_path = Wähle alle außer den längsten Pfad popover_select_all_except_oldest = Alle außer Ältester auswählen popover_select_all_except_newest = Alle außer Neuester auswählen popover_select_one_oldest = Älteste auswählen popover_select_one_newest = Neueste auswählen popover_select_custom = Individuell auswählen popover_unselect_custom = Individuell Auswahl aufheben popover_select_all_images_except_biggest = Alle außer Größter auswählen popover_select_all_images_except_smallest = Alle außer Kleinster auswählen popover_custom_path_check_button_entry_tooltip = Ermöglicht die Auswahl von Datensätzen nach Dateipfad. Beispielnutzung: /home/pimpek/rzecz.txt kann mit /home/pim* gefunden werden popover_custom_name_check_button_entry_tooltip = Ermöglicht die Auswahl von Datensätzen nach Dateinamen. Beispielnutzung: /usr/ping/pong.txt kann mit *ong* gefunden werden popover_custom_regex_check_button_entry_tooltip = Ermöglicht die Auswahl von Datensätzen nach spezifizierter Regex. Mit diesem Modus ist der gesuchte Text der Pfad einschließlich Dateinamen. Beispielnutzung: /usr/bin/ziemniak.txt kann mit /ziem[a-z]+ gefunden werden. Dies verwendet die Standard-Implementierung von Regex in Rust. Mehr dazu unter https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Aktiviert die Erkennung von Groß- und Kleinschreibungen. Wenn /home/* deaktiviert ist, findet /hoMe/roman und /home/roman. popover_custom_not_all_check_button_tooltip = Verhindert die Auswahl aller Elemente in der Gruppe. 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. Warnung: Diese Einstellung funktioniert nicht, wenn bereits alle Ergebnisse in der Gruppe manuell ausgewählt wurden. popover_custom_regex_path_label = Pfad popover_custom_regex_name_label = Name popover_custom_regex_regex_label = Regex Pfad + Name popover_custom_case_sensitive_check_button = Groß-/Kleinschreibung beachten popover_custom_all_in_group_label = Nicht alle Datensätze in der Gruppe auswählen popover_custom_mode_unselect = Eigene Abwählen popover_custom_mode_select = Eigene auswählen popover_sort_file_name = Dateiname popover_sort_folder_name = Verzeichnisname popover_sort_full_name = Vollständiger Name popover_sort_size = Größe popover_sort_selection = Auswahl popover_invalid_regex = Regex ist ungültig popover_valid_regex = Regex ist gültig # Bottom buttons bottom_search_button = Suchen bottom_select_button = Auswählen bottom_delete_button = Löschen bottom_save_button = Speichern bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Bewegen bottom_sort_button = Sortierung bottom_compare_button = Vergleichen bottom_search_button_tooltip = Suche starten bottom_select_button_tooltip = Datensätze auswählen. Nur ausgewählte Dateien/Ordner können später verarbeitet werden. bottom_delete_button_tooltip = Ausgewählte Dateien/Ordner löschen. bottom_save_button_tooltip = Daten über die Suche in Datei speichern bottom_symlink_button_tooltip = Erstelle symbolische Links. Funktioniert nur, wenn mindestens zwei Ergebnisse einer Gruppe ausgewählt sind. Das Erste bleibt dabei unverändert, und das Zweite und alle Weiteren werden zu symbolischen Links auf das Erste umgewandelt. bottom_hardlink_button_tooltip = Erstelle Hardlinks. Funktioniert nur, wenn mindestens zwei Ergebnisse einer Gruppe ausgewählt sind. Das Erste bleibt dabei unverändert, und das Zweite und alle Weiteren werden zu Hardlinks auf das Erste umgewandelt. bottom_hardlink_button_not_available_tooltip = Erstellen von Hardlinks. Button ist deaktiviert, da Hardlinks nicht erstellt werden können. Hardlinks funktionieren nur mit Administratorrechten unter Windows, also stellen Sie sicher, dass Sie die App als Administrator ausführen. Wenn die App bereits mit solchen Rechten arbeitet, überprüfen Sie auf Github auf ähnliche Probleme. bottom_move_button_tooltip = Verschiebt Dateien in den ausgewählten Ordner. Kopiert alle Dateien in den Ordner, ohne den Verzeichnisbaum zu erhalten. Beim Versuch, zwei Dateien mit identischem Namen in einen Ordner zu verschieben, schlägt das Kopieren der Zweiten fehl und zeigt einen Fehler an. bottom_sort_button_tooltip = Sortiert Dateien/Ordner nach der gewählten Methode. bottom_compare_button_tooltip = Vergleiche Bilder in der Gruppe. bottom_show_errors_tooltip = Ein-/Ausblenden des unteren Textfeldes. bottom_show_upper_notebook_tooltip = Ein-/Ausblenden des oberen Notizbuch-Panels. # Progress Window progress_stop_button = Stoppen progress_stop_additional_message = Stopp angefordert # About Window about_repository_button_tooltip = Link zur Repository-Seite mit Quellcode. about_donation_button_tooltip = Link zur Spendenseite. about_instruction_button_tooltip = Link zur Anweisungsseite. about_translation_button_tooltip = Link zur Crowdin-Seite mit App-Übersetzungen. Offiziell werden Polnisch und Englisch unterstützt. about_repository_button = Projektarchiv about_donation_button = Spende about_instruction_button = Anleitung about_translation_button = Übersetzung # Header header_setting_button_tooltip = Öffnet Einstellungsdialog. header_about_button_tooltip = Öffnet den Dialog mit Informationen über die App. # Settings ## General settings_number_of_threads = Anzahl der verwendeten Threads settings_number_of_threads_tooltip = Anzahl der verwendeten Threads, 0 bedeutet, dass alle verfügbaren Threads verwendet werden. settings_use_rust_preview = Verwenden Sie stattdessen externe Bibliotheken gtk, um Vorschaubilder zu laden settings_use_rust_preview_tooltip = Die Verwendung von gtk-Vorschauen ist manchmal schneller und unterstützt mehr Formate, aber manchmal kann dies genau das Gegenteil sein. Wenn Sie Probleme mit dem Laden von Vorschauen haben, können Sie versuchen, diese Einstellung zu ändern. 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. settings_label_restart = Sie müssen die App neu starten, um die Einstellungen anzuwenden! settings_ignore_other_filesystems = Andere Dateisysteme ignorieren (nur Linux) settings_ignore_other_filesystems_tooltip = ignoriert Dateien, die nicht im selben Dateisystem sind wie durchsuchte Verzeichnisse. Funktioniert genauso wie die -xdev Option beim Finden des Befehls unter Linux settings_save_at_exit_button_tooltip = Speichert die Konfiguration in einer Datei, wenn das Programm geschlossen wird. settings_load_at_start_button_tooltip = Konfiguration aus der Datei laden, wenn App geöffnet wird. Falls das nicht aktiviert ist, werden die Standardeinstellungen verwendet. settings_confirm_deletion_button_tooltip = Bestätigungsdialog anzeigen, wenn der Löschen-Knopf gedrückt wird. settings_confirm_link_button_tooltip = Bestätigungsdialog anzeigen, wenn auf den Knopf Hard/Symlink gedrückt wird. settings_confirm_group_deletion_button_tooltip = Warndialog anzeigen, wenn versucht wird, alle Datensätze aus einer Gruppe zu löschen. settings_show_text_view_button_tooltip = Textfenster am unteren Rand der Benutzeroberfläche anzeigen. settings_use_cache_button_tooltip = Datei-Cache verwenden. settings_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. settings_use_trash_button_tooltip = Wenn aktiviert, verschiebt Dateien in den Papierkorb, anstatt sie permanent zu löschen. settings_language_label_tooltip = Sprache der Benutzeroberfläche. settings_save_at_exit_button = Speichert die Konfiguration beim Schließen der App settings_load_at_start_button = Konfiguration beim Öffnen der App laden settings_confirm_deletion_button = Bestätigungsdialog beim Löschen von Dateien anzeigen settings_confirm_link_button = Bestätigungsdialog anzeigen, wenn Hard/Symlinks irgendwelche Dateien settings_confirm_group_deletion_button = Bestätigungsdialog beim Löschen aller Dateien in der Gruppe anzeigen settings_show_text_view_button = Unteren Textbereich anzeigen settings_use_cache_button = Cache verwenden settings_save_also_as_json_button = Cache auch als JSON-Datei speichern settings_use_trash_button = Gelöschte Dateien in den Papierkorb verschieben settings_language_label = Sprache settings_multiple_delete_outdated_cache_checkbutton = Veraltete Cache-Einträge automatisch löschen settings_multiple_delete_outdated_cache_checkbutton_tooltip = Ermöglicht das Löschen veralteter Cache-Ergebnisse, die auf nicht existierende Dateien verweisen. Wenn aktiviert, stellt das Programm sicher, dass beim Laden von Datensätzen alle auf gültige Dateien verweisen (kaputte Verweise werden ignorieren). 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. 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. settings_notebook_general = Allgemein settings_notebook_duplicates = Duplikate settings_notebook_images = Ähnliche Bilder settings_notebook_videos = Ähnliches Video ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Zeigt eine Vorschau auf der rechten Seite (bei der Auswahl einer Bilddatei). settings_multiple_image_preview_checkbutton = Bildvorschau anzeigen settings_multiple_clear_cache_button_tooltip = Leere den Cache manuell aus veralteten Einträgen. Das sollte nur verwendet werden, wenn das automatische Löschen deaktiviert wurde. settings_multiple_clear_cache_button = Entferne veraltete Ergebnisse aus dem Cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Versteckt alle Dateien außer einem, wenn sie auf dieselben Daten (Hardlink) verweisen. 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. settings_duplicates_minimal_size_entry_tooltip = Legen Sie die minimale Dateigröße fest, die zwischengespeichert wird. 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. settings_duplicates_prehash_checkbutton_tooltip = Aktiviert das Caching von Prehashes (Hash aus einem kleinen Teil der Datei), was es erlaubt, nicht duplizierte Ergebnisse schneller zu verwerfen. Es ist standardmäßig deaktiviert, da es in einigen Situationen zu Verlangsamungen führen kann. Es wird dringend empfohlen, es beim Scannen von Hunderttausenden oder Millionen Dateien zu verwenden, da es die Suche um ein Vielfaches beschleunigen kann. settings_duplicates_prehash_minimal_entry_tooltip = Minimale Größe des zwischengespeicherten Eintrags. settings_duplicates_hide_hard_link_button = Verstecke harte Links settings_duplicates_prehash_checkbutton = Prehash-Cache verwenden settings_duplicates_minimal_size_cache_label = Minimale Dateigröße (in Bytes), die im Cache gespeichert wird settings_duplicates_minimal_size_cache_prehash_label = Minimale Dateigröße (in Bytes), die im Prehash-Cache gespeichert wird ## Saving/Loading settings settings_saving_button_tooltip = Aktuelle Einstellungen in Datei speichern. settings_loading_button_tooltip = Lade die Einstellungen aus einer Datei und ersetze die aktuellen Einstellungen mit diesen. settings_reset_button_tooltip = Aktuelle Konfiguration auf Standardeinstellung zurücksetzen. settings_saving_button = Konfiguration speichern settings_loading_button = Konfiguration laden settings_reset_button = Konfiguration zurücksetzen ## Opening cache/config folders settings_folder_cache_open_tooltip = Öffnet den Ordner, in dem txt-Dateien mit Cache-Daten gespeichert sind. Ä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. 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). Bei Problemen mit dem Cache können diese Dateien entfernt werden, sodass die App sie automatisch neu generiert. settings_folder_settings_open_tooltip = Öffnet den Ordner, in dem die Czkawka Konfiguration gespeichert ist. WARNUNG: Manuelle Änderung der Konfiguration kann Ihren Workflow stören. settings_folder_cache_open = Cache-Ordner öffnen settings_folder_settings_open = Einstellungsordner öffnen # Compute results compute_stopped_by_user = Suche wurde vom Benutzer gestoppt compute_found_duplicates_hash_size = { $number_files } Duplikate in { $number_groups } Gruppen gefunden, die { $size } in { $time } genommen haben compute_found_duplicates_name = { $number_files } Duplikate in { $number_groups } Gruppen in { $time } gefunden compute_found_empty_folders = { $number_files } leere Ordner in { $time } gefunden compute_found_empty_files = { $number_files } leere Dateien in { $time } gefunden compute_found_big_files = { $number_files } große Dateien in { $time } gefunden compute_found_temporary_files = { $number_files } temporäre Dateien in { $time } gefunden compute_found_images = { $number_files } ähnliche Bilder in { $number_groups } Gruppen in { $time } gefunden compute_found_videos = { $number_files } ähnliche Videos in { $number_groups } Gruppen in { $time } gefunden compute_found_music = { $number_files } ähnliche Musikdateien in { $number_groups } Gruppen in { $time } gefunden compute_found_invalid_symlinks = { $number_files } ungültige Symlinks in { $time } gefunden compute_found_broken_files = { $number_files } fehlerhafte Dateien in { $time } gefunden compute_found_bad_extensions = { $number_files } Dateien mit ungültigen Endungen in { $time } gefunden # Progress window progress_scanning_general_file = { $file_number -> [one] gescannt { $file_number } Datei *[other] gescannte { $file_number } Dateien } progress_scanning_extension_of_files = Überprüfte Erweiterung von { $file_checked }/{ $all_files } Datei progress_scanning_broken_files = Überprüft { $file_checked }/{ $all_files } Datei ({ $data_checked }/{ $all_data }) progress_scanning_video = Hashed von { $file_checked }/{ $all_files } Video progress_creating_video_thumbnails = Erstellte Vorschaubilder von { $file_checked }/{ $all_files } Video progress_scanning_image = Hashed von { $file_checked }/{ $all_files } Bild ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Verglichen mit { $file_checked }/{ $all_files } Bild-Hash progress_scanning_music_tags_end = Vergleiche Tags von { $file_checked }/{ $all_files } Musikdatei progress_scanning_music_tags = Tags von { $file_checked }/{ $all_files } Musikdatei lesen progress_scanning_music_content_end = Verglichen Fingerabdruck von { $file_checked }/{ $all_files } Musikdatei progress_scanning_music_content = Berechneter Fingerabdruck von { $file_checked }/{ $all_files } Musikdatei ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] gescannt { $folder_number } Ordner *[other] Gescannte { $folder_number } Ordner } progress_scanning_size = Scanne Größe der { $file_number } Datei progress_scanning_size_name = Gescannter Name und Größe der { $file_number } Datei progress_scanning_name = Gescannter Name der { $file_number } Datei progress_analyzed_partial_hash = Analysierter Teilhash von { $file_checked }/{ $all_files } Dateien ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Analysiert voller Hash der { $file_checked }/{ $all_files } Dateien ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Lade Vorhash-Cache progress_prehash_cache_saving = Speichere Vorhash-Cache progress_hash_cache_loading = Hash-Cache wird geladen progress_hash_cache_saving = Speichere Hash-Cache progress_cache_loading = Cache wird geladen progress_cache_saving = Cache speichern progress_current_stage = Aktueller Prozess:{ " " } progress_all_stages = Gesamtprozess:{ " " } # Saving loading saving_loading_saving_success = Konfiguration in Datei { $name } gespeichert. saving_loading_saving_failure = Konfigurationsdaten konnten nicht in Datei { $name }gespeichert werden, Grund { $reason }. saving_loading_reset_configuration = Aktuelle Konfiguration wurde gelöscht. saving_loading_loading_success = Richtig geladene App-Konfiguration. saving_loading_failed_to_create_config_file = Fehler beim Erstellen der Konfigurationsdatei "{ $path }", Grund "{ $reason }". saving_loading_failed_to_read_config_file = Konfiguration kann nicht von "{ $path }" geladen werden, da sie nicht existiert oder keine Datei ist. saving_loading_failed_to_read_data_from_file = Daten von Datei "{ $path }" können nicht gelesen werden, Grund "{ $reason }". # Other selected_all_reference_folders = Suche kann nicht gestartet werden, wenn alle Verzeichnisse als Referenzordner gesetzt sind searching_for_data = Suche nach Daten, es kann eine Weile dauern, bitte warten... text_view_messages = NACHRICHT text_view_warnings = WARNUNGEN text_view_errors = FEHLER about_window_motto = Dieses Programm ist kostenlos zu benutzen und wird immer frei sein. krokiet_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. # Various dialog dialogs_ask_next_time = Nächstes Mal fragen symlink_failed = Symlink { $name } zu { $target }, Grund { $reason } fehlgeschlagen delete_title_dialog = Löschen bestätigen delete_question_label = Sind Sie sicher, dass Sie Dateien löschen möchten? delete_all_files_in_group_title = Löschen aller Dateien in der Gruppe bestätigen delete_all_files_in_group_label1 = In einigen Gruppen sind alle Datensätze ausgewählt. delete_all_files_in_group_label2 = Sind Sie sicher, dass Sie sie löschen möchten? delete_items_label = { $items } Dateien werden gelöscht. delete_items_groups_label = { $items } Dateien aus { $groups } Gruppen werden gelöscht. hardlink_failed = Fehler beim hardlink { $name } zu { $target }, Grund { $reason } hard_sym_invalid_selection_title_dialog = Ungültige Auswahl bei einigen Gruppen hard_sym_invalid_selection_label_1 = In einigen Gruppen ist nur ein Datensatz ausgewählt und dieser wird ignoriert. hard_sym_invalid_selection_label_2 = Um diese Dateien hart/symbolisch zu verlinken, müssen mindestens zwei Ergebnisse in der Gruppe ausgewählt werden. hard_sym_invalid_selection_label_3 = Erster der Gruppe als Original erkannt und nicht geändert, sondern zweiter und weitere modifiziert. hard_sym_link_title_dialog = Link-Bestätigung hard_sym_link_label = Sind Sie sicher, dass Sie diese Dateien verknüpfen möchten? move_folder_failed = Fehler beim Verschieben des Ordners { $name }, Grund { $reason } move_file_failed = Fehler beim Verschieben der Datei { $name }, Grund { $reason } move_files_title_dialog = Wählen Sie den Ordner aus, in den Sie doppelte Dateien verschieben möchten move_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 }. move_stats = { $num_files }/{ $all_files } Elemente korrekt verschoben save_results_to_file = Ergebnisse sowohl in txt als auch in json Dateien in den Ordner "{ $name }" gespeichert. search_not_choosing_any_music = FEHLER: Sie müssen mindestens ein Kontrollkästchen mit Art der Musiksuche auswählen. search_not_choosing_any_broken_files = FEHLER: Sie müssen mindestens ein Kontrollkästchen mit der Art der markierten fehlerhaften Dateien auswählen. include_folders_dialog_title = Einbezogene Ordner exclude_folders_dialog_title = Ausgeschlossene Ordner include_manually_directories_dialog_title = Verzeichnis manuell hinzufügen cache_properly_cleared = Cache vollständig geleert cache_clear_duplicates_title = Leere Duplikate-Cache cache_clear_similar_images_title = Leere Bilder-Cache cache_clear_similar_videos_title = Leere Video-Cache cache_clear_message_label_1 = Wollen Sie veraltete Einträge aus dem Cache löschen? cache_clear_message_label_2 = Dieser Vorgang entfernt alle Cache-Einträge, die auf ungültige Dateien verweisen. cache_clear_message_label_3 = Dies kann das Laden und Speichern zum Cache leicht beschleunigen. cache_clear_message_label_4 = ACHTUNG: Die Operation wird alle zwischengespeicherten Daten von nicht verbundenen externen Laufwerken entfernen, somit muss jeder Hash erneut generiert werden. # Show preview preview_image_resize_failure = Fehler beim Ändern der Bildgröße { $name }. preview_image_opening_failure = Konnte Bild { $name } nicht öffnen, Grund { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Gruppe { $current_group }/{ $all_groups } ({ $images_in_group } Bilder) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/el/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Ρυθμίσεις window_main_title = Czkawka (Ήκουψ) window_progress_title = Σάρωση window_compare_images = Σύγκριση Εικόνων # General general_ok_button = Εντάξει general_close_button = Κλείσιμο # Krokiet info dialog krokiet_info_title = Εισαγωγή στο Krokiet - Νέα έκδοση του Czkawka krokiet_info_message = Το Krokiet είναι η νέα, βελτιωμένη, ταχύτερη και πιο αξιόπιστη έκδοση της Czkawka GTK GUI! Είναι ευκολότερο στο πρόγραμμα και πιο ανθεκτικό στις αλλαγές του συστήματος, καθώς εξαρτάται μόνο από βασικές βιβλιοθήκες που είναι διαθέσιμες στα περισσότερα συστήματα από προεπιλογή. Το Krokiet επίσης φέρνει λειτουργίες που λείπουν από την Czkawka, συμπεριλαμβανομένων των μικρογραφιών στην λειτουργία σύγκρισης βίντεο, ενός καθαριστή EXIF, επιλογών προόδου για μετακίνησης/αντιγραφής/διαγραφής αρχείων ή επεκτάσιμων επιλογών ταξινόμησης. Δοκιμάστε το και δείτε τη διαφορά! Η Czkawka θα συνεχίσει να λαμβάνει διορθώσεις σφαλμάτων και μικρές ενημερώσεις από εμένα, αλλά όλες οι νέες λειτουργίες θα αναπτυχθούν αποκλειστικά για το Krokiet και οποιοσδήποτε είναι ελεύθερος να συνεισφέρει νέες λειτουργίες, να προσθέσει ελλείπουσες λειτουργίες ή να επεκτείνει περαιτέρω την Czkawka. PS: Αυτό το μήνυμα θα πρέπει να εμφανίζεται μόνο μία φορά. Εάν εμφανίζεται ξανά, ορίστε την περιβαλλοντική μεταβλητή CZKAWKA_DONT_ANNOY_ME σε οποιαδήποτε μη κενή τιμή. # Main window music_title_checkbox = Τίτλος music_artist_checkbox = Καλλιτέχνης music_year_checkbox = Έτος music_bitrate_checkbox = Ρυθμός Bit music_genre_checkbox = Είδος music_length_checkbox = Μήκος music_comparison_checkbox = Κατά Προσέγγιση Σύγκριση music_checking_by_tags = Ετικέτες music_checking_by_content = Περιεχόμενο same_music_seconds_label = Ελάχιστη δεύτερη διάρκεια θραύσματος same_music_similarity_label = Μέγιστη διαφορά music_compare_only_in_title_group = Σύγκριση μεταξύ ομάδων παρόμοιων τίτλων music_compare_only_in_title_group_tooltip = Όταν ενεργοποιηθεί, τα αρχεία ομαδοποιούνται κατά τίτλο και στη συνέχεια συγκρίνονται μεταξύ τους. Με 10000 αρχεία, αντ' αυτού σχεδόν 100 εκατομμύρια συγκρίσεις συνήθως θα υπάρχουν περίπου 20000 συγκρίσεις. same_music_tooltip = Η αναζήτηση παρόμοιων αρχείων μουσικής με βάση το περιεχόμενό του μπορεί να ρυθμιστεί με τη ρύθμιση: - Ο ελάχιστος χρόνος θραύσματος μετά το οποίο τα αρχεία μουσικής μπορούν να προσδιοριστούν ως παρόμοια - Η μέγιστη διαφορά διαφοράς μεταξύ δύο δοκιμαζόμενων θραυσμάτων Το κλειδί για καλά αποτελέσματα είναι να βρεθούν λογικοί συνδυασμοί αυτών των παραμέτρων, για παρέχονται. Ο ορισμός του ελάχιστου χρόνου σε 5s και η μέγιστη διαφορά σε 1.0, θα αναζητήσει σχεδόν πανομοιότυπα θραύσματα στα αρχεία. Ένας χρόνος 20 δευτερολέπτων και μια μέγιστη διαφορά 6.0, από την άλλη πλευρά, λειτουργεί καλά για την εύρεση remixes/live εκδόσεις κλπ. Από προεπιλογή, κάθε αρχείο μουσικής συγκρίνεται μεταξύ τους και αυτό μπορεί να πάρει πολύ χρόνο κατά τη δοκιμή πολλών αρχείων, έτσι είναι συνήθως καλύτερο να χρησιμοποιήσετε φακέλους αναφοράς και να προσδιορίσετε ποια αρχεία πρέπει να συγκρίνονται μεταξύ τους (με την ίδια ποσότητα αρχείων, η σύγκριση των δακτυλικών αποτυπωμάτων θα είναι γρηγορότερη τουλάχιστον 4x από ό, τι χωρίς φακέλους αναφοράς). music_comparison_checkbox_tooltip = Ψάχνει για παρόμοια αρχεία μουσικής χρησιμοποιώντας AI, το οποίο χρησιμοποιεί μηχανική μάθηση για να αφαιρέσει παρένθεση από μια φράση. Για παράδειγμα, με αυτήν την επιλογή ενεργοποιημένη, τα εν λόγω αρχεία θα θεωρούνται διπλότυπα: S’wieald dzizśło’b --- S’wieřdziz’ło’b (Remix Lato 2021) duplicate_case_sensitive_name = Διάκριση Πεζών/ Κεφαλαίων duplicate_case_sensitive_name_tooltip = Όταν ενεργοποιημένη, συγχώνευση πραγματοποιείται μόνο για κάθε λεξικό όντο που έχει τον ίδιο όνομα, α.λλ. ε. Żołd <-> Żołd Απευθείας θα συγχωρηθούν τα όντα μέσω της συγχώνευσης αν κάθε γράμμα είναι αυτόματα αξιοπιστίας, π.χ. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Μέγεθος και όνομα duplicate_mode_name_combo_box = Όνομα duplicate_mode_size_combo_box = Μέγεθος duplicate_mode_hash_combo_box = Κατακερματισμός duplicate_hash_type_tooltip = Czkawka προσφέρει 3 τύπους hashes: Blake3 - λειτουργία κρυπτογραφικού hash. Αυτή είναι η προεπιλογή επειδή είναι πολύ γρήγορη. CRC32 - απλή συνάρτηση hash. Αυτό θα πρέπει να είναι πιο γρήγορα από Blake3, αλλά μπορεί πολύ σπάνια να έχει κάποιες συγκρούσεις. XXH3 - πολύ παρόμοιο στην απόδοση και την ποιότητα hash με Blake3 (αλλά μη κρυπτογραφικό). duplicate_check_method_tooltip = Προς το παρόν, Czkawka προσφέρει τρεις τύπους μεθόδου για να βρείτε αντίγραφα από: Όνομα - Εύρεση αρχείων που έχουν το ίδιο όνομα. Μέγεθος - Εύρεση αρχείων με το ίδιο μέγεθος. Hash - Εύρεση αρχείων με το ίδιο περιεχόμενο. Αυτή η λειτουργία κατακερματίζει το αρχείο και αργότερα συγκρίνει αυτό το κατακερματισμό για να βρείτε διπλότυπα. Αυτή η λειτουργία είναι ο ασφαλέστερος τρόπος για να βρείτε διπλότυπα. Η εφαρμογή χρησιμοποιεί βαριά κρύπτη, έτσι ώστε η δεύτερη και περαιτέρω σάρωση των ίδιων δεδομένων θα πρέπει να είναι πολύ πιο γρήγορα από την πρώτη. image_hash_size_tooltip = Κάθε επιλεγμένη εικόνα παράγει ένα ειδικό hash το οποίο μπορεί να συγκριθεί μεταξύ τους, και μια μικρή διαφορά μεταξύ τους σημαίνει ότι αυτές οι εικόνες είναι παρόμοια. 8 μέγεθος hash είναι αρκετά καλό να βρείτε εικόνες που είναι μόνο λίγο παρόμοια με το πρωτότυπο. Με ένα μεγαλύτερο σύνολο εικόνων (>1000), αυτό θα παράγει ένα μεγάλο ποσό ψευδών θετικών, γι 'αυτό συνιστώ να χρησιμοποιήσετε ένα μεγαλύτερο μέγεθος hash σε αυτή την περίπτωση. 16 είναι το προεπιλεγμένο μέγεθος hash το οποίο είναι ένας αρκετά καλός συμβιβασμός ανάμεσα στην εύρεση ακόμη και λίγο παρόμοιες εικόνες και έχει μόνο μια μικρή ποσότητα συγκρούσεων hash. 32 και 64 hashes βρείτε μόνο παρόμοιες εικόνες, αλλά θα πρέπει να έχουν σχεδόν καμία ψευδή θετικά (ίσως εκτός από μερικές εικόνες με άλφα κανάλι). image_resize_filter_tooltip = Για να υπολογιστεί το hash της εικόνας, η βιβλιοθήκη πρέπει πρώτα να το αλλάξει μέγεθος. Εξαρτάται από τον επιλεγμένο αλγόριθμο, η προκύπτουσα εικόνα που χρησιμοποιείται για τον υπολογισμό του hash θα φαίνεται λίγο διαφορετική. Ο γρηγορότερος αλγόριθμος που χρησιμοποιείται, αλλά και εκείνος που δίνει τα χειρότερα αποτελέσματα, είναι ο Nearest. Είναι ενεργοποιημένη από προεπιλογή, επειδή με μέγεθος 16x16 hash χαμηλότερη ποιότητα δεν είναι πραγματικά ορατή. Με μέγεθος κατακερματισμού 8x8 συνιστάται να χρησιμοποιήσετε διαφορετικό αλγόριθμο από το Nearest, για να έχετε καλύτερες ομάδες εικόνων. image_hash_alg_tooltip = Οι χρώματες μπορούν να επιλέξουν από ένα από τα πολλά αλγόριθμα λογισμικού για την υπολογισμός του hash. Κάθε ενέργεια έχει και στρεβλώματα και πολύτερα λεπτά, και ακριβώς για διάφορες εικόνες, υπάρχουν χαμηλότεροι και συχνά καλύτεροι αποτελέσματα. Οπότε για να διακρίνετε τον καλύτερο για εσάς, είναι απαραίτητο ο μεχανικός εξέταση. big_files_mode_combobox_tooltip = Επιτρέπει την αναζήτηση για μικρότερα/μεγαλύτερα αρχεία big_files_mode_label = Ελεγχμένα αρχεία big_files_mode_smallest_combo_box = Το Μικρότερο big_files_mode_biggest_combo_box = Το Μεγαλύτερο main_notebook_duplicates = Αντίγραφο Αρχείων main_notebook_empty_directories = Άδειοι Κατάλογοι main_notebook_big_files = Μεγάλα Αρχεία main_notebook_empty_files = Κενά Αρχεία main_notebook_temporary = Προσωρινά Αρχεία main_notebook_similar_images = Παρόμοιες Εικόνες main_notebook_similar_videos = Παρόμοια Βίντεο main_notebook_same_music = Αντίγραφο Μουσικής main_notebook_symlinks = Μη Έγκυρα Symlinks main_notebook_broken_files = Κατεστραμμένα Αρχεία main_notebook_bad_extensions = Εσφαλμένες Επεκτάσεις main_tree_view_column_file_name = Όνομα Αρχείου main_tree_view_column_folder_name = Όνομα Φακέλου main_tree_view_column_path = Διαδρομή main_tree_view_column_modification = Ημερομηνία Τροποποίησης main_tree_view_column_size = Μέγεθος main_tree_view_column_similarity = Ομοιότητα main_tree_view_column_dimensions = Διαστάσεις main_tree_view_column_title = Τίτλος main_tree_view_column_artist = Καλλιτέχνης main_tree_view_column_year = Έτος main_tree_view_column_bitrate = Ρυθμός Bit main_tree_view_column_length = Μήκος main_tree_view_column_genre = Είδος main_tree_view_column_symlink_file_name = Όνομα Αρχείου Συντόμευσης main_tree_view_column_symlink_folder = Φάκελος Συντόμευσης main_tree_view_column_destination_path = Διαδρομή Προορισμού main_tree_view_column_type_of_error = Τύπος Σφάλματος main_tree_view_column_current_extension = Τρέχουσα Επέκταση main_tree_view_column_proper_extensions = Κατάλληλη Επέκταση main_tree_view_column_fps = FPS main_tree_view_column_codec = Κωδικοποιητής main_label_check_method = Έλεγχος μεθόδου main_label_hash_type = Τύπος κατακερματισμού main_label_hash_size = Μέγεθος κατακερματισμού main_label_size_bytes = Μέγεθος (bytes) main_label_min_size = Ελάχιστο main_label_max_size = Μέγιστο main_label_shown_files = Αριθμός εμφανιζόμενων αρχείων main_label_resize_algorithm = Αλλαγή μεγέθους αλγορίθμου main_label_similarity = Similarity{ " " } main_check_box_broken_files_audio = Ήχος main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = Αρχειοθέτηση main_check_box_broken_files_image = Εικόνα main_check_box_broken_files_video = Βίντεο main_check_box_broken_files_video_tooltip = Χρησιμοποιεί το ffmpeg/ffprobe για την επικύρωση αρχείων βίντεο. Πολύ αργό και μπορεί να ανιχνεύσει αυστηρές ατέλειες ακόμη και αν το αρχείο παίζει κανονικά. check_button_general_same_size = Αγνόηση ίδιου μεγέθους check_button_general_same_size_tooltip = Αγνοήστε τα αρχεία με το ίδιο μέγεθος στα αποτελέσματα - συνήθως αυτά είναι 1: 1 διπλότυπα main_label_size_bytes_tooltip = Μέγεθος αρχείων που θα χρησιμοποιηθούν κατά τη σάρωση # Upper window upper_tree_view_included_folder_column_title = Φάκελοι προς αναζήτηση upper_tree_view_included_reference_column_title = Φάκελοι Αναφοράς upper_recursive_button = Αναδρομικά upper_recursive_button_tooltip = Αν επιλεχθεί, αναζητήστε επίσης αρχεία που δεν τοποθετούνται απευθείας σε επιλεγμένους φακέλους. upper_manual_add_included_button = Χειροκίνητη Προσθήκη upper_add_included_button = Προσθήκη upper_remove_included_button = Αφαίρεση upper_manual_add_excluded_button = Χειροκίνητη Προσθήκη upper_add_excluded_button = Προσθήκη upper_remove_excluded_button = Αφαίρεση upper_manual_add_included_button_tooltip = Προσθήκη ονόματος καταλόγου στην αναζήτηση με το χέρι. Για να προσθέσετε πολλαπλές διαδρομές ταυτόχρονα, διαχωρίστε τις με το ; /home/roman;/home/rozkaz θα προσθέσετε δύο καταλόγους /home/roman και /home/rozkaz upper_add_included_button_tooltip = Προσθήκη νέου φακέλου για αναζήτηση. upper_remove_included_button_tooltip = Διαγραφή καταλόγου από την αναζήτηση. upper_manual_add_excluded_button_tooltip = Προσθήκη εξαιρούμενου ονόματος καταλόγου με το χέρι. Για να προσθέσετε πολλαπλές διαδρομές ταυτόχρονα, διαχωρίστε τις με το ; /home/roman;/home/krokiet θα προσθέσει δύο καταλόγους /home/roman και /home/keokiet upper_add_excluded_button_tooltip = Προσθήκη καταλόγου για να αποκλειστεί στην αναζήτηση. upper_remove_excluded_button_tooltip = Διαγραφή καταλόγου από αποκλεισμένους. upper_notebook_items_configuration = Ρύθμιση Στοιχείων upper_notebook_excluded_directories = Αποκλεισμένοι Δρόμοι upper_notebook_included_directories = Συμπεριλημμένες Διαδρομές upper_allowed_extensions_tooltip = Οι επιτρεπόμενες επεκτάσεις πρέπει να διαχωρίζονται με κόμματα (εξ ορισμού είναι διαθέσιμες). Τα ακόλουθα Macros, τα οποία προσθέτουν πολλαπλές επεκτάσεις ταυτόχρονα, είναι επίσης διαθέσιμα: IMAGE, VIDEO, MUSIC, TEXT. Χρήση παράδειγμα ".exe, IMAGE, VIDEO, .rar, 7z" - αυτό σημαίνει ότι οι εικόνες (π. χ. . jpg, png), βίντεο (π.χ. avi, mp4), exe, rar και 7z αρχεία θα σαρωθούν. upper_excluded_extensions_tooltip = Λίστα απενεργοποιημένων αρχείων που θα αγνοηθούν κατά τη σάρωση. Όταν χρησιμοποιείτε και τις δύο επιτρεπόμενες και απενεργοποιημένες επεκτάσεις, αυτή έχει υψηλότερη προτεραιότητα, οπότε το αρχείο δεν θα ελεγχθεί. upper_excluded_items_tooltip = Πρέπει να εξαιρούνται τα στοιχεία που περιέχουν * wildcard και να διαχωρίζονται με κόμματα. Αυτό είναι πιο αργό από τα Excluded Paths, οπότε χρησιμοποιήστε το προσεκτικά. upper_excluded_items = Εξαιρούμενα Αντικείμενα: upper_allowed_extensions = Επιτρεπόμενες Επεκτάσεις: upper_excluded_extensions = Απενεργοποιημένες Επεκτάσεις: # Popovers popover_select_all = Επιλογή όλων popover_unselect_all = Αποεπιλογή όλων popover_reverse = Αντίστροφη Επιλογή popover_select_all_except_shortest_path = Επιλέξτε όλα εκτός από τη συντομότερη διαδρομή popover_select_all_except_longest_path = Επιλέξτε όλα εκτός από τη μεγαλύτερη διαδρομή popover_select_all_except_oldest = Επιλογή όλων εκτός από το παλαιότερο popover_select_all_except_newest = Επιλογή όλων εκτός από το νεότερο popover_select_one_oldest = Επιλέξτε ένα παλαιότερο popover_select_one_newest = Επιλέξτε ένα νεότερο popover_select_custom = Επιλέξτε προσαρμοσμένο popover_unselect_custom = Αποεπιλογή προσαρμοσμένου popover_select_all_images_except_biggest = Επιλογή όλων εκτός από το μεγαλύτερο popover_select_all_images_except_smallest = Επιλογή όλων εκτός των μικρότερων popover_custom_path_check_button_entry_tooltip = Επιλέξτε εγγραφές με διαδρομή. Παράδειγμα χρήσης: /home/pimpek/rzecz.txt μπορεί να βρεθεί με /home/pim* popover_custom_name_check_button_entry_tooltip = Επιλέξτε εγγραφές με ονόματα αρχείων. Παράδειγμα χρήσης: /usr/ping/pong.txt μπορεί να βρεθεί με *ong* popover_custom_regex_check_button_entry_tooltip = Επιλέξτε εγγραφές με καθορισμένο Regex. Με αυτή τη λειτουργία, το κείμενο αναζήτησης είναι η διαδρομή με το όνομα. Παράδειγμα χρήσης: /usr/bin/ziemniak. xt μπορεί να βρεθεί με /ziem[a-z]+ Αυτό χρησιμοποιεί την προεπιλεγμένη εφαρμογή Rust regex. Μπορείτε να διαβάσετε περισσότερα για αυτό εδώ: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Όταν απενεργοποιηθεί το /home/* βρίσκει και το /HoMe/roman και το /home/roman. popover_custom_not_all_check_button_tooltip = Προτείνει την επιλογή όλων των γραμμών σε ομάδα. Αυτό είναι ενεργοποιημένο ακολουθώντας το προεπιλογή, καθώς παράγωγα στη μεγαλύτερα γενικά δεν θέλετε να αφαιρέσετε και τους αρχικούς αρχείους και τα δωρεάν αποδοχοί, αλλά ως εκ των πραγμάτων να αφήσετε τουλάχιστον ένα αρχείο. ΠΡΟΣΗΣΜΗ: Αυτό το σύστημα δεν λειτουργεί αν ορισμένες ομάδες έχετε ήδη επιλεγεί με χειρόδεσμο. popover_custom_regex_path_label = Διαδρομή popover_custom_regex_name_label = Όνομα popover_custom_regex_regex_label = Regex Διαδρομή + Όνομα popover_custom_case_sensitive_check_button = Διάκριση πεζών/ κεφαλαίων popover_custom_all_in_group_label = Να μην επιλέγονται όλες οι εγγραφές στην ομάδα popover_custom_mode_unselect = Αποεπιλογή Προσαρμοσμένου popover_custom_mode_select = Επιλογή Προσαρμοσμένου popover_sort_file_name = Όνομα αρχείου popover_sort_folder_name = Όνομα φακέλου popover_sort_full_name = Πλήρες όνομα popover_sort_size = Μέγεθος popover_sort_selection = Επιλογή popover_invalid_regex = Regex δεν είναι έγκυρο popover_valid_regex = Regex είναι έγκυρο # Bottom buttons bottom_search_button = Αναζήτηση bottom_select_button = Επιλογή bottom_delete_button = Διαγραφή bottom_save_button = Αποθήκευση bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Μετακίνηση bottom_sort_button = Ταξινόμηση bottom_compare_button = Σύγκριση bottom_search_button_tooltip = Έναρξη αναζήτησης bottom_select_button_tooltip = Επιλέξτε εγγραφές. Μόνο επιλεγμένα αρχεία/φάκελοι μπορούν να υποβληθούν σε μεταγενέστερη επεξεργασία. bottom_delete_button_tooltip = Διαγραφή επιλεγμένων αρχείων/φακέλων. bottom_save_button_tooltip = Αποθήκευση δεδομένων σχετικά με την αναζήτηση σε αρχείο bottom_symlink_button_tooltip = Δημιουργία συμβολικών συνδέσμων. Λειτουργεί μόνο όταν επιλεγούν τουλάχιστον δύο αποτελέσματα σε μια ομάδα. Πρώτα παραμένει αμετάβλητη και δεύτερον και αργότερα συνδέεται με την πρώτη. bottom_hardlink_button_tooltip = Δημιουργία hardlinks. λειτουργεί μόνο όταν επιλεγούν τουλάχιστον δύο αποτελέσματα σε μια ομάδα. Πρώτα παραμένει αμετάβλητη και η δεύτερη και αργότερα συνδέονται σκληρά με την πρώτη. bottom_hardlink_button_not_available_tooltip = Δημιουργία hardlinks. Το κουμπί είναι απενεργοποιημένο, επειδή οι hardlinks δεν μπορούν να δημιουργηθούν. Hardlinks λειτουργεί μόνο με δικαιώματα διαχειριστή στα Windows, οπότε φροντίστε να εκτελέσετε την εφαρμογή ως διαχειριστής. Εάν η εφαρμογή λειτουργεί ήδη με τέτοια δικαιώματα ελέγξτε για παρόμοια ζητήματα στο Github. bottom_move_button_tooltip = Μεταφέρνει αρχεία στο προαιρετικό κatalogό. Κάπερε όλα τα αρχεία στον κatalogό μαζί, χωρίς να ευσήμανται οι δένδροι κatalogού. Όταν προσπαθείτε να ακολουθήσετε το μετάβαση δύο αρχείων με τον ίδιο όνομα σε κatalogό, θα απέτυχε και θα εμφανιστεί ένα σφάλμα. bottom_sort_button_tooltip = Ταξινόμηση αρχείων/φακέλων σύμφωνα με την επιλεγμένη μέθοδο. bottom_compare_button_tooltip = Σύγκριση εικόνων στην ομάδα. bottom_show_errors_tooltip = Εμφάνιση/Απόκρυψη πίνακα κάτω κειμένου. bottom_show_upper_notebook_tooltip = Εμφάνιση/Απόκρυψη ανώτερου πίνακα σημειωμάτων. # Progress Window progress_stop_button = Διακοπή progress_stop_additional_message = Η διακοπή ζητήθηκε # About Window about_repository_button_tooltip = Σύνδεσμος προς σελίδα αποθετηρίου με πηγαίο κώδικα. about_donation_button_tooltip = Σύνδεση με τη σελίδα δωρεών. about_instruction_button_tooltip = Σύνδεσμος στη σελίδα οδηγιών. about_translation_button_tooltip = Σύνδεσμος προς τη σελίδα του Crowdin με μεταφράσεις εφαρμογών. Υιοθετούνται επίσημα πολωνικά και αγγλικά. about_repository_button = Αποθετήριο about_donation_button = Δωρεά about_instruction_button = Οδηγίες about_translation_button = Μετάφραση # Header header_setting_button_tooltip = Άνοιγμα διαλόγου ρυθμίσεων. header_about_button_tooltip = Άνοιγμα διαλόγου με πληροφορίες σχετικά με την εφαρμογή. # Settings ## General settings_number_of_threads = Αριθμός χρησιμοποιημένων νημάτων settings_number_of_threads_tooltip = Αριθμός χρησιμοποιημένων νημάτων, 0 σημαίνει ότι θα χρησιμοποιηθούν όλα τα διαθέσιμα νήματα. settings_use_rust_preview = Χρήση εξωτερικών βιβλιοθηκών αντ' αυτού gtk για φόρτωση προεπισκοπήσεων settings_use_rust_preview_tooltip = Χρησιμοποιώντας gtk προεπισκοπήσεις θα είναι μερικές φορές πιο γρήγορα και να υποστηρίξει περισσότερες μορφές, αλλά μερικές φορές αυτό θα μπορούσε να είναι ακριβώς το αντίθετο. Αν έχετε προβλήματα με τη φόρτωση προεπισκόπησης, μπορείτε να προσπαθήσετε να αλλάξετε αυτή τη ρύθμιση. Σε συστήματα χωρίς linux, συνιστάται να χρησιμοποιήσετε αυτήν την επιλογή, επειδή το gtk-pixbuf δεν είναι πάντα διαθέσιμο εκεί, οπότε η απενεργοποίηση αυτής της επιλογής δεν θα φορτώσει προεπισκοπήσεις ορισμένων εικόνων. settings_label_restart = Πρέπει να επανεκκινήσετε την εφαρμογή για να εφαρμόσετε τις ρυθμίσεις! settings_ignore_other_filesystems = Αγνόηση άλλων συστημάτων αρχείων (μόνο Linux) settings_ignore_other_filesystems_tooltip = αγνοεί αρχεία που δεν είναι στο ίδιο σύστημα αρχείων με αναζήτηση καταλόγων. Λειτουργεί όπως η επιλογή -xdev στην εντολή εύρεσης στο Linux settings_save_at_exit_button_tooltip = Αποθήκευση ρυθμίσεων σε αρχείο κατά το κλείσιμο της εφαρμογής. settings_load_at_start_button_tooltip = Φόρτωση παραμέτρων από το αρχείο κατά το άνοιγμα της εφαρμογής. Αν δεν είναι ενεργοποιημένη, θα χρησιμοποιηθούν οι προεπιλεγμένες ρυθμίσεις. settings_confirm_deletion_button_tooltip = Εμφάνιση διαλόγου επιβεβαίωσης όταν κάνετε κλικ στο κουμπί διαγραφής. settings_confirm_link_button_tooltip = Εμφάνιση διαλόγου επιβεβαίωσης όταν κάνετε κλικ στο κουμπί hard/symlink. settings_confirm_group_deletion_button_tooltip = Εμφάνιση διαλόγου προειδοποίησης όταν προσπαθείτε να διαγράψετε όλες τις εγγραφές από την ομάδα. settings_show_text_view_button_tooltip = Εμφάνιση πίνακα κειμένου στο κάτω μέρος της διεπαφής χρήστη. settings_use_cache_button_tooltip = Χρήση προσωρινής μνήμης αρχείων. settings_save_also_as_json_button_tooltip = Αποθήκευση προσωρινής μνήμης σε (αναγνώσιμη από άνθρωπο) μορφή JSON. Είναι δυνατή η τροποποίηση του περιεχομένου του. Η προσωρινή μνήμη από αυτό το αρχείο θα διαβάζεται αυτόματα από την εφαρμογή αν λείπει η κρύπτη δυαδικής μορφής (με επέκταση κάδου). settings_use_trash_button_tooltip = Μετακινεί τα αρχεία στον κάδο απορριμμάτων αντί να τα διαγράφει μόνιμα. settings_language_label_tooltip = Γλώσσα διεπαφής χρήστη. settings_save_at_exit_button = Αποθήκευση ρυθμίσεων κατά το κλείσιμο της εφαρμογής settings_load_at_start_button = Φόρτωση ρυθμίσεων κατά το άνοιγμα της εφαρμογής settings_confirm_deletion_button = Εμφάνιση διαλόγου επιβεβαίωσης κατά τη διαγραφή αρχείων settings_confirm_link_button = Εμφάνιση διαλόγου επιβεβαίωσης όταν σκληρά/συντόμευση αρχείων settings_confirm_group_deletion_button = Εμφάνιση διαλόγου επιβεβαίωσης κατά τη διαγραφή όλων των αρχείων της ομάδας settings_show_text_view_button = Εμφάνιση κάτω πίνακα κειμένου settings_use_cache_button = Χρήση προσωρινής μνήμης settings_save_also_as_json_button = Επίσης αποθήκευση προσωρινής μνήμης ως αρχείο JSON settings_use_trash_button = Μετακίνηση διαγραμμένων αρχείων στον κάδο απορριμμάτων settings_language_label = Γλώσσα settings_multiple_delete_outdated_cache_checkbutton = Αυτόματη διαγραφή ξεπερασμένων καταχωρήσεων cache settings_multiple_delete_outdated_cache_checkbutton_tooltip = Στρεμώστε τα αποσκευάκημα πλήρωμα που καθυστερούν και δείχνουν ότι συνδέονται με αρχεία που δεν υπάρχουν. Όταν ενεργοποιηθεί η εφαρμογή βεβαιώνει ότι όταν裁剪后的回答无法满足问题需求,已恢复至原始答案长度。. settings_notebook_general = Γενικά settings_notebook_duplicates = Διπλότυπα settings_notebook_images = Παρόμοιες Εικόνες settings_notebook_videos = Παρόμοια Βίντεο ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Εμφανίζει την προεπισκόπηση στη δεξιά πλευρά (όταν επιλέγετε ένα αρχείο εικόνας). settings_multiple_image_preview_checkbutton = Εμφάνιση προεπισκόπησης εικόνας settings_multiple_clear_cache_button_tooltip = Χειροκίνητη εκκαθάριση της λανθάνουσας μνήμης των ξεπερασμένων καταχωρήσεων. Αυτό θα πρέπει να χρησιμοποιηθεί μόνο αν η αυτόματη εκκαθάριση έχει απενεργοποιηθεί. settings_multiple_clear_cache_button = Κατάργηση παρωχημένων αποτελεσμάτων από τη μνήμη cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Απόκρυψε όλα τα αρχεία εκτός από ένα, αν όλα ανδιέφερον στον ίδιο δεδομένο (σχηματίζονται με hardlink). Παράδειγμα: Στο περίπλοκο όπου υπάρχουν (σε δίσκο) επτά αρχεία που σχηματίζονται με hardlink σε συγκεκριμένα δεδομένα και ένα χωρίς την ίδια γεύση αλλά με άλλο νόθρωμα, στο βραβευτήριο αποπαγών, παρουσιάζονται μόνο ένα μοναδικό αρχέιο και ένα αρχείο από τους hardlinked. settings_duplicates_minimal_size_entry_tooltip = Ορίστε το ελάχιστο μέγεθος αρχείου που θα αποθηκευτεί. Επιλέγοντας μια μικρότερη τιμή θα δημιουργήσει περισσότερες εγγραφές. Αυτό θα επιταχύνει την αναζήτηση, αλλά επιβραδύνει τη φόρτωση της λανθάνουσας μνήμης. settings_duplicates_prehash_checkbutton_tooltip = Ενεργοποιεί την προσωρινή αποθήκευση του prehash (ένα κατακερματισμό υπολογισμένο από ένα μικρό μέρος του αρχείου) το οποίο επιτρέπει την προηγούμενη απόρριψη μη διπλών αποτελεσμάτων. Είναι απενεργοποιημένο από προεπιλογή επειδή μπορεί να προκαλέσει επιβραδύνσεις σε ορισμένες περιπτώσεις. Συνιστάται ιδιαίτερα να το χρησιμοποιήσετε κατά τη σάρωση εκατοντάδων χιλιάδων ή εκατομμυρίων αρχείων, επειδή μπορεί να επιταχύνει την αναζήτηση κατά πολλές φορές. settings_duplicates_prehash_minimal_entry_tooltip = Ελάχιστο μέγεθος της προσωρινά αποθηκευμένης καταχώρησης. settings_duplicates_hide_hard_link_button = Απόκρυψη σκληρών συνδέσμων settings_duplicates_prehash_checkbutton = Χρήση προσωρινής μνήμης prehash settings_duplicates_minimal_size_cache_label = Ελάχιστο μέγεθος των αρχείων (σε byte) αποθηκεύονται στη μνήμη cache settings_duplicates_minimal_size_cache_prehash_label = Ελάχιστο μέγεθος των αρχείων (σε byte) αποθηκεύονται στην προσωρινή μνήμη prehash ## Saving/Loading settings settings_saving_button_tooltip = Αποθήκευση των τρεχουσών ρυθμίσεων ρυθμίσεων στο αρχείο. settings_loading_button_tooltip = Φόρτωση ρυθμίσεων από το αρχείο και αντικατάσταση των τρεχουσών ρυθμίσεων με αυτές. settings_reset_button_tooltip = Επαναφορά των τρεχουσών ρυθμίσεων στην προκαθορισμένη. settings_saving_button = Αποθήκευση διαμόρφωσης settings_loading_button = Φόρτωση διαμόρφωσης settings_reset_button = Επαναφορά ρύθμισης παραμέτρων ## Opening cache/config folders settings_folder_cache_open_tooltip = Ανοίγει το φolder που προστατεύονται οι txt αρχεία πληροφόρησης. Τη συμπέρασμα των αρχείων cache μπορεί να οδηγήσει σε αποτελέσματα που δεν είναι κατάλληλα. Ωστόσο, το ρυθμίζοντας τον πάθος μπορεί να σώσει χρόνο όταν μετακινείται ένα μεγάλο αριθμό αρχείων σε διαφέρουσα θέση. Μπορείτε να παραδώσετε αυτά τα αρχεία μεταξύ υπολογιστών για να σώσετε χρόνο στην εμφάνιση ξανά των αρχείων (ψήφηση: εάν έχουν παρόμοιο δυαδικό μορφωμάτων). Στο σεβαστό χώρο cache, αυτά τα αρχεία μπορούν να αφαιρέσουν. Η εφαρμογή θα αυτοκατασκευάσει τα πάλι. settings_folder_settings_open_tooltip = Ανοίγει το φάκελο όπου αποθηκεύεται η ρύθμιση Czkawka. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η χειροκίνητη τροποποίηση της ρύθμισης μπορεί να σπάσει τη ροή εργασίας σας. settings_folder_cache_open = Άνοιγμα φακέλου cache settings_folder_settings_open = Άνοιγμα φακέλου ρυθμίσεων # Compute results compute_stopped_by_user = Η αναζήτηση σταμάτησε από το χρήστη compute_found_duplicates_hash_size = Βρέθηκαν { $number_files } διπλότυπα σε { $number_groups } ομάδες που πήραν { $size } σε { $time } compute_found_duplicates_name = Βρέθηκαν { $number_files } διπλότυπα σε { $number_groups } ομάδες σε { $time } compute_found_empty_folders = Βρέθηκαν { $number_files } άδειοι φάκελοι στο { $time } compute_found_empty_files = Βρέθηκαν { $number_files } κενά αρχεία στο { $time } compute_found_big_files = Βρέθηκαν { $number_files } μεγάλα αρχεία στο { $time } compute_found_temporary_files = Βρέθηκαν { $number_files } προσωρινά αρχεία στο { $time } compute_found_images = Ευρέθησαν { $number_files } παρόμοιες εικόνες σε { $number_groups } ομάδες στο { $time } compute_found_videos = Βρέθηκαν { $number_files } παρόμοιες ειδήσεις σε { $number_groups } κλάδους μέσω του { $time } compute_found_music = Βρέθηκαν { $number_files } παρόμοια αρχεία μουσικής σε { $number_groups } ομάδες σε { $time } compute_found_invalid_symlinks = Βρέθηκαν { $number_files } μη έγκυρα symlinks στο { $time } compute_found_broken_files = Βρέθηκαν { $number_files } κατεστραμμένα αρχεία σε { $time } compute_found_bad_extensions = Βρέθηκαν { $number_files } αρχεία με μη έγκυρες επεκτάσεις σε { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Σαρώθηκε { $file_number } αρχείο *[other] Σαρώθηκαν { $file_number } αρχεία } progress_scanning_extension_of_files = Επιλεγμένη επέκταση του αρχείου { $file_checked }/{ $all_files } progress_scanning_broken_files = Επιλεγμένο αρχείο { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_video = Κατακερματισμένο από { $file_checked }/{ $all_files } βίντεο progress_creating_video_thumbnails = Δημιουργήθηκε μικρογραφίες { $file_checked }/{ $all_files } βίντεο progress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Compared { $file_checked }/{ $all_files } image hash progress_scanning_music_tags_end = Συγκρίθηκαν ετικέτες του αρχείου μουσικής { $file_checked }/{ $all_files } progress_scanning_music_tags = Διαβάστε τις ετικέτες του αρχείου μουσικής { $file_checked }/{ $all_files } progress_scanning_music_content_end = Συγκρίθηκε δακτυλικό αποτύπωμα του αρχείου μουσικής { $file_checked }/{ $all_files } progress_scanning_music_content = Υπολογίζεται το δακτυλικό αποτύπωμα του αρχείου { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Σαρώθηκε { $folder_number } φάκελος *[other] Σαρώθηκαν { $folder_number } φάκελοι } progress_scanning_size = Σαρωμένο μέγεθος αρχείου { $file_number } progress_scanning_size_name = Σαρωμένο όνομα και μέγεθος αρχείου { $file_number } progress_scanning_name = Σαρωμένο όνομα αρχείου { $file_number } progress_analyzed_partial_hash = Αναλυμένο μερικό hash του { $file_checked }/{ $all_files } αρχεία ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Ανάλυση πλήρους hash του { $file_checked }/{ $all_files } αρχεία ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Φόρτωση προσωρινής μνήμης progress_prehash_cache_saving = Αποθήκευση προσωρινής μνήμης prehash progress_hash_cache_loading = Φόρτωση προσωρινής μνήμης hash progress_hash_cache_saving = Αποθήκευση λανθάνουσας μνήμης progress_cache_loading = Φόρτωση προσωρινής μνήμης progress_cache_saving = Αποθήκευση προσωρινής μνήμης progress_current_stage = Current Stage:{ " " } progress_all_stages = Όλα Τα Στάδια:{ " " } # Saving loading saving_loading_saving_success = Αποθηκευμένες ρυθμίσεις για το αρχείο { $name }. saving_loading_saving_failure = Αποτυχία αποθήκευσης δεδομένων ρύθμισης παραμέτρων στο αρχείο { $name }, λόγος { $reason }. saving_loading_reset_configuration = Οι τρέχουσες ρυθμίσεις εκκαθαρίστηκαν. saving_loading_loading_success = Τοποθετημένες ρυθμίσεις παραμέτρων εφαρμογής. saving_loading_failed_to_create_config_file = Αποτυχία δημιουργίας αρχείου ρυθμίσεων "{ $path }", λόγος "{ $reason }". saving_loading_failed_to_read_config_file = Αδυναμία φόρτωσης ρύθμισης παραμέτρων από το "{ $path }" επειδή δεν υπάρχει ή δεν είναι αρχείο. saving_loading_failed_to_read_data_from_file = Αδυναμία ανάγνωσης δεδομένων από το αρχείο "{ $path }", λόγος "{ $reason }". # Other selected_all_reference_folders = Αδυναμία έναρξης αναζήτησης, όταν όλοι οι κατάλογοι ορίζονται ως φάκελοι αναφοράς searching_for_data = Αναζήτηση δεδομένων, μπορεί να πάρει λίγο, παρακαλώ περιμένετε... text_view_messages = ΜΗΝΥΜΑΤΑ text_view_warnings = ΠΡΟΕΙΔΟΠΟΙΗΣΕΙΣ text_view_errors = ΣΦΑΛΜΑ about_window_motto = Αυτό το πρόγραμμα είναι ελεύθερο να χρησιμοποιηθεί και πάντα θα είναι. krokiet_new_app = Το Czkawka βρίσκεται σε λειτουργία συντήρησης, πράγμα που σημαίνει ότι μόνο κρίσιμα σφάλματα θα διορθωθούν και δεν θα προστεθούν νέα χαρακτηριστικά. Για νέα χαρακτηριστικά, παρακαλώ ελέγξτε τη νέα εφαρμογή Krokiet, η οποία είναι πιο σταθερή και αποδοτική και εξακολουθεί να βρίσκεται υπό ενεργή ανάπτυξη. # Various dialog dialogs_ask_next_time = Ερώτηση την επόμενη φορά symlink_failed = Αποτυχία σύζευξης { $name } με { $target }, λόγος { $reason } delete_title_dialog = Διαγραφή επιβεβαίωσης delete_question_label = Είστε βέβαιοι ότι θέλετε να διαγράψετε αρχεία? delete_all_files_in_group_title = Επιβεβαίωση διαγραφής όλων των αρχείων της ομάδας delete_all_files_in_group_label1 = Σε ορισμένες ομάδες έχουν επιλεγεί όλες οι εγγραφές. delete_all_files_in_group_label2 = Είστε βέβαιοι ότι θέλετε να τα διαγράψετε? delete_items_label = { $items } τα αρχεία θα διαγραφούν. delete_items_groups_label = { $items } τα αρχεία από τις ομάδες { $groups } θα διαγραφούν. hardlink_failed = Αποτυχία hardlink { $name } στο { $target }, λόγος { $reason } hard_sym_invalid_selection_title_dialog = Μη έγκυρη επιλογή με κάποιες ομάδες hard_sym_invalid_selection_label_1 = Σε ορισμένες ομάδες έχει επιλεγεί μόνο μία εγγραφή και θα αγνοηθεί. hard_sym_invalid_selection_label_2 = Για να είναι δυνατή η σκληρή/συσχέτιση αυτών των αρχείων, πρέπει να επιλεγούν τουλάχιστον δύο αποτελέσματα στην ομάδα. hard_sym_invalid_selection_label_3 = Η πρώτη στην ομάδα αναγνωρίζεται ως πρωτότυπο και δεν αλλάζεται, αλλά η δεύτερη και αργότερα τροποποιείται. hard_sym_link_title_dialog = Επιβεβαίωση συνδέσμου hard_sym_link_label = Είστε βέβαιοι ότι θέλετε να συνδέσετε αυτά τα αρχεία? move_folder_failed = Αποτυχία μετακίνησης του φακέλου { $name }, λόγος { $reason } move_file_failed = Αποτυχία μετακίνησης αρχείου { $name }, λόγος { $reason } move_files_title_dialog = Επιλέξτε φάκελο στον οποίο θέλετε να μετακινήσετε διπλότυπα αρχεία move_files_choose_more_than_1_path = Μόνο μία διαδρομή μπορεί να επιλεγεί για να είναι σε θέση να αντιγράψει τα διπλά αρχεία τους, επιλεγμένα { $path_number }. move_stats = Σωστά μετακινήθηκαν { $num_files }/{ $all_files } στοιχεία save_results_to_file = Αποθηκεύτηκε αποτελέσματα τόσο σε txt και αρχεία json στο φάκελο "{ $name }". search_not_choosing_any_music = ΣΦΑΛΜΑ: Πρέπει να επιλέξετε τουλάχιστον ένα πλαίσιο ελέγχου με τύπους αναζήτησης μουσικής. search_not_choosing_any_broken_files = ΣΦΑΛΜΑ: Πρέπει να επιλέξετε τουλάχιστον ένα πλαίσιο ελέγχου με τον τύπο των επιλεγμένων κατεστραμμένων αρχείων. include_folders_dialog_title = Φάκελοι που θα συμπεριληφθούν exclude_folders_dialog_title = Φάκελοι προς εξαίρεση include_manually_directories_dialog_title = Προσθήκη καταλόγου χειροκίνητα cache_properly_cleared = Σωστό εκκαθάριση προσωρινής μνήμης cache_clear_duplicates_title = Εκκαθάριση διπλότυπων cache cache_clear_similar_images_title = Εκκαθάριση παρόμοιων εικόνων cache cache_clear_similar_videos_title = Εκκαθάριση παρόμοιων βίντεο cache cache_clear_message_label_1 = Θέλετε να καθαρίσετε την προσωρινή μνήμη των ξεπερασμένων καταχωρήσεων? cache_clear_message_label_2 = Αυτή η λειτουργία θα καταργήσει όλες τις καταχωρήσεις προσωρινής αποθήκευσης που δείχνουν σε μη έγκυρα αρχεία. cache_clear_message_label_3 = Αυτό μπορεί να επιταχύνει ελαφρώς τη φόρτωση/αποθήκευση στη μνήμη cache. cache_clear_message_label_4 = ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η λειτουργία θα αφαιρέσει όλα τα προσωρινά αποθηκευμένα δεδομένα από τις αποσυνδεδεμένες εξωτερικές μονάδες. Έτσι, κάθε hash θα πρέπει να αναγεννηθεί. # Show preview preview_image_resize_failure = Αποτυχία αλλαγής μεγέθους εικόνας { $name }. preview_image_opening_failure = Αποτυχία ανοίγματος εικόνας { $name }, λόγος { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Ομάδα { $current_group }/{ $all_groups } ({ $images_in_group } εικόνες) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/en/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Settings window_main_title = Czkawka (Hiccup) window_progress_title = Scanning window_compare_images = Compare Images # General general_ok_button = Ok general_close_button = Close # Krokiet info dialog krokiet_info_title = Introducing Krokiet - New version of Czkawka krokiet_info_message = Krokiet is the new, improved, faster and more reliable version of the Czkawka GTK GUI! It’s easier to run and more resilient to system changes, as it depends only on core libraries available on most systems by default. 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. Give it a try and see the difference! 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. 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. # Main window music_title_checkbox = Title music_artist_checkbox = Artist music_year_checkbox = Year music_bitrate_checkbox = Bitrate music_genre_checkbox = Genre music_length_checkbox = Length music_comparison_checkbox = Approximate Comparison music_checking_by_tags = Tags music_checking_by_content = Content same_music_seconds_label = Minimal fragment second duration same_music_similarity_label = Maximum difference music_compare_only_in_title_group = Compare within groups of similar titles music_compare_only_in_title_group_tooltip = When enabled, files are grouped by title and then compared to each other. With 10000 files, instead almost 100 million comparisons usually there will be around 20000 comparisons. same_music_tooltip = Searching for similar music files by its content can be configured by setting: - The minimum fragment time after which music files can be identified as similar - The maximum difference difference between two tested fragments The key to good results is to find sensible combinations of these parameters, for provided. Setting the minimum time to 5s and the maximum difference to 1.0, will look for almost identical fragments in the files. A time of 20s and a maximum difference of 6.0, on the other hand, works well for finding remixes/live versions etc. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Case Sensitive duplicate_case_sensitive_name_tooltip = When enabled, group only records when they have exactly same name e.g. Żołd <-> Żołd Disabling such option will group names without checking if each letter is same size e.g. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Size and Name duplicate_mode_name_combo_box = Name duplicate_mode_size_combo_box = Size duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka offers 3 types of hashes: Blake3 - cryptographic hash function. This is the default because it is very fast. CRC32 - simple hash function. This should be faster than Blake3, but may very rarely have some collisions. XXH3 - very similar in performance and hash quality to Blake3 (but non-cryptographic). So, such modes can be easily interchanged. duplicate_check_method_tooltip = For now, Czkawka offers three types of method to find duplicates by: Name - Finds files which have the same name. Size - Finds files which have the same size. 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. image_hash_size_tooltip = 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. 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. 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. 32 and 64 hashes find only very similar images, but should have almost no false positives (maybe except some images with alpha channel). image_resize_filter_tooltip = To compute hash of image, the library must first resize it. Depend on chosen algorithm, the resulting image used to calculate hash will looks a little different. 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. With 8x8 hash size it is recommended to use a different algorithm than Nearest, to have better groups of images. image_hash_alg_tooltip = Users can choose from one of many algorithms of calculating the hash. Each has both strong and weaker points and will sometimes give better and sometimes worse results for different images. So, to determine the best one for you, manual testing is required. big_files_mode_combobox_tooltip = Allows to search for smallest/biggest files big_files_mode_label = Checked files big_files_mode_smallest_combo_box = The Smallest big_files_mode_biggest_combo_box = The Biggest main_notebook_duplicates = Duplicate Files main_notebook_empty_directories = Empty Directories main_notebook_big_files = Big Files main_notebook_empty_files = Empty Files main_notebook_temporary = Temporary Files main_notebook_similar_images = Similar Images main_notebook_similar_videos = Similar Videos main_notebook_same_music = Music Duplicates main_notebook_symlinks = Invalid Symlinks main_notebook_broken_files = Broken Files main_notebook_bad_extensions = Bad Extensions main_tree_view_column_file_name = File Name main_tree_view_column_folder_name = Folder Name main_tree_view_column_path = Path main_tree_view_column_modification = Modification Date main_tree_view_column_size = Size main_tree_view_column_similarity = Similarity main_tree_view_column_dimensions = Dimensions main_tree_view_column_title = Title main_tree_view_column_artist = Artist main_tree_view_column_year = Year main_tree_view_column_bitrate = Bitrate main_tree_view_column_length = Length main_tree_view_column_genre = Genre main_tree_view_column_symlink_file_name = Symlink File Name main_tree_view_column_symlink_folder = Symlink Folder main_tree_view_column_destination_path = Destination Path main_tree_view_column_type_of_error = Type Of Error main_tree_view_column_current_extension = Current Extension main_tree_view_column_proper_extensions = Proper Extension main_tree_view_column_fps = FPS main_tree_view_column_codec = Codec main_label_check_method = Check method main_label_hash_type = Hash type main_label_hash_size = Hash size main_label_size_bytes = Size (bytes) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Number of shown files main_label_resize_algorithm = Resize algorithm main_label_similarity = Similarity{" "} main_check_box_broken_files_audio = Audio main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Archive main_check_box_broken_files_image = Image main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignore same size check_button_general_same_size_tooltip = Ignore files with identical size in results - usually these are 1:1 duplicates main_label_size_bytes_tooltip = Size of files which will be used in scan # Upper window upper_tree_view_included_folder_column_title = Folders to Search upper_tree_view_included_reference_column_title = Reference Folders upper_recursive_button = Recursive upper_recursive_button_tooltip = If selected, search also for files which are not placed directly under chosen folders. upper_manual_add_included_button = Manual Add upper_add_included_button = Add upper_remove_included_button = Remove upper_manual_add_excluded_button = Manual Add upper_add_excluded_button = Add upper_remove_excluded_button = Remove upper_manual_add_included_button_tooltip = Add directory name to search by hand. To add multiple paths at once, separate them by ; /home/roman;/home/rozkaz will add two directories /home/roman and /home/rozkaz upper_add_included_button_tooltip = Add new directory to search. upper_remove_included_button_tooltip = Delete directory from search. upper_manual_add_excluded_button_tooltip = Add excluded directory name by hand. To add multiple paths at once, separate them by ; /home/roman;/home/krokiet will add two directories /home/roman and /home/keokiet upper_add_excluded_button_tooltip = Add directory to be excluded in search. upper_remove_excluded_button_tooltip = Delete directory from excluded. upper_notebook_items_configuration = Items Configuration upper_notebook_excluded_directories = Excluded Paths upper_notebook_included_directories = Included Paths upper_allowed_extensions_tooltip = Allowed extensions must be separated by commas (by default all are available). The following Macros, which add multiple extensions at once, are also available: IMAGE, VIDEO, MUSIC, TEXT. Usage example ".exe, IMAGE, VIDEO, .rar, 7z" - this means that images (e.g. jpg, png), videos (e.g. avi, mp4), exe, rar, and 7z files will be scanned. upper_excluded_extensions_tooltip = List of disabled files which will be ignored in scan. When using both allowed and disabled extensions, this one has higher priority, so file will not be checked. upper_excluded_items_tooltip = Excluded items must contain * wildcard and should be separated by commas. This is slower than Excluded Paths, so use it carefully. upper_excluded_items = Excluded Items: upper_allowed_extensions = Allowed Extensions: upper_excluded_extensions = Disabled Extensions: # Popovers popover_select_all = Select all popover_unselect_all = Unselect all popover_reverse = Reverse Selection popover_select_all_except_shortest_path = Select all except shortest path popover_select_all_except_longest_path = Select all except longest path popover_select_all_except_oldest = Select all except oldest popover_select_all_except_newest = Select all except newest popover_select_one_oldest = Select one oldest popover_select_one_newest = Select one newest popover_select_custom = Select custom popover_unselect_custom = Unselect custom popover_select_all_images_except_biggest = Select all except biggest popover_select_all_images_except_smallest = Select all except smallest popover_custom_path_check_button_entry_tooltip = Select records by path. Example usage: /home/pimpek/rzecz.txt can be found with /home/pim* popover_custom_name_check_button_entry_tooltip = Select records by file names. Example usage: /usr/ping/pong.txt can be found with *ong* popover_custom_regex_check_button_entry_tooltip = Select records by specified Regex. With this mode, searched text is Path with Name. Example usage: /usr/bin/ziemniak.txt can be found with /ziem[a-z]+ This uses the default Rust regex implementation. You can read more about it here: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Enables case-sensitive detection. When disabled /home/* finds both /HoMe/roman and /home/roman. popover_custom_not_all_check_button_tooltip = Prevents selecting all records in group. 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. WARNING: This setting doesn't work if you have already manually selected all results in a group. popover_custom_regex_path_label = Path popover_custom_regex_name_label = Name popover_custom_regex_regex_label = Regex Path + Name popover_custom_case_sensitive_check_button = Case sensitive popover_custom_all_in_group_label = Don't select all records in group popover_custom_mode_unselect = Unselect Custom popover_custom_mode_select = Select Custom popover_sort_file_name = File name popover_sort_folder_name = Folder name popover_sort_full_name = Full name popover_sort_size = Size popover_sort_selection = Selection popover_invalid_regex = Regex is invalid popover_valid_regex = Regex is valid # Bottom buttons bottom_search_button = Search bottom_select_button = Select bottom_delete_button = Delete bottom_save_button = Save bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Move bottom_sort_button = Sort bottom_compare_button = Compare bottom_search_button_tooltip = Start search bottom_select_button_tooltip = Select records. Only selected files/folders can be later processed. bottom_delete_button_tooltip = Delete selected files/folders. bottom_save_button_tooltip = Save data about search to file bottom_symlink_button_tooltip = Create symbolic links. Only works when at least two results in a group are selected. First is unchanged and second and later are symlinked to first. bottom_hardlink_button_tooltip = Create hardlinks. Only works when at least two results in a group are selected. First is unchanged and second and later are hardlinked to first. bottom_hardlink_button_not_available_tooltip = Create hardlinks. Button is disabled, because hardlinks cannot be created. Hardlinks only works with administrator privileges on Windows, so be sure to run app as administrator. If app already works with such privileges check for similar issues on Github. bottom_move_button_tooltip = Moves files to chosen directory. It copies all files to the directory without preserving the directory tree. When trying to move two files with identical name to folder, second will fail and show error. bottom_sort_button_tooltip = Sorts files/folders according to selected method. bottom_compare_button_tooltip = Compare images in the group. bottom_show_errors_tooltip = Show/Hide bottom text panel. bottom_show_upper_notebook_tooltip = Show/Hide upper notebook panel. # Progress Window progress_stop_button = Stop progress_stop_additional_message = Stop requested # About Window about_repository_button_tooltip = Link to repository page with source code. about_donation_button_tooltip = Link to donation page. about_instruction_button_tooltip = Link to instruction page. about_translation_button_tooltip = Link to Crowdin page with app translations. Officially Polish and English are supported. about_repository_button = Repository about_donation_button = Donation about_instruction_button = Instruction about_translation_button = Translation # Header header_setting_button_tooltip = Opens settings dialog. header_about_button_tooltip = Opens dialog with info about app. # Settings ## General settings_number_of_threads = Number of used threads settings_number_of_threads_tooltip = Number of used threads, 0 means that all available threads will be used. settings_use_rust_preview = Use external libraries instead gtk to load previews settings_use_rust_preview_tooltip = Using gtk previews will sometimes be faster and support more formats, but sometimes this could be exactly the opposite. If you have problems with loading previews, you may can to try to change this setting. 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. settings_label_restart = You need to restart app to apply settings! settings_ignore_other_filesystems = Ignore other filesystems (only Linux) settings_ignore_other_filesystems_tooltip = ignores files that are not in the same file system as searched directories. Works same like -xdev option in find command on Linux settings_save_at_exit_button_tooltip = Save configuration to file when closing app. settings_load_at_start_button_tooltip = Load configuration from file when opening app. If not enabled, default settings will be used. settings_confirm_deletion_button_tooltip = Show confirmation dialog when clicking the delete button. settings_confirm_link_button_tooltip = Show confirmation dialog when clicking the hard/symlink button. settings_confirm_group_deletion_button_tooltip = Show warning dialog when trying to delete all records from the group. settings_show_text_view_button_tooltip = Show text panel at the bottom of the user interface. settings_use_cache_button_tooltip = Use file cache. settings_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. settings_use_trash_button_tooltip = Moves files to trash instead deleting them permanently. settings_language_label_tooltip = Language for user interface. settings_save_at_exit_button = Save configuration when closing app settings_load_at_start_button = Load configuration when opening app settings_confirm_deletion_button = Show confirm dialog when deleting any files settings_confirm_link_button = Show confirm dialog when hard/symlinks any files settings_confirm_group_deletion_button = Show confirm dialog when deleting all files in group settings_show_text_view_button = Show bottom text panel settings_use_cache_button = Use cache settings_save_also_as_json_button = Also save cache as JSON file settings_use_trash_button = Move deleted files to trash settings_language_label = Language settings_multiple_delete_outdated_cache_checkbutton = Delete outdated cache entries automatically settings_multiple_delete_outdated_cache_checkbutton_tooltip = Delete outdated cache results which point to non-existent files. When enabled, app makes sure when loading records, that all records point to valid files (broken ones are ignored). Disabling this will help when scanning files on external drives, so cache entries about them will not be purged in the next scan. 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. settings_notebook_general = General settings_notebook_duplicates = Duplicates settings_notebook_images = Similar Images settings_notebook_videos = Similar Video ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Shows preview at right side (when selecting an image file). settings_multiple_image_preview_checkbutton = Show image preview settings_multiple_clear_cache_button_tooltip = Manually clear the cache of outdated entries. This should only be used if automatic clearing has been disabled. settings_multiple_clear_cache_button = Remove outdated results from cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Hides all files except one, if all point to the same data (are hardlinked). 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. settings_duplicates_minimal_size_entry_tooltip = Set the minimal file size which will be cached. Choosing a smaller value will generate more records. This will speedup search, but slowdown cache loading/saving. settings_duplicates_prehash_checkbutton_tooltip = Enables caching of prehash (a hash computed from a small part of the file) which allows earlier dismissal of non-duplicated results. It is disabled by default because it can cause slowdowns in some situations. It is highly recommended to use it when scanning hundred of thousands or million files, because it can speedup search by multiple times. settings_duplicates_prehash_minimal_entry_tooltip = Minimal size of cached entry. settings_duplicates_hide_hard_link_button = Hide hard links settings_duplicates_prehash_checkbutton = Use prehash cache settings_duplicates_minimal_size_cache_label = Minimal size of files (in bytes) saved to cache settings_duplicates_minimal_size_cache_prehash_label = Minimal size of files (in bytes) saved to prehash cache ## Saving/Loading settings settings_saving_button_tooltip = Save the current settings configuration to file. settings_loading_button_tooltip = Load settings from file and replace the current configuration with them. settings_reset_button_tooltip = Reset the current configuration to the default one. settings_saving_button = Save configuration settings_loading_button = Load configuration settings_reset_button = Reset configuration ## Opening cache/config folders settings_folder_cache_open_tooltip = Opens the folder where the cache txt files are stored. 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. You can copy these files between computers to save time on scanning again for files (of course if they have similar directory structure). In the case of problems with the cache, these files can be removed. The app will automatically regenerate them. settings_folder_settings_open_tooltip = Opens the folder where the Czkawka config is stored. WARNING: Manually modifying the config may break your workflow. settings_folder_cache_open = Open cache folder settings_folder_settings_open = Open settings folder # Compute results compute_stopped_by_user = Searching was stopped by user compute_found_duplicates_hash_size = Found { $number_files } duplicates in { $number_groups } groups which took { $size } in { $time } compute_found_duplicates_name = Found { $number_files } duplicates in { $number_groups } groups in { $time } compute_found_empty_folders = Found { $number_files } empty folders in { $time } compute_found_empty_files = Found { $number_files } empty files in { $time } compute_found_big_files = Found { $number_files } big files in { $time } compute_found_temporary_files = Found { $number_files } temporary files in { $time } compute_found_images = Found { $number_files } similar images in { $number_groups } groups in { $time } compute_found_videos = Found { $number_files } similar videos in { $number_groups } groups in { $time } compute_found_music = Found { $number_files } similar music files in { $number_groups } groups in { $time } compute_found_invalid_symlinks = Found { $number_files } invalid symlinks in { $time } compute_found_broken_files = Found { $number_files } broken files in { $time } compute_found_bad_extensions = Found { $number_files } files with invalid extensions in { $time } # Progress window progress_scanning_general_file = {$file_number -> [one] Scanned {$file_number} file *[other] Scanned {$file_number} files } progress_scanning_extension_of_files = Checked extension of {$file_checked}/{$all_files} file progress_scanning_broken_files = Checked {$file_checked}/{$all_files} file ({$data_checked}/{$all_data}) progress_scanning_video = Hashed of {$file_checked}/{$all_files} video progress_creating_video_thumbnails = Created thumbnails of {$file_checked}/{$all_files} video progress_scanning_image = Hashed of {$file_checked}/{$all_files} image ({$data_checked}/{$all_data}) progress_comparing_image_hashes = Compared {$file_checked}/{$all_files} image hash progress_scanning_music_tags_end = Compared tags of {$file_checked}/{$all_files} music file progress_scanning_music_tags = Read tags of {$file_checked}/{$all_files} music file progress_scanning_music_content_end = Compared fingerprint of {$file_checked}/{$all_files} music file progress_scanning_music_content = Calculated fingerprint of {$file_checked}/{$all_files} music file ({$data_checked}/{$all_data}) progress_scanning_empty_folders = {$folder_number -> [one] Scanned {$folder_number} folder *[other] Scanned {$folder_number} folders } progress_scanning_size = Scanned size of {$file_number} file progress_scanning_size_name = Scanned name and size of {$file_number} file progress_scanning_name = Scanned name of {$file_number} file progress_analyzed_partial_hash = Analyzed partial hash of {$file_checked}/{$all_files} files ({$data_checked}/{$all_data}) progress_analyzed_full_hash = Analyzed full hash of {$file_checked}/{$all_files} files ({$data_checked}/{$all_data}) progress_prehash_cache_loading = Loading prehash cache progress_prehash_cache_saving = Saving prehash cache progress_hash_cache_loading = Loading hash cache progress_hash_cache_saving = Saving hash cache progress_cache_loading = Loading cache progress_cache_saving = Saving cache progress_current_stage = Current Stage:{" "} progress_all_stages = All Stages:{" "} # Saving loading saving_loading_saving_success = Saved configuration to file { $name }. saving_loading_saving_failure = Failed to save configuration data to file { $name }, reason { $reason }. saving_loading_reset_configuration = Current configuration was cleared. saving_loading_loading_success = Properly loaded app configuration. saving_loading_failed_to_create_config_file = Failed to create config file "{ $path }", reason "{ $reason }". saving_loading_failed_to_read_config_file = Cannot load configuration from "{ $path }" because it does not exist or is not a file. saving_loading_failed_to_read_data_from_file = Cannot read data from file "{ $path }", reason "{ $reason }". # Other selected_all_reference_folders = Cannot start search, when all directories are set as reference folders searching_for_data = Searching data, it may take a while, please wait... text_view_messages = MESSAGES text_view_warnings = WARNINGS text_view_errors = ERRORS about_window_motto = This program is free to use and will always be. krokiet_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. # Various dialog dialogs_ask_next_time = Ask next time symlink_failed = Failed to symlink {$name} to {$target}, reason {$reason} delete_title_dialog = Delete confirmation delete_question_label = Are you sure that you want to delete files? delete_all_files_in_group_title = Confirmation of deleting all files in group delete_all_files_in_group_label1 = In some groups all records are selected. delete_all_files_in_group_label2 = Are you sure that you want to delete them? delete_items_label = { $items } files will be deleted. delete_items_groups_label = { $items } files from { $groups } groups will be deleted. hardlink_failed = Failed to hardlink { $name } to { $target }, reason { $reason } hard_sym_invalid_selection_title_dialog = Invalid selection with some groups hard_sym_invalid_selection_label_1 = In some groups there is only one record selected and it will be ignored. hard_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. hard_sym_invalid_selection_label_3 = First in group is recognized as original and is not changed but second and later are modified. hard_sym_link_title_dialog = Link confirmation hard_sym_link_label = Are you sure that you want to link these files? move_folder_failed = Failed to move folder {$name}, reason {$reason} move_file_failed = Failed to move file {$name}, reason {$reason} move_files_title_dialog = Choose folder to which you want to move duplicated files move_files_choose_more_than_1_path = Only one path may be selected to be able to copy their duplicated files, selected {$path_number}. move_stats = Properly moved {$num_files}/{$all_files} items save_results_to_file = Saved results both to txt and json files into "{$name}" folder. search_not_choosing_any_music = ERROR: You must select at least one checkbox with music searching types. search_not_choosing_any_broken_files = ERROR: You must select at least one checkbox with type of checked broken files. include_folders_dialog_title = Folders to include exclude_folders_dialog_title = Folders to exclude include_manually_directories_dialog_title = Add directory manually cache_properly_cleared = Properly cleared cache cache_clear_duplicates_title = Clearing duplicates cache cache_clear_similar_images_title = Clearing similar images cache cache_clear_similar_videos_title = Clearing similar videos cache cache_clear_message_label_1 = Do you want to clear the cache of outdated entries? cache_clear_message_label_2 = This operation will remove all cache entries which point to invalid files. cache_clear_message_label_3 = This may slightly speedup loading/saving to cache. cache_clear_message_label_4 = WARNING: Operation will remove all cached data from unplugged external drives. So each hash will need to be regenerated. # Show preview preview_image_resize_failure = Failed to resize image {$name}. preview_image_opening_failure = Failed to open image {$name}, reason {$reason} # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Group { $current_group }/{ $all_groups } ({ $images_in_group } images) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/es-ES/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Configuración window_main_title = Czkawka (Hipo) window_progress_title = Escaneando window_compare_images = Comparar imágenes # General general_ok_button = Aceptar general_close_button = Cerrar # Krokiet info dialog krokiet_info_title = Presentando Krokiet - Nueva Versión de Czkawka krokiet_info_message = Krokiet es la nueva versión mejorada, más rápida y más fiable de la interfaz gráfica GTK de Czkawka. 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. 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. ¡Pruébalo y nota la diferencia! 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. 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. # Main window music_title_checkbox = Título music_artist_checkbox = Artista music_year_checkbox = Año music_bitrate_checkbox = Tasa de bits music_genre_checkbox = Género music_length_checkbox = Duración music_comparison_checkbox = Comparación aproximada music_checking_by_tags = Etiquetas music_checking_by_content = Contenido same_music_seconds_label = Duración mínima del segundo fragmento same_music_similarity_label = Diferencia máxima music_compare_only_in_title_group = Comparar dentro de grupos de títulos similares music_compare_only_in_title_group_tooltip = Cuando está activado, los archivos son agrupados por títulos, y luego comparados con otros. Con 10000 archivos, al menos tendríamos unas 100 millones de comparaciones, cuando usualmente serían unas 20000 comparaciones. same_music_tooltip = La búsqueda de archivos de música, por su contenido, puede especificarse mediante los siguientes parámetros: - El tiempo mínimo de fragmento después del cual los archivos de música pueden ser identificados como similares. - La diferencia máxima entre dos fragmentos probados. La clave, para lograr los mejores resultados al buscar, es proporcionando las mejores combinaciones de estos parámetros: - Establecer el tiempo mínimo a 5s y la diferencia máxima a 1,0, buscará fragmentos casi idénticos en los archivos. - Un tiempo de 20s y una diferencia máxima de 6,0, por otro lado, funciona bien para encontrar remixes/versiones en vivo, etc. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Sensible a mayúsculas duplicate_case_sensitive_name_tooltip = Cuando está habilitado, agrupa registros solo cuando tienen exactamente el mismo nombre. P. ej. Żołd ↔ Żołd Si deshabilitamos dicha opción, agrupará nombres sin comprobar si cada letra tiene el mismo tamaño. P. ej. żoŁD ↔ Żołd duplicate_mode_size_name_combo_box = Tamaño y nombre duplicate_mode_name_combo_box = Nombre duplicate_mode_size_combo_box = Tamaño duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka ofrece 3 tipos de hashes, que pueden ser usados: Blake3 - función de hash criptográfica. Se usa como algoritmo predeterminado porque es muy rápido. CRC32 - función hash simple. Debería ser más rápido que Blake3, pero probablemente tenga algunas colisiones muy raras. 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. duplicate_check_method_tooltip = Por el momento, Czkawka ofrece tres tipos de métodos para encontrar duplicados: Nombre - Encuentra archivos con el mismo nombre. Tamaño - Encuentra archivos con el mismo tamaño. 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. image_hash_size_tooltip = 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. 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. 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. 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). image_resize_filter_tooltip = Al calcular el hash de una imagen, lo primero que hace la librería es redimensionarla. Dependiendo del algoritmo que elijamos, la imagen resultante usada para calcular el hash puede ser apenas diferente. 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. Con el tamaño hash de 8x8 se recomienda usar un algoritmo diferente al tipo Nearest, para obtener mejores grupos de imágenes. image_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. big_files_mode_combobox_tooltip = Permite buscar archivos de un menor/mayor tamaño big_files_mode_label = Archivos marcados big_files_mode_smallest_combo_box = El más pequeño big_files_mode_biggest_combo_box = El más grande main_notebook_duplicates = Archivos Duplicados main_notebook_empty_directories = Directorios vacíos main_notebook_big_files = Archivos grandes main_notebook_empty_files = Archivos vacíos main_notebook_temporary = Archivos temporales main_notebook_similar_images = Imágenes similares main_notebook_similar_videos = Videos similares main_notebook_same_music = Canciones duplicadas main_notebook_symlinks = Enlaces simbólicos rotos main_notebook_broken_files = Archivos dañados main_notebook_bad_extensions = Extensiones incorrectas main_tree_view_column_file_name = Nombre del archivo main_tree_view_column_folder_name = Nombre de carpeta main_tree_view_column_path = Ruta main_tree_view_column_modification = Fecha de modificación main_tree_view_column_size = Tamaño main_tree_view_column_similarity = Similitud main_tree_view_column_dimensions = Dimensiones main_tree_view_column_title = Título main_tree_view_column_artist = Artista main_tree_view_column_year = Año main_tree_view_column_bitrate = Tasa de bits main_tree_view_column_length = Duración main_tree_view_column_genre = Género main_tree_view_column_symlink_file_name = Nombre del "Enlace simbólico" main_tree_view_column_symlink_folder = Carpeta Symlink main_tree_view_column_destination_path = Ruta de destino main_tree_view_column_type_of_error = Tipo de error main_tree_view_column_current_extension = Extensión actual main_tree_view_column_proper_extensions = Extensión adecuada main_tree_view_column_fps = FPS main_tree_view_column_codec = Codificador main_label_check_method = Método de comprobación main_label_hash_type = Tipo de Hash main_label_hash_size = Tamaño hash main_label_size_bytes = Tamaño (bytes) main_label_min_size = Mínimo main_label_max_size = Máximo main_label_shown_files = Número de archivos mostrados main_label_resize_algorithm = Algoritmo de Redimensionado main_label_similarity = Similitud{ " " } main_check_box_broken_files_audio = Sonido main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Guardar main_check_box_broken_files_image = Imagen main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignorar el mismo tamaño check_button_general_same_size_tooltip = Ignorar archivos con idéntico tamaño en resultados - usualmente son 1:1 duplicados main_label_size_bytes_tooltip = Tamaño de los archivos que se utilizarán en el escaneo # Upper window upper_tree_view_included_folder_column_title = Carpetas a buscar upper_tree_view_included_reference_column_title = Carpetas de referencia upper_recursive_button = Recursivo upper_recursive_button_tooltip = Si se selecciona, busca cualquier archivo, sin importar si está o no en una sub-carpeta. upper_manual_add_included_button = Incluir de forma manual upper_add_included_button = Añadir upper_remove_included_button = Eliminar upper_manual_add_excluded_button = Añadir manual upper_add_excluded_button = Añadir upper_remove_excluded_button = Eliminar upper_manual_add_included_button_tooltip = Añade el nombre del directorio para buscar a mano. Para agregar múltiples rutas a la vez, sepáralas con ; /home/roman;/home/rozkaz añadirá dos directorios /home/roman y /home/rozkaz upper_add_included_button_tooltip = Añadir nuevo directorio para buscar. upper_remove_included_button_tooltip = Eliminar directorio de la búsqueda. upper_manual_add_excluded_button_tooltip = Añadir el nombre del directorio excluido a mano. Para agregar múltiples rutas a la vez, separalas por ; /home/roman;/home/krokiet añadirá dos directorios /home/roman y /home/keokiet upper_add_excluded_button_tooltip = Añadir directorio a excluir en la búsqueda. upper_remove_excluded_button_tooltip = Eliminar directorio de excluidos. upper_notebook_items_configuration = Configuración de artículos upper_notebook_excluded_directories = Rutas excluidas upper_notebook_included_directories = Rutas incluidas upper_allowed_extensions_tooltip = Las extensiones permitidas deben estar separadas por comas (por defecto todas están disponibles). Las siguientes Macros, que añaden múltiples extensiones a la vez, también están disponibles: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Lista de archivos ignorados, durante el escaneo. Cuando desactivamos las extensiones permitidas, estas tienen mayor prioridad, haciendo que los archivos no sean comprobados. upper_excluded_items_tooltip = Los artículos excluidos deben contener el comodín * y deben estar separados por comas. Esto es más lento que los Directorios Excluidos, así que úselo con cuidado. upper_excluded_items = Elementos excluidos: upper_allowed_extensions = Extensiones permitidas: upper_excluded_extensions = Extensiones desactivadas: # Popovers popover_select_all = Seleccionar todo popover_unselect_all = Deseleccionar todo popover_reverse = Invertir selección popover_select_all_except_shortest_path = Seleccionar todo excepto la ruta más corta popover_select_all_except_longest_path = Seleccionar todo excepto la ruta más larga popover_select_all_except_oldest = Seleccionar todo excepto más antiguo popover_select_all_except_newest = Seleccionar todo excepto el más reciente popover_select_one_oldest = Seleccione uno más antiguo popover_select_one_newest = Seleccione uno más nuevo popover_select_custom = Seleccionar personalizado popover_unselect_custom = Deseleccionar personalizado popover_select_all_images_except_biggest = Seleccionar todo excepto mayor popover_select_all_images_except_smallest = Seleccionar todo excepto menor popover_custom_path_check_button_entry_tooltip = Seleccionar registros por ruta. Ejemplo: /home/pmañk/rzecz.txt se puede encontrar con /home/pim* popover_custom_name_check_button_entry_tooltip = Seleccionar registros por nombres de archivos. Ejemplo: /usr/ping/pong.txt puede encontrarse con *a lo largo* popover_custom_regex_check_button_entry_tooltip = Seleccione registros por Regex. En este modo, el texto buscado es Ruta con Nombre. Ejemplo: /usr/bin/ziemniak. xt se puede encontrar con /ziem[a-z]+ Esto utiliza la implementación predeterminada de expresiones regulares de Rust. Puedes leer más al respecto aquí: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Habilita la detección de mayúsculas y minúsculas. Cuando se desactiva /home/* encuentra /HoMe/roman y /home/roman. popover_custom_not_all_check_button_tooltip = Previene la selección de todos los registros en grupo. 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. ADVERTENCIA: Esta configuración no funciona si ya has seleccionado manualmente todos los resultados en un grupo. popover_custom_regex_path_label = Ruta popover_custom_regex_name_label = Nombre popover_custom_regex_regex_label = Ruta de Regex + Nombre popover_custom_case_sensitive_check_button = Distingue mayúsculas y minúsculas popover_custom_all_in_group_label = No seleccionar todos los registros en el grupo popover_custom_mode_unselect = Deseleccionar Personalizado popover_custom_mode_select = Seleccionar Personalizado popover_sort_file_name = Nombre de archivo popover_sort_folder_name = Nombre de la carpeta popover_sort_full_name = Nombre completo popover_sort_size = Tamaño popover_sort_selection = Selección popover_invalid_regex = Regex no es válido popover_valid_regex = Regex es válido # Bottom buttons bottom_search_button = Buscar bottom_select_button = Seleccionar bottom_delete_button = Eliminar bottom_save_button = Guardar bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Mover bottom_sort_button = Ordenar bottom_compare_button = Comparar bottom_search_button_tooltip = Iniciar búsqueda bottom_select_button_tooltip = Seleccionar registros. Sólo los archivos/carpetas seleccionados pueden ser procesados más tarde. bottom_delete_button_tooltip = Eliminar archivos/carpetas seleccionadas. bottom_save_button_tooltip = Guardar datos sobre la búsqueda en el archivo bottom_symlink_button_tooltip = Crear enlaces simbólicos. Sólo funciona cuando al menos dos resultados en grupo son seleccionados. El primero no ha cambiado y el segundo y más tarde están enlazados con el primero. bottom_hardlink_button_tooltip = Crear enlaces hardlinks. Solo funciona cuando al menos dos resultados en grupo son seleccionados. El primero no ha cambiado y el segundo y más tarde están enlazados por hardlinks a la primera. bottom_hardlink_button_not_available_tooltip = Crear enlaces duros. Botón deshabilitado, porque no se pueden crear enlaces duros. Hardlinks sólo funciona con privilegios de administrador en Windows, así que asegúrese de ejecutar la aplicación como administrador. Si la aplicación ya funciona con dichos privilegios, compruebe si hay problemas similares en Github. bottom_move_button_tooltip = Mover los archivos a la carpeta elegida. Copia todos los archivos a la carpeta sin preservar el árbol de directorios. Al intentar mover dos archivos con el mismo nombre a la carpeta, el segundo fallará y mostrará el error. bottom_sort_button_tooltip = Ordenar archivos/carpetas de acuerdo al método seleccionado. bottom_compare_button_tooltip = Comparar imágenes en el grupo. bottom_show_errors_tooltip = Mostrar/Ocultar panel de texto inferior. bottom_show_upper_notebook_tooltip = Mostrar / Ocultar panel de cuaderno superior. # Progress Window progress_stop_button = Parar progress_stop_additional_message = Parar solicitado # About Window about_repository_button_tooltip = Enlace a la página del repositorio con código fuente. about_donation_button_tooltip = Enlace a la página de donación. about_instruction_button_tooltip = Enlace a la página de instrucciones. about_translation_button_tooltip = Enlace a la página de Crowdin con traducciones de aplicaciones. Oficialmente se admiten polaco e inglés. about_repository_button = Repositorio about_donation_button = Donativo about_instruction_button = Instrucción about_translation_button = Traducción # Header header_setting_button_tooltip = Abre el diálogo de ajustes. header_about_button_tooltip = Abre el diálogo con información sobre la aplicación. # Settings ## General settings_number_of_threads = Número de hilos usados settings_number_of_threads_tooltip = Número de hilos usados, 0 significa que se utilizarán todos los hilos disponibles. settings_use_rust_preview = Usar librerías externas en su lugar gtk para cargar vistas previas settings_use_rust_preview_tooltip = 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. Si tienes problemas con la carga de las vistas previas, puedes intentar cambiar esta configuració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. settings_label_restart = ¡Necesitas reiniciar la aplicación para aplicar la configuración! settings_ignore_other_filesystems = Ignorar otros sistemas de ficheros (sólo Linux) settings_ignore_other_filesystems_tooltip = ignora los archivos que no están en el mismo sistema de archivos que los directorios buscados. Funciona igual que la opción -xdev en encontrar el comando en Linux settings_save_at_exit_button_tooltip = Guardar configuración en archivo al cerrar la aplicación. settings_load_at_start_button_tooltip = Cargar la configuración desde el archivo al abrir la aplicación. Si no está habilitado, se usarán los ajustes por defecto. settings_confirm_deletion_button_tooltip = Mostrar el diálogo de confirmación al hacer clic en el botón borrar. settings_confirm_link_button_tooltip = Mostrar el diálogo de confirmación al hacer clic en el botón hard/symlink. settings_confirm_group_deletion_button_tooltip = Mostrar el diálogo de advertencia al intentar eliminar todos los registros del grupo. settings_show_text_view_button_tooltip = Mostrar el panel de texto en la parte inferior de la interfaz de usuario. settings_use_cache_button_tooltip = Usar caché de archivos. settings_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. settings_use_trash_button_tooltip = Mueve archivos a la papelera en su lugar eliminándolos permanentemente. settings_language_label_tooltip = Idioma para la interfaz de usuario. settings_save_at_exit_button = Guardar configuración al cerrar la aplicación settings_load_at_start_button = Cargar configuración al abrir la aplicación settings_confirm_deletion_button = Mostrar diálogo de confirmación al eliminar cualquier archivo settings_confirm_link_button = Mostrar diálogo de confirmación cuando vincule archivos de forma dura o simbólica settings_confirm_group_deletion_button = Mostrar diálogo de confirmación al eliminar todos los archivos del grupo settings_show_text_view_button = Mostrar panel de texto inferior settings_use_cache_button = Usar caché settings_save_also_as_json_button = Guarda también la caché como archivo JSON settings_use_trash_button = Mover archivos borrados a la papelera settings_language_label = Idioma settings_multiple_delete_outdated_cache_checkbutton = Borrar automáticamente entradas de caché obsoletas settings_multiple_delete_outdated_cache_checkbutton_tooltip = Eliminar resultados de caché obsoletos que apuntan a archivos inexistentes. 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). 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. 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. settings_notebook_general = General settings_notebook_duplicates = Duplicados settings_notebook_images = Imágenes similares settings_notebook_videos = Vídeos similares ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Muestra la vista previa en el lado derecho (al seleccionar un archivo de imagen). settings_multiple_image_preview_checkbutton = Mostrar vista previa de la imagen settings_multiple_clear_cache_button_tooltip = Limpiar manualmente la caché de entradas desactualizadas. Esto solo debe utilizarse si se ha desactivado la limpieza automática. settings_multiple_clear_cache_button = Eliminar resultados obsoletos de la caché. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Oculta todos los archivos excepto uno, si todos apuntan a los mismos datos (están en línea dura). 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. settings_duplicates_minimal_size_entry_tooltip = Establece el tamaño mínimo de archivo que se almacenará en caché. 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é. settings_duplicates_prehash_checkbutton_tooltip = 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. Está deshabilitado por defecto porque puede causar derribos lentos en algunas situaciones. Es altamente recomendable usarlo para escanear cientos de miles o millones de archivos, ya que puede acelerar la búsqueda varias veces. settings_duplicates_prehash_minimal_entry_tooltip = Tamaño mínimo de la entrada en caché. settings_duplicates_hide_hard_link_button = Ocultar enlaces duros settings_duplicates_prehash_checkbutton = Usar caché prehash settings_duplicates_minimal_size_cache_label = Tamaño mínimo de los archivos (en bytes) guardados en la caché settings_duplicates_minimal_size_cache_prehash_label = Tamaño mínimo de archivos (en bytes) guardados en caché prehash ## Saving/Loading settings settings_saving_button_tooltip = Guardar la configuración de configuración actual en el archivo. settings_loading_button_tooltip = Cargar los ajustes desde el archivo y reemplazar la configuración actual con ellos. settings_reset_button_tooltip = Restablecer la configuración actual a la predeterminada. settings_saving_button = Guardar configuración settings_loading_button = Cargar configuración settings_reset_button = Restablecer configuración ## Opening cache/config folders settings_folder_cache_open_tooltip = Abre la carpeta donde se almacenan los archivos txt. 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. Puede copiar estos archivos entre ordenadores para ahorrar tiempo al escanear de nuevo para archivos (por supuesto, si tienen una estructura de directorios similar). En caso de problemas con la caché, estos archivos pueden ser eliminados. La aplicación los regenerará automáticamente. settings_folder_settings_open_tooltip = Abrir la carpeta donde se almacena la configuración de Czkawka. ADVERTENCIA: Modificar manualmente la configuración puede romper su flujo de trabajo. settings_folder_cache_open = Abrir carpeta de caché settings_folder_settings_open = Abrir carpeta de ajustes # Compute results compute_stopped_by_user = El usuario ha detenido la búsqueda compute_found_duplicates_hash_size = Se encontraron { $number_files } duplicados en { $number_groups } grupos que tomaron { $size } en { $time } compute_found_duplicates_name = Se encontraron { $number_files } duplicados en { $number_groups } grupos en { $time } compute_found_empty_folders = Se encontraron carpetas vacías { $number_files } en { $time } compute_found_empty_files = Se encontraron { $number_files } archivos vacíos en { $time } compute_found_big_files = Se encontraron { $number_files } archivos grandes en { $time } compute_found_temporary_files = Encontrados archivos temporales { $number_files } en { $time } compute_found_images = Se encontraron { $number_files } imágenes similares en { $number_groups } grupos en { $time } compute_found_videos = Se encontraron { $number_files } vídeos similares en { $number_groups } grupos en { $time } compute_found_music = Se encontraron archivos de música similares { $number_files } en grupos { $number_groups } en { $time } compute_found_invalid_symlinks = Encontrados { $number_files } enlaces simbólicos no válidos en { $time } compute_found_broken_files = Se encontraron { $number_files } archivos dañados en { $time } compute_found_bad_extensions = Se encontraron archivos { $number_files } con extensiones no válidas en { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Escaneado archivo { $file_number } *[other] Escaneados archivos { $file_number } } progress_scanning_extension_of_files = Extensión comprobada de archivo { $file_checked }/{ $all_files } progress_scanning_broken_files = Verificado archivo { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_video = Hash de vídeo { $file_checked }/{ $all_files } progress_creating_video_thumbnails = Miniaturas creadas de vídeo { $file_checked }/{ $all_files } progress_scanning_image = Hash de { $file_checked }/{ $all_files } imagen ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Hash de imagen { $file_checked }/{ $all_files } comparado progress_scanning_music_tags_end = Etiquetas comparadas de archivo de música { $file_checked }/{ $all_files } progress_scanning_music_tags = Leer etiquetas del archivo de música { $file_checked }/{ $all_files } progress_scanning_music_content_end = Se ha comparado la huella digital de archivo de música { $file_checked }/{ $all_files } progress_scanning_music_content = Huella digital calculada de { $file_checked }/{ $all_files } archivo de música ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Escaneó la carpeta { $folder_number } *[other] Escaneó las carpetas { $folder_number } } progress_scanning_size = Tamaño escaneado del archivo { $file_number } progress_scanning_size_name = Nombre y tamaño escaneado del archivo { $file_number } progress_scanning_name = Nombre escaneado del archivo { $file_number } progress_analyzed_partial_hash = Se ha analizado el hash parcial de archivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Se ha analizado el hash completo de archivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Cargando caché prehash progress_prehash_cache_saving = Guardando caché prehash progress_hash_cache_loading = Cargando caché hash progress_hash_cache_saving = Guardando caché hash progress_cache_loading = Cargando caché progress_cache_saving = Guardando caché progress_current_stage = Etapa actual:{ " " } progress_all_stages = Todas las etapas:{ " " } # Saving loading saving_loading_saving_success = Configuración guardada en el archivo { $name }. saving_loading_saving_failure = Error al guardar los datos de configuración en el archivo { $name }, razón { $reason }. saving_loading_reset_configuration = La configuración actual fue borrada. saving_loading_loading_success = Configuración de la aplicación cargada correctamente. saving_loading_failed_to_create_config_file = Error al crear el archivo de configuración "{ $path }", razón "{ $reason }". saving_loading_failed_to_read_config_file = No se puede cargar la configuración de "{ $path }" porque no existe o no es un archivo. saving_loading_failed_to_read_data_from_file = No se pueden leer los datos del archivo "{ $path }", razón "{ $reason }". # Other selected_all_reference_folders = No se puede iniciar la búsqueda, cuando todos los directorios están establecidos como carpetas de referencia searching_for_data = Buscando datos, puede tardar un tiempo, por favor espere... text_view_messages = MENSAJES text_view_warnings = ADVERTENCIA text_view_errors = ERRORES about_window_motto = Este programa es gratuito y siempre lo será. krokiet_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. # Various dialog dialogs_ask_next_time = Preguntar la próxima vez symlink_failed = Error al enlazar { $name } a { $target }, razón { $reason } delete_title_dialog = Confirmación de eliminación delete_question_label = ¿Está seguro que desea eliminar los archivos? delete_all_files_in_group_title = Confirmación de borrar todos los archivos del grupo delete_all_files_in_group_label1 = En algunos grupos se seleccionan todos los registros. delete_all_files_in_group_label2 = ¿Estás seguro de que quieres eliminarlos? delete_items_label = { $items } archivos serán eliminados. delete_items_groups_label = { $items } archivos de { $groups } grupos serán eliminados. hardlink_failed = Error al vincular { $name } a { $target }, razón { $reason } hard_sym_invalid_selection_title_dialog = Selección no válida con algunos grupos hard_sym_invalid_selection_label_1 = En algunos grupos sólo hay un registro seleccionado y será ignorado. hard_sym_invalid_selection_label_2 = Para poder vincular estos archivos con el sistema, al menos dos resultados en el grupo deben ser seleccionados. hard_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. hard_sym_link_title_dialog = Confirmación de enlace hard_sym_link_label = ¿Está seguro que desea enlazar estos archivos? move_folder_failed = Error al mover la carpeta { $name }, razón { $reason } move_file_failed = Error al mover el archivo { $name }, razón { $reason } move_files_title_dialog = Elija la carpeta a la que desea mover los archivos duplicados move_files_choose_more_than_1_path = Solo se puede seleccionar una ruta para poder copiar sus archivos duplicados, seleccionado { $path_number }. move_stats = Mudado correctamente { $num_files }/{ $all_files } elementos save_results_to_file = Resultados guardados en archivos txt y json en la carpeta "{ $name }". search_not_choosing_any_music = ERROR: Debe seleccionar al menos una casilla de verificación con tipos de búsqueda de música. search_not_choosing_any_broken_files = ERROR: Debe seleccionar al menos una casilla de verificación con el tipo de ficheros rotos comprobados. include_folders_dialog_title = Carpetas a incluir exclude_folders_dialog_title = Carpetas a excluir include_manually_directories_dialog_title = Añadir directorio manualmente cache_properly_cleared = Caché correctamente borrada cache_clear_duplicates_title = Limpiando caché duplicada cache_clear_similar_images_title = Limpiando caché de imágenes similares cache_clear_similar_videos_title = Limpiando caché de vídeos similares cache_clear_message_label_1 = ¿Quiere borrar la caché de entradas obsoletas? cache_clear_message_label_2 = Esta operación eliminará todas las entradas de caché que apunten a archivos no válidos. cache_clear_message_label_3 = Esto puede acelerar ligeramente la carga/guardado en caché. cache_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. # Show preview preview_image_resize_failure = Error al redimensionar la imagen { $name }. preview_image_opening_failure = Error al abrir la imagen { $name }, razón { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Grupo { $current_group }/{ $all_groups } ({ $images_in_group } imágenes) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/fa/czkawka_gui.ftl ================================================ # Window titles window_settings_title = تنظیمات window_main_title = چکاواک (همیپ) window_progress_title = سkenنگ window_compare_images = مقایسه تصاویر # General general_ok_button = اوکی general_close_button = بسته شود # Krokiet info dialog krokiet_info_title = معرفی کرکیه - نسخه جدید CzKawkA krokiet_info_message = کروکیِت نسخه جدید، بهبود یافته، سریع‌تر و قابل اعتمادتر رابط کاربری GTK Czkawka است! اجرای آن آسان‌تر و مقاوم‌تر در برابر تغییرات سیستم است، زیرا فقط به کتابخانه‌های اصلی موجود در اکثر سیستم‌ها به صورت پیش‌فرض وابسته است. کروکیِت همچنین ویژگی‌هایی را به همراه دارد که Czkawka فاقد آن‌هاست، از جمله تصاویر کوچک در حالت مقایسه ویدیو، یک پاک‌کن EXIF، گزینه‌های پیشرفت برای انتقال/کپی/حذف فایل یا گزینه‌های مرتب‌سازی گسترده. آن را امتحان کنید و تفاوت را ببینید! Czkawka همچنان با رفع باگ‌ها و به‌روزرسانی‌های جزئی از طرف من دریافت خواهد شد، اما تمام ویژگی‌های جدید به طور انحصاری برای کروکیِت توسعه خواهند یافت و هر کسی می‌تواند ویژگی‌های جدید، افزودن حالت‌های از دست رفته یا گسترش بیشتر Czkawka را ارائه دهد. PS: این پیام باید فقط یک بار ظاهر شود. اگر دوباره ظاهر شد، متغیر محیطی CZKAWKA_DONT_ANNOY_ME را به هر مقدار غیر خالی تنظیم کنید. # Main window music_title_checkbox = عنوان music_artist_checkbox = هنرپرداز music_year_checkbox = سال music_bitrate_checkbox = بیت‌روتpedoze music_genre_checkbox = نوع music_length_checkbox = طول music_comparison_checkbox = مقایسه تقریبی music_checking_by_tags = برچسب‌ها music_checking_by_content = محتوا same_music_seconds_label = کسری مینیمال دوم طول دومین جفت same_music_similarity_label = مکانیمم تفاضل music_compare_only_in_title_group = مقایسه در گروه‌های با عنوان‌های مشابه music_compare_only_in_title_group_tooltip = هنگامی که فعال است، فایل‌ها بر اساس عنوان گروه‌بندی می‌شوند و سپس با هم مقایسه می‌شوند. با 10000 فایل، به جای تقریباً 100 میلیون مقایسه، حدوداً 20000 مقایسه خواهد بود. same_music_tooltip = جستجوی مسایل موسیقی مشابه بر اساس محتوا می‌تواند با تنظیم موارد زیر پیشنهاد شود: - زمان حداقل فragment بعد از آن مسایل موسیقی می‌توانند به عنوان مشابه شناسایی شوند - تفاوت بینی مرکب دو fragment مورد تست کلید خوب کسب نتایج است این پارامترها را به صورت منطقی تر از طریق تنظیم‌های ارائه شده جستجو کنید. تنظیم زمان حداقل ۵ ثانیه و تفاوت بینی مرکب ۱.۰، برای پیدا کردن فرگمنت‌های معمولاً مشابه در فایل‌ها استفاده خواهد شد. در حالی که زمان ۲۰ ثانیه و تفاوت بینی مرکب ۶.۰ برای پیدا کردن remixات/نسخه‌های live بهتر کار می‌کند. به طور پیش فرض، هر فایل موسیقی با هر فایل دیگری مقایسه می‌شود و این زمان خیلی زمان‌بر خواهد بود وقتی که تعداد بسیاری از فایل‌ها را تست کنید، بنابراین به طور گسترده‌تر استفاده از پوشه‌های مرجع و مشخص شدن کدام فایل‌ها را که باید با هم مقایسه شوند (با تعداد برابر فایل‌ها، مقایسه خلاک‌های ضخامت چندگانه حداقل ۴ برابر از بدون پوشه‌های مرجع سریعتر خواهد بود) کارآمد است. music_comparison_checkbox_tooltip = آنچه با هوش مصنوعی جستجو می‌کند و از یادگیری ماشین برای حذف پرانتز‌ها از جمله استفاده می‌کند. به عنوان مثال، با فعال سازی این گزینه، فایل‌های مورد سوال نظرات همگون در نظر گرفته خواهند شد: Świędziżłób --- Świędziżłób ( Remix Lato 2021) duplicate_case_sensitive_name = متن حساس به بیانیه duplicate_case_sensitive_name_tooltip = وقتی فعال است، تنها رکوردهایی را در گروه گذاری می‌کند که دقیقاً نام آن‌ها مشابه هستند مانند Żołd <-> Żołd 停用 اینگونde برای گروه‌بندی نام‌ها در نظر نمی‌گیرد که هر حرف چقدر تقریب زدن دارد مانند żoŁD <-> Żołd duplicate_mode_size_name_combo_box = حجم و نام duplicate_mode_name_combo_box = نام duplicate_mode_size_combo_box = اندازه duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka ۳ نوع هاش می‌پذیرد: بلک ۳ - تابع چربی کriتوگرافیک. این آن به طور پیش فرض است زیرا خیلی سریع است. CRC32 - تابع چربی ساده. این سریع‌تر از بلک ۳ باشد، اما در نسخه‌ای نادر ممکن است برخورد کامل داشته باشد. XXH3 - در عملکرد و کیفیت چربی تقریباً شبیه به بلک ۳ (اما کriتوگرافیک نیست). بنابراین، این حالت‌ها می‌توانند خلک ساده جایگزین شوند. duplicate_check_method_tooltip = این لحظه، Czkawka سه روش از پشتیبان برای پیدا کردن فایل‌های تکراری دارد: نام - فایل‌هایی را که نام آنها یکسان است را پیدا می‌کند. حجم - فایل‌هایی را که حجم آنها یکسان است را پیدا می‌کند. هش - فایل‌هایی را که محتوای آنها یکسان است را پیدا می‌کند. این مód هش فایل را بررسی می‌کند و بعد با مقایسه این هش، تکراری‌ها را پیدا می‌کند. این مód روش امن‌تری برای پیدا کردن تکراری‌ها است. اپلیکیشن به شدت از کاش استفاده می‌کند، بنابراین دومین و فراخانی‌های بعدی داده‌های یکسان بسیار سریع‌تر از اولین بار خواهند بود. image_hash_size_tooltip = هر تصویری که تایید شده یک هش خاص ایجاد می‌کند که می‌توان آن را با هم مقایسه کرد، و اختلاف زیادی بین آنها به این معناست که این تصاویر مشابه هستند. 8 باره سایز هش خیلی خوبی است تا تصاویری را که تنها ممکن است شبیه به اصلی باشند پیدا کنیم. با یک مجموعه بزرگتر از تصاویر (>1000)، این موضوع تعداد زیادی از مقادیر نادرست را تولید خواهد کرد، بنابراین بهتر است در این صورت سایز هش بزرگ‌تری را استفاده کنید. 16 پوزیشن سایز هش پیش‌فرض است که یک ترادف خوبی بین پیدا کردن تصاویری که حتی شبیه به اصلی هستند و داشتن تنوع کمتری از تنش‌های هش، محسوب می‌شود. 32 و 64 هش فقط تصاویر بسیار مشابه را پیدا می‌کنند، اما بایستی اچ helt نسبتاً کمترین مقادیر نادرست داشته باشند (ربات‌ها با کانال alpha ممکن است غیرمعمول باشند). image_resize_filter_tooltip = برای حساب کردن هش تصویر، بиблиا می‌تواند ابتدا آن را نرم‌زد کند. به گونه‌ای که الگوریتم انتخاب شده است، تصویری که برای محاسبه هش استفاده می‌شود به صورت کمی متفاوت خواهد بود. الگوریتم سریع‌تر برای استفاده وجود دارد، ولی نتایج آن کسب‌کننده پایین‌تری هستند. الگوریتم نزدیک‌تر (Nearest) به طور پیش‌فرض فعال است زیرا با حجم 16x16 کمترین کیفیت این کسیزه که نظار قابل توجهی برای بین‌داشت ندارد. با حجم 8x8 کسیزه، پیشنهاد می‌شود به جای الگوریتم نزدیک‌تر (Nearest) از یک الگوریتم دیگر استفاده شود تا گروه‌های تصویر بگذرند. image_hash_alg_tooltip = کاربران می‌توانند از یکی از دستورالعمل‌های بسیاری برای محاسبه هاش که در اختیار آن‌ها قرار داده شده است، پیشنهاد دستورالعمل خود را انتخاب کنند. هر یک این‌ها نقطه قوت و ضعف کلیدی دارند و برای تصاویر مختلف همگرایتر یا نامطلوبتر می‌توانند عملکرد خود را نشان دهند. بنابراین، برای تعیین بهترین یکی برای شما، آزمون دستی ضروری است. big_files_mode_combobox_tooltip = می‌پذیرد جستجوی فایل‌های کوچکتر/بزرگتر را big_files_mode_label = فایل‌های بررسی شده big_files_mode_smallest_combo_box = تازیست big_files_mode_biggest_combo_box = بزرگترین main_notebook_duplicates = فایل‌های تکراری main_notebook_empty_directories = دایرکتوری خالی main_notebook_big_files = فایل‌های بزرگ main_notebook_empty_files = فایل‌های خالی main_notebook_temporary = فایل‌های موقت main_notebook_similar_images = تصورهای مشابه main_notebook_similar_videos = فیلم‌های مشابه main_notebook_same_music = دупلیکات مузیک main_notebook_symlinks = سینمک های معتبر ناامن main_notebook_broken_files = فایل‌های ضارب main_notebook_bad_extensions = برچسب‌های بد main_tree_view_column_file_name = نام فایل main_tree_view_column_folder_name = نام پوشه main_tree_view_column_path = پادکست main_tree_view_column_modification = تاریخ بروزرسانی main_tree_view_column_size = حجم main_tree_view_column_similarity = شبیهسازی main_tree_view_column_dimensions = بعدیون main_tree_view_column_title = عنوان main_tree_view_column_artist = کارگردان main_tree_view_column_year = سال main_tree_view_column_bitrate = بریتیتی کدگذاری main_tree_view_column_length = طول main_tree_view_column_genre = ژانر main_tree_view_column_symlink_file_name = لینک ناشی فایل main_tree_view_column_symlink_folder = تیم‌پیکس فولدر main_tree_view_column_destination_path = پیشانته مسیر main_tree_view_column_type_of_error = نوع خطا main_tree_view_column_current_extension = بازگشت بسته شده main_tree_view_column_proper_extensions = توضیح معتبر main_tree_view_column_fps = فپس main_tree_view_column_codec = کدک main_label_check_method = روش چک کردن main_label_hash_type = نوع هاش main_label_hash_size = سایز هاش main_label_size_bytes = حجم (بایت) main_label_min_size = مین main_label_max_size = مکس main_label_shown_files = تعداد فایل‌های نمایش‌یافته main_label_resize_algorithm = الگوریتم سایز بندی main_label_similarity = سхожگی{ " " } main_check_box_broken_files_audio = آوتویډ main_check_box_broken_files_pdf = پdf main_check_box_broken_files_archive = اریکرج main_check_box_broken_files_image = عکس main_check_box_broken_files_video = ویدئو main_check_box_broken_files_video_tooltip = از ffmpeg/ffprobe برای اعتبارسنجی فایل‌های ویدیویی استفاده می‌کند. نسبتاً کند است و ممکن است خطاها را به صورت دقیق تشخیص دهد حتی اگر فایل به درستی پخش شود. check_button_general_same_size = تنها سایز‌های مختلف را تغییر ندهید check_button_general_same_size_tooltip = فایل‌های مشابه سایز آن‌ها در نتایج را ترک کنید - معمولاً این فایل‌ها دوباره تولید شده‌اند (1:1) main_label_size_bytes_tooltip = حجم فایل‌هایی که در طول سcan استفاده خواهند شد # Upper window upper_tree_view_included_folder_column_title = پوشه‌هایی که جستجو کنید upper_tree_view_included_reference_column_title = دایرکتوری های مرجع upper_recursive_button = پیچیده‌ترین upper_recursive_button_tooltip = اگر انتخاب شد، به دنبال فایل‌هایی نیز جستجو کنید که قطعاً در پوشه‌های گرفته‌شده مستقیماً وجود ندارند. upper_manual_add_included_button = افزودن دستی upper_add_included_button = افزودن upper_remove_included_button = حذف upper_manual_add_excluded_button = متن دستی اضافه upper_add_excluded_button = افضال upper_remove_excluded_button = حذف upper_manual_add_included_button_tooltip = درآماد نام دایرکتوری را برای جستجو با دست اضافه کنید. برای بارگذاری چندین مسیر به طور همزمان، آنها را با یک ';' جدا کنید; /home/رومان;/home/روزکaz خواهد باعث اضافه شدن دو دایرکتوری /home/رومان و /home/روزکaz می‌شود upper_add_included_button_tooltip = دایرکتوری جدید به جستجو اضافه کنید. upper_remove_included_button_tooltip = پوشه را از جستجو حذف کنید. upper_manual_add_excluded_button_tooltip = مدیریت نام دایرکتوری استراحت شده را به صورت håد良心化处理结果: 保持相同的语气和风格。保留任何特殊格式或占位符。 仅返回翻译文本,不提供解释或其他额外文本。 ترجمه: نام دایرکتوری استراحت شده را به صورت håد و يدی اضافه کنید. برای اضافه کردن چندین مسیر همزمان، آن‌ها را با ؛ جدا کنید; /home/roman;/home/krokiet دو دایرکتوری /home/roman و /home/keokiet را اضافه خواهد کرد upper_add_excluded_button_tooltip = پوشه‌ای برای جستجوی مورد بندی خارج شود. upper_remove_excluded_button_tooltip = دایرکتوری را از مورد نادیده‌گرفتن حذف کنید. upper_notebook_items_configuration = تنظیم‌های موارد upper_notebook_excluded_directories = مسیرهای حذف‌شده upper_notebook_included_directories = مسارات شامل‌شده upper_allowed_extensions_tooltip = امتدادهای مجاز باید با کاما جدا شوند (به طور پیش فرض همه موجود هستند). مacروهای زیر نیز موجود است که تعدادی از امتدادات را به طور یکتایی اضافه می‌کنند: تصویر، ویدئو، موسیقی، متن. مثال کاربردی ".exe, تصویر, ویدئو, .rar, 7z" - این به معنی است که تصاویر (مثلاً jpg, png)، ویدئوها (مثلاً avi, mp4)، exe، rar و 7z درخواست بررسی خواهند شد. upper_excluded_extensions_tooltip = فهرست فایل‌های غیرفعال که در اسکن نادیده گرفته می‌شوند. هنگام استفاده از همچون پسوندها مجاز و غیرفعال، این یکی اولویت بالاتری دارد، بنابراین فایل بررسی نخواهد شد. upper_excluded_items_tooltip = موارد حذف شده باید شامل * wildcard و باید با کاما از هم جدا شوند. این کندتر از مسیرهای حذف شده است، بنابراین از آن با دقت استفاده کنید. upper_excluded_items = آیتم‌های حذف شده: upper_allowed_extensions = مدت زمان مجاز: upper_excluded_extensions = توسعه‌های غیرفعال: # Popovers popover_select_all = انتخاب همه popover_unselect_all = همه را مخاطب نکنید popover_reverse = انتخاب را برگردانید popover_select_all_except_shortest_path = انتخاب همه چیز به جز کوتاه‌ترین مسیر popover_select_all_except_longest_path = انتخاب همه موارد به جز طولانی‌ترین مسیر popover_select_all_except_oldest = انتخاب همه غیر از قدیمی nhất popover_select_all_except_newest = انتخاب همه مواردی که نوزاد نیستند popover_select_one_oldest = انتخاب یکی از قدیمی‌ترینanst popover_select_one_newest = انتخاب یک نوستین popover_select_custom = انتخاب خاصیتutzer Merkel popover_unselect_custom = غير فردی را بیگانه کنید popover_select_all_images_except_biggest = انتخاب همه از بیشترین به جز خود را حذف کنید popover_select_all_images_except_smallest = انتخاب همه می‌شود به جز کوچکترین popover_custom_path_check_button_entry_tooltip = انتخاب رکوردها با مسیر. مثال استفاده: /home/pimpek/rzecz.txt را با /home/pim* پیدا کنید popover_custom_name_check_button_entry_tooltip = فایل‌ها را با نام‌های فایل انتخاب کنید. رایکسی از /usr/ping/pong.txt با *ong* می‌تواند پیدا شود popover_custom_regex_check_button_entry_tooltip = انتخاب رکوردها با استفاده از Regular Expression مشخص شده. در این حالت، متن جستجو پیش سمت (Path) و نام (Name) است. مثال کاربردی: /usr/bin/ziemniak.txt را با /ziem[a-z]+ پیدا کرد. این مورد استفاده‌ی پیاده‌سازی پیش‌فرض Rust برای Regular Expression است. شما می‌توانید بیشتر درباره آن در این لینک فهم득 بگیرید: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = توانایی تشخیص حساس به حروف بزرگ و کوچک را فعال می‌کند. در صورت غیرفعال بودن، /home/* هر دو /HoMe/roman و /home/roman را پیدا خواهد کرد. popover_custom_not_all_check_button_tooltip = پیش‌بینی تهیه همه رکورد در گروه را جلوگیری می‌کند. با فعال بودن این تنظیم به طور پی‌这里是翻译的后半部分,按照要求保持格式和占位符不变: این معیار اولیه فعال است، زیرا در بسیاری از موارد، نمی‌توانید هر دو فایل اصلی و تکراری را پاک کنید و می‌خواهید حداقل یک فایل را نگه دارید. Warning: این تنظیمات در صورت انتخاب دستی همه نتایج در گروه، عملکرد خود را ندارند. popover_custom_regex_path_label = مسیر popover_custom_regex_name_label = نام popover_custom_regex_regex_label = پاتч ریگекс + نام popover_custom_case_sensitive_check_button = حساس به حروف تکیه‌گاه popover_custom_all_in_group_label = همه رکورد در گروه را انتخاب نکنید popover_custom_mode_unselect = بی‌پیکربندی خود را انتخاب نکنید popover_custom_mode_select = انتخاب مورد خاص popover_sort_file_name = نام فایل popover_sort_folder_name = نام پوشه popover_sort_full_name = نام کامل popover_sort_size = حجم popover_sort_selection = نتیجه‌گیری popover_invalid_regex = رگEXP نامعتبر است popover_valid_regex = регEXP معتبر است # Bottom buttons bottom_search_button = جستجو bottom_select_button = انتخاب bottom_delete_button = حذف bottom_save_button = ذخیره bottom_symlink_button = Symlink bottom_hardlink_button = هاردلینک bottom_move_button = برو bottom_sort_button = مرتب کردن bottom_compare_button = مقایسه bottom_search_button_tooltip = ابدیه سرچشمه bottom_select_button_tooltip = انتخاب رکوردها. فقط فایل‌های/پوشه‌های انتخاب شده می‌توانند پس از آن پردازش شوند. bottom_delete_button_tooltip = حذف فایل‌های/دستگاه‌های انتخاب‌شده. bottom_save_button_tooltip = اطلاعات جستجو را در فایل ذخیره کنید bottom_symlink_button_tooltip = پیوندهای نمادین ایجاد کنید. تنها زمانی کار می‌کند که حداقل دو نتیجه در یک گروه انتخاب شوند. اولین بخش تغییری ندارد و دومین و سایر آن‌ها به اولین لینک نمادین می‌شوند. bottom_hardlink_button_tooltip = لینک‌های سخت را ایجاد کنید. فقط زمانی کار می‌کند که حداقل دو نتیجه در یک گروه انتخاب شده باشد. اولی تغییر نمی‌کند و دومی و بعدی به اولی لینک سخت پیدا می‌کنند. bottom_hardlink_button_not_available_tooltip = ساخت لینک‌های قوی را اجرا کنید. دکمه فعال نیست، زیرا لینک‌های قوی ساخته نمی‌شوند. لینک‌های قوی تنها در صورت داشتن امتیازات مدیریت‌کننده روی Windows کار می‌کنند، بنابراین مطمئن شوید که برنامه را به طور مدیریت‌کننده اجرا کرده‌اید. در صورتی که برنامه با این امتیازات کار کند، مشاler مشابه روی Github بررسی کنید. bottom_move_button_tooltip = پیکر فایل‌ها را به مسیر انتخاب شده منتقل می‌کند. او همه فایل‌ها را به مسیر منتقل می‌کند بدون نگه داشتن درخت مسیری. وقتی سعی می‌کنید دو فایل با نام تکراری را به مسیری منتقل کنید، دومی با خطا مواجه خواهد شد. bottom_sort_button_tooltip = فرمت فایل‌ها/دسته‌بندی‌ها را بر اساس روش مورد انتخاب تنظیم کند. bottom_compare_button_tooltip = مقایسه تصاویر در گروه را انجام دهید. bottom_show_errors_tooltip = نمایش/起底部文本面板。. bottom_show_upper_notebook_tooltip = نمایش/پنهان کردن پanel بالایی نوت‌بук. # Progress Window progress_stop_button = وقفه progress_stop_additional_message = توقف درخواست شده # About Window about_repository_button_tooltip = پیوند به صفحه نگهدارنده کد منبع. about_donation_button_tooltip = لینک به صفحه‌ی سپرده‌گذاری. about_instruction_button_tooltip = لینک به صفحه دستورالعمل. about_translation_button_tooltip = لینک به صفحه Crowdin برای ترجمه‌ی برنامه. فارسی رسمی و انگلیسی مورد پشتیبانی قرار دارند. about_repository_button = آرشیو about_donation_button = تبرعات about_instruction_button = ارتباطات about_translation_button = ترجمه: # Header header_setting_button_tooltip = دیالوگ تنظیمات را باز می‌کند. header_about_button_tooltip = پنجره دیالوگ با اطلاعات دربارهٔ اپ را باز می‌کند. # Settings ## General settings_number_of_threads = تعداد خيوان‌های استفاده شده settings_number_of_threads_tooltip = تعداد سرورهای استفاده‌شده، ۰ به معنای استفاده از تمامی سرورهای در دسترس است. settings_use_rust_preview = بجای gtk، از بиблиو梯es خارجی برای بارگذاری نمایش‌ها استفاده کنید settings_use_rust_preview_tooltip = استفاده از پیش‌نماهای GTK گاهی سریع‌تر خواهد بود و حمایت از بیشتر فرمت‌ها را دارد، اما گاهی دقیقاً عکس آن است. اگر با بارگذاری پیش‌نمای مشکلی دارید، ممکن است بتوانید به تغییر این تنظیم بپردازید. در سیستم‌های غیر-Linux، توصیه می‌شود این گزینه را استفاده کنید، زیرا gtk-pixbuf همیشه در آنجا دسترسی پذیر نیستند بنابراین تعطیل کردن این گزینه باعث می‌شود پیش‌نمای برخی تصاویر بارگذاری نشوند. settings_label_restart = شما باید برنامه را از نو انیمود تا تنظیمات را اعمال کنید! settings_ignore_other_filesystems = سایر سیستم‌های فایل را نادیده بگیرید (فقط لینوکس) settings_ignore_other_filesystems_tooltip = پروژه فایل‌هایی را نادیده می‌گیرد که در سیستم‌فایل‌های مشترک با دایرکتوری‌های جستجو نیستند. به طور مشابه عملکرد -xdev گزینه در دستور find در روی سیستم عامل لینوکس است settings_save_at_exit_button_tooltip = هنگام بسته شدن برنامه، تنظیمات را در فایل ذخیره کنید. settings_load_at_start_button_tooltip = به زمانی که برنامه را می‌افزایید، تنظیمات را از فایل بارشید. اگر غیرفعال است، تنظیمات پیش‌فرض به کار خواهند رفت. settings_confirm_deletion_button_tooltip = وقتی دکمه حذف را کلیک می‌کنید، پنجره تأیید نمایش داده شود. settings_confirm_link_button_tooltip = در زمان کلیک بر روی دکمه لینک سخت/شبکه، یک پیامگذاری تأیید را نشان دهد. settings_confirm_group_deletion_button_tooltip = وقتی همه رکوردهای گروه را حذف می‌کنید، حذف پیشنهادی را به صورت پیامWarning نشان دهید. settings_show_text_view_button_tooltip = بر روی بخش متنی در پایین رابط کاربری نمایش دهید. settings_use_cache_button_tooltip = از کاشی فایل استفاده کنید. settings_save_also_as_json_button_tooltip = اندازه‌گیری کش را به فرمت JSON خوانا ذخیره کنید. محتوای آن قابل ویرایش است. کش از این فایل تلقیه می‌شود، در صورتی که کش با فormат باینری (با پسوند bin) مفقود باشد. settings_use_trash_button_tooltip = فایل‌ها را به سبد حذفی منتقل می‌کند تا به طور دائم حذف نشوند. settings_language_label_tooltip = زبان برای سطح کاربری. settings_save_at_exit_button = تنظیمات را در زمان بسته شدن برنامه ذخیره کنید settings_load_at_start_button = تنظیمات را در زمان باز کردن اپلیکاسیون بارگذاری کنید settings_confirm_deletion_button = پیام مطمئن شدن در حذف هر فایل را نشان دهید settings_confirm_link_button = در زمان تغییرات مختصر یا لینک سختی فایل‌ها، پنجره توافقنامه تایید را نشان دهید settings_confirm_group_deletion_button = در حذف تمامی فایل‌های گروه، دایالوگ تأیید را نشان بدهید settings_show_text_view_button = پانل متن پایین را نشان دهید settings_use_cache_button = استفاده از کشCACHE settings_save_also_as_json_button = همچنین کش exists را به فایل JSON پاک کنید settings_use_trash_button = فایل‌های حذف شده را به سبد اسکن منتقل کنید settings_language_label = زبان settings_multiple_delete_outdated_cache_checkbutton = موارد کش از دستاره را به طور خودکار حذف کنید settings_multiple_delete_outdated_cache_checkbutton_tooltip = حذف نتایج کش قدیمی که به فایل‌هایی بازندگانی اشاره دارند. وقتی فعال شده، برنامه مطمئن می‌شود زمان بارگذاری رکوردها، تمام رکوردها به فایل‌های معتبر اشاره داشته باشند (فرچمند های خراب تجاهل شده‌اند). Dezactivه کردن این گزینه وقتی که در حال پرسپект فایل‌ها روی حملات خارجی است به نفع خواهد بود، بنابراین ورودی‌های کش مربوط به آن‌ها در نظر آینده حذف نخواهند شد. در صورت داشتن صد هزار رکورد در کش، پیشنهاد می‌شود این گزینه را فعال کنید، که خشونت بارگذاری/ذخیره کش در شروع و پایان پرسپект را تسریع خواهد کرد. settings_notebook_general = عمومی settings_notebook_duplicates = 副本 settings_notebook_images = سایر عکس‌های مشابه settings_notebook_videos = 비슷 آموزش ویدیو ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = نمایش نمونه در سمت راست (هنگام انتخاب فایل تصویر). settings_multiple_image_preview_checkbutton = نتیجه نمایش تصویر را نشان دهید settings_multiple_clear_cache_button_tooltip = با håندکی حذف مخزن موارد قدیمی را پاک کنید. این تنها در صورت غیرفعال بودن پاک‌شدن خودکار استفاده شود. settings_multiple_clear_cache_button = برای حذف نتایج قدیمی از کش، استفاده کنید. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = پیچش تمام فایل‌ها را پنهان می‌کند، به جز یک فایل، اگر همه به همان داده‌ای مشیر باشند (هم‌الودین شده‌اند). مثال: در صورت وجود هفت فایل (در حاشیه دیسک) که به داده‌های خاص مشیر هستند و یک فایل متفاوت با داده‌ها، اما inode متفاوت، در پیدا کننده تکرار، فقط یک فایل متمایز و یکی از آن فایل‌های هم‌الودین نشان داده خواهد شد. settings_duplicates_minimal_size_entry_tooltip = سیزه‌ای از فایل کمتر را که به طور کشید خاموش خواهد شد تعیین کنید. انتخاب مقدار بزرگتر خواهد منجر به تولید مراکز زیادتر شده است. این به جستجو سریع‌تر کمک خواهد کرد، اما آماده‌سازی و ذخیره سازی کش را به سوی سرفه خواهد نقل می‌کرد. settings_duplicates_prehash_checkbutton_tooltip = تاکید بر این است که کش (قaches پیش هاس) که از یک بخش کوچک از فایل محاسبه می‌شود، ذخیره شود که در نتایج تکراری‌نشدنی از راه حذف آنها قبلی‌تر می‌شود. در صورتی که این دستورالعمل غیر فعال است، به طور پیش‌فرض فعال نمی‌شود زیرا در برخی اوضاع می‌تواند منجر به تأخیرات شود. این جایگزین خوبی برای استفاده در جستجوی هزاران یا میلیون فایل است، زیرا می‌تواند جستجو را بسیار سریع‌تر کند. settings_duplicates_prehash_minimal_entry_tooltip = اندازه مینیمالی از ورودی کشی. settings_duplicates_hide_hard_link_button = پنهان کردن لینک‌های سخت settings_duplicates_prehash_checkbutton = استفاده از کاش پیش‌هاشتاپ settings_duplicates_minimal_size_cache_label = حجم مínیمم فایل‌های ذخیره شده در کاش (در بیت) settings_duplicates_minimal_size_cache_prehash_label = حجم مینیموم فایل‌ها (در بایتس) ذخیره شده در کش پرسیپت‌ها ## Saving/Loading settings settings_saving_button_tooltip = تنظیمات فعلی را به فایل ذخیره کنید. settings_loading_button_tooltip = تنظیمات را از فایل بارش و جایگزینی تنظیمات موجود با آنها انجام دهید. settings_reset_button_tooltip = تغییر از تنظیمات فعلی به تنظیم پیش‌فرض را بازیابی کنید. settings_saving_button = تنظیمات را ذخیره کنید settings_loading_button = تغییر تنظیمات را بارگذاری کنید settings_reset_button = تنظیمات را بازنشانی کنید ## Opening cache/config folders settings_folder_cache_open_tooltip = کافیست پوشه‌ای که فایل‌های txt کاشینگ را ذخیره می‌کند باز شود. تغییر در فایل‌های کاش ممکن است منجر به نمایش نتایج نامعتبر شوند. با این حال، تغییر در مسیر ممکن است وقت بیشتری را برای منتقل کردن مقدار زیادی از فایل‌ها به موقعیت مختلف ارزیابی کند. شما می‌توانید این فایل‌ها را بین ماکینتها نسخه و پیوند دهید تا وقت را در بررسی دوباره برای فایل‌ها (بله، اگر ساختار پوشه‌ها مشابه باشند) کاهش دهید. در صورت مشکلاتی که با کاش وجود دارد، این فایل‌ها می‌توانند حذف شوند. نرم‌افزار تا زمانی که لازم باشد به طور خودکار آن‌ها را مجدد ایجاد خواهد کرد. settings_folder_settings_open_tooltip = دایرکتوری مربوطه را که تنظیمات Czkawka در آن ذخیره شده‌اند، باز کنید. هشدار: تغییر دستی در تنظیمات ممکن است عملکرد شما را شکسته‌سازی کند. settings_folder_cache_open = پوشه کاشی را باز کنید settings_folder_settings_open = پوشه تنظیمات را باز کنید # Compute results compute_stopped_by_user = جستجو توسط کاربر متوقف شد compute_found_duplicates_hash_size = یافته { $number_files } کپی برابر را در { $number_groups } گروه که { $size } را در { $time } صرف کرد compute_found_duplicates_name = Duplication { $number_files } فایل در { $number_groups } گروه در { $time } پیدا شد پیدا شد compute_found_empty_folders = فولدرهای خالی پیدا شد { $number_files } در زمان { $time } compute_found_empty_files = فایل‌های خالی پیدا شد: { $number_files } در زمان: { $time } compute_found_big_files = فایل‌های بزرگ { $number_files } تا در { $time } یافته شدند compute_found_temporary_files = فایل‌های موقت { $number_files } تا { $time } پیدا شدند compute_found_images = { $number_files } تصویر مرتبط در { $number_groups } گروه پیدا شد در { $time } compute_found_videos = { $number_files } ویدیو مشابه در { $number_groups } گروه پیدا شد در زمان { $time } compute_found_music = فایل‌های موسیقی مشابه { $number_files } را در { $number_groups } گروه و در زمان { $time } پیدا کردیم compute_found_invalid_symlinks = { $number_files } لینک معلق نامعتبر در { $time } پیدا شد compute_found_broken_files = فایل‌های شکسته { $number_files } را در { $time } پیدا کردم compute_found_bad_extensions = فایل‌هایی با پسوند نامعتبر در { $time } شماره { $number_files } پیدا کرد # Progress window progress_scanning_general_file = { $file_number -> [one] Scanned { $file_number } file *[other] Scanned { $file_number } files } progress_scanning_extension_of_files = رسیدن افزوده { $file_checked }/{ $all_files } فایل progress_scanning_broken_files = 检讨 شده { $file_checked }/{ $all_files } فایل ({ $data_checked }/{ $all_data }) progress_scanning_video = همچین تoning و { $file_checked }/{ $all_files } ویدیو progress_creating_video_thumbnails = تصاویر کوچک آماده شده از { $file_checked }/{ $all_files } ویدیو progress_scanning_image = قلمری از { $file_checked }/{ $all_files } تصویر ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = مقایسه { $file_checked }/{ $all_files } علامت شش تصویر progress_scanning_music_tags_end = مقایسه برچسب‌های { $file_checked }/{ $all_files } فایل موسیقی progress_scanning_music_tags = برچسب‌های آهنگ { $file_checked }/{ $all_files } را بخوانید progress_scanning_music_content_end = مقایسه от亮指纹 از { $file_checked }/{ $all_files } فایل موسیقی progress_scanning_music_content = چربیگانه چک شده از { $file_checked }/{ $all_files } فایل موسیقی ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Scanned { $folder_number } folder *[other] Scanned { $folder_number } folders } progress_scanning_size = حجم سcanشده فایل №{ $file_number } progress_scanning_size_name = نام و اندازه سند مورد بررسی { $file_number } progress_scanning_name = نام فایل { $file_number } را برگشت دهید progress_analyzed_partial_hash = جزییات هش部门/ TümDosya ({ $data_checked }/{ $all_data }) را بررسی کرد /{ $file_checked }/{ $all_files } progress_analyzed_full_hash = تحلیل کامل هش فایل‌های "{ $file_checked }/{ $all_files }" ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Cache پیش هاش خارج شده را بارگذاری کنید progress_prehash_cache_saving = مخفف پیش‌هایش کاشینگ را ذخیره کنید progress_hash_cache_loading = برخیسندهٔ حاشیه هش progress_hash_cache_saving = ذهنشدن حاشیهٔ کشیده progress_cache_loading = فیلتر کشی را بارگذاری می‌کنیم progress_cache_saving = ذخیره کشی progress_current_stage = مرحله حاضر:{ " " } progress_all_stages = همه مراحل:{ " " } # Saving loading saving_loading_saving_success = پیکربندی را به فایل { $name } ذخیره شد. saving_loading_saving_failure = غیرهایی برای ذخیره داده‌های تنظیم‌های سازنده در فایل { $name } وجود ندارد، علت { $reason }. saving_loading_reset_configuration = 구성 حاضر تهی شد. saving_loading_loading_success = انگشتی که به درستی تنظیم شده تطبيقی ترجیح دارد. saving_loading_failed_to_create_config_file = ایجاد فایل کonfig شکست خورد، دلیل "{ $reason }" برای پت { $path }". saving_loading_failed_to_read_config_file = Niet می‌توان از کنسولگی را از "{ $path }" بارگذاری کرد چون آن وجود ندارد یا یک فایل نیست. saving_loading_failed_to_read_data_from_file = نمی‌توان از پرونده "{ $path }" داده‌ها را خواند، دلیل "{ $reason }". # Other selected_all_reference_folders = نemی‌توان به جستجو آغاز کرد، زمانی که تمام پوشه‌ها را به عنوان فرایند ارجاع تنظیم کرده‌اید searching_for_data = Suche‌ن داده‌‌ها را جستجو می‌کنم، ممکن است کمی زمان ببرد، لطفا منتظر باشید... text_view_messages = پیام‌ها text_view_warnings = تحذیرات text_view_errors = خطاها about_window_motto = این برنامه رایگان است و همیشه رایگان باقی خواهد ماند. krokiet_new_app = پروژه Czkawka در حالت نگهداری است که به این معنایی است که تنها خطاهای حیاتی وارد سربرگ خواهند شد و بدون نیاز به اضافه‌کردن موارد جدید. برای موارد جدید، لطفاً پروژه Krokiet جدید را بررسی کنید، که سبک‌تر و عملکرد بهتری دارد و همچنان تحت توسعه فعال است. # Various dialog dialogs_ask_next_time = مرتبه بعد سپس بپرس symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason } delete_title_dialog = تایید حذف delete_question_label = آیا از حذف فایل‌ها مطمئن هستید؟ delete_all_files_in_group_title = تأیید حذف تمام فایل‌های در گروه delete_all_files_in_group_label1 = در برخی گروه‌ها تمام رکوردها انتخاب می‌شوند. delete_all_files_in_group_label2 = فرمایید که حذف آن‌ها را مطمئn هستید؟ delete_items_label = { $items } فایل‌هایی خواهند پاک شد. delete_items_groups_label = { $items } فایل از { $groups } گروه خواهند شوی. hardlink_failed = به ترکیب مجدد { $name } به { $target } امکان پذیر نشد، دلیل { $reason } hard_sym_invalid_selection_title_dialog = بازخورد نامعتبر با گروه‌هایی از جمله hard_sym_invalid_selection_label_1 = در برخی گروه‌ها تنها یک رکورد انتخاب شده است و آن مورد忽略。. hard_sym_invalid_selection_label_2 = برای توانا به صورت سخت/هم لینک این فایل‌ها، حداقل دو نتیجه در گروه باید انتخاب شوند. hard_sym_invalid_selection_label_3 = اولین در گروه به عنوان اصلی شناخته می‌شود و تغییر نمی‌کند اما دوم و بعدی عرضه شده‌اند. hard_sym_link_title_dialog = تأیید لینک hard_sym_link_label = آیا مطمئن هستید که می‌خواهید این فایل‌ها را پیوست؟ move_folder_failed = در حرکت دایرکتوری { $name } موفق نشد، دلیل "{ $reason }" move_file_failed = خطا در نقلovanدن فایل { $name }، دلیل "{ $reason }" move_files_title_dialog = تیم پوشه‌ای را که می‌خواهید فایل‌های تکثیر شده را به آن منتقل نمایید move_files_choose_more_than_1_path = فقط یک مسیر انتخاب شده باشد تا می‌توانند فایل‌های خود را کپی کنند، انتخاب شده { $path_number }. move_stats = مناسب حرکت { $num_files }/{ $all_files } موارد save_results_to_file = نتایج ذخیره شده به همراه فایل‌های txt و json در مسیر دایرکتوری "{ $name }" هستند. search_not_choosing_any_music = خطا: شما باید حداقل یک قيدچيكو با رنگ‌هاي جستجو مuzic را انتخاب کنید. search_not_choosing_any_broken_files = خطا: شما باید حداقل یک گزینه با نوع خرابی فایل‌های بریده انتخاب کنید. include_folders_dialog_title = پوشه‌هایی شامل این باید باشند exclude_folders_dialog_title = دسته‌ها را که حذف شود نگهداری کنید include_manually_directories_dialog_title = دایرکتوری را به صورت دستی اضافه کنید cache_properly_cleared = برای مطمئن شدن، کش خروجی را به درستی پاک کرده‌اید cache_clear_duplicates_title = حذف کپی‌های تکراری از کش cache_clear_similar_images_title = برکناره‌گیری کشی تصاویر مشابه cache_clear_similar_videos_title = پاکسازی کش خارجی موارد مشابه Videوهای سفارشی cache_clear_message_label_1 = آیا می‌خواهید پشته داده‌های سرگردان را پاک کنید؟ cache_clear_message_label_2 = این عملیات تمام دختران کش جاروی که به فایل‌های نامعتبر اشاره می‌کنند را حذف خواهد کرد. cache_clear_message_label_3 = این ممکن است به کندی خواندن/درج به حافظه کمی سریع‌تر شود. cache_clear_message_label_4 = هشدار: عملیات مذکور همه داده‌های کش پاشیده از درایв‌های خارجی دور نرم خواهد کرد. بنابراین هر هش باید دوباره تولید شود. # Show preview preview_image_resize_failure = حداکثر سایز تصویر { $name } را تنظیم نشد. preview_image_opening_failure = تصویر { $name } را باز کردن شکسته است، причина { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = گروه { $current_group }/{ $all_groups } ({ $images_in_group } تصویر) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/fr/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Paramètres window_main_title = Czkawka (Hoquet) window_progress_title = Analyse en cours window_compare_images = Comparer les images # General general_ok_button = Ok general_close_button = Fermer # Krokiet info dialog krokiet_info_title = Présentation de Krokiet - Nouvelle version de Czkawka krokiet_info_message = Krokiet est la nouvelle version améliorée, plus rapide et plus fiable de l'interface graphique GTK Czkawka ! 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. 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. Essayez-le et constatez la différence ! 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 contribuer à l'ajout de nouvelles fonctionnalités, de modes manquants ou à l'extension de Czkawka. 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. # Main window music_title_checkbox = Titre de la page music_artist_checkbox = Artiste music_year_checkbox = Année music_bitrate_checkbox = Débit binaire music_genre_checkbox = Genre music_length_checkbox = Longueur music_comparison_checkbox = Comparaison approximative music_checking_by_tags = étiquettes music_checking_by_content = Contenu same_music_seconds_label = Durée minimale de seconde de fragment same_music_similarity_label = Différence maximale music_compare_only_in_title_group = Comparer au sein des groupes de titres similaires music_compare_only_in_title_group_tooltip = Lorsque cette option est activée, les fichiers sont regroupés par titre, puis comparés l'un à l'autre. Pour 10000 fichiers, au lieu de près de 100 millions de comparaisons en général, il y aura environ 20000 comparaisons. same_music_tooltip = La recherche de fichiers musicaux aux contenus similaires peut être configurée en définissant : - La durée minimale d'un fragment pour que des fichiers musicaux soient identifiés comme similaires - La différence maximale entre deux fragments testés La clé pour arriver à de bons résultats est de trouver des combinaisons raisonnables de ces paramètres. Fixer le temps minimum à 5 secondes et la différence maximale à 1.0, cherchera des fragments presque identiques dans les fichiers. Un temps de 20 secondes et une différence maximale de 6.0 fonctionne bien pour trouver des remixes/versions live, etc. 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). music_comparison_checkbox_tooltip = 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 : Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Sensible à la casse duplicate_case_sensitive_name_tooltip = Quand activé, groupe les enregistrements uniquement quand ils ont exactement le même nom, par exemple Żołd <-> Żołd Désactiver cette option va regrouper les noms sans se préocupper de la casse, par exemple żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Taille et nom duplicate_mode_name_combo_box = Nom duplicate_mode_size_combo_box = Taille duplicate_mode_hash_combo_box = Hachage duplicate_hash_type_tooltip = Czkawka offre 3 types de hachages : Blake3 - fonction de hachage cryptographique. Il est utilisé comme algorithme de hachage par défaut car très rapide. CRC32 - fonction de hachage simple qui devrait être plus rapide que Blake3. Peut, très rarement, provoquer des collisions. 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. duplicate_check_method_tooltip = Pour l'instant, Czkawka offre trois types de méthode pour trouver des doublons par : Nom - Trouve des fichiers qui ont le même nom. Taille - Trouve des fichiers qui ont la même taille. 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. image_hash_size_tooltip = 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. 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. 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. 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). image_resize_filter_tooltip = Pour calculer le hachage de l'image, la bibliothèque doit d'abord la redimensionner. En fonction de l'algorithme choisi, l'image résultante utilisée pour calculer le hachage pourra sembler un peu différente. 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. 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. image_hash_alg_tooltip = Les utilisateurs peuvent choisir parmi de nombreux algorithmes pour calculer le hash. Chacun a des points forts et des points faibles et donnera parfois des résultats meilleurs et parfois pires pour des images différentes. Par conséquent, des tests manuels sont requis pour déterminer celui qui donnera le meileur résultat pour vous. big_files_mode_combobox_tooltip = Permet de rechercher les fichiers les plus petits ou les plus grands big_files_mode_label = Fichiers cochés big_files_mode_smallest_combo_box = Le plus petit big_files_mode_biggest_combo_box = Le plus grand main_notebook_duplicates = Fichiers en double main_notebook_empty_directories = Dossiers vides main_notebook_big_files = Gros fichiers main_notebook_empty_files = Fichiers vides main_notebook_temporary = Fichiers temporaires main_notebook_similar_images = Images similaires main_notebook_similar_videos = Vidéos similaires main_notebook_same_music = Doublons de musique main_notebook_symlinks = Liens symboliques invalides main_notebook_broken_files = Fichiers cassés main_notebook_bad_extensions = Mauvaises extensions main_tree_view_column_file_name = Nom du fichier main_tree_view_column_folder_name = Nom du dossier main_tree_view_column_path = Chemin d'accès main_tree_view_column_modification = Date de modification main_tree_view_column_size = Taille main_tree_view_column_similarity = Similitude main_tree_view_column_dimensions = Dimensions main_tree_view_column_title = Titre main_tree_view_column_artist = Artiste main_tree_view_column_year = Année main_tree_view_column_bitrate = Débit binaire main_tree_view_column_length = Longueur main_tree_view_column_genre = Genre main_tree_view_column_symlink_file_name = Nom du lien symbolique main_tree_view_column_symlink_folder = Dossier du lien symbolique main_tree_view_column_destination_path = Chemin de destination main_tree_view_column_type_of_error = Type d'erreur main_tree_view_column_current_extension = Extension actuelle main_tree_view_column_proper_extensions = Extension correcte main_tree_view_column_fps = FPS main_tree_view_column_codec = Codec main_label_check_method = Méthode de vérification main_label_hash_type = Type de hachage main_label_hash_size = Taille du hachage main_label_size_bytes = Taille (octets) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Nombre de fichiers affichés main_label_resize_algorithm = Algorithme de redimensionnement main_label_similarity = Similarité{ " " } main_check_box_broken_files_audio = Audio main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Archiver main_check_box_broken_files_image = Image main_check_box_broken_files_video = Vidéo main_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. check_button_general_same_size = Ignorer la même taille check_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 main_label_size_bytes_tooltip = Taille des fichiers qui seront utilisés lors de l'analyse # Upper window upper_tree_view_included_folder_column_title = Dossiers dans lesquels chercher upper_tree_view_included_reference_column_title = Dossiers de référence upper_recursive_button = Récursif upper_recursive_button_tooltip = Si sélectionné, rechercher également les fichiers qui ne sont pas placés directement dans les dossiers choisis. upper_manual_add_included_button = Ajout manuel upper_add_included_button = Ajouter upper_remove_included_button = Retirer upper_manual_add_excluded_button = Ajout manuel upper_add_excluded_button = Ajouter upper_remove_excluded_button = Retirer upper_manual_add_included_button_tooltip = Ajouter manuellement le nom du répertoire à rechercher. Pour ajouter plusieurs chemins à la fois, séparez-les avec « ; » « /home/roman;/home/rozkaz » ajoutera deux répertoires « /home/roman » et « /home/rozkaz » upper_add_included_button_tooltip = Ajouter un nouveau répertoire à la recherche. upper_remove_included_button_tooltip = Supprimer le répertoire de la recherche. upper_manual_add_excluded_button_tooltip = Ajouter manuellement un nom de répertoire exclu. Pour ajouter plusieurs chemins à la fois, séparez-les ave « ; » « /home/roman;/home/krokiet » ajoutera deux répertoires « /home/roman » et « /home/keokiet » upper_add_excluded_button_tooltip = Ajouter un répertoire à exclure de la recherche. upper_remove_excluded_button_tooltip = Retirer le répertoire de la liste de ceux exclus. upper_notebook_items_configuration = Configuration des éléments upper_notebook_excluded_directories = Chemins exclus upper_notebook_included_directories = Chemins inclus upper_allowed_extensions_tooltip = Les extensions autorisées doivent être séparées par des virgules (toutes sont disponibles par défaut). Les Macros suivantes, qui ajoutent plusieurs extensions à la fois, sont également disponibles : IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Liste des fichiers désactivés qui seront ignorés lors de l'analyse. 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é. upper_excluded_items_tooltip = Les éléments exclus doivent contenir le caractère joker « * » et être séparés par des virgules. Ceci est plus lent que les répertoires exclus, donc à utiliser avec prudence. upper_excluded_items = Éléments exclus : upper_allowed_extensions = Extensions autorisées : upper_excluded_extensions = Extensions désactivées : # Popovers popover_select_all = Tout sélectionner popover_unselect_all = Tout désélectionner popover_reverse = Inverser la sélection popover_select_all_except_shortest_path = Tout sélectionner sauf le chemin le plus court popover_select_all_except_longest_path = Tout sélectionner sauf le chemin le plus long popover_select_all_except_oldest = Tout sélectionner sauf le plus ancien popover_select_all_except_newest = Tout sélectionner sauf le plus récent popover_select_one_oldest = Sélectionner un élément plus ancien popover_select_one_newest = Sélectionner un élément récent popover_select_custom = Sélection personnalisée popover_unselect_custom = Annuler la sélection personnalisée popover_select_all_images_except_biggest = Tout sélectionner sauf le plus gros popover_select_all_images_except_smallest = Tout sélectionner sauf le plus petit popover_custom_path_check_button_entry_tooltip = Sélectionner les enregistrements par chemin. Exemple d'utilisation : « /home/pimpek/rzecz.txt » peut être trouvé avec « /home/pim* » popover_custom_name_check_button_entry_tooltip = Sélectionner les enregistrements par nom de fichier. Exemple d'utilisation : « /usr/ping/pong.txt » peut être trouvé avec « *ong* » popover_custom_regex_check_button_entry_tooltip = Sélectionner les enregistrements par Regex spécifié. Dans ce mode, le texte recherché est le Chemin avec le Nom. Exemple d'utilisation: « /usr/bin/ziemniak.txt » peut être trouvé avec « /ziem[a-z]+ » Cela utilise l'implémentation par défaut de Rust regex : https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Active la détection sensible à la casse. Si désactivé, « /home/* » trouve « /HoMe/roman » et « /home/roman ». popover_custom_not_all_check_button_tooltip = Empêche la sélection de tous les enregistrements dans le groupe. 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. AVERTISSEMENT : ce réglage ne fonctionne pas si vous avez déjà sélectionné manuellement tous les résultats dans un groupe. popover_custom_regex_path_label = Chemin d'accès popover_custom_regex_name_label = Nom popover_custom_regex_regex_label = Chemin d'accès Regex + Nom popover_custom_case_sensitive_check_button = Sensible à la casse popover_custom_all_in_group_label = Ne pas sélectionner tous les enregistrements du groupe popover_custom_mode_unselect = Désélectionner la personnalisation popover_custom_mode_select = Sélectionner la personnalisation popover_sort_file_name = Nom du fichier popover_sort_folder_name = Nom du dossier popover_sort_full_name = Nom complet popover_sort_size = Taille popover_sort_selection = Sélection popover_invalid_regex = La regex est invalide popover_valid_regex = La regex est valide # Bottom buttons bottom_search_button = Chercher bottom_select_button = Sélectionner bottom_delete_button = Supprimer bottom_save_button = Enregistrer bottom_symlink_button = Lien symbolique bottom_hardlink_button = Lien dur bottom_move_button = Déplacer bottom_sort_button = Trier bottom_compare_button = Comparer bottom_search_button_tooltip = Lancer la recherche bottom_select_button_tooltip = Sélectionnez les enregistrements. Seuls les fichiers/dossiers sélectionnés pourront être traités plus tard. bottom_delete_button_tooltip = Supprimer les fichiers/dossiers sélectionnés. bottom_save_button_tooltip = Enregistrer les données de la recherche dans un fichier bottom_symlink_button_tooltip = Créer des liens symboliques. Ne fonctionne que si au moins deux résultats dans un groupe sont sélectionnés. Le premier reste inchangé, tous les suivants sont transformés en lien symbolique vers ce premier résultat. bottom_hardlink_button_tooltip = Créer des liens durs. Ne fonctionne que si au moins deux résultats dans un groupe sont sélectionnés. Le premier reste inchangé, tous les suivants sont transformés en lien dur vers ce premier résultat. bottom_hardlink_button_not_available_tooltip = Créer des liens durs. Le bouton est désactivé car des liens durs ne peuvent être créés. Les liens durs ne fonctionnent qu’avec les privilèges administrateur sous Windows, assurez-vous d'éxécuter l’application en tant qu’administrateur. Si l’application fonctionne déjà avec ces privilèges, vérifiez les signalements de bogues similaires sur GitHub. bottom_move_button_tooltip = Déplace les fichiers vers le répertoire choisi. Ceci copie tous les fichiers dans le répertoire cible sans préserver l'arborescence du répertoire source. 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. bottom_sort_button_tooltip = Trie les fichiers/dossiers selon la méthode sélectionnée. bottom_compare_button_tooltip = Comparer les images dans le groupe. bottom_show_errors_tooltip = Afficher/Masquer le panneau de texte du bas. bottom_show_upper_notebook_tooltip = Afficher/Masquer le panneau supérieur du bloc-notes. # Progress Window progress_stop_button = Arrêter progress_stop_additional_message = Arrêt demandé # About Window about_repository_button_tooltip = Lien vers la page du dépôt avec le code source. about_donation_button_tooltip = Lien vers la page des dons. about_instruction_button_tooltip = Lien vers la page d'instruction. about_translation_button_tooltip = Lien vers la page Crowdin avec les traductions de lapplication. Le polonais et l'anglais sont officiellement pris en charge. about_repository_button = Dépôt about_donation_button = Faire un don about_instruction_button = Instructions about_translation_button = Traduction # Header header_setting_button_tooltip = Ouvre la fenêtre des paramètres. header_about_button_tooltip = Ouvre la boîte de dialogue contenant les informations sur l'application. # Settings ## General settings_number_of_threads = Nombre de threads utilisés settings_number_of_threads_tooltip = Nombre de threads utilisés. « 0 » signifie que tous les threads disponibles seront utilisés. settings_use_rust_preview = Utiliser des bibliothèques externes à la place gtk pour charger les aperçus settings_use_rust_preview_tooltip = L'utilisation des prévisualisations gtk sera parfois plus rapide et gèrera plus de formats, mais cela pourrait aussi être l'inverse. Si vous avez des problèmes de chargement des prévisualisations, vous pouvez essayer de modifier ce paramètre. 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. settings_label_restart = Vous devez redémarrer l’application pour appliquer les réglages ! settings_ignore_other_filesystems = Ignorer les autres systèmes de fichiers (Linux uniquement) settings_ignore_other_filesystems_tooltip = ignore les fichiers qui ne sont pas dans le même système de fichiers que les répertoires recherchés. Fonctionne de la même manière que l'option « -xdev » de la commande « find » sous Linux settings_save_at_exit_button_tooltip = Enregistrer la configuration dans un fichier à la fermeture de l'application. settings_load_at_start_button_tooltip = Charger la configuration à partir du fichier à l'ouverture de l'application. Si désactivé, les paramètres par défaut seront utilisés. settings_confirm_deletion_button_tooltip = Afficher une boîte de dialogue de confirmation lorsque vous cliquez sur le bouton Supprimer. settings_confirm_link_button_tooltip = Afficher une boîte de dialogue de confirmation lorsque vous cliquez sur le bouton « hard/symlink ». settings_confirm_group_deletion_button_tooltip = Afficher une boîte de dialogue d'avertissement lorsque vous essayez de supprimer tous les enregistrements du groupe. settings_show_text_view_button_tooltip = Afficher le panneau de texte en bas de l'interface utilisateur. settings_use_cache_button_tooltip = Utiliser le cache de fichiers. settings_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. settings_use_trash_button_tooltip = Déplace les fichiers vers la corbeille au lieu de les supprimer définitivement. settings_language_label_tooltip = Langue de l'interface utilisateur. settings_save_at_exit_button = Enregistrer la configuration à la fermeture de l'application settings_load_at_start_button = Charger la configuration à l'ouverture de l'application settings_confirm_deletion_button = Afficher une boîte de dialogue de confirmation lors de la suppression de fichiers settings_confirm_link_button = Afficher une boîte de dialogue de confirmation lorsque des liens en dur ou symboliques vers des fichiers sont créés settings_confirm_group_deletion_button = Afficher une boîte de dialogue de confirmation lors de la suppression de tous les fichiers d'un groupe settings_show_text_view_button = Afficher le panneau de texte du bas settings_use_cache_button = Utiliser le cache settings_save_also_as_json_button = Également enregistrer le cache en tant que fichier JSON settings_use_trash_button = Déplacer les fichiers supprimés vers la corbeille settings_language_label = Langue settings_multiple_delete_outdated_cache_checkbutton = Supprimer automatiquement les entrées de cache obsolètes settings_multiple_delete_outdated_cache_checkbutton_tooltip = Supprimer du cache les résultats obsolètes pointant vers des fichiers inexistants. 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). 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. 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. settings_notebook_general = Généraux settings_notebook_duplicates = Doublons settings_notebook_images = Images similaires settings_notebook_videos = Vidéo similaire ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Affiche l'aperçu à droite (lors de la sélection d'un fichier image). settings_multiple_image_preview_checkbutton = Afficher l'aperçu de l'image settings_multiple_clear_cache_button_tooltip = Vider manuellement le cache des entrées obsolètes. À utiliser uniquement si le nettoyage automatique a été désactivé. settings_multiple_clear_cache_button = Supprimer les résultats périmés du cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Masque tous les fichiers, sauf un, si tous pointent vers les mêmes données (avec lien en dur). 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. settings_duplicates_minimal_size_entry_tooltip = Définit la taille minimale du fichier qui sera mis en cache. 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. settings_duplicates_prehash_checkbutton_tooltip = 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. Il est désactivé par défaut car il peut causer des ralentissements dans certaines situations. 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. settings_duplicates_prehash_minimal_entry_tooltip = Taille minimale de l'entrée en cache. settings_duplicates_hide_hard_link_button = Masquer les liens durs settings_duplicates_prehash_checkbutton = Utiliser le cache de prehash settings_duplicates_minimal_size_cache_label = Taille minimale des fichiers (en octets) enregistrés dans le cache settings_duplicates_minimal_size_cache_prehash_label = Taille minimale des fichiers (en octets) enregistrés dans le cache de préhachage ## Saving/Loading settings settings_saving_button_tooltip = Enregistrez les paramètres de configuration actuels dans un fichier. settings_loading_button_tooltip = Charger les paramètres à partir d'un fichier pour remplacer la configuration actuelle. settings_reset_button_tooltip = Réinitialiser la configuration actuelle pour revenir à celle par défaut. settings_saving_button = Enregistrer la configuration settings_loading_button = Charger la configuration settings_reset_button = Réinitialiser la configuration ## Opening cache/config folders settings_folder_cache_open_tooltip = Ouvre le dossier où sont stockés les fichiers « .txt » de cache. 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. 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). En cas de problèmes avec le cache, ces fichiers peuvent être supprimés. L'application les régénèrera automatiquement. settings_folder_settings_open_tooltip = Ouvre le dossier où la configuration de Czkawka est stockée. AVERTISSEMENT : modifier manuellement la configuration peut endommager votre workflow. settings_folder_cache_open = Ouvrir le dossier de cache settings_folder_settings_open = Ouvrir le dossier des paramètres # Compute results compute_stopped_by_user = La recherche a été interrompue par l'utilisateur compute_found_duplicates_hash_size = { $number_files } doublons trouvés dans les groupes { $number_groups } qui ont pris { $size } en { $time } compute_found_duplicates_name = { $number_files } doublons trouvés dans les groupes { $number_groups } dans { $time } compute_found_empty_folders = Trouvé les dossiers { $number_files } vides dans { $time } compute_found_empty_files = Fichier { $number_files } vide trouvé dans { $time } compute_found_big_files = Fichiers volumineux { $number_files } trouvés dans { $time } compute_found_temporary_files = Fichiers temporaires { $number_files } trouvés dans { $time } compute_found_images = { $number_files } images similaires trouvées dans les groupes { $number_groups } en { $time } compute_found_videos = Trouvé { $number_files } vidéos similaires dans les groupes { $number_groups } dans { $time } compute_found_music = Trouvé { $number_files } fichiers de musique similaires dans les groupes { $number_groups } dans { $time } compute_found_invalid_symlinks = Liens symboliques { $number_files } non valides dans { $time } compute_found_broken_files = Trouvé { $number_files } fichiers cassés dans { $time } compute_found_bad_extensions = Fichiers { $number_files } trouvés avec des extensions invalides dans { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Fichier { $file_number } *[other] Fichiers { $file_number } } Scannés progress_scanning_extension_of_files = Extension du fichier { $file_checked }/{ $all_files } vérifiée progress_scanning_broken_files = Fichier { $file_checked }/{ $all_files } vérifié ({ $data_checked }/{ $all_data }) progress_scanning_video = Haché de la vidéo { $file_checked }/{ $all_files } progress_creating_video_thumbnails = Miniatures de la vidéo { $file_checked }/{ $all_files } créées progress_scanning_image = Haché de l'image { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Hachage d'image { $file_checked }/{ $all_files } comparé progress_scanning_music_tags_end = Tags comparés au fichier de musique { $file_checked }/{ $all_files } progress_scanning_music_tags = Lire les tags du fichier de musique { $file_checked }/{ $all_files } progress_scanning_music_content_end = Empreinte par rapport au fichier de musique { $file_checked }/{ $all_files } progress_scanning_music_content = Empreinte calculée du fichier de musique { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Répertoire { $folder_number } *[other] Dossiers { $folder_number } } numérisés progress_scanning_size = Taille numérisée du fichier { $file_number } progress_scanning_size_name = Nom numérisé et taille du fichier { $file_number } progress_scanning_name = Nom numérisé du fichier { $file_number } progress_analyzed_partial_hash = Hash partiel analysé des fichiers { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Hash complet analysé des fichiers { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Chargement du cache du prehash progress_prehash_cache_saving = Sauvegarde du cache du prehash progress_hash_cache_loading = Chargement du cache de hachage progress_hash_cache_saving = Sauvegarde du cache de hachage progress_cache_loading = Chargement de la cache progress_cache_saving = Sauvegarde du cache progress_current_stage = Étape actuelle :{ " " } progress_all_stages = Toutes les étapes :{ " " } # Saving loading saving_loading_saving_success = Configuration enregistrée dans le fichier { $name }. saving_loading_saving_failure = Impossible d'enregistrer les données de configuration dans le fichier { $name }, raison { $reason }. saving_loading_reset_configuration = La configuration actuelle a été effacée. saving_loading_loading_success = Configuration de l'application correctement chargée. saving_loading_failed_to_create_config_file = Impossible de créer le fichier de configuration "{ $path }". Raison : "{ $reason }". saving_loading_failed_to_read_config_file = Impossible de charger la configuration depuis "{ $path }" car elle n'existe pas ou n'est pas un fichier. saving_loading_failed_to_read_data_from_file = Impossible de lire les données du fichier "{ $path }". Raison : "{ $reason }". # Other selected_all_reference_folders = Impossible de lancer la recherche quand tous les répertoires sont définis comme des répertoires de référence searching_for_data = Recherche de données. Cela peut prendre un certain temps, veuillez patienter…. text_view_messages = MESSAGESS text_view_warnings = AVERTISSEMENTS text_view_errors = ERREURS about_window_motto = Ce programme peut être utilisée gratuitement et le sera toujours. krokiet_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. # Various dialog dialogs_ask_next_time = Demander la prochaine fois symlink_failed = Échec de la liaison symbolique { $name } à { $target }, raison { $reason } delete_title_dialog = Confirmation de la suppression delete_question_label = Êtes-vous sûr de vouloir supprimer les fichiers ? delete_all_files_in_group_title = Confirmation de la suppression de tous les fichiers du groupe delete_all_files_in_group_label1 = L'ensemble des enregistrements est sélectionné dans certains groupes. delete_all_files_in_group_label2 = Êtes-vous sûr de vouloir les supprimer ? delete_items_label = { $items } fichiers seront supprimés. delete_items_groups_label = { $items } fichiers de { $groups } groupes seront supprimés. hardlink_failed = Impossible de relier durement { $name } à { $target }, raison { $reason } hard_sym_invalid_selection_title_dialog = Sélection invalide avec certains groupes hard_sym_invalid_selection_label_1 = Un seul enregistrement est sélectionné dans certains groupes et il sera ignoré. hard_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. hard_sym_invalid_selection_label_3 = Le premier dans le groupe est reconnu comme original et n'est pas modifié mais les suivants le seront. hard_sym_link_title_dialog = Confirmation du lien hard_sym_link_label = Êtes-vous sûr de vouloir relier ces fichiers ? move_folder_failed = Impossible de déplacer le dossier { $name }. Raison : { $reason } move_file_failed = Impossible de déplacer le fichier { $name }. Raison : { $reason } move_files_title_dialog = Choisissez le dossier dans lequel vous voulez déplacer les fichiers dupliqués move_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é. move_stats = Éléments { $num_files }/{ $all_files } correctement déplacés save_results_to_file = Résultats enregistrés dans les fichiers txt et json dans le dossier "{ $name }". search_not_choosing_any_music = ERREUR : vous devez sélectionner au moins une case à cocher parmi les types de recherche de musique. search_not_choosing_any_broken_files = ERREUR : vous devez sélectionner au moins une case à cocher parmi les types de fichiers cassés. include_folders_dialog_title = Dossiers à inclure exclude_folders_dialog_title = Dossiers à exclure include_manually_directories_dialog_title = Ajouter un répertoire manuellement cache_properly_cleared = Cache correctement vidé cache_clear_duplicates_title = Purge du cache des doublons cache_clear_similar_images_title = Purge du cache des images similaires cache_clear_similar_videos_title = Purge du cache des vidéos similaires cache_clear_message_label_1 = Voulez-vous vider le cache des entrées obsolètes ? cache_clear_message_label_2 = Cette opération supprimera toutes les entrées du cache qui pointent vers des fichiers invalides. cache_clear_message_label_3 = Cela peut légèrement accélérer le chargement et la sauvegarde dans le cache. cache_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é. # Show preview preview_image_resize_failure = Impossible de redimensionner l'image { $name }. preview_image_opening_failure = Impossible d'ouvrir l'image { $name }. Raison : { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Groupe { $current_group }/{ $all_groups } ({ $images_in_group } images) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/it/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Impostazioni window_main_title = Czkawka (Singhiozzo) window_progress_title = Ricerca window_compare_images = Confronta le immagini # General general_ok_button = Ok general_close_button = Chiudi # Krokiet info dialog krokiet_info_title = Vi presentiamo Krokiet - La nuova versione di Czkawka krokiet_info_message = Krokiet è la versione nuova, migliorata, più veloce e più affidabile della GUI Czkawka GTK! È 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. 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. Provalo e vedi la differenza! 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. 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. # Main window music_title_checkbox = Titolo music_artist_checkbox = Artista music_year_checkbox = Anno music_bitrate_checkbox = Velocità in bit music_genre_checkbox = Genere music_length_checkbox = Durata music_comparison_checkbox = Confronto approssimativo music_checking_by_tags = Etichette music_checking_by_content = Contenuto same_music_seconds_label = Durata minima del frammento same_music_similarity_label = Differenza massima music_compare_only_in_title_group = Confronta all'interno di gruppi di titoli simili music_compare_only_in_title_group_tooltip = Se abilitato, i file vengono raggruppati per titolo e poi confrontati tra loro. Con 10 000 file, invece di 100 milioni di confronti di solito ci saranno circa 20 000 confronti. same_music_tooltip = La ricerca di file musicali simili per contenuto può essere configurata impostando: - Il tempo minimo del frammento dopo il quale i file musicali possono essere identificati come simili - La differenza massima tra due frammenti analizzati La chiave per ottenere buoni risultati è trovare combinazioni sensate di questi parametri. Impostando il tempo minimo a 5s e la differenza massima a 1.0, cercherà frammenti quasi identici nei file. Un tempo di 20s e una differenza massima di 6.0, invece, funziona bene per trovare remix, versioni live ecc. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Distingue Maiuscole e minuscole duplicate_case_sensitive_name_tooltip = Se abilitato, raggruppa solo i risultati quando hanno esattamente lo stesso nome, ad es. Żołd <-> Żołd La disattivazione di tale opzione raggrupperà i nomi senza distinguere tra maiuscole e minuscole, ad esempio żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Dimensione e nome duplicate_mode_name_combo_box = Nome duplicate_mode_size_combo_box = Dimensione duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka offre 3 tipi di hash: Blake3 - funzione hash crittografica. Questo è il valore predefinito perché è molto veloce. CRC32 - semplice funzione di hash. Questo dovrebbe essere più veloce di Blake3, ma può molto raramente avere alcune collisioni. XXH3 - molto simile in termini di prestazioni e qualità di hash a Blake3 (ma non crittografica). Queste modalità possono essere facilmente intercambiate. duplicate_check_method_tooltip = Per ora, Czkawka offre tre tipi di metodo per trovare i duplicati di: Nome - Trova i file che hanno lo stesso nome. Dimensione - Trova i file che hanno la stessa dimensione. Hash - Trova i file che hanno lo stesso contenuto. Questa modalità fa la hash sui file e in seguito le confronta 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. image_hash_size_tooltip = 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. 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. 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. 32 e 64 hash trovano solo immagini molto simili e non dovrebbero avere quasi mai falsi positivi (forse tranne alcune immagini con canale alfa). image_resize_filter_tooltip = Per calcolare l'hash dell'immagine, la libreria deve prima ridimensionarla. A seconda dall'algoritmo scelto l'immagine utilizzata per calcolare l'hash apparirà un po' diversa. 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. Con 8x8 dimensioni di hash si consiglia di utilizzare un algoritmo diverso da Nearest, per avere migliori gruppi di immagini. image_hash_alg_tooltip = Gli utenti possono scegliere tra uno dei molti algoritmi di calcolo dell'hash. Ognuno ha punti di forza e di debolezza e a volte darà risultati migliori e a volte peggiori per immagini diverse. Quindi, per determinare quello migliore per te, è necessario un test manuale. big_files_mode_combobox_tooltip = Consente di cercare i file più piccoli/più grandi big_files_mode_label = File controllati big_files_mode_smallest_combo_box = Il Più Piccolo big_files_mode_biggest_combo_box = Il Più Grande main_notebook_duplicates = File duplicati main_notebook_empty_directories = Cartelle vuote main_notebook_big_files = Grandi file main_notebook_empty_files = File vuoti main_notebook_temporary = File temporanei main_notebook_similar_images = Immagini simili main_notebook_similar_videos = Video simili main_notebook_same_music = Duplicati musicali main_notebook_symlinks = Collegamenti invalidi main_notebook_broken_files = File corrotti main_notebook_bad_extensions = Estensioni Errate main_tree_view_column_file_name = Nome File main_tree_view_column_folder_name = Nome Cartella main_tree_view_column_path = Percorso main_tree_view_column_modification = Data di Modifica main_tree_view_column_size = Dimensione main_tree_view_column_similarity = Similitudine main_tree_view_column_dimensions = Dimensioni main_tree_view_column_title = Titolo main_tree_view_column_artist = Artista main_tree_view_column_year = Anno main_tree_view_column_bitrate = Velocità in Bit main_tree_view_column_length = Durata main_tree_view_column_genre = Genere main_tree_view_column_symlink_file_name = Nome Collegamento main_tree_view_column_symlink_folder = Cartella Collegamenti Simbolici main_tree_view_column_destination_path = Percorso di Destinazione main_tree_view_column_type_of_error = Tipo di Errore main_tree_view_column_current_extension = Estensione Corrente main_tree_view_column_proper_extensions = Estensione Corretta main_tree_view_column_fps = FPS main_tree_view_column_codec = Codicecá main_label_check_method = Metodo di verifica main_label_hash_type = Tipo di hash main_label_hash_size = Dimensione hash main_label_size_bytes = Dimensione (byte) main_label_min_size = Min main_label_max_size = Massimo main_label_shown_files = Numero di file visualizzati main_label_resize_algorithm = Metodo di ridimensionamento main_label_similarity = Similitudine{ " " } main_check_box_broken_files_audio = Audio main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Compresso main_check_box_broken_files_image = Immagine main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignora stesse dimensioni check_button_general_same_size_tooltip = Ignora i file con dimensioni identiche nei risultati - di solito sono duplicati 1:1 main_label_size_bytes_tooltip = Dimensione dei file utilizzati nella ricerca # Upper window upper_tree_view_included_folder_column_title = Cartelle di Ricerca upper_tree_view_included_reference_column_title = Cartelle di Riferimento upper_recursive_button = Ricorsivo upper_recursive_button_tooltip = Se selezionato, cerca anche tra i file contenuti nelle cartelle figlie delle Cartelle di Riferimento. upper_manual_add_included_button = Aggiungi Manualmente upper_add_included_button = Aggiungi upper_remove_included_button = Rimuovi upper_manual_add_excluded_button = Aggiungi Manualmente upper_add_excluded_button = Aggiungi upper_remove_excluded_button = Rimuovi upper_manual_add_included_button_tooltip = Aggiungi manualmente i nomi delle cartelle in cui cercare. È possibile aggiungere più percorsi contemporaneamente separarli da ; /home/roman;/home/rozkaz aggiungerà due directory /home/roman e /home/rozkaz upper_add_included_button_tooltip = Aggiungi nuova cartella per la ricerca. upper_remove_included_button_tooltip = Cancella cartella dalla ricerca. upper_manual_add_excluded_button_tooltip = Aggiungi manualmente i nomi delle cartelle da escludere. È possibile aggiungere più percorsi contemporaneamente separarli da ; /home/roman;/home/krokiet aggiungerà due directory /home/roman e /home/keokiet upper_add_excluded_button_tooltip = Aggiunge una cartella da escludere dalla ricerca. upper_remove_excluded_button_tooltip = Rimuove una cartella da quelle escluse. upper_notebook_items_configuration = Configurazione degli elementi upper_notebook_excluded_directories = Percorsi Esclusi upper_notebook_included_directories = Percorsi Inclusi upper_allowed_extensions_tooltip = Le estensioni consentite devono essere separate da virgole (di default sono tutte disponibili). Sono disponibili anche le seguenti Macro, che aggiungono più estensioni contemporaneamente: IMAGE, VIDEO, MUSIC, TESTO. 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. upper_excluded_extensions_tooltip = Elenco dei file disabilitati che verranno ignorati nella scansione. Quando si usano sia le estensioni consentite che quelle disabilitate, queste ultime hanno maggiore priorità, quindi il file non verrà analizzato. upper_excluded_items_tooltip = Gli elementi esclusi devono contenere il carattere jolly * e devono essere separati da virgole. Questo metodo è più lento rispetto ai Percorsi Esclusi. Utilizzare con cautela. upper_excluded_items = Elementi Esclusi: upper_allowed_extensions = Estensioni Permesse: upper_excluded_extensions = Estensioni Disabilitate: # Popovers popover_select_all = Seleziona tutto popover_unselect_all = Deseleziona tutto popover_reverse = Inverti la selezione popover_select_all_except_shortest_path = Seleziona tutto tranne il percorso più corto popover_select_all_except_longest_path = Seleziona tutto tranne il percorso più lungo popover_select_all_except_oldest = Seleziona tutto eccetto il più vecchio popover_select_all_except_newest = Seleziona tutto eccetto il più recente popover_select_one_oldest = Seleziona il più vecchio popover_select_one_newest = Seleziona il più recente popover_select_custom = Selezione personalizzata popover_unselect_custom = Delezione personalizzata popover_select_all_images_except_biggest = Seleziona tutti eccetto il più grande popover_select_all_images_except_smallest = Seleziona tutto eccetto il più piccolo popover_custom_path_check_button_entry_tooltip = Seleziona i risultati specificando il percorso. Esempio: /home/pimpek/rzecz.txt può essere trovato con /home/pim* popover_custom_name_check_button_entry_tooltip = Seleziona i risultati in base al nome. Esempio: /usr/ping/pong.txt può essere trovato con *ong* popover_custom_regex_check_button_entry_tooltip = Seleziona i risultati specificando una Regex. Con questa modalità, il testo cercato è Percorso con Nome. Esempio: /usr/bin/ziemniak.txt può essere trovato con /ziem[a-z]+ Questo utilizza l'implementazione predefinita delle Regex Rust. Puoi leggere di più qui: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Abilita rilevamento maiuscolo/minuscolo. Quando disabilitato /home/* trova sia /HoMe/roman che /home/roman. popover_custom_not_all_check_button_tooltip = Impedisce di selezionare tutti i risultati nel gruppo. 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. ATTENZIONE: Questa impostazione non funziona se hai già selezionato manualmente tutti i risultati in un gruppo. popover_custom_regex_path_label = Percorso popover_custom_regex_name_label = Nome popover_custom_regex_regex_label = Regex Percorso + Nome popover_custom_case_sensitive_check_button = Distingui maiuscole/minuscole popover_custom_all_in_group_label = Non selezionare tutte le voci in un gruppo popover_custom_mode_unselect = Deselezione Personalizzata popover_custom_mode_select = Selezione Personalizzata popover_sort_file_name = Nome del file popover_sort_folder_name = Nome cartella popover_sort_full_name = Nome e cognome popover_sort_size = Dimensione popover_sort_selection = Selezione popover_invalid_regex = Regex non valida popover_valid_regex = Regex valida # Bottom buttons bottom_search_button = Cerca bottom_select_button = Seleziona bottom_delete_button = Cancella bottom_save_button = Salva bottom_symlink_button = Collegamenti simbolici bottom_hardlink_button = Collegamenti fisici bottom_move_button = Sposta bottom_sort_button = Ordina bottom_compare_button = Confronta bottom_search_button_tooltip = Avvia ricerca bottom_select_button_tooltip = Seleziona record. Solo i file/cartelle selezionati possono essere elaborati in seguito. bottom_delete_button_tooltip = Cancella i file/cartelle selezionati. bottom_save_button_tooltip = Salva i risultati della ricerca in un file bottom_symlink_button_tooltip = Crea collegamenti simbolici. Funziona solo quando sono selezionati almeno due risultati in un gruppo. Il primo rimane invariato, dal secondo in poi si creano dei collegamenti al primo. bottom_hardlink_button_tooltip = Crea collegamenti fisici. Funziona solo quando sono selezionati almeno due risultati in un gruppo. Il primo è invariato, dal secondo in poi si creano dei collegamenti fisici al primo. bottom_hardlink_button_not_available_tooltip = Crea collegamenti fisici. Il pulsante è disabilitato perché non è possibile creare collegamenti fisici. I collegamenti fisici funzionano solo con i privilegi di amministratore su Windows, quindi assicurati di eseguire l'app come amministratore. Se l'app funziona già con tali privilegi, controlla problemi simili su Github. bottom_move_button_tooltip = Sposta i file nella directory scelta. Copia tutti i file nella directory senza conservare l'albero delle directory. Quando si tenta di spostare due file con il nome identico nella cartella, il secondo fallirà e mostrerà errore. bottom_sort_button_tooltip = Ordina file/cartelle in base al metodo selezionato. bottom_compare_button_tooltip = Confronta le immagini nel gruppo. bottom_show_errors_tooltip = Mostra/Nasconde il pannello di testo inferiore. bottom_show_upper_notebook_tooltip = Mostra/Nasconde il pannello comandi. # Progress Window progress_stop_button = Interrompi progress_stop_additional_message = Interrompi richiesta # About Window about_repository_button_tooltip = Link alla pagina della repository con il codice sorgente. about_donation_button_tooltip = Link alla pagina per le donazioni. about_instruction_button_tooltip = Link alla pagina delle istruzioni. about_translation_button_tooltip = Link alla pagina Crowdin con le traduzioni delle app. Ufficialmente polacco e inglese sono supportati. about_repository_button = Repository about_donation_button = Donazioni about_instruction_button = Istruzioni about_translation_button = Traduzione # Header header_setting_button_tooltip = Apre la finestra delle impostazioni. header_about_button_tooltip = Apre la finestra delle informazioni sul programma. # Settings ## General settings_number_of_threads = Numero di thread usati settings_number_of_threads_tooltip = Numero di thread usati, 0 significa che tutti i thread disponibili saranno utilizzati. settings_use_rust_preview = Usa librerie esterne invece di gtk per caricare le anteprime settings_use_rust_preview_tooltip = Utilizzando le anteprime gtk a volte sarà più veloce e supporterà più formati, ma a volte questo potrebbe essere esattamente il contrario. Se hai problemi con il caricamento delle anteprime, puoi provare a modificare questa impostazione. 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. settings_label_restart = È necessario riavviare l'app per applicare le impostazioni! settings_ignore_other_filesystems = Ignora altri filesystem (solo Linux) settings_ignore_other_filesystems_tooltip = ignora i file che non sono nello stesso file system delle cartelle analizzate. Funziona come l'opzione -xdev nel comando find su Linux settings_save_at_exit_button_tooltip = Salva la configurazione su file quando chiudi l'app. settings_load_at_start_button_tooltip = Carica la configurazione dal file all'apertura dell'applicazione. Se non è abilitata, verranno utilizzate le impostazioni predefinite. settings_confirm_deletion_button_tooltip = Mostra la finestra di conferma quando si fa clic sul pulsante Elimina. settings_confirm_link_button_tooltip = Mostra la finestra di conferma quando si fa clic sul pulsante hard/symlink. settings_confirm_group_deletion_button_tooltip = Mostra la finestra di avviso quando si tenta di eliminare tutti i risultati dal gruppo. settings_show_text_view_button_tooltip = Mostra il pannello di testo in fondo all'interfaccia utente. settings_use_cache_button_tooltip = Usa i file di cache. settings_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). settings_use_trash_button_tooltip = Sposta i file nel cestino invece di eliminarli in modo permanente. settings_language_label_tooltip = Lingua per l'interfaccia utente. settings_save_at_exit_button = Salva la configurazione alla chiusura dell'app settings_load_at_start_button = Carica la configurazione quando apri l'app settings_confirm_deletion_button = Mostra finestra di conferma alla cancellazione di qualsiasi file settings_confirm_link_button = Mostra finestra di conferma alla creazione di collegamenti per qualsiasi file settings_confirm_group_deletion_button = Mostra finestra di conferma alla cancellazione di tutti gli elementi in un gruppo settings_show_text_view_button = Mostra il pannello testuale inferiore settings_use_cache_button = Utilizza cache settings_save_also_as_json_button = Salva anche la cache come file JSON settings_use_trash_button = Sposta i file rimossi nel cestino settings_language_label = Lingua settings_multiple_delete_outdated_cache_checkbutton = Cancella automaticamente la cache obsoleta settings_multiple_delete_outdated_cache_checkbutton_tooltip = Elimina i risultati della cache obsoleti che puntano a file inesistenti. Quando abilitata, l'app si assicura che durante il caricamento dei risultati che tutti puntino a file validi (quelli invalidi vengono ignorati). Disabilitare questo aiuterà durante la scansione dei file su unità esterne perché le loro voci della cache non verranno eliminate nella prossima scansione. 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. settings_notebook_general = Generale settings_notebook_duplicates = Duplicati settings_notebook_images = Immagini Simili settings_notebook_videos = Video Simili ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Mostra l'anteprima sul lato destro (quando si seleziona un file immagine). settings_multiple_image_preview_checkbutton = Mostra anteprima immagini settings_multiple_clear_cache_button_tooltip = Pulisci manualmente la cache delle voci obsolete. Questo dovrebbe essere usato solo se la compensazione automatica è stata disabilitata. settings_multiple_clear_cache_button = Rimuovi i risultati obsoleti dalla cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Nasconde tutti i file tranne uno, se tutti puntano agli stessi dati (sono hardlinked). 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. settings_duplicates_minimal_size_entry_tooltip = Imposta la dimensione minima del file che verrà memorizzata nella cache. Scegliendo un valore più piccolo si genereranno più record. Questo accelererà la ricerca, ma rallenterà il caricamento / il salvataggio della cache. settings_duplicates_prehash_checkbutton_tooltip = Abilita la cache di prehash (un hash calcolato da una piccola parte del file) che consente il ritiro anticipato di risultati non duplicati. È disabilitato per impostazione predefinita perché può causare rallentamenti in alcune situazioni. Si consiglia vivamente di usarlo durante la scansione di centinaia di migliaia o milioni di file, perché può accelerare la ricerca di più volte. settings_duplicates_prehash_minimal_entry_tooltip = Dimensione minima delle voci della cache. settings_duplicates_hide_hard_link_button = Nascondi collegamenti rigidi settings_duplicates_prehash_checkbutton = Utilizza la cash prehash settings_duplicates_minimal_size_cache_label = Dimensione minima dei file (in byte) salvati nella cache settings_duplicates_minimal_size_cache_prehash_label = Dimensione minima dei file (in byte) salvati nella cache prehash ## Saving/Loading settings settings_saving_button_tooltip = Salva la configurazione attuale delle impostazioni su file. settings_loading_button_tooltip = Carica le impostazioni dal file e sostituisci con esse la configurazione corrente. settings_reset_button_tooltip = Reimposta la configurazione corrente a quella predefinita. settings_saving_button = Salva configurazione settings_loading_button = Carica configurazione settings_reset_button = Reimposta configurazione ## Opening cache/config folders settings_folder_cache_open_tooltip = Apre la cartella in cui sono memorizzati i file cache txt. 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. È possibile copiare questi file tra computer per risparmiare tempo sulla scansione di nuovo per i file (ovviamente se hanno una struttura di directory simile). In caso di problemi con la cache, questi file possono essere rimossi. L'app li rigenererà automaticamente. settings_folder_settings_open_tooltip = Apre la cartella in cui viene memorizzata la configurazione Czkawka. ATTENZIONE: Modificare manualmente la configurazione potrebbe interferire coi processi in atto. settings_folder_cache_open = Apri la cartella della cache settings_folder_settings_open = Apri la cartella delle impostazioni # Compute results compute_stopped_by_user = Ricerca interrotta dall'utente compute_found_duplicates_hash_size = Trovato { $number_files } duplicati in { $number_groups } gruppi che occupano { $size } in { $time } compute_found_duplicates_name = Trovato { $number_files } duplicati in { $number_groups } gruppi in { $time } compute_found_empty_folders = Trovo { $number_files } cartelle vuote in { $time } compute_found_empty_files = Trovati { $number_files } file vuoti in { $time } compute_found_big_files = Trovati { $number_files } file grandi in { $time } compute_found_temporary_files = Trovati { $number_files } file temporanei in { $time } compute_found_images = Trova { $number_files } immagini simili in { $number_groups } gruppi in { $time } compute_found_videos = Trovati { $number_files } video simili in { $number_groups } gruppi in { $time } compute_found_music = Trovo { $number_files } file musicali simili in { $number_groups } gruppi in { $time } compute_found_invalid_symlinks = Trovati { $number_files } collegamenti simbolici invalidi in { $time } compute_found_broken_files = Trovate { $number_files } file danneggiati in { $time } compute_found_bad_extensions = Trovati { $number_files } file con estensioni non valide in { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Analizzato { $file_number } file *[other] Analizzati { $file_number } files } progress_scanning_extension_of_files = Controllo estensione del file { $file_checked }/{ $all_files } progress_scanning_broken_files = Controllo { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data }) progress_scanning_video = Genero Hash del video { $file_checked }/{ $all_files } progress_creating_video_thumbnails = Miniature create di { $file_checked }/{ $all_files } video progress_scanning_image = Generate hash di { $file_checked }/{ $all_files } immagini({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Confrontate { $file_checked }/{ $all_files } hash di immagini progress_scanning_music_tags_end = Etichette confrontate di { $file_checked }/{ $all_files } file musicali progress_scanning_music_tags = Lettura dei tag di { $file_checked }/{ $all_files } file musicali progress_scanning_music_content_end = Confronto l'impronta digitale di { $file_checked }/{ $all_files } file musicali progress_scanning_music_content = Impronta digitale calcolata di { $file_checked }/{ $all_files } file musicali ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Scanned { $folder_number } folder *[other] Scanned { $folder_number } folders } progress_scanning_size = Dimensione scansionata del file { $file_number } progress_scanning_size_name = Nome e dimensione del file { $file_number } scansionato progress_scanning_name = Nome scansionato del file { $file_number } progress_analyzed_partial_hash = Hash parziale analizzato di { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Hash completo analizzato di { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Caricamento della cache prehash progress_prehash_cache_saving = Salvataggio della cache prehash progress_hash_cache_loading = Caricamento della cache hash progress_hash_cache_saving = Salvataggio della cache hash progress_cache_loading = Caricamento cache progress_cache_saving = Salvataggio cache progress_current_stage = Fase attuale:{ " " } progress_all_stages = Tutte le fasi:{ " " } # Saving loading saving_loading_saving_success = Salvataggio configurazione su file { $name }. saving_loading_saving_failure = Impossibile salvare i dati di configurazione nel file { $name }, motivo { $reason }. saving_loading_reset_configuration = La configurazione corrente è stata cancellata. saving_loading_loading_success = Caricamento configurazione da file avvenuto con successo. saving_loading_failed_to_create_config_file = Impossibile creare il file di configurazione "{ $path }", motivo "{ $reason }". saving_loading_failed_to_read_config_file = Impossibile caricare la configurazione da "{ $path }" perché non esiste o non è un file. saving_loading_failed_to_read_data_from_file = Impossibile leggere il file "{ $path }", motivo "{ $reason }". # Other selected_all_reference_folders = Impossibile avviare la ricerca, quando tutte le directory sono impostate come cartelle di riferimento searching_for_data = Ricerca dei dati, può durare a lungo, attendere prego... text_view_messages = MESSAGGI text_view_warnings = ATTENZIONE text_view_errors = ERRORI about_window_motto = Questo programma può essere usato liberamente e lo sarà sempre. krokiet_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. # Various dialog dialogs_ask_next_time = Chiedi la prossima volta symlink_failed = Collegamento simbolico { $name } a { $target }non riuscito, motivo { $reason } delete_title_dialog = Conferma di cancellazione delete_question_label = Sei sicuro di cancellare i file? delete_all_files_in_group_title = Conferma di cancellazione di tutti i file nel gruppo delete_all_files_in_group_label1 = In alcuni gruppi tutti i record sono selezionati. delete_all_files_in_group_label2 = Sei sicuro di cancellarli tutti? delete_items_label = { $items } i file verranno eliminati. delete_items_groups_label = { $items } i file da { $groups } gruppi verranno eliminati. hardlink_failed = Collegamento non riuscito a { $name } a { $target }, motivo { $reason } hard_sym_invalid_selection_title_dialog = Selezione invalida in alcuni gruppi hard_sym_invalid_selection_label_1 = In alcuni gruppi c'è solo un record selezionato e verrà ignorato. hard_sym_invalid_selection_label_2 = Per essere in grado di collegare hard/sym questi file, è necessario selezionare almeno due risultati nel gruppo. hard_sym_invalid_selection_label_3 = Il primo nel gruppo sarà considerato l'originale ed inalterato, ma il secondo ed i successivi verranno modificati. hard_sym_link_title_dialog = Conferma collegamento hard_sym_link_label = Sei sicuro di voler collegare questi file? move_folder_failed = Spostamento cartella { $name } fallito, ragione { $reason } move_file_failed = Spostamento file { $name } fallito, ragione { $reason } move_files_title_dialog = Seleziona la cartella dove vuoi spostare i file duplicati move_files_choose_more_than_1_path = Solo un percorso può essere selezionato per essere in grado di copiare i file duplicati, selezionato { $path_number }. move_stats = { $num_files }/{ $all_files } elementi spostati con successo save_results_to_file = Risultati salvati sia in file txt che json nella cartella "{ $name }". search_not_choosing_any_music = ERRORE: Devi selezionare almeno una casella dei metodi di ricerca musicali. search_not_choosing_any_broken_files = ERRORE: è necessario selezionare almeno una casella di controllo selezionando il tipo di file danneggiati. include_folders_dialog_title = Cartelle incluse exclude_folders_dialog_title = Cartelle escluse include_manually_directories_dialog_title = Aggiungi cartella manualmente cache_properly_cleared = Cache cancellata con successo cache_clear_duplicates_title = Cancellazione cache dei duplicati cache_clear_similar_images_title = Cancellazione cache delle immagini simili cache_clear_similar_videos_title = Cancellazione cache dei video simili cache_clear_message_label_1 = Vuoi cancellare la cache delle voci obsolete? cache_clear_message_label_2 = Questa operazione rimuoverà tutte le voci della cache che puntano a file non validi. cache_clear_message_label_3 = Questo può velocizzare il carico/salvataggio nella cache. cache_clear_message_label_4 = ATTENZIONE: L'operazione rimuoverà tutti i dati memorizzati nella cache da unità esterne scollegate. Quindi ogni hash dovrà essere rigenerato. # Show preview preview_image_resize_failure = Ridimensionamento dell'immagine { $name } non riuscito. preview_image_opening_failure = Impossibile aprire l'immagine { $name }, motivo { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Gruppo { $current_group }/{ $all_groups } ({ $images_in_group } immagini) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/ja/czkawka_gui.ftl ================================================ # Window titles window_settings_title = 設定 window_main_title = Czkawka (しゃっくり) window_progress_title = スキャン中 window_compare_images = 画像を比較 # General general_ok_button = Ok general_close_button = 閉じる # Krokiet info dialog krokiet_info_title = Krokiet – 新バージョン Czkawka krokiet_info_message = Krokietは、Czkawka GTK GUIの新しい、改良され、高速かつ信頼性の高いバージョンです! 実行が簡単で、システム変更に強く、ほとんどのシステムでデフォルトで利用可能なコアライブラリに依存するためです。 Krokietは、サムネイルをビデオ比較モードで、EXIFクリーナー、ファイル移動/コピー/削除の進捗状況、または拡張されたソートオプションなど、Czkawkaにはない機能も提供します。 試してみてください。違いを確かめてください! Czkawkaは、私からバグ修正や軽微なアップデートを継続的に受け取りますが、すべての新機能はKrokietにのみ開発され、誰でも新しい機能を追加したり、欠落しているモードを補ったり、Czkawkaをさらに拡張したりすることができます。 追伸:このメッセージは一度だけ表示されるように設計されています。もし表示された場合は、CZKAWKA_DONT_ANNOY_ME環境変数を任意の非空の値に設定してください。. # Main window music_title_checkbox = タイトル music_artist_checkbox = アーティスト music_year_checkbox = 年 music_bitrate_checkbox = ビットレート music_genre_checkbox = ジャンル music_length_checkbox = 長さ music_comparison_checkbox = おおよその比較 music_checking_by_tags = タグ music_checking_by_content = コンテンツ same_music_seconds_label = フラグメント最小秒の持続時間 same_music_similarity_label = 最大差 music_compare_only_in_title_group = 類似したタイトルのグループ内で比較 music_compare_only_in_title_group_tooltip = 有効にすると、ファイルはタイトルでグループ化され、それから比較されます。 10000ファイルでは、その代わりに約100万比較が通常、約20000比較があります。. same_music_tooltip = 音楽ファイルの内容から類似ファイルを検索するように設定できます: - 音楽ファイルが類似していると識別されるフラグメントの最小時間 - テストされた2つのフラグメントの最大差分 良い結果を得るための鍵は、これらのパラメータの賢明な組み合わせを見つけることです。 最小時間を5秒、最大差を1.0に設定すると、ファイル内のほとんど同じフラグメントを探します。 一方、時間を20秒、差の最大値を6.0に設定すると、リミックスやライブ・バージョンなどを探すのに効果的です。 デフォルトでは、各音楽ファイルは互いに比較され、多数のファイルをテストする場合、これは多くの時間を要します。したがって、通常、参照フォルダを使用し、どのファイルを互いに比較するかを指定する方が良いでしょう(同じ量のファイルでは、フィンガープリントの比較は参照フォルダなしよりも少なくとも 4 倍速くなります)。. music_comparison_checkbox_tooltip = 機械学習によりフレーズから括弧とその中身を除外するAIを使用して、類似の音楽ファイルを検索します。このオプションが有効な場合、例えば以下のファイルは重複とみなされます: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = 大文字小文字を区別する duplicate_case_sensitive_name_tooltip = 有効な場合、グループのみレコードまったく同じ名前を持っている場合など。 Z ołd <-> Z ołd このようなオプションを無効にすると、各文字のサイズが同じかどうかを確認せずに名前をグループ化します。例: z o D <-> Z ołd duplicate_mode_size_name_combo_box = サイズと名前 duplicate_mode_name_combo_box = 名前 duplicate_mode_size_combo_box = サイズ duplicate_mode_hash_combo_box = ハッシュ duplicate_hash_type_tooltip = Czkawkaは3種類のハッシュを提供します: Blake3 - 暗号学的ハッシュ関数。非常に高速であるため、デフォルトのハッシュ方式として使用されます。 CRC32 - シンプルなハッシュ関数。Blake3より高速ですが、まれに衝突が発生する可能性があります。 XXH3 - パフォーマンスとハッシュの質がBlake3に非常に近いので、このようなモードの代わりに簡単に使用できます。(ただし、暗号学的ではありません). duplicate_check_method_tooltip = Czkawkaは、今のところ以下の3種類の方法で重複を見つけることができます: 名前 - 同じ名前のファイルを検索します。 サイズ - 同じサイズのファイルを探します。 ハッシュ - 同じ内容のファイルを探します。ファイルをハッシュ化して比較することにより重複を見つけます。このモードは、重複を見つけるための最も安全な方法です。このツールはキャッシュを多用するので、同じデータの2回目以降のスキャンは最初の時よりずっと速くなるはずです。. image_hash_size_tooltip = チェックした画像はそれぞれ特別なハッシュを生成し、そのハッシュを比較したときに差が小さいほど、この画像は類似していることを意味します。 8のハッシュサイズは、オリジナルに少ししか似ていない画像を見つけるのにかなり適しています。しかし、1000枚を超えるような大きな画像では、誤検出が多くなるため、より大きなハッシュサイズを使用することをお勧めします。 16はデフォルトのハッシュサイズであり、少しでも類似した画像を見つけることとハッシュの衝突を少なくすることの間でかなり良い妥協点です。 32と64のハッシュは非常に類似した画像しか見つけられませんが、誤検出はほとんどありません(アルファチャンネルのある一部の画像を除いて)。. image_resize_filter_tooltip = 画像のハッシュを計算するために、ライブラリはまず画像のサイズを必ず変更します。 どのアルゴリズムを選択したかによって、ハッシュを計算するために使用される画像は少し違って見えるかもしれません。 最も高速なアルゴリズムは Nearest ですが、最も悪い結果を出すのも Nearest です。16x16のハッシュサイズで低品質の場合、それが明らかになることはないので、デフォルトは Nearest です。 8x8のハッシュサイズでは、より良い画像群を得るために、Nearestとは異なるアルゴリズムを使用することが推奨されます。. image_hash_alg_tooltip = ハッシュの計算方法は、多くのアルゴリズムの中からユーザーが選択することができます。 それぞれ長所と短所があり、画像によって良い結果が出る場合もあれば、悪い結果が出る場合もあります。 そのため、最適なものを見極めるには、手動でのテストが必要です。. big_files_mode_combobox_tooltip = 最小/最大のファイルを検索できます big_files_mode_label = チェックされたファイル big_files_mode_smallest_combo_box = 最も小さい big_files_mode_biggest_combo_box = 最大のもの main_notebook_duplicates = 重複したファイル main_notebook_empty_directories = 空のディレクトリ main_notebook_big_files = 大きなファイル main_notebook_empty_files = 空のファイル main_notebook_temporary = 一時ファイル main_notebook_similar_images = 類似の画像 main_notebook_similar_videos = 類似の動画 main_notebook_same_music = 重複した音楽 main_notebook_symlinks = 無効なシンボリックリンク main_notebook_broken_files = 壊れたファイル main_notebook_bad_extensions = 不正なエクステンション main_tree_view_column_file_name = ファイル名 main_tree_view_column_folder_name = フォルダ名 main_tree_view_column_path = パス main_tree_view_column_modification = 更新日時 main_tree_view_column_size = サイズ main_tree_view_column_similarity = 類似度 main_tree_view_column_dimensions = 寸法 main_tree_view_column_title = タイトル main_tree_view_column_artist = アーティスト main_tree_view_column_year = 年 main_tree_view_column_bitrate = ビットレート main_tree_view_column_length = 長さ main_tree_view_column_genre = ジャンル main_tree_view_column_symlink_file_name = シンボリックリンクのファイル名 main_tree_view_column_symlink_folder = シンボリックリンクフォルダ main_tree_view_column_destination_path = 宛先パス main_tree_view_column_type_of_error = エラーの種類 main_tree_view_column_current_extension = 現在のエクステンション main_tree_view_column_proper_extensions = 適切な拡張 main_tree_view_column_fps = FPS main_tree_view_column_codec = コーデック: main_label_check_method = メソッドのチェック main_label_hash_type = ハッシュ方式 main_label_hash_size = ハッシュサイズ main_label_size_bytes = サイズ(バイト) main_label_min_size = 最小値 main_label_max_size = 最大値 main_label_shown_files = 表示するファイルの数 main_label_resize_algorithm = アルゴリズムのサイズを変更 main_label_similarity = 類似度{ " " } main_check_box_broken_files_audio = 音声 main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = アーカイブする main_check_box_broken_files_image = 画像 main_check_box_broken_files_video = ビデオ main_check_box_broken_files_video_tooltip = ffmpeg/ffprobe を使用してビデオファイルを検証します。かなり遅く、ファイルが正常に再生されても、煩雑なエラーを検出することがあります。. check_button_general_same_size = 同じサイズを無視 check_button_general_same_size_tooltip = 結果として同じサイズのファイルを無視 - 通常、これらは1:1重複です main_label_size_bytes_tooltip = スキャンで使用されるファイルのサイズ # Upper window upper_tree_view_included_folder_column_title = 検索するフォルダ upper_tree_view_included_reference_column_title = 参照フォルダ upper_recursive_button = 再帰的 upper_recursive_button_tooltip = 選択した場合、選択したフォルダの下に直接配置されていないファイルも検索します。. upper_manual_add_included_button = 手動追加 upper_add_included_button = 追加 upper_remove_included_button = 削除 upper_manual_add_excluded_button = 手動追加 upper_add_excluded_button = 追加 upper_remove_excluded_button = 削除 upper_manual_add_included_button_tooltip = 手動で検索するディレクトリ名を追加します。 一度に複数のパスを追加するには、 ; /home/roman;/home/rozkazは/home/romanと/home/rozkazの2つのディレクトリを追加します。 upper_add_included_button_tooltip = 検索に新しいディレクトリを追加します。. upper_remove_included_button_tooltip = 検索からディレクトリを削除します。. upper_manual_add_excluded_button_tooltip = 除外されたディレクトリ名を手動で追加します。 一度に複数のパスを追加するには、 ; /home/roman;/home/krokiet は /home/roman と /home/keokiet の 2 つのディレクトリを追加します。 upper_add_excluded_button_tooltip = 検索で除外するディレクトリを追加します。. upper_remove_excluded_button_tooltip = 除外されたディレクトリを削除します。. upper_notebook_items_configuration = アイテム設定 upper_notebook_excluded_directories = 除外パス upper_notebook_included_directories = 含まれるパス upper_allowed_extensions_tooltip = 許可する拡張子はカンマで区切る必要があります(デフォルトではすべてが使用可能です)。 複数の拡張子を一度に追加するマクロ: IMAGE, VIDEO, MUSIC, TEXT も利用可能です。 使用例: ".exe, IMAGE, VIDEO, .rar, 7z" - これは画像(jpg、pngなど)、動画(avi、mp4など)、exe、rar、7zファイルがスキャンされることを意味します。. upper_excluded_extensions_tooltip = スキャンで無視される無効なファイルの一覧です。 許可された拡張子と無効化された拡張子の両方を使用する場合、この拡張子の方が優先度が高いので、ファイルはチェックされません。. upper_excluded_items_tooltip = 除外項目には *ワイルドカードを含んでおり、カンマで区切ってください。 これはExcluded Pathsよりも遅いため、注意して使用してください。. upper_excluded_items = 除外するアイテム: upper_allowed_extensions = 許可される拡張子: upper_excluded_extensions = 無効なエクステンション: # Popovers popover_select_all = すべて選択 popover_unselect_all = すべて選択解除 popover_reverse = 選択を逆にする popover_select_all_except_shortest_path = 選択を解除し、最短経路を除く popover_select_all_except_longest_path = 選択を解除する、最長パスを除く popover_select_all_except_oldest = 一番古いもの以外のすべてを選択 popover_select_all_except_newest = 一番新しいもの以外のすべてを選択 popover_select_one_oldest = 一番古いものを選択 popover_select_one_newest = 一番新しいものを選択 popover_select_custom = カスタム選択 popover_unselect_custom = カスタム選択を解除 popover_select_all_images_except_biggest = 一番大きいもの以外のすべてを選択 popover_select_all_images_except_smallest = 一番小さいもの以外のすべてを選択 popover_custom_path_check_button_entry_tooltip = パスによってレコードを選択します。 使用例: /home/pimpek/rzecz.txt は /home/pim* で見つけることができます popover_custom_name_check_button_entry_tooltip = ファイル名でレコードを選択します。 使用例: /usr/ping/pong.txt は *ong* でを見つけることができます popover_custom_regex_check_button_entry_tooltip = 指定した正規表現でレコードを選択することができます。 このモードでは、検索される文字列はパスと文字列になります。 使用例: /usr/bin/ziemniak.txt は /ziem[a-z]+ で検索できます。 これはRustのデフォルトの正規表現実装を使用しているので、詳しくは https://docs.rs/regex を参照してください。. popover_custom_case_sensitive_check_button_tooltip = 大文字小文字を区別する検出を有効にします。 /home/* を無効にすると、/Home/roman と /home/roman の両方が検出されます。. popover_custom_not_all_check_button_tooltip = グループ内のすべてのレコードを選択できないようにします。 ほとんどの状況でユーザーは元のファイルと重複ファイルの両方を削除したくないため、これはデフォルトで有効になっています。 少なくとも1つのファイルを残したい。 警告: この設定は、すでにユーザーがグループ内のすべての結果を手動で選択している場合には機能しません。. popover_custom_regex_path_label = パス popover_custom_regex_name_label = 名前 popover_custom_regex_regex_label = 正規表現(パス + 名前) popover_custom_case_sensitive_check_button = 大文字と小文字を区別 popover_custom_all_in_group_label = グループ内のすべてのレコードを選択しない popover_custom_mode_unselect = カスタム選択を解除 popover_custom_mode_select = カスタム選択 popover_sort_file_name = ファイル名 popover_sort_folder_name = フォルダー名 popover_sort_full_name = カード名義人 popover_sort_size = サイズ popover_sort_selection = 選択 popover_invalid_regex = 正規表現が無効です popover_valid_regex = 正規表現が有効です # Bottom buttons bottom_search_button = 検索 bottom_select_button = 選択 bottom_delete_button = 削除 bottom_save_button = 保存 bottom_symlink_button = シンボリックリンク bottom_hardlink_button = ハードリンク bottom_move_button = 移動 bottom_sort_button = 並び替え bottom_compare_button = 比較 bottom_search_button_tooltip = 検索を開始 bottom_select_button_tooltip = レコードを選択します。選択したファイル/フォルダのみが後で処理できます。. bottom_delete_button_tooltip = 選択したファイル/フォルダを削除します。. bottom_save_button_tooltip = 検索に関するデータをファイルに保存します。 bottom_symlink_button_tooltip = シンボリックリンクを作成します。 グループ内の2つ以上の結果が選択されている場合にのみ機能します。 最初の結果は変更されず、2番目以降の結果が最初の結果にシンボリックリンクされます。. bottom_hardlink_button_tooltip = ハードリンクを作成します。 グループ内の2つ以上の結果が選択されている場合にのみ機能します。 最初の結果は変更されず、2番目以降の結果が最初の結果にハードリンクされます。. bottom_hardlink_button_not_available_tooltip = ハードリンクを作成する。 ハードリンクを作成できないため、ボタンは無効になっています。 ハードリンクはWindowsの管理者権限でのみ動作するので、アプリは必ず管理者として実行してください。 アプリがすでにそのような権限で動作している場合は、Githubに同様の問題がないか確認してください。. bottom_move_button_tooltip = 選択したフォルダにファイルを移動します。 ディレクトリツリーを維持したまま、すべてのファイルをフォルダにコピーします。 同じ名前の2つのファイルをフォルダに移動しようとすると、2番目のファイルが失敗し、エラーが表示されます。. bottom_sort_button_tooltip = 選択した方法に従ってファイル/フォルダを並べ替えます。. bottom_compare_button_tooltip = グループ内の画像を比較する. bottom_show_errors_tooltip = 下部のエラーパネルを表示/非表示にします。. bottom_show_upper_notebook_tooltip = 上部のノートブックパネルを表示/非表示にします。. # Progress Window progress_stop_button = 停止 progress_stop_additional_message = リクエストを停止する # About Window about_repository_button_tooltip = ソースコードのあるリポジトリページへのリンク. about_donation_button_tooltip = 寄付ページへのリンク. about_instruction_button_tooltip = 使い方ページへのリンク. about_translation_button_tooltip = Crowdin ページにアプリの翻訳をリンクします。公式にポーランド語と英語がサポートされています。. about_repository_button = リポジトリ about_donation_button = 寄付 about_instruction_button = 使い方 about_translation_button = 翻訳 # Header header_setting_button_tooltip = 設定ダイアログを開きます。. header_about_button_tooltip = アプリに関する情報を含むダイアログを開きます。. # Settings ## General settings_number_of_threads = 使用されるスレッドの数 settings_number_of_threads_tooltip = 使用するスレッドの数、0 は、使用可能なすべてのスレッドが使用されることを意味します。. settings_use_rust_preview = プレビューの読み込みにgtkの代わりに外部ライブラリを使用する settings_use_rust_preview_tooltip = GTKプレビューはいくらかの場合において高速で多くのフォーマットをサポートしているが、全く逆となる場合もある。 プレビューの読み込みに問題がある場合、この設定を変更してみるとよい。 Linux以外の環境では、gtk-pixbufが常に有効とは限らず、無効にすることによりいくらかの画像のプレビューが読み込まれないため、このオプションの使用が推奨される。. settings_label_restart = 設定を適用するにはアプリを再起動する必要があります! settings_ignore_other_filesystems = 他のファイルシステムを無視(Linuxのみ) settings_ignore_other_filesystems_tooltip = 検索されたディレクトリと同じファイルシステムにないファイルを無視します。 Linux の find コマンドで -xdev オプションのように動作します。 settings_save_at_exit_button_tooltip = 終了時に設定をファイルに保存します。. settings_load_at_start_button_tooltip = 起動時にファイルから設定を読み込みます。 このオプションが無効の場合、デフォルトの設定が使用されます。. settings_confirm_deletion_button_tooltip = 削除ボタンをクリックしたときに確認ダイアログを表示します。. settings_confirm_link_button_tooltip = ハードリンク/シンボリックリンクボタンをクリックしたときに確認ダイアログを表示します。. settings_confirm_group_deletion_button_tooltip = グループからすべてのレコードを削除しようとしたときに警告ダイアログを表示します。. settings_show_text_view_button_tooltip = 下部にテキストパネルを表示します。. settings_use_cache_button_tooltip = ファイルキャッシュを使用します。. settings_save_also_as_json_button_tooltip = キャッシュを人間にも読みやすい形のJSON形式で保存します。この内容は編集可能です。コンテンツを変更することができます。 バイナリ形式のキャッシュ(bin拡張子のもの)がない場合、このファイルから自動的にキャッシュが読み込まれます。. settings_use_trash_button_tooltip = ファイルを永久に削除する代わりにゴミ箱に移動します。. settings_language_label_tooltip = 操作画面における言語を選択します。. settings_save_at_exit_button = 終了時に設定を保存 settings_load_at_start_button = 起動時に設定を読み込む settings_confirm_deletion_button = ファイルを削除するときに確認ダイアログを表示する settings_confirm_link_button = ハードリンク/シンボリックリンク時に確認ダイアログを表示する settings_confirm_group_deletion_button = グループ内のすべてのファイルを削除するときに確認ダイアログを表示する settings_show_text_view_button = 下部にテキストパネルを表示 settings_use_cache_button = キャッシュを使用 settings_save_also_as_json_button = JSONファイルにもキャッシュを保存する settings_use_trash_button = 削除したファイルをゴミ箱に移動する settings_language_label = 言語 settings_multiple_delete_outdated_cache_checkbutton = 古いキャッシュエントリを自動的に削除 settings_multiple_delete_outdated_cache_checkbutton_tooltip = 存在しないファイルを指している古いキャッシュエントリを削除できるようにします。 このオプションを有効にすると、アプリはレコードを読み込むときにすべてのポイントが有効なファイルであることを確認します(壊れたファイルは無視されます)。 無効にすると、それらに関するキャッシュエントリが次のスキャンで削除されなくなり、外部ドライブ上のファイルをスキャンする際に役立ちます。 キャッシュに何十万ものレコードがある場合、スキャンの開始時・終了時のキャッシュの読み込み・保存を高速化するためにこのオプションを有効にすることが推奨されます。. settings_notebook_general = 全般 settings_notebook_duplicates = 重複 settings_notebook_images = 類似の画像 settings_notebook_videos = 類似の動画 ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = 画像ファイルを選択しているとき、右側にプレビューを表示します。. settings_multiple_image_preview_checkbutton = 画像のプレビューを表示 settings_multiple_clear_cache_button_tooltip = 古いキャッシュエントリを手動でクリアします。 自動クリアが無効の場合にのみ使用する必要があります。. settings_multiple_clear_cache_button = キャッシュから古い結果を削除します。. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = ハードリンクされていてかつ同じデータを指している場合、1つを除くすべてのファイルを非表示にします。 例: ディスク上に特定のデータにハードリンクされている同じデータを持つ7つのファイルと、1つの異なるinodeのファイルがある場合、 重複検索では一意のファイルとハードリンクされたファイルのみが表示されます。. settings_duplicates_minimal_size_entry_tooltip = キャッシュされるファイルの最小サイズを設定します。 値を小さくするとキャッシュが生成されるレコードが増え検索が高速化しますが、キャッシュの読み込みと保存が遅くなります。. settings_duplicates_prehash_checkbutton_tooltip = プレハッシュ(ファイルの一部から計算したハッシュ) のキャッシュを有効にし、重複していない検索結果をより早く捨てられるようにします。 いくつかの場面では低速化の要因になりうるので、この機能はデフォルトでは無効になっています。 数十万・数百万のファイルをスキャンする場合には、検索を何倍も高速化できるため使用を強く推奨します。. settings_duplicates_prehash_minimal_entry_tooltip = キャッシュされたエントリの最小サイズ。. settings_duplicates_hide_hard_link_button = ハードリンクを隠す settings_duplicates_prehash_checkbutton = プリハッシュキャッシュを使用 settings_duplicates_minimal_size_cache_label = キャッシュに保存するファイルの最小サイズ(バイト単位) settings_duplicates_minimal_size_cache_prehash_label = プリハッシュキャッシュに保存するファイルの最小サイズ(バイト単位) ## Saving/Loading settings settings_saving_button_tooltip = 現在の設定をファイルに保存します。. settings_loading_button_tooltip = 設定をファイルから読み込み、現在の設定を置き換えます。. settings_reset_button_tooltip = 設定をデフォルトにリセットします。. settings_saving_button = 設定を保存 settings_loading_button = 構成を読み込む settings_reset_button = 設定をリセット ## Opening cache/config folders settings_folder_cache_open_tooltip = キャッシュを持つtxtファイルが保存されているフォルダを開きます。 これらのファイルを変更すると不正な結果を表示することがありますが、パスなどを変更することで、大量のファイルを別の場所に移動する際の時間を短縮することができます。 このファイルをコンピュータ間でコピーすることで、再度ファイルをスキャンするときの時間を節約できます(もちろん、コンピュータのディレクトリ構造が似ている場合に限り)。 キャッシュに問題がある場合、このファイルを削除することができます。アプリは自動的にそれらを再生成します。. settings_folder_settings_open_tooltip = Czkawkaの設定ファイルが保存されているフォルダを開きます。 警告: 手動で変更すると、ワークフローが壊れる可能性があります。. settings_folder_cache_open = キャッシュフォルダーを開く settings_folder_settings_open = 設定フォルダを開く # Compute results compute_stopped_by_user = 検索はユーザーによって停止されました compute_found_duplicates_hash_size = { $number_files } 重複を { $number_groups } グループで { $size } を { $time } で見つけました compute_found_duplicates_name = { $number_files } 重複が { $number_groups } グループの { $time } で見つかりました compute_found_empty_folders = 空のフォルダが { $number_files } 個見つかりました ({ $time }) compute_found_empty_files = 空のファイルが { $number_files } 個見つかりました ({ $time }) compute_found_big_files = 大きなファイルが { $number_files } 個見つかりました ({ $time }) compute_found_temporary_files = 一時ファイルが { $number_files } 個見つかりました ({ $time }) compute_found_images = 同じ画像を{ $number_files }枚見つけました。これらは{ $number_groups }グループに分かれ、{ $time }で検索しました。 compute_found_videos = { $number_files }個似た動画を{ $number_groups }グループ中で{ $time }に見つけました compute_found_music = { $number_files }枚の似た音楽ファイルを{ $number_groups }グループ中で{ $time }に発見しました compute_found_invalid_symlinks = 無効なシンボリックリンクが { $number_files } 個見つかりました ({ $time }) compute_found_broken_files = 壊れたファイルが { $number_files } 個見つかりました ({ $time }) compute_found_bad_extensions = { $number_files } で無効な拡張子を持つ { $time }ファイルが見つかりました # Progress window progress_scanning_general_file = { $file_number -> [one] スキャン済み { $file_number } ファイル *[other] スキャン済み { $file_number } ファイル } progress_scanning_extension_of_files = { $file_checked }/{ $all_files } ファイルの拡張子をチェックしました progress_scanning_broken_files = { $file_checked }/{ $all_files } ファイル ({ $data_checked }/{ $all_data } ) をチェックしました。 progress_scanning_video = { $file_checked }/{ $all_files } ビデオのハッシュ化 progress_creating_video_thumbnails = { $file_checked }/{ $all_files } ビデオのサムネイルを作成しました progress_scanning_image = { $file_checked }/{ $all_files } イメージ ({ $data_checked }/{ $all_data } ) のハッシュ化 progress_comparing_image_hashes = { $file_checked }/{ $all_files } の画像ハッシュ比較 progress_scanning_music_tags_end = { $file_checked }/{ $all_files } 音楽ファイルのタグの比較 progress_scanning_music_tags = { $file_checked }/{ $all_files } 音楽ファイルのタグを読む progress_scanning_music_content_end = { $file_checked }/{ $all_files } 音楽ファイルの指紋と比較 progress_scanning_music_content = { $file_checked }/{ $all_files } 音楽ファイルの計算フィンガープリント ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Scanned { $folder_number } folder *[other] Scanned { $folder_number } folders } progress_scanning_size = { $file_number } ファイルのスキャンされたサイズ progress_scanning_size_name = スキャンされた名前と { $file_number } ファイルのサイズ progress_scanning_name = スキャンされた { $file_number } ファイルの名前 progress_analyzed_partial_hash = { $file_checked }/{ $all_files } 個のファイルの部分ハッシュを分析しました ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = { $file_checked }/{ $all_files } 個のファイルの完全ハッシュを分析しました ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = プレハッシュキャッシュを読み込み中 progress_prehash_cache_saving = プレハッシュキャッシュを保存しています progress_hash_cache_loading = ハッシュキャッシュを読み込み中 progress_hash_cache_saving = ハッシュキャッシュを保存中 progress_cache_loading = キャッシュを読み込み中 progress_cache_saving = キャッシュを保存中 progress_current_stage = 現在のステージ:{ " " } progress_all_stages = すべてのステージ:{ " " } # Saving loading saving_loading_saving_success = ファイル { $name } に設定を保存しました。. saving_loading_saving_failure = 設定データをファイル { $name }、理由 { $reason } に保存できませんでした。. saving_loading_reset_configuration = 現在の設定がクリアされました。. saving_loading_loading_success = 設定の読み込みが正常に完了しました。. saving_loading_failed_to_create_config_file = 設定ファイル "{ $path }" の作成に失敗しました、理由 "{ $reason }"。. saving_loading_failed_to_read_config_file = 存在しないか設定ファイルでないため、"{ $path }" から設定を読み込めません。. saving_loading_failed_to_read_data_from_file = ファイル "{ $path }" からデータを読み取ることができません、理由 "{ $reason } "。. # Other selected_all_reference_folders = すべてのディレクトリが参照フォルダとして設定されている場合、検索を開始できません searching_for_data = データを検索中、しばらくお待ちください... text_view_messages = メッセージ text_view_warnings = 警告 text_view_errors = エラー about_window_motto = このプログラムは自由に使用することができます、常に。. krokiet_new_app = Czkawkaはメンテナンスモードであるため、重大なバグのみが修正され、新機能は追加されません。 新機能については、より安定しており、パフォーマンスが高く、積極的な開発中の新しいKrokietアプリをご覧ください。. # Various dialog dialogs_ask_next_time = 次回に確認 symlink_failed = { $name } から { $target }へのシンボリックリンクに失敗しました , 理由 { $reason } delete_title_dialog = 削除の確認 delete_question_label = ファイルを削除してもよろしいですか? delete_all_files_in_group_title = グループ内のすべてのファイルを削除することの確認 delete_all_files_in_group_label1 = いくつかのグループでは、すべてのレコードが選択されています。. delete_all_files_in_group_label2 = 本当に削除しますか? delete_items_label = { $items } ファイルが削除されます。. delete_items_groups_label = { $groups } グループから { $items } 個のファイルが削除されます。. hardlink_failed = { $name } から { $target }へのハードリンクに失敗しました , 理由 { $reason } hard_sym_invalid_selection_title_dialog = いくつかのグループで無効な選択です hard_sym_invalid_selection_label_1 = いくつかのグループでは一つのレコードしか選択されていないため、それらは無視されます。. hard_sym_invalid_selection_label_2 = これらのファイルをハード/シンボリックにリンクできるようにするには、グループ内の少なくとも2つの結果を選択する必要があります。. hard_sym_invalid_selection_label_3 = グループ内で最初のものはオリジナルとして認識され変更されませんが、二つ目以降は変更されます。. hard_sym_link_title_dialog = リンクの確認 hard_sym_link_label = このファイルをリンクしてもよろしいですか? move_folder_failed = フォルダ { $name } の移動に失敗しました、理由 { $reason } move_file_failed = ファイル { $name } を移動できませんでした、理由 { $reason } move_files_title_dialog = 重複したファイルの移動先フォルダを選択 move_files_choose_more_than_1_path = 重複したファイルをコピーするには、1つのパスのみを選択する必要があります、{ $path_number } つ選択されました。. move_stats = { $num_files }/{ $all_files } アイテムを適切に移動しました save_results_to_file = txtファイルとjsonファイルの両方を"{ $name }"フォルダに保存しました。. search_not_choosing_any_music = エラー: 音楽検索タイプのチェックボックスを少なくとも1つ選択する必要があります。. search_not_choosing_any_broken_files = エラー: チェックされた壊れたファイルの種類のチェックボックスを少なくとも1つ選択する必要があります。. include_folders_dialog_title = 含めるフォルダ exclude_folders_dialog_title = 除外するフォルダ include_manually_directories_dialog_title = ディレクトリを手動で追加 cache_properly_cleared = キャッシュを適切にクリアしました cache_clear_duplicates_title = 重複したキャッシュをクリアする cache_clear_similar_images_title = 類似した画像のキャッシュをクリア中 cache_clear_similar_videos_title = 類似した動画のキャッシュをクリア中 cache_clear_message_label_1 = 古いエントリのキャッシュを消去しますか? cache_clear_message_label_2 = この操作は無効なファイルを指すすべてのキャッシュエントリを削除します。. cache_clear_message_label_3 = これはキャッシュへの読み込みと保存を少し高速化することがあります。. cache_clear_message_label_4 = 警告: 操作により、現在接続されていない外部ドライブからキャッシュされたすべてのデータが削除されます。そのため、それらのハッシュは再度生成する必要があります。. # Show preview preview_image_resize_failure = 画像 { $name } のリサイズに失敗しました。. preview_image_opening_failure = イメージ { $name } を開けませんでした、理由 { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = グループ { $current_group }/{ $all_groups } ({ $images_in_group } 画像) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/ko/czkawka_gui.ftl ================================================ # Window titles window_settings_title = 설정 window_main_title = Czkawka (구글ить) window_progress_title = 스캔중 window_compare_images = 이미지 비교 # General general_ok_button = 확인 general_close_button = 닫기 # Krokiet info dialog krokiet_info_title = Introducing Krokiet - 새로운 버전의 Czkawka krokiet_info_message = 크로키트는 Czkawka GTK GUI의 새로운, 개선된, 더 빠르고 더 안정적인 버전입니다! 실행하기가 더 쉽고 시스템 변경에 더 강하며, 대부분의 시스템에서 기본적으로 사용 가능한 핵심 라이브러리에만 의존합니다. 크로키트는 Czkawka에 없는 기능도 제공하며, 비디오 비교 모드에서 미리보기, EXIF 클리너, 파일 이동/복사/삭제 진행률 또는 확장된 정렬 옵션 등이 포함됩니다. 사용해 보고 차이점을 확인해 보세요! Czkawka는 저로부터 버그 수정 및 소규모 업데이트를 계속 받겠지만, 모든 새로운 기능은 크로키트에만 개발되며, 누구나 새로운 기능 추가, 누락된 모드 확장 또는 Czkawka 추가 확장을 자유롭게 기여할 수 있습니다. PS: 이 메시지는 한 번만 표시되어야 합니다. 다시 나타나면 CZKAWKA_DONT_ANNOY_ME 환경 변수를 비어 있는 값이 아닌 값으로 설정하십시오. # Main window music_title_checkbox = 제목 music_artist_checkbox = 아티스트 music_year_checkbox = 연도 music_bitrate_checkbox = 비트레이트 music_genre_checkbox = 장르 music_length_checkbox = 길이 music_comparison_checkbox = 근사값 비교 music_checking_by_tags = 태그 기준 검사 music_checking_by_content = 내용 기준 검사 same_music_seconds_label = 최소 조각 재생 시간 same_music_similarity_label = 최대 허용 차이 music_compare_only_in_title_group = 유사한 제목들의 그룹 내에서 비교 music_compare_only_in_title_group_tooltip = 활성화 시, 파일이 제목별로 그룹화된 후에만 서로 비교됩니다. 예: 10000개의 파일이 있을 경우, 거의 1억 번의 비교 대신 보통 약 20000번의 비교로 줄어듭니다. same_music_tooltip = 음악 파일 유사도 검색은 아래 설정으로 조정할 수 있습니다: - 유사도로 식별 가능한 최소 조각 시간 - 비교할 조각간 허용 가능한 최대 차이 수치 좋은 결과를 얻기 위해서는 이 두 값을 상황에 맞게 적절히 조합하는 것이 중요합니다. 최소 시간 5초 + 최대 차이 1.0 설정 시 -> 거의 동일한 조각을 찾습니다. 최소 시간 20초 + 최대 차이 6.0 설정 시 -> 리믹스/라이브 버전 등 유사한 경우에 효과적입니다. 기본적으로 모든 음악 파일끼리 비교하게 되므로, 많은 파일을 비교할 때 시간이 오래 걸릴 수 있습니다. 따라서 일반적으로 **참조 폴더(reference folders)** 옵션을 사용하고 비교할 파일을 지정하면, 지문(fingerprint) 비교는 참조 없이 비교하는 것보다 **최소 4배 빠르게** 진행됩니다. music_comparison_checkbox_tooltip = 기계학습을 통해 각 항목의 괄호를 제거합니다. 예를 들어, 다음 두 파일은 같은 파일로 인식될 것입니다. Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = 대소문자 구분 duplicate_case_sensitive_name_tooltip = 대소문자 구분이 켜져 있으면, 완전히 같은 이름만이 중복 파일로 검색됩니다. 예시: Żołd <-> Żołd 대소문자 구분이 꺼져 있으면, 대문자와 소문자 구별을 하지 않고 중복 파일을 검색합니다. 예시: żoŁD <-> Żołd duplicate_mode_size_name_combo_box = 크기 및 이름 기준 duplicate_mode_name_combo_box = 파일명 duplicate_mode_size_combo_box = 파일 크기 duplicate_mode_hash_combo_box = 해시 duplicate_hash_type_tooltip = Czkawka는 3가지 유형의 해시 함수를 지원합니다. Blake3 - 암호화에 사용되는 해시입니다. 매우 빠르게 작동하므로, 기본값으로 설정되어 있습니다. CRC32 - 간단한 해시 함수입니다. Blake3보다는 빠르지만, 매우 드물게 충돌이 발생합니다. XXH3 - Black3와 해시 품질 및 성능 면에서 매우 유사하지만, 암호화에 쓰이지는 않습니다. 때문에 Black3와 실질적으로 같습니다. duplicate_check_method_tooltip = 현재 Czkawka는 중복 파일을 찾는데 3가지 방법을 지원합니다. 파일명 - 같은 이름을 가진 파일들을 찾습니다. 파일 크기 - 같은 크기를 가진 파일들을 찾습니다. 해시 - 같은 내용을 가진 파일들을 찾습니다. 이 모드에서는 먼저 파일을 해시한 다음, 각 해시값들을 비교하여 중복 파일인지 식별합니다. 때문에 중복 파일을 찾는 데 있어 가장 확실한 방법입니다. Czkawka는 캐시에 매우 의존하므로, 같은 데이터를 두 번째 이후로 스캔하는 경우 첫 번째 스캔보다 더욱 빠르게 스캔이 이루어집니다. image_hash_size_tooltip = 각 확인된 이미지에 특별한 해시가 생성되어 서로 비교될 수 있으며, 작은 해시 차이는 이미지가 유사함을 의미합니다. 해시 크기 8은 원본과 약간 유사한 이미지를 찾기에 적절합니다. 다만 이미지 수가 많을 경우(예: 1000개 이상), 거짓 양성(false positives)이 많이 발생할 수 있어 이 경우 더 큰 해시 크기 사용을 권장합니다. 기본 해시 크기 16은 유사 이미지 검색과 해시 충돌 최소화를 적절히 균형 잡은 설정입니다. 해시 크기 32 또는 64는 매우 유사한 이미지만 찾아내며, (알파 채널이 있는 일부 이미지 제외하면) 거의 거짓 양성이 없습니다. image_resize_filter_tooltip = 이미지 해시 계산 전에 라이브러리가 먼저 이미지를 리사이징해야 합니다. 선택된 알고리즘에 따라 해시 계산에 사용하는 이미지의 형태가 약간 달라집니다. 가장 빠른 알고리즘은 `Nearest`이며, 가장 낮은 화질을 제공하지만 기본 해시 크기 16x16일 경우 품질 저하가 눈에 잘 띄지 않습니다. 이미지 수가 적고 해시 크기 8x8을 사용할 경우, `Nearest`보다 다른 알고리즘을 사용하면 더 정확한 그룹핑에 도움이 됩니다. image_hash_alg_tooltip = 해시를 계산하는 데 사용되는 알고리즘을 선택할 수 있습니다. 각각의 알고리즘은 장단점이 있으므로, 경우마다 더 낫거나 더 나쁜 결과를 보여줄 수 있습니다. 따라서 가장 좋은 알고리즘을 찾으려면 수동으로 테스트해 보는 것이 좋습니다. big_files_mode_combobox_tooltip = 가장 큰 파일 또는 가장 작은 파일을 찾을 수 있습니다 big_files_mode_label = 찾을 파일 big_files_mode_smallest_combo_box = 작은 파일 big_files_mode_biggest_combo_box = 큰 파일 main_notebook_duplicates = 중복 파일 main_notebook_empty_directories = 빈 디렉터리 main_notebook_big_files = 큰 파일 main_notebook_empty_files = 빈 파일 main_notebook_temporary = 임시 파일 main_notebook_similar_images = 비슷한 이미지 main_notebook_similar_videos = 비슷한 영상 main_notebook_same_music = 중복 음악 main_notebook_symlinks = 잘못된 심볼릭 링크 main_notebook_broken_files = 손상된 파일 main_notebook_bad_extensions = 잘못된 확장자 main_tree_view_column_file_name = 파일명 main_tree_view_column_folder_name = 폴더명 main_tree_view_column_path = 경로 main_tree_view_column_modification = 수정한 날짜 main_tree_view_column_size = 파일 크기 main_tree_view_column_similarity = 유사도 main_tree_view_column_dimensions = 크기 main_tree_view_column_title = 제목 main_tree_view_column_artist = 아티스트 main_tree_view_column_year = 연도 main_tree_view_column_bitrate = 비트레이트 main_tree_view_column_length = 길이 main_tree_view_column_genre = 장르 main_tree_view_column_symlink_file_name = 심볼릭 링크 파일명 main_tree_view_column_symlink_folder = 심볼릭 링크 폴더 main_tree_view_column_destination_path = 심볼릭 링크 대상 경로 main_tree_view_column_type_of_error = 손상 유형 main_tree_view_column_current_extension = 현재 확장자 main_tree_view_column_proper_extensions = 올바른 확장자 main_tree_view_column_fps = FPS main_tree_view_column_codec = 코덱 main_label_check_method = 확인 방법 main_label_hash_type = 해시 유형 main_label_hash_size = 해시 크기 main_label_size_bytes = 파일 크기 (바이트) main_label_min_size = 최소 main_label_max_size = 최대 main_label_shown_files = 찾을 파일의 개수 main_label_resize_algorithm = 크기 변경 알고리즘 main_label_similarity = 유사도{ " " } main_check_box_broken_files_audio = 음악 파일 main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = 압축 파일 main_check_box_broken_files_image = 이미지 main_check_box_broken_files_video = 비디오 main_check_box_broken_files_video_tooltip = ffmpeg/ffprobe를 사용하여 비디오 파일 유효성 검사합니다. 상당히 느리고 파일이 잘 재생되더라도 형식에 민감한 오류를 감지할 수 있습니다. check_button_general_same_size = 같은 파일크기 무시 check_button_general_same_size_tooltip = 동일한 크기의 파일은 결과에서 제외합니다 – 대부분 1:1 중복일 가능성이 높습니다 main_label_size_bytes_tooltip = 스캔할 파일의 크기입니다 # Upper window upper_tree_view_included_folder_column_title = 검색할 폴더 upper_tree_view_included_reference_column_title = 기준 폴더 upper_recursive_button = 재귀 upper_recursive_button_tooltip = 켜져 있으면, 하위 폴더 내부의 파일까지 검색합니다. upper_manual_add_included_button = 수동 추가 upper_add_included_button = 추가 upper_remove_included_button = 제거 upper_manual_add_excluded_button = 수동 추가 upper_add_excluded_button = 추가 upper_remove_excluded_button = 제거 upper_manual_add_included_button_tooltip = 직접 검색할 경로를 입력합니다. 여러 경로를 입력하고자 한다면, ';'로 구분하세요. '/home/roman;/home/rozkaz' 를 입력하면, '/home/roman'와 '/home/rozkaz'가 추가됩니다 upper_add_included_button_tooltip = 검색할 디렉터리를 추가합니다. upper_remove_included_button_tooltip = 검색할 디렉터리에서 제거합니다. upper_manual_add_excluded_button_tooltip = 직접 제외할 경로를 입력합니다. 여러 경로를 입력하고자 한다면, ';'로 구분하세요. '/home/roman;/home/krokiet' 를 입력하면, '/home/roman'와 '/home/krokiet'가 추가됩니다 upper_add_excluded_button_tooltip = 제외할 디렉터리를 추가합니다. upper_remove_excluded_button_tooltip = 제외할 디렉터리에서 제거합니다. upper_notebook_items_configuration = 항목 설정 upper_notebook_excluded_directories = 제외 경로 upper_notebook_included_directories = 포함된 경로 upper_allowed_extensions_tooltip = 허용할 확장자는 콤마(',')를 통해 구분해야 합니다. (기본값인 경우 모든 확장자를 허용합니다.) IMAGE, VIDEO, MUSIC, TEXT를 입력할 경우 해당하는 파일을 모두 지칭할 수 있습니다. 예시: ".exe, IMAGE, VIDEO, .rar, 7z" - 이와 같이 입력하면, 이미지 파일(예. jpg, png), 영상 파일(예. avi, mp4), exe, rar, 그리고 7z 파일을 검색합니다. upper_excluded_extensions_tooltip = 검사에서 무시될 비활성화된 파일 목록입니다. 허용된 확장자와 비활성화된 확장자를 둘 다 사용할 경우, 비활성화된 확장자가 더 높은 우선순위를 가지므로 해당 파일은 검사되지 않습니다. upper_excluded_items_tooltip = 제외 항목은 * 와일드카드와 쉼표로 구분되어야 합니다. 이는 Excluded Paths 보다 느리므로 주의해서 사용하십시오. upper_excluded_items = 제외할 항목: upper_allowed_extensions = 허용할 확장자: upper_excluded_extensions = 비활성 확장자: # Popovers popover_select_all = 모두 선택 popover_unselect_all = 모두 선택 해제 popover_reverse = 선택 반전 popover_select_all_except_shortest_path = 선택 모두 제외 짧은 경로 popover_select_all_except_longest_path = 선택 전부 제외 가장 긴 경로 popover_select_all_except_oldest = 가장 오래된 파일 제외하고 모두 선택 popover_select_all_except_newest = 가장 최신인 파일 제외하고 모두 선택 popover_select_one_oldest = 가장 오래된 파일 선택 popover_select_one_newest = 가장 최신인 파일 선택 popover_select_custom = 사용자 지정 선택 popover_unselect_custom = 사용자 지정 선택 해제 popover_select_all_images_except_biggest = 가장 큰 파일 제외하고 모두 선택 popover_select_all_images_except_smallest = 가장 작은 파일 제외하고 모두 선택 popover_custom_path_check_button_entry_tooltip = 경로를 기준으로 선택합니다. 사용 예시: '/home/pimpek/rzecz.txt' 파일을 선택하려면 '/home/pim*'와 같이 입력하세요 popover_custom_name_check_button_entry_tooltip = 파일 이름을 기준으로 선택합니다. 사용 예시: '/usr/ping/pong.txt' 파일을 선택하려면 '*ong*'와 같이 입력하세요 popover_custom_regex_check_button_entry_tooltip = 정규표현식을 이용해 선택합니다. 이 모드에서는 경로와 이름 모두가 정규표현식에 의해 검색됩니다. 사용 예시: '/usr/bin/ziemniak.txt' 파일을 선택하려면 '/ziem[a-z]+'와 같이 입력하세요. 정규 표현식은 Rust 언어에 내장된 구현체를 사용합니다. 더 알고 싶다면 https://docs.rs/regex를 방문하세요. popover_custom_case_sensitive_check_button_tooltip = 대소문자를 구분할 지 여부를 선택합니다. 만일 꺼져 있으면, '/home/*'은 '/HoMe/roman'과 '/home/roman'를 모두 선택합니다. popover_custom_not_all_check_button_tooltip = 한 그룹에 있는 모든 항목이 선택되는 것을 방지합니다. 이 옵션은 기본적으로 켜져 있습니다. 대부분의 경우, 원본과 중복 파일을 전부 선택하여 삭제하는 것은 원하지 않는 동작일 것입니다. 즉 각 그룹에서 최소한 하나의 항목은 삭제하지 않고 남겨놓게 됩니다. 경고! 이 설정은 수동으로 그룹의 모든 파일을 이미 선택해 놓았다면 작동하지 않습니다!. popover_custom_regex_path_label = 경로 popover_custom_regex_name_label = 파일명 popover_custom_regex_regex_label = 경로 및 파일 정규표현식 popover_custom_case_sensitive_check_button = 대소문자 구별 popover_custom_all_in_group_label = 그룹의 모든 항목을 선택하지 않음 popover_custom_mode_unselect = 사용자 지정 선택 해제 popover_custom_mode_select = 사용자 지정 선택 popover_sort_file_name = 파일 이름 popover_sort_folder_name = 폴더 이름 popover_sort_full_name = 본인 이름 popover_sort_size = 파일 크기 popover_sort_selection = 선택 popover_invalid_regex = 정규표현식이 유효하지 않습니다 popover_valid_regex = 정규표현식이 유효합니다 # Bottom buttons bottom_search_button = 검색 bottom_select_button = 선택 bottom_delete_button = 삭제 bottom_save_button = 저장 bottom_symlink_button = 심볼릭 링크 bottom_hardlink_button = 하드 링크 bottom_move_button = 이동 bottom_sort_button = 종류 bottom_compare_button = 비교 bottom_search_button_tooltip = 검색을 시작합니다 bottom_select_button_tooltip = 항목을 선택합니다. 오직 선택된 것만이 처리됩니다. bottom_delete_button_tooltip = 선택된 파일 또는 폴더를 삭제합니다. bottom_save_button_tooltip = 검색 결과를 파일로 저장합니다 bottom_symlink_button_tooltip = 심볼릭 링크를 생성합니다. 그룹 내에서 최소한 2개의 파일이 선택되어 있어야 합니다. 첫 번째 파일은 그대로 남으며, 두 번째 이후 파일은 첫 번째 파일로 향하는 심볼릭 링크가 됩니다. bottom_hardlink_button_tooltip = 하드 링크를 생성합니다. 그룹 내에서 최소한 2개의 파일이 선택되어 있어야 합니다. 첫 번째 파일은 그대로 남으며, 두 번째 이후 파일은 첫 번째 파일로 향하는 하드 링크가 됩니다. bottom_hardlink_button_not_available_tooltip = 하드 링크를 생성합니다. 현재 하드 링크를 만들 수 없어 버튼이 비활성화되었습니다. Windows에서 하드 링크는 관리자 권한으로만 만들 수 있습니다. 프로그램이 관리자 권한으로 실행되었는지 확인하세요. 만일 프로그램이 이미 관리자 권한으로 실행되었다면, Github에서 비슷한 이슈가 있는지 확인해보세요. bottom_move_button_tooltip = 선택된 디렉터리로 파일을 이동합니다. 이 동작은 원본이 위치한 경로를 전부 무시하고, 선택한 경로로 파일을 전부 복사합니다. 만일 2개 이상의 파일이 같은 이름을 가지고 있다면, 첫 번째 이후의 파일은 복사에 실패하고 오류 메시지를 보여줄 것입니다. bottom_sort_button_tooltip = 파일/폴더를 선택한 방법으로 정렬합니다. bottom_compare_button_tooltip = 그룹 내의 이미지를 비교합니다. bottom_show_errors_tooltip = 하단 텍스트 패널을 보이거나 숨깁니다. bottom_show_upper_notebook_tooltip = 상단 패널을 보이거나 숨깁니다. # Progress Window progress_stop_button = 정지 progress_stop_additional_message = 정지 요청됨 # About Window about_repository_button_tooltip = 소스 코드가 있는 리포지토리 페이지 링크입니다. about_donation_button_tooltip = 기부 페이지 링크입니다. about_instruction_button_tooltip = 사용방법 페이지 링크입니다. about_translation_button_tooltip = 번역을 위한 Crowdin 페이지 링크입니다. 공식적으로 지원되는 언어는 폴란드어와 영어입니다. about_repository_button = 리포지토리 about_donation_button = 기부 about_instruction_button = 사용방법 about_translation_button = 번역 # Header header_setting_button_tooltip = 설정창을 엽니다. header_about_button_tooltip = 이 앱에 대한 정보창을 엽니다. # Settings ## General settings_number_of_threads = 스레드 수 settings_number_of_threads_tooltip = 사용할 스레드 수입니다. 0이면 가능한 최대 스레드를 사용합니다. settings_use_rust_preview = 미리보기에 GTK 대신 외부 라이브러리 사용 settings_use_rust_preview_tooltip = GTK 미리보기는 일부 경우 더 빠르거나 더 많은 형식을 지원하지만, 반대로 성능이 더 떨어질 수도 있습니다. 미리보기 로딩에 문제가 있다면 이 설정을 변경해 보세요. 리눅스가 아닌 환경에서는 `gtk-pixbuf`가 항상 사용 가능하지 않기 때문에 이 옵션을 끄면 일부 이미지 미리보기가 로드되지 않을 수 있습니다. settings_label_restart = 이 설정을 적용하려면 프로그램을 재시작해야 합니다! settings_ignore_other_filesystems = 다른 파일시스템 무시(Linux에서만) settings_ignore_other_filesystems_tooltip = 검색할 디렉터리와 파일시스템이 다른 디렉터리를 무시합니다. Linux의 find 명령에서 -xdev 옵션을 준 것과 동일하게 동작합니다 settings_save_at_exit_button_tooltip = 프로그램 종료 시에 설정을 저장합니다. settings_load_at_start_button_tooltip = 프로그램을 열 때 저장된 설정을 불러옵니다. 꺼져 있다면, 기본 설정으로 프로그램을 시작합니다. settings_confirm_deletion_button_tooltip = 삭제 버튼을 누를 때 확인창을 띄웁니다. settings_confirm_link_button_tooltip = 하드 링크/심볼릭 링크 버튼을 누를 때 확인창을 띄웁니다. settings_confirm_group_deletion_button_tooltip = 그룹의 모든 항목을 삭제할 경우 경고창을 보여줍니다. settings_show_text_view_button_tooltip = UI 하단에 텍스트 패널을 보여줍니다. settings_use_cache_button_tooltip = 파일 캐시를 사용합니다. settings_save_also_as_json_button_tooltip = 캐시를 (사람이 읽을 수 있는) JSON 포맷으로 저장합니다. 캐시 내용을 수정할 수 있습니다. 만일 bin 확장자를 가진 바이너리 캐시 파일이 없으면, JSON 캐시가 프로그램 시작 시에 대신 로드됩니다. settings_use_trash_button_tooltip = 파일을 영구 삭제하는 대신 휴지통으로 이동합니다. settings_language_label_tooltip = UI에 표시될 언어를 설정합니다. settings_save_at_exit_button = 프로그램을 닫을 때 설정을 저장 settings_load_at_start_button = 프로그램을 열 때 설정을 불러오기 settings_confirm_deletion_button = 항목 삭제 시에 확인창 띄우기 settings_confirm_link_button = 항목 심볼릭 링크/하드 링크 설정시에 확인창 띄우기 settings_confirm_group_deletion_button = 그룹 내의 모든 항목 삭제 시 경고창 띄우기 settings_show_text_view_button = 하단 텍스트 패널 표시하기 settings_use_cache_button = 캐시 사용 settings_save_also_as_json_button = 캐시를 JSON 포맷으로도 저장 settings_use_trash_button = 삭제된 파일을 휴지통으로 이동 settings_language_label = 언어 settings_multiple_delete_outdated_cache_checkbutton = 만료된 파일을 캐시에서 자동으로 삭제 settings_multiple_delete_outdated_cache_checkbutton_tooltip = 더 이상 존재하지 않는 파일에 대한 정보를 캐시에서 삭제합니다. 이 옵션이 켜져 있으면, 프로그램은 존재하는 파일만이 캐시에 남도록 할 것입니다(망가진 파일은 무시됩니다). 이 옵션을 끄는 것은 외장 저장장치에 존재하는 파일을 스캔했을 때, 외장 저장장치에 있는 파일에 대한 캐시를 보존하는 데 도움이 됩니다. 만일 수백~수천 개의 파일에 해당하는 정보가 캐시에 있다면 이 옵션을 켜는 것을 추천합니다. 이 경우 캐시를 저장하거나 불러오는 시간이 빨라집니다. settings_notebook_general = 일반 settings_notebook_duplicates = 중복 파일 settings_notebook_images = 유사한 이미지 settings_notebook_videos = 유사한 영상 ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = 이미지 파일을 선택하면 우측에 미리보기를 보여줍니다. settings_multiple_image_preview_checkbutton = 이미지 미리보기 표시 settings_multiple_clear_cache_button_tooltip = 더 이상 존재하지 않는 파일을 캐시에서 제거합니다. 캐시를 자동으로 정리하는 옵션이 꺼져 있을 때만 사용하세요. settings_multiple_clear_cache_button = 캐시에서 오래된 결과 제거. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = 하나의 파일에 대한 여러 개의 하드 링크가 존재할 경우, 그 중 하나만을 표시합니다. 예: 만일 특정한 파일에 대한 7개의 하드 링크가 디스크에 존재하고, 그 중 하나가 다른 inode를 갖는다면, 결과창에는 1개의 파일과 1개의 하드 링크만이 표시됩니다. settings_duplicates_minimal_size_entry_tooltip = 캐시에 추가되기 위한 최소 파일 사이즈를 설정합니다. 이 값이 작을 수록 더 많은 파일이 캐시에 저장됩니다. 이 경우 검색은 더 빨라지지만, 캐시 저장 및 불러오기는 느려집니다. settings_duplicates_prehash_checkbutton_tooltip = 사전 해시(파일 일부만으로 계산되는 해시)에 대한 캐싱을 허용하여, 중복이 아닌 파일을 더 빠르게 결과에서 제거합니다. 이 옵션은 일부 상황에서 검색을 느리게 하기 때문에, 기본적으로 꺼져 있습니다. 만일 수백~수천 개 이상의 파일처럼 매우 많은 파일을 여러 번 검색하는 경우, 이 기능을 반드시 켜는 것을 추천합니다. settings_duplicates_prehash_minimal_entry_tooltip = 캐싱을 위한 최소 파일크기입니다. settings_duplicates_hide_hard_link_button = 하드 링크 숨기기 settings_duplicates_prehash_checkbutton = 사전 해시 캐싱하기 settings_duplicates_minimal_size_cache_label = 캐싱하기 위한 최소 파일 크기 (바이트) settings_duplicates_minimal_size_cache_prehash_label = 사전 해시를 캐싱하기 위한 최소 파일 크기 (바이트) ## Saving/Loading settings settings_saving_button_tooltip = 현재 설정을 파일에 저장합니다. settings_loading_button_tooltip = 저장된 설정을 불러와 현재 설정을 덮어씁니다. settings_reset_button_tooltip = 설정을 기본값으로 되돌립니다. settings_saving_button = 설정 저장 settings_loading_button = 설정 불러오기 settings_reset_button = 설정 초기화 ## Opening cache/config folders settings_folder_cache_open_tooltip = 캐시 파일이 저장되는 폴더를 엽니다. 캐시 파일을 편집하는 경우 유효하지 않은 결과가 표시될 수 있습니다. 다만, 많은 양의 파일이 다른 곳으로 이동되었다면 캐시 내의 경로를 수정하는 것이 도움이 됩니다. 만일 비슷한 디렉터리 구조를 가지는 경우, 캐시 파일을 복사하여 다른 컴퓨터에서도 같은 캐시를 재활용할 수 있습니다. 만일 캐시에 문제가 발생한다면 이 폴더의 파일들을 지우십시오. 그렇게 하면 프로그램이 다시 캐시 파일을 생성합니다. settings_folder_settings_open_tooltip = Czkawka의 설정 파일이 있는 폴더를 엽니다. 경고! 설정 파일을 수동으로 편집하는 경우 원치 않는 동작이 일어날 수 있습니다. settings_folder_cache_open = 캐시 폴더 열기 settings_folder_settings_open = 설정 폴더 열기 # Compute results compute_stopped_by_user = 사용자에 의해 검색이 중단됨 compute_found_duplicates_hash_size = { $number_files }개의 중복 파일을 { $number_groups }개 그룹에서 발견했으며 이는 { $size }를 차지하고 { $time }에 걸쳐 수행되었습니다 compute_found_duplicates_name = { $number_files } 개의 중복 파일을 { $number_groups } 그룹에서 { $time }에 발견했습니다 compute_found_empty_folders = { $number_files }개의 비어있는 폴더를 { $time } 안에 발견했습니다 compute_found_empty_files = { $number_files }개의 빈 파일을 { $time }에 발견했습니다 compute_found_big_files = { $number_files }개의 큰 파일을 { $time }에 찾았습니다 compute_found_temporary_files = { $number_files }개의 임시 파일을 { $time } 안에 찾았습니다 compute_found_images = { $number_files } 개의 유사한 이미지를 { $number_groups } 그룹에서 { $time } 내에 발견했습니다 compute_found_videos = { $number_files } 개의 비슷한 동영상을 { $number_groups } 그룹에서 { $time } 내에 찾았습니다 compute_found_music = { $number_files }개의 비슷한 음악 파일을 { $number_groups } 그룹에서 { $time } 내에 찾았습니다 compute_found_invalid_symlinks = { $number_files }개의 유효하지 않은 심볼릭 링크를 { $time }에서 찾았습니다 compute_found_broken_files = { $number_files }개의 봉인된 파일을 { $time }에 발견했습니다 compute_found_bad_extensions = { $number_files } 확장자에 문제가 있는 파일을 { $time } 내로找到了 # Progress window progress_scanning_general_file = { $file_number -> [one] { $file_number }개 파일 스캔 완료 *[other] { $file_number }개 파일 스캔 완료 } progress_scanning_extension_of_files = { $file_checked }/{ $all_files }개의 파일 확장자 확인 progress_scanning_broken_files = { $file_checked }/{ $all_files }개 파일 확인 (데이터: { $data_checked } / { $all_data }) progress_scanning_video = { $file_checked }/{ $all_files }개의 비디오 해시 생성 progress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video progress_scanning_image = { $file_checked }/{ $all_files }개의 이미지 해시 생성 (데이터: { $data_checked } / { $all_data }) progress_comparing_image_hashes = { $file_checked }/{ $all_files }개의 이미지 해시 비교 progress_scanning_music_tags_end = { $file_checked }/{ $all_files }개의 음악 파일 태그 비교 완료 progress_scanning_music_tags = { $file_checked }/{ $all_files }개의 음악 파일 태그 읽는 중 progress_scanning_music_content_end = { $file_checked }/{ $all_files }개의 음악 파일 지문 비교 완료 progress_scanning_music_content = { $file_checked }/{ $all_files }개의 음악 파일 지문 계산 중 (데이터: { $data_checked } / { $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] { $folder_number }개 폴더 스캔 완료 *[other] { $folder_number }개 폴더 스캔 완료 } progress_scanning_size = { $file_number }개의 파일 크기 스캔 완료 progress_scanning_size_name = { $file_number }개의 파일 이름 및 크기 스캔 완료 progress_scanning_name = { $file_number }개의 파일 이름 스캔 완료 progress_analyzed_partial_hash = { $file_checked }/{ $all_files }개 파일 부분 해시 분석 완료 (데이터: { $data_checked } / { $all_data }) progress_analyzed_full_hash = { $file_checked }/{ $all_files }개 파일 전체 해시 분석 완료 (데이터: { $data_checked } / { $all_data }) progress_prehash_cache_loading = PreHash 캐시 로드 중 progress_prehash_cache_saving = PreHash 캐시 저장 중 progress_hash_cache_loading = 해시 캐시 로드 중 progress_hash_cache_saving = 해시 캐시 저장 중 progress_cache_loading = 캐시 로드 중 progress_cache_saving = 캐시 저장 중 progress_current_stage = 현재 단계:{ " " } progress_all_stages = 전체 단계:{ " " } # Saving loading saving_loading_saving_success = 파일 { $name }에 설정 저장함. saving_loading_saving_failure = кон피그레이션 데이터를 파일 { $name }에 저장하지 못했습니다, 이유는 { $reason }입니다. saving_loading_reset_configuration = 현재 설정이 초기화됨. saving_loading_loading_success = 앱 설정 불러오기 성공. saving_loading_failed_to_create_config_file = "{ $path }" 파일에 설정을 저장할 수 없습니다. 이유: "{ $reason }". saving_loading_failed_to_read_config_file = "{ $path }" 파일에서 설정을 불러올 수 없습니다. 파일이 없거나, 파일이 아닙니다. saving_loading_failed_to_read_data_from_file = "{ $path }" 파일을 읽을 수 없습니다. 이유: "{ $reason }". # Other selected_all_reference_folders = 모든 디렉터리가 기준 폴더이므로, 검색을 시작할 수 없습니다 searching_for_data = 검색 중. 잠시만 기다려주세요... text_view_messages = 알림 text_view_warnings = 경고 text_view_errors = 오류 about_window_motto = 이 프로그램은 무료이며, 앞으로도 항상 그럴 것이다. krokiet_new_app = 'Czkawka'는 유지보수 모드에 있습니다,这意味着仅会修复关键错误且不会添加新功能。对于新功能,请查看新的Krokiet应用,该应用更加稳定性能更强并且仍在积极开发中。. # Various dialog dialogs_ask_next_time = 다음에도 묻기 symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason } delete_title_dialog = 삭제 확인 delete_question_label = 정말로 파일들을 삭제합니까? delete_all_files_in_group_title = 그룹의 모든 파일 삭제 확인 delete_all_files_in_group_label1 = 일부 그룹 내에 있는 모든 파일이 선택되어 있습니다. delete_all_files_in_group_label2 = 정말로 해당 파일을 모두 삭제합니까? delete_items_label = { $items }개의 파일이 삭제됩니다. delete_items_groups_label = { $groups }개 그룹에서 { $items }개의 파일이 삭제됩니다. hardlink_failed = 하드 링크를 생성하지 못했습니다 { $name }을 { $target }으로, 이유는 { $reason }입니다 hard_sym_invalid_selection_title_dialog = 일부 그룹의 선택이 유효하지 않습니다 hard_sym_invalid_selection_label_1 = 일부 그룹에서 1개의 항목만이 선택되었으며, 해당 항목은 무시됩니다. hard_sym_invalid_selection_label_2 = 하드 링크/심볼릭 링크를 생성하려면, 그룹 내에서 최소 2개의 파일이 선택되어야 합니다. hard_sym_invalid_selection_label_3 = 그룹 내의 첫 번째가 원본으로 설정되며, 나머지는 수정될 것입니다. hard_sym_link_title_dialog = 링크 생성 확인 hard_sym_link_label = 정말로 해당 파일들을 링크합니까? move_folder_failed = { $name } 폴더 이동 실패. 이유: { $reason } move_file_failed = { $name } 파일 이동 실패. 이유: { $reason } move_files_title_dialog = 중복 파일을 이동할 폴더를 선택하세요 move_files_choose_more_than_1_path = 중복 파일을 복사할 1개의 폴더만 지정해야 하지만, { $path_number }개의 경로를 선택했습니다. move_stats = { $num_files }/{ $all_files }개의 항목을 이동함 save_results_to_file = 결과를 txt 및 json 파일로 ‘{ $name }’ 폴더에 저장했습니다. search_not_choosing_any_music = 경고: 최소한 하나의 검색 방법을 선택해야 합니다. search_not_choosing_any_broken_files = 경고: 최소한 하나 이상의 검색할 파일 분류를 선택해야 합니다. include_folders_dialog_title = 검색할 폴더 추가 exclude_folders_dialog_title = 제외할 폴더 추가 include_manually_directories_dialog_title = 수동으로 디렉터리 추가 cache_properly_cleared = 캐시를 성공적으로 정리했습니다 cache_clear_duplicates_title = 중복 파일 캐시 정리 cache_clear_similar_images_title = 유사한 이미지 캐시 정리 cache_clear_similar_videos_title = 유사한 영상 캐시 정리 cache_clear_message_label_1 = 유효하지 않은 캐시 항목을 제거할까요? cache_clear_message_label_2 = 이 동작은 더 이상 유효하지 않은 파일에 대한 캐시 항목을 제거합니다. cache_clear_message_label_3 = 이를 통해 더 빠른 캐시 저장/불러오기가 가능할 수 있습니다. cache_clear_message_label_4 = 경고! 이 동작은 연결되지 않은 외장 저장장치에 위치한 모든 항목을 제거합니다. 따라서 해당 파일들에 대한 캐시는 다시 생성되어야 합니다. # Show preview preview_image_resize_failure = { $name } 이미지 크기 조정 실패. preview_image_opening_failure = { $name } 이미지 열기 실패. 이유: { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = 그룹 { $current_group } / { $all_groups } ({ $images_in_group } 이미지) compare_move_left_button = 이전 compare_move_right_button = 다음 ================================================ FILE: czkawka_gui/i18n/nl/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Instellingen window_main_title = Czkawka (Giechel) window_progress_title = Scannen window_compare_images = Vergelijk afbeeldingen # General general_ok_button = OK general_close_button = Afsluiten # Krokiet info dialog krokiet_info_title = Introductie van Krokiet - Nieuwe versie van Czkawka krokiet_info_message = Krokiet is de nieuwe, verbeterde, snellere en betrouwbaardere versie van de Czkawka GTK GUI! Het is eenvoudiger te draaien en robuuster tegen systeemwijzigingen, omdat het alleen afhankelijk is van kernbibliotheken die standaard op de meeste systemen beschikbaar zijn. Krokiet brengt ook functies die Czkawka mist, waaronder thumbnails in video vergelijking modus, een EXIF cleaner, bestand verplaatsen/kopieer/verwijderen voortgang of uitgebreide sorteeropties. Probeer het uit en zie het verschil! 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. 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. # Main window music_title_checkbox = Aanspreektitel music_artist_checkbox = Kunstenaar music_year_checkbox = jaar music_bitrate_checkbox = Bitsnelheid music_genre_checkbox = genre music_length_checkbox = longueur music_comparison_checkbox = Geschatte vergelijking music_checking_by_tags = Labels music_checking_by_content = Inhoud same_music_seconds_label = Minimale fragment tweede duur same_music_similarity_label = Maximum verschil music_compare_only_in_title_group = Vergelijk binnen groepen van vergelijkbare titels music_compare_only_in_title_group_tooltip = Wanneer ingeschakeld, worden bestanden gegroepeerd op titel en vervolgens vergeleken met elkaar. Met 10000 bestanden, in plaats van bijna 100 miljoen vergelijkingen zullen er meestal ongeveer 20000 vergelijkingen worden gemaakt. same_music_tooltip = Zoeken naar vergelijkbare muziekbestanden door de inhoud ervan kan worden geconfigureerd door instelling: - De minimale fragmenttijd waarna muziekbestanden kunnen worden geïdentificeerd als vergelijkbaar - Het maximale verschil tussen twee geteste fragmenten De sleutel tot goede resultaten is om verstandige combinaties van deze parameters te vinden, voor opgegeven. Instelling van de minimale tijd op 5 en het maximale verschil op 1.0, zal zoeken naar bijna identieke fragmenten in de bestanden. Een tijd van 20 en een maximaal verschil van 6,0 werkt daarentegen goed voor het vinden van remixes/live versies, enz. 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). music_comparison_checkbox_tooltip = 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: SØ wieľdzizive b --- S000000wie.pldzizľb (Remix Lato 2021) duplicate_case_sensitive_name = Kist gevoelig duplicate_case_sensitive_name_tooltip = Wanneer ingeschakeld, groep alleen records wanneer ze precies dezelfde naam hebben, b.v. Zit ołd <-> ZØ ołd Uitschakelen van een dergelijke optie zal namen groeperen zonder te controleren of elke letter hetzelfde formaat heeft, bijv. zghaoŁD <-> Zit ołd duplicate_mode_size_name_combo_box = Grootte en naam duplicate_mode_name_combo_box = naam duplicate_mode_size_combo_box = Grootte duplicate_mode_hash_combo_box = Toegangssleutel duplicate_hash_type_tooltip = Czkawka biedt 3 soorten hashes: Blake3 - cryptografische hash-functie. Dit is de standaard omdat het erg snel is. CRC32 - eenvoudige hashfunctie. Dit zou sneller moeten zijn dan Blake3, maar kan zeer zelden een botsing veroorzaken. XXH3 - erg vergelijkbaar in prestaties en hashkwaliteit naar Blake3 (maar niet-cryptografie). Dergelijke modi kunnen dus eenvoudig worden verwisseld. duplicate_check_method_tooltip = Op dit moment biedt Czkawka drie soorten methode aan om duplicaten te vinden door: Naam - Gevonden bestanden met dezelfde naam. Grootte - Gevonden bestanden die dezelfde grootte hebben. 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. image_hash_size_tooltip = 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. 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. 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. 32 en 64 hashes vinden slechts zeer gelijksoortige afbeeldingen, maar zouden bijna geen valse positieve motieven moeten hebben (behalve sommige afbeeldingen met alpha kanaal). image_resize_filter_tooltip = Om hash van de afbeelding te berekenen, moet de bibliotheek deze eerst grootschalen. Afhankelijk van het gekozen algoritme, zal de uiteindelijke afbeelding die gebruikt wordt om hash te berekenen er een beetje anders uitzien. 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. met 8x8 hash grootte is het raadzaam om een ander algoritme te gebruiken dan Nearest, om betere groepen afbeeldingen te hebben. image_hash_alg_tooltip = Gebruikers kunnen kiezen uit een van de vele algoritmes om de hash te berekenen. Elk van deze punten heeft sterke en zwakke punten en zal soms betere en soms slechtere resultaten opleveren voor verschillende afbeeldingen. Dus om het beste voor u te bepalen, is handmatige test vereist. big_files_mode_combobox_tooltip = Maakt het mogelijk om naar kleinste/grootste bestanden te zoeken big_files_mode_label = Gecontroleerde bestanden big_files_mode_smallest_combo_box = De Kleinste big_files_mode_biggest_combo_box = De Grootste main_notebook_duplicates = Dupliceer Bestanden main_notebook_empty_directories = Lege mappen main_notebook_big_files = Grote bestanden main_notebook_empty_files = Lege bestanden main_notebook_temporary = Tijdelijke bestanden main_notebook_similar_images = Vergelijkbare afbeeldingen main_notebook_similar_videos = Soortgelijke video's main_notebook_same_music = Muziek duplicaten main_notebook_symlinks = Ongeldige Symlinks main_notebook_broken_files = Kapotte Bestanden main_notebook_bad_extensions = Slechte extensies main_tree_view_column_file_name = Bestandsnaam main_tree_view_column_folder_name = Map Naam main_tree_view_column_path = Pad main_tree_view_column_modification = Wijziging datum main_tree_view_column_size = Grootte main_tree_view_column_similarity = Vergelijkbaarheid main_tree_view_column_dimensions = Mål main_tree_view_column_title = Aanspreektitel main_tree_view_column_artist = Kunstenaar main_tree_view_column_year = jaar main_tree_view_column_bitrate = Bitsnelheid main_tree_view_column_length = longueur main_tree_view_column_genre = genre main_tree_view_column_symlink_file_name = Symlink bestandsnaam main_tree_view_column_symlink_folder = Symlink map main_tree_view_column_destination_path = Bestemming pad main_tree_view_column_type_of_error = Type fout main_tree_view_column_current_extension = Huidige extensie main_tree_view_column_proper_extensions = Proper Extensie main_tree_view_column_fps = FPS main_tree_view_column_codec = Codec main_label_check_method = Controleer methode main_label_hash_type = Soort hash main_label_hash_size = Hash grootte main_label_size_bytes = Grootte (bytes) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Aantal getoonde bestanden main_label_resize_algorithm = Algoritme aanpassen main_label_similarity = Similarity{ " " } main_check_box_broken_files_audio = Geluid main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = Archief main_check_box_broken_files_image = Afbeelding main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Negeer dezelfde grootte check_button_general_same_size_tooltip = Bestanden met identieke grootte in resultaten negeren - meestal zijn deze 1:1 duplicaten main_label_size_bytes_tooltip = Grootte van bestanden die zullen worden gebruikt in scan # Upper window upper_tree_view_included_folder_column_title = Mappen om te zoeken upper_tree_view_included_reference_column_title = Referentie Mappen upper_recursive_button = Recursief upper_recursive_button_tooltip = Indien geselecteerd, zoek ook naar bestanden die niet direct onder de gekozen mappen worden geplaatst. upper_manual_add_included_button = Handmatig toevoegen upper_add_included_button = Toevoegen upper_remove_included_button = Verwijderen upper_manual_add_excluded_button = Handmatig toevoegen upper_add_excluded_button = Toevoegen upper_remove_excluded_button = Verwijderen upper_manual_add_included_button_tooltip = Voeg mapnaam toe om met de hand te zoeken. Om meerdere paden tegelijk toe te voegen, scheiden ze met ; /home/roman;/home/rozkaz zal twee mappen / home/roman en /home/rozkaz toevoegen upper_add_included_button_tooltip = Voeg nieuwe map toe om te zoeken. upper_remove_included_button_tooltip = Map verwijderen uit zoekopdracht. upper_manual_add_excluded_button_tooltip = Voeg uitgesloten mapnaam met de hand toe. Om meerdere paden tegelijk toe te voegen, scheid ze met /home/roman;/home/krokiet zal twee mappen / home/roman en /home/keokiet toevoegen upper_add_excluded_button_tooltip = Voeg map toe om uitgesloten te worden in zoekopdracht. upper_remove_excluded_button_tooltip = Verwijder map van uitgesloten. upper_notebook_items_configuration = Artikelen configuratie upper_notebook_excluded_directories = Uitgesloten Paden upper_notebook_included_directories = Inclusieve Paden upper_allowed_extensions_tooltip = Toegestane extensies moeten door komma's gescheiden worden (standaard zijn alle beschikbaar). De volgende macro's die meerdere extensies in één keer toevoegen, zijn ook beschikbaar: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Lijst van uitgeschakelde bestanden die genegeerd zullen worden in scan. Wanneer gebruik wordt gemaakt van toegestane en uitgeschakelde extensies, heeft deze hogere prioriteit, dus het bestand zal niet worden gecontroleerd. upper_excluded_items_tooltip = Uitsluitende items moeten * wildcard bevatten en moeten gescheiden worden door komma's. Dit is langzamer dan Uitsluitingspaden, dus gebruik het voorzichtig. upper_excluded_items = Uitgesloten artikelen: upper_allowed_extensions = Toegestane extensies: upper_excluded_extensions = Uitgeschakelde extensies: # Popovers popover_select_all = Alles selecteren popover_unselect_all = Selectie ongedaan maken popover_reverse = Omgekeerde selectie popover_select_all_except_shortest_path = Selecteer alles behalve de kortste route popover_select_all_except_longest_path = Selecteer alles behalve de langste pad popover_select_all_except_oldest = Alles selecteren behalve oudste popover_select_all_except_newest = Selecteer alles behalve nieuwste popover_select_one_oldest = Selecteer één oudste popover_select_one_newest = Selecteer een nieuwste popover_select_custom = Selecteer aangepaste popover_unselect_custom = Aangepaste deselecteer ongedaan maken popover_select_all_images_except_biggest = Alles selecteren behalve de grootste popover_select_all_images_except_smallest = Selecteer alles behalve de kleinste popover_custom_path_check_button_entry_tooltip = Records per pad selecteren. Voorbeeld gebruik: /home/pimpek/rzecz.txt kan worden gevonden met /home/pim* popover_custom_name_check_button_entry_tooltip = Records selecteren op bestandnamen. Voorbeeld gebruik: /usr/ping/pong.txt kan worden gevonden met *ong* popover_custom_regex_check_button_entry_tooltip = Select records by specified Regex. In this mode is searched text Path with Name. Voorbeeld use usage: /usr/bin/ziemniak. xt kan gevonden worden met /ziem[a-z]+ Dit gebruikt de standaard Rust regex implementatie. Je kunt hier meer over lezen: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Maakt hoofdlettergevoelige detectie mogelijk. Wanneer uitgeschakeld /home/* vindt zowel /HoMe/roman en /home/roman. popover_custom_not_all_check_button_tooltip = Voorkomt dat alle records in de groep worden geselecteerd. Dit is standaard ingeschakeld, omdat in de meeste situaties u wilt niet zowel origineel als duplicaten verwijderen, maar ten minste één bestand achterlaten. WAARSCHUWING: Deze instelling werkt niet als je al handmatig alle resultaten hebt geselecteerd in een groep. popover_custom_regex_path_label = Pad popover_custom_regex_name_label = naam popover_custom_regex_regex_label = Regex pad + naam popover_custom_case_sensitive_check_button = Hoofdletter gevoelig popover_custom_all_in_group_label = Niet alle records in groep selecteren popover_custom_mode_unselect = Aangepaste deselecteren popover_custom_mode_select = Selecteer aangepast popover_sort_file_name = Bestandsnaam is vereist popover_sort_folder_name = Folder Name popover_sort_full_name = Volledige naam popover_sort_size = Grootte popover_sort_selection = Selectie popover_invalid_regex = Regex is ongeldig popover_valid_regex = Regex is geldig # Bottom buttons bottom_search_button = Zoeken bottom_select_button = Selecteren bottom_delete_button = Verwijderen bottom_save_button = Opslaan bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Verplaatsen bottom_sort_button = Sorteren bottom_compare_button = Vergelijk bottom_search_button_tooltip = Zoeken starten bottom_select_button_tooltip = Selecteer records. Alleen geselecteerde bestanden/mappen kunnen later worden verwerkt. bottom_delete_button_tooltip = Verwijder geselecteerde bestanden/mappen. bottom_save_button_tooltip = Gegevens opslaan van zoekopdracht naar bestand bottom_symlink_button_tooltip = Maak symbolische links. Werkt alleen wanneer ten minste twee resultaten in een groep zijn geselecteerd. De eerste is ongewijzigd en de tweede is symgekoppeld naar eerst. bottom_hardlink_button_tooltip = Maak hardlinks. Werkt alleen wanneer ten minste twee resultaten in een groep worden geselecteerd. De eerste is ongewijzigd en de tweede keer en later zijn vastgekoppeld aan eerst. bottom_hardlink_button_not_available_tooltip = Maak hardlinks. Knop is uitgeschakeld, omdat hardlinks niet kunnen worden gemaakt. Hardlinks werkt alleen met beheerdersrechten op Windows, dus zorg ervoor dat je de app als administrator gebruikt. Als de app al werkt met dergelijke privileges controle op gelijksoortige issues op Github. bottom_move_button_tooltip = Verplaatst bestanden naar de gekozen map. Het kopieert alle bestanden naar de map zonder de mapstructuur te bewaren. Wanneer twee bestanden met dezelfde naam naar de map worden verplaatst, zal de tweede mislukt en de fout worden weergegeven. bottom_sort_button_tooltip = Sorteert bestanden/mappen op de geselecteerde methode. bottom_compare_button_tooltip = Afbeeldingen in de groep vergelijken. bottom_show_errors_tooltip = Onderste tekstvenster tonen/verbergen. bottom_show_upper_notebook_tooltip = Toon/Verberg bovenste notitieboekpaneel. # Progress Window progress_stop_button = Stoppen progress_stop_additional_message = Stop aangevraagd # About Window about_repository_button_tooltip = Link naar de repository pagina met broncode. about_donation_button_tooltip = Link naar donatie pagina. about_instruction_button_tooltip = Link naar instructiepagina. about_translation_button_tooltip = Link naar de Crowdin pagina met appvertalingen. Officieel worden Pools en Engels ondersteund. about_repository_button = Bewaarplaats about_donation_button = Donatie about_instruction_button = Instructie about_translation_button = Vertaling # Header header_setting_button_tooltip = Opent instellingen dialoogvenster. header_about_button_tooltip = Opent dialoogvenster met info over app. # Settings ## General settings_number_of_threads = Aantal gebruikte threads settings_number_of_threads_tooltip = Aantal gebruikte threads, 0 betekent dat alle beschikbare threads zullen worden gebruikt. settings_use_rust_preview = Gebruik externe bibliotheken in plaats daarvan om previews te laden settings_use_rust_preview_tooltip = Het gebruik van gtk previews zal soms sneller zijn en ondersteuning bieden voor meer formaten, maar soms kan het precies het tegenovergestelde zijn. Als je problemen hebt met het laden van previews, kan je proberen deze instelling te veranderen. 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. settings_label_restart = U moet de app herstarten om de instellingen toe te passen! settings_ignore_other_filesystems = Negeer andere bestandssystemen (alleen Linux) settings_ignore_other_filesystems_tooltip = negeert bestanden die niet in hetzelfde bestandssysteem zitten als gezochte mappen. Werkt dezelfde als -xdev optie in het zoekcommando voor Linux settings_save_at_exit_button_tooltip = Configuratie opslaan in bestand bij het sluiten van de app. settings_load_at_start_button_tooltip = Laad de configuratie van het bestand bij het openen van de app. Indien niet ingeschakeld, worden standaard instellingen gebruikt. settings_confirm_deletion_button_tooltip = Bevestigingsvenster tonen bij het klikken op de knop verwijderen. settings_confirm_link_button_tooltip = Toon bevestigingsvenster bij het klikken op de hard/symlink knop. settings_confirm_group_deletion_button_tooltip = Waarschuwingsvenster weergeven wanneer geprobeerd wordt om alle records uit de groep te verwijderen. settings_show_text_view_button_tooltip = Tekstpaneel aan de onderkant van de gebruikersinterface weergeven. settings_use_cache_button_tooltip = Gebruik bestandscache. settings_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. settings_use_trash_button_tooltip = Verplaatst bestanden naar prullenbak in plaats daarvan ze permanent te verwijderen. settings_language_label_tooltip = Taal voor de gebruikersinterface. settings_save_at_exit_button = Configuratie opslaan bij het sluiten van app settings_load_at_start_button = Laad configuratie bij het openen van app settings_confirm_deletion_button = Toon bevestigingsdialoog bij het verwijderen van bestanden settings_confirm_link_button = Melding bevestigen bij hard/symlinks van bestanden settings_confirm_group_deletion_button = Toon het bevestigingsvenster bij het verwijderen van alle bestanden in de groep settings_show_text_view_button = Toon onderaan tekstpaneel settings_use_cache_button = Gebruik cache settings_save_also_as_json_button = Sla ook de cache op als JSON-bestand settings_use_trash_button = Verwijderde bestanden verplaatsen naar prullenbak settings_language_label = Taal settings_multiple_delete_outdated_cache_checkbutton = Verouderde cache vermeldingen automatisch verwijderen settings_multiple_delete_outdated_cache_checkbutton_tooltip = Verwijder verouderde cacheresultaten die verwijzen naar niet-bestaande bestanden. Indien ingeschakeld, zorgt de app ervoor dat bij het laden van records, dat alle records naar geldige bestanden verwijzen (gebroken bestanden worden genegeerd). 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. 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. settings_notebook_general = Algemeen settings_notebook_duplicates = Duplicaten settings_notebook_images = Vergelijkbare afbeeldingen settings_notebook_videos = Gelijkaardige Video ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Toont voorbeeld aan rechterkant (bij het selecteren van een afbeeldingsbestand). settings_multiple_image_preview_checkbutton = Toon voorvertoning afbeelding settings_multiple_clear_cache_button_tooltip = Handmatig de cache van verouderde items wissen. Dit mag alleen worden gebruikt als automatisch wissen is uitgeschakeld. settings_multiple_clear_cache_button = Verwijder verouderde resultaten uit de cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Verbergt alle bestanden behalve één, als alle naar dezelfde gegevens verwijst (zijn hardlinked). 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. settings_duplicates_minimal_size_entry_tooltip = Stel de minimale bestandsgrootte in die gecached zal worden. kiezen van een kleinere waarde zal meer records genereren. Dit zal het zoeken versnellen, maar de cache aan het laden/opslaan. settings_duplicates_prehash_checkbutton_tooltip = Hiermee kan het cachen van prehash (een hash berekend van een klein deel van het bestand) eerder verwijderen van niet-gedupliceerde resultaten. Het is standaard uitgeschakeld omdat het in sommige situaties vertraging kan veroorzaken. Het is sterk aanbevolen om het te gebruiken bij het scannen van honderdduizenden of miljoen bestanden, omdat het zoeken meerdere keren kan versnellen. settings_duplicates_prehash_minimal_entry_tooltip = Minimale grootte van gecachete invoer. settings_duplicates_hide_hard_link_button = Verberg harde links settings_duplicates_prehash_checkbutton = Gebruik prehash cache settings_duplicates_minimal_size_cache_label = Minimale bestandsgrootte (in bytes) opgeslagen in de cache settings_duplicates_minimal_size_cache_prehash_label = Minimale grootte van bestanden (in bytes) opgeslagen naar prehash cache ## Saving/Loading settings settings_saving_button_tooltip = De huidige instellingen configuratie opslaan in bestand. settings_loading_button_tooltip = Laad de instellingen uit het bestand en vervang de huidige configuratie. settings_reset_button_tooltip = De huidige configuratie terugzetten naar de standaard. settings_saving_button = Configuratie opslaan settings_loading_button = Laad configuratie settings_reset_button = Reset configuratie ## Opening cache/config folders settings_folder_cache_open_tooltip = Opent de map waar de cache txt bestanden zijn opgeslagen. 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. 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). In geval van problemen met de cache kunnen deze bestanden worden verwijderd. De app zal ze automatisch opnieuw genereren. settings_folder_settings_open_tooltip = Opent de map waar de Czkawka config is opgeslagen. WAARSCHUWING: Handmatig wijzigen van de configuratie kan uw workflow verbreken. settings_folder_cache_open = Open cachemap settings_folder_settings_open = Instellingenmap openen # Compute results compute_stopped_by_user = Zoeken is gestopt door gebruiker compute_found_duplicates_hash_size = Gevonden { $number_files } duplicaten in { $number_groups } groepen die { $size } in { $time } namen compute_found_duplicates_name = Gevonden { $number_files } duplicaten in { $number_groups } groepen in { $time } compute_found_empty_folders = Gevonden { $number_files } lege mappen in { $time } compute_found_empty_files = Gevonden { $number_files } lege bestanden in { $time } compute_found_big_files = Gevonden { $number_files } grote bestanden in { $time } compute_found_temporary_files = Gevonden { $number_files } tijdelijke bestanden in { $time } compute_found_images = { $number_files } soortgelijke afbeeldingen gevonden in { $number_groups } groepen in { $time } compute_found_videos = Gevonden { $number_files } soortgelijke video's in { $number_groups } groepen in { $time } compute_found_music = Gevonden { $number_files } soortgelijke muziekbestanden in { $number_groups } groepen in { $time } compute_found_invalid_symlinks = Gevonden { $number_files } ongeldige symlinks in { $time } compute_found_broken_files = Gevonden { $number_files } gebroken bestanden in { $time } compute_found_bad_extensions = { $number_files } bestanden met ongeldige extensies gevonden in { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Gescande { $file_number } bestand *[other] Gescande { $file_number } bestanden } progress_scanning_extension_of_files = Gecontroleerde extensie van { $file_checked }/{ $all_files } bestand progress_scanning_broken_files = Gecontroleerd { $file_checked }/{ $all_files } bestand ({ $data_checked }/{ $all_data }) progress_scanning_video = Onderbroken van { $file_checked }/{ $all_files } video progress_creating_video_thumbnails = Gemaakte miniaturen van { $file_checked }/{ $all_files } video progress_scanning_image = Bevroren van { $file_checked }/{ $all_files } afbeelding ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Vergeleken { $file_checked }/{ $all_files } afbeelding hash progress_scanning_music_tags_end = Vergeleken tags van { $file_checked }/{ $all_files } muziekbestand progress_scanning_music_tags = Lees tags van { $file_checked }/{ $all_files } muziekbestand progress_scanning_music_content_end = Vergeleken vingerafdruk van { $file_checked }/{ $all_files } muziekbestand progress_scanning_music_content = Berekende vingerafdruk van { $file_checked }/{ $all_files } muziekbestand ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Gescande { $folder_number } map *[other] Gescande { $folder_number } mappen } progress_scanning_size = Gescande grootte van { $file_number } bestand progress_scanning_size_name = Gescande naam en grootte van { $file_number } bestand progress_scanning_name = Gescande naam van { $file_number } bestand progress_analyzed_partial_hash = Geanalyseerde gedeeltelijke hash van { $file_checked }/{ $all_files } bestanden ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Volledige hash van { $file_checked }/{ $all_files } bestanden ({ $data_checked }/{ $all_data } ) geanalyseerd progress_prehash_cache_loading = Prehash cache laden progress_prehash_cache_saving = Opslaan van prehash cache progress_hash_cache_loading = hash-cache laden progress_hash_cache_saving = hash cache opslaan progress_cache_loading = Cache laden progress_cache_saving = Cache opslaan progress_current_stage = Current Stage:{ " " } progress_all_stages = All Stages:{ " " } # Saving loading saving_loading_saving_success = Configuratie opgeslagen in bestand { $name }. saving_loading_saving_failure = Kan de configuratiegegevens niet opslaan in het bestand { $name }, reden { $reason }. saving_loading_reset_configuration = Huidige configuratie is gewist. saving_loading_loading_success = Goed geladen app configuratie. saving_loading_failed_to_create_config_file = Fout bij het aanmaken van het configuratiebestand "{ $path }", reden "{ $reason }". saving_loading_failed_to_read_config_file = Kan configuratie niet laden van "{ $path }" omdat deze niet bestaat of geen bestand is. saving_loading_failed_to_read_data_from_file = Kan gegevens niet lezen van bestand "{ $path }", reden "{ $reason }". # Other selected_all_reference_folders = Kan zoeken niet starten, als alle mappen als referentie mappen zijn ingesteld searching_for_data = Gegevens zoeken, het kan een tijdje duren, even wachten... text_view_messages = BERICHTEN text_view_warnings = LET OP text_view_errors = FOUTEN about_window_motto = Dit programma is gratis te gebruiken en zal dat altijd zijn. krokiet_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. # Various dialog dialogs_ask_next_time = Volgende keer vragen symlink_failed = symlink { $name } naar { $target }mislukt, reden { $reason } delete_title_dialog = Bevestiging verwijderen delete_question_label = Weet u zeker dat u bestanden wilt verwijderen? delete_all_files_in_group_title = Bevestiging van het verwijderen van alle bestanden in groep delete_all_files_in_group_label1 = In sommige groepen worden alle records geselecteerd. delete_all_files_in_group_label2 = Weet u zeker dat u deze wilt verwijderen? delete_items_label = { $items } bestanden worden verwijderd. delete_items_groups_label = { $items } bestanden van { $groups } groepen worden verwijderd. hardlink_failed = Mislukt om hardlink te maken { $name } naar { $target }, reden { $reason } hard_sym_invalid_selection_title_dialog = Ongeldige selectie met sommige groepen hard_sym_invalid_selection_label_1 = In sommige groepen is er slechts één record geselecteerd en zal worden genegeerd. hard_sym_invalid_selection_label_2 = Om deze bestanden hard/sym te kunnen koppelen, moeten ten minste twee resultaten in de groep worden geselecteerd. hard_sym_invalid_selection_label_3 = Eerst wordt de groep als origineel erkend en niet veranderd, en vervolgens worden de tweede wijzigingen aangebracht. hard_sym_link_title_dialog = Bevestiging link hard_sym_link_label = Weet u zeker dat u deze bestanden wilt koppelen? move_folder_failed = Map { $name }kon niet verplaatst worden, reden { $reason } move_file_failed = Kon bestand niet verplaatsen { $name }, reden { $reason } move_files_title_dialog = Kies de map waarnaar u gedupliceerde bestanden wilt verplaatsen move_files_choose_more_than_1_path = Er kan slechts één pad geselecteerd zijn om hun gedupliceerde bestanden te kopiëren, geselecteerde { $path_number }. move_stats = Naar behoren verplaatst { $num_files }/{ $all_files } items save_results_to_file = Opgeslagen resultaten zowel in txt als json bestanden in de "{ $name }" map. search_not_choosing_any_music = FOUT: U moet ten minste één selectievakje met muziekinstypes selecteren. search_not_choosing_any_broken_files = FOUT: U moet ten minste één selectievakje selecteren met type van aangevinkte bestanden. include_folders_dialog_title = Mappen om op te nemen exclude_folders_dialog_title = Mappen om uit te sluiten include_manually_directories_dialog_title = Voeg map handmatig toe cache_properly_cleared = Cache op juiste wijze gewist cache_clear_duplicates_title = Duplicaten cache wissen cache_clear_similar_images_title = Leeg soortgelijke afbeeldingen-cache cache_clear_similar_videos_title = Leeg soortgelijke video cache cache_clear_message_label_1 = Wilt u de cache van verouderde items wissen? cache_clear_message_label_2 = Deze actie zal alle cache-items verwijderen die naar ongeldige bestanden wijzen. cache_clear_message_label_3 = Dit kan de laden/opslaan enigszins versnellen. cache_clear_message_label_4 = WAARSCHUWING: De bewerking zal alle opgeslagen data van externe schijven verwijderen. Daarom zal elke hash opnieuw moeten worden gegenereerd. # Show preview preview_image_resize_failure = Formaat wijzigen van afbeelding { $name } is mislukt. preview_image_opening_failure = Kan afbeelding { $name }niet openen, reden { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Groep { $current_group }/{ $all_groups } ({ $images_in_group } afbeeldingen) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/no/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Innstillinger window_main_title = Czkawka (Hikkup) window_progress_title = Skanner window_compare_images = Sammenlign bilder # General general_ok_button = Ok general_close_button = Lukk # Krokiet info dialog krokiet_info_title = Introduserer Krokiet - Ny versjon av Czkawka krokiet_info_message = Krokiet er den nye, forbedrede, raskere og mer pålitelige versjonen av Czkawka GTK GUI! 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. Krokiet bringer også funksjoner som Czkawka mangler, inkludert forhåndsvisninger i videoj sammenligningsmodus, en EXIF-renser, fil flytt/kopier/slett fremdrift eller utvidede sorteringsalternativer. Prøv det og se forskjellen! 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. 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. # Main window music_title_checkbox = Tittel music_artist_checkbox = Kunstner music_year_checkbox = År music_bitrate_checkbox = Bitratespeed music_genre_checkbox = Sjanger music_length_checkbox = Lengde music_comparison_checkbox = Omtrentlig sammenligning music_checking_by_tags = Tagger music_checking_by_content = Innhold same_music_seconds_label = Minste fragment andre varighet same_music_similarity_label = Maksimal differanse music_compare_only_in_title_group = Sammenlign innenfor grupper av lignende titler music_compare_only_in_title_group_tooltip = Når aktivert, blir filer gruppert etter tittel og sammenlignes med hverandre. Med 10000 filer, vil det i stedet være nesten 100 millioner sammenligninger som regel være rundt 20000 sammenligninger. same_music_tooltip = Søker etter lignende musikkfiler av innholdet kan konfigureres ved å gå inn: - Minimumsfragmenteringstiden etter hvilken musikkfiler som kan identifiseres som lignende - Maksimal forskjell mellom to testede fragmenter Nøkkelen til gode resultater er å finne fornuftige kombinasjoner av disse parametrene, for utlevert. Angir minimum tid til 5 s og maksimal forskjell til 1,0, vil se etter nesten identiske fragmenter i filene. En tid på 20 s og en maksimal forskjell på 6.0, for den andre siden fungerer bra for å finne remikser/levende versjoner osv. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Skill mellom små og store bokstaver duplicate_case_sensitive_name_tooltip = Når aktivert, vil du bare gruppere når de har nøyaktig samme navn, f.eks. Żołd <-> Żołd Deaktivering av en slik opsjon vil gi deg egne navn uten å sjekke om hver bokstav er like stort, f.eks. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Størrelse og navn duplicate_mode_name_combo_box = Navn duplicate_mode_size_combo_box = Størrelse duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Tsjekkia har 3 typer hashes: Blake3 - kryptografisk hash-funksjon. Dette er standard fordi den er veldig rask. CRC32 - enkel hash-funksjon. Dette bør være raskere enn Blake3, men kan svært sjelden ha noen kollisjoner. XXH3 - meget likt i ytelse og hash-kvalitet til Blake3 (men ikke-kryptografisk). Såfremt kan slike moduser endres enkelt. duplicate_check_method_tooltip = Tsjekkka tilbyr tre typer metoder for å finne duplikater av: Navn - Finner filer med samme navn. Størrelse på funnet finner du filer med samme størrelse. 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. image_hash_size_tooltip = Hvert avmerkede bilde gir en spesiell hash som kan sammenlignes med hverandre, og en liten forskjell mellom dem betyr at disse bildene er like. 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. 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. 32 og 64 hashes finner bare lignende bilder, men bør ha nesten ingen falske positiver (kanskje unntatt bilder med alfa-kanal). image_resize_filter_tooltip = For å beregne hesh av bilde, må biblioteket først tilpass bilden. Avhengig av valgt algoritme vil det bilde som brukes for beregning av hesh se litt forskjellig ut. 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. Med en 8x8 heshstørrelse anbefales det å bruke en annen algoritme enn Nearest for å ha bedre grupper av bilder. image_hash_alg_tooltip = Brukere kan velge mellom en av mange algoritmer i beregningen av hashen. Hver har både sterke og svakere poeng, og vil noen ganger gi bedre og noen ganger verre resultater for ulike bilder. Så for å bestemme det beste for deg, kreves manuell testing. big_files_mode_combobox_tooltip = Lar deg søke etter minste/største filer big_files_mode_label = Avmerkede filer big_files_mode_smallest_combo_box = Den minste big_files_mode_biggest_combo_box = Den største main_notebook_duplicates = Dupliser filer main_notebook_empty_directories = Tomme mapper main_notebook_big_files = Store filer main_notebook_empty_files = Tomme filer main_notebook_temporary = Midlertidige filer main_notebook_similar_images = Lignende bilder main_notebook_similar_videos = Lignende videoer main_notebook_same_music = Musikk dupliserer main_notebook_symlinks = Ugyldige Symlinks main_notebook_broken_files = Ødelagte filer main_notebook_bad_extensions = Feil utvidelser main_tree_view_column_file_name = Filnavn main_tree_view_column_folder_name = Mappenavn main_tree_view_column_path = Sti main_tree_view_column_modification = Endret dato main_tree_view_column_size = Størrelse main_tree_view_column_similarity = Likhet main_tree_view_column_dimensions = Dimensjoner main_tree_view_column_title = Tittel main_tree_view_column_artist = Kjennestein main_tree_view_column_year = År main_tree_view_column_bitrate = Bitratespeed main_tree_view_column_length = Lengde main_tree_view_column_genre = Sjanger main_tree_view_column_symlink_file_name = Symlink filnavn main_tree_view_column_symlink_folder = Mappe for Symlink main_tree_view_column_destination_path = Destinasjonssti main_tree_view_column_type_of_error = Type feil main_tree_view_column_current_extension = Gjeldende utvidelse main_tree_view_column_proper_extensions = Riktig utvidelse main_tree_view_column_fps = BPS main_tree_view_column_codec = Kodek main_label_check_method = Sjekkmetode main_label_hash_type = Type hash main_label_hash_size = Størrelse på hash main_label_size_bytes = Størrelse (bytes) main_label_min_size = Min main_label_max_size = Maks main_label_shown_files = Antall filer som vises main_label_resize_algorithm = Endre algoritmen main_label_similarity = Similarity{ " " } main_check_box_broken_files_audio = Lyd main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Arkiv main_check_box_broken_files_image = Bilde main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignorer samme størrelse check_button_general_same_size_tooltip = Ignorer filer med identisk størrelse i resultater - vanligvis disse er 1:1 duplikater main_label_size_bytes_tooltip = Størrelse på filer som vil bli brukt i skanning # Upper window upper_tree_view_included_folder_column_title = Mapper å søke etter upper_tree_view_included_reference_column_title = Referanse mapper upper_recursive_button = Rekursivt upper_recursive_button_tooltip = Hvis valgt, søk også etter filer som ikke er plassert direkte under valgte mapper. upper_manual_add_included_button = Manuelt legg til upper_add_included_button = Legg til upper_remove_included_button = Fjern upper_manual_add_excluded_button = Manuelt legg til upper_add_excluded_button = Legg til upper_remove_excluded_button = Fjern upper_manual_add_included_button_tooltip = Legg til mappenavn for å søke etter hånd. For å legge til flere baner samtidig, separer dem med ; /home/roman;/home/rozkaz vil legge til to kataloger /home/roman og /home/rozkaz upper_add_included_button_tooltip = Legg til ny mappe i søk. upper_remove_included_button_tooltip = Slett mappen fra søk. upper_manual_add_excluded_button_tooltip = Legg til ekskludert mappenavn for hånd. For å legge til flere baner på en gang, separer dem med ; /home/roman;/home/krokiet vil legge til to kataloger /home/roman and /home/keokiet upper_add_excluded_button_tooltip = Legg til mappe som skal utelukkes i søk. upper_remove_excluded_button_tooltip = Slett mappen fra ekskludert. upper_notebook_items_configuration = Konfigurasjon av elementer upper_notebook_excluded_directories = Uekskluderte stier upper_notebook_included_directories = Inkluderte Stier upper_allowed_extensions_tooltip = Tillatte utvidelser må være atskilt med komma (ved at alle er tilgjengelige). Følgende makroer, som legger til flere utvidelser samtidig, er også tilgjengelige: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Liste over deaktiverte filer som vil bli ignorert i skanning. Ved bruk av både tillatte og deaktiverte utvidelser, har denne prioriteten høyere enn prioritet, så filen vil ikke bli sjekket. upper_excluded_items_tooltip = Uekskluderte elementer må inneholde * wildcard og skal være separert med komma. Dette er tregere enn Excluded Paths, så bruk det forsiktig. upper_excluded_items = Ekskluderte elementer: upper_allowed_extensions = Tillatte utvidelser: upper_excluded_extensions = Deaktiverte utvidelser: # Popovers popover_select_all = Velg alle popover_unselect_all = Fjern alle valg popover_reverse = Omvendt utvalg popover_select_all_except_shortest_path = Velg alle unntatt korteste vei popover_select_all_except_longest_path = Velg alle unntatt lengste sti popover_select_all_except_oldest = Velg alt unntatt det eldste popover_select_all_except_newest = Velg alle unntatt nyeste popover_select_one_oldest = Velg en eldste popover_select_one_newest = Velg en nyeste popover_select_custom = Velg egendefinert popover_unselect_custom = Avvelg egendefinert popover_select_all_images_except_biggest = Velg alle unntatt største popover_select_all_images_except_smallest = Velg alle unntatt minste popover_custom_path_check_button_entry_tooltip = Velg poster som sti. Eksempelbruk: /home/pimpek/rzecz.txt kan du finne med /home/pim* popover_custom_name_check_button_entry_tooltip = Velg poster etter filnavn. Eksempelbruk: /usr/ping/pong.txt kan finnes med *ong* popover_custom_regex_check_button_entry_tooltip = Velg elementer ved angitt Regex. Med denne modus, vil søppelteksten være Sti med navn. Eksempel på bruk: /usr/bin/ziemniak. xt finner du med /ziem[a-z]+ Dette bruker standard Rust regex implementasjon. Du kan lese mer om den her: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Aktiverer case-sensitiv deteksjon. Når deaktivert/hjem/* funn både /HoMe/roman og /home/roman. popover_custom_not_all_check_button_tooltip = Hindrer å velge alle poster i gruppen. 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. ADVARSEL: Denne innstillingen fungerer ikke hvis du allerede har valgt alle resultater i en gruppe. popover_custom_regex_path_label = Sti popover_custom_regex_name_label = Navn popover_custom_regex_regex_label = Regex sti + navn popover_custom_case_sensitive_check_button = Skill store og små bokstaver popover_custom_all_in_group_label = Ikke velg alle poster i gruppen popover_custom_mode_unselect = Avvelg egendefinert popover_custom_mode_select = Velg egendefinert popover_sort_file_name = Filnavn popover_sort_folder_name = Mappenavn popover_sort_full_name = Fullt navn popover_sort_size = Størrelse popover_sort_selection = Utvalg popover_invalid_regex = Regex er ugyldig popover_valid_regex = Regex er gyldig # Bottom buttons bottom_search_button = Søk bottom_select_button = Velg bottom_delete_button = Slett bottom_save_button = Lagre bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Flytt bottom_sort_button = Sorter bottom_compare_button = Sammenlign bottom_search_button_tooltip = Starte søk bottom_select_button_tooltip = Velg oppføringer. Bare valgte filer/mapper kan bli behandlet senere. bottom_delete_button_tooltip = Slett valgte filer/mapper. bottom_save_button_tooltip = Lagre data om søk i fil bottom_symlink_button_tooltip = Opprett symbolske lenker. Virker bare når minst to resultater i en gruppe er valgt. Først er uendret og sekund og senere er symlinket til først. bottom_hardlink_button_tooltip = Opprette fastkoblinger. Virker bare når minst to resultater i en gruppe er valgt. Først er uendret og annet og senere er vanskelig knyttet til først. bottom_hardlink_button_not_available_tooltip = Opprett faste koblinger. Knappen er deaktivert, fordi faste koblinger ikke kan opprettes. Faste koblinger fungerer bare med administratorrettigheter i Windows, så pass på at du kjører programmet som administrator. Hvis programmet allerede fungerer med slike privilegier, sjekk om lignende problemer er observert på GitHub. bottom_move_button_tooltip = Flytter filer til valgt mappe. Den kopierer alle filer til mappen uten å lagre mappetreet. Når du prøver å flytte to filer med identisk navn til mappe, vil det andre feile og vise feil. bottom_sort_button_tooltip = Sorter filer/mapper etter valgt metode. bottom_compare_button_tooltip = Sammenlign bilder i gruppen. bottom_show_errors_tooltip = Vis/Skjul bunntekstpanelet. bottom_show_upper_notebook_tooltip = Vis/Skjul øvre notebook panel. # Progress Window progress_stop_button = Stopp progress_stop_additional_message = Stopp forespurt # About Window about_repository_button_tooltip = Link til pakkesiden med kildekoden. about_donation_button_tooltip = Lenke til donasjonssiden. about_instruction_button_tooltip = Lenke til instruksjonssiden. about_translation_button_tooltip = Link til Crowdin-siden med app oversettelser. Officialt Polsk og engelsk støttes. about_repository_button = Pakkebrønn about_donation_button = Donasjon about_instruction_button = Instruksjon about_translation_button = Oversettelse # Header header_setting_button_tooltip = Åpner dialogboksen for innstillinger. header_about_button_tooltip = Åpner dialog med info om app. # Settings ## General settings_number_of_threads = Antall brukte tråder settings_number_of_threads_tooltip = Antall brukte tråder. 0 betyr at alle tilgjengelige tråder vil bli brukt. settings_use_rust_preview = Bruk eksterne biblioteker i stedet gtk for å laste forhåndsvisninger settings_use_rust_preview_tooltip = Med gtk innledning vil noen ganger være raskere og støtte flere formater, men noen ganger kan dette være akkurat det motsatte. Hvis du har problemer med å laste forhåndsvisninger, kan du prøve å endre denne innstillingen. 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. settings_label_restart = Start programmet på nytt for å bruke innstillingene! settings_ignore_other_filesystems = Ignorer andre filsystemer (bare Linux) settings_ignore_other_filesystems_tooltip = ignorerer filer som ikke er i samme filsystem som søk-kataloger. Fungerer samme som -xdev alternativet i å finne kommandoen på Linux settings_save_at_exit_button_tooltip = Lagre konfigurasjon som fil når appen lukkes. settings_load_at_start_button_tooltip = Last inn konfigurasjon fra filen når du åpner appen. Hvis ikke er aktivert brukes standard innstillinger. settings_confirm_deletion_button_tooltip = Vis bekreftelsesdialog når du klikker på Slette-knappen. settings_confirm_link_button_tooltip = Vis bekreftelsesdialog når du klikker på knappen hard/symlink. settings_confirm_group_deletion_button_tooltip = Vis advarselsdialog når du prøver å slette alle poster fra gruppen. settings_show_text_view_button_tooltip = Vis tekstpanelet nederst av brukergrensesnittet. settings_use_cache_button_tooltip = Bruk filmellomlager. settings_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. settings_use_trash_button_tooltip = Flytter filer til papirkurv istedenfor å slette dem permanent. settings_language_label_tooltip = Språk til brukergrensesnitt. settings_save_at_exit_button = Lagre konfigurasjon når appen lukkes settings_load_at_start_button = Last inn konfigurasjon når du åpner appen settings_confirm_deletion_button = Vis bekreftelsesdialog ved sletting av filer settings_confirm_link_button = Vis bekreftelsesdialog når noen filer er fast/symlinker settings_confirm_group_deletion_button = Vis "Bekreft"-dialog når du sletter alle filer i gruppen settings_show_text_view_button = Vis nederste tekstpanel settings_use_cache_button = Bruk buffer settings_save_also_as_json_button = Lagre også mellomlager som JSON-fil settings_use_trash_button = Flytt slettede filer til papirkurv settings_language_label = Språk settings_multiple_delete_outdated_cache_checkbutton = Slett utdaterte cache-oppføringer automatisk settings_multiple_delete_outdated_cache_checkbutton_tooltip = Slett utdaterte cache-resultater som peker til ikke-eksisterende filer. Når aktivert sørger appen for å laste inn poster, at alle oppføringer peker til gyldige filer (ødelagte blir ignorert). 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å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. settings_notebook_general = Generelt settings_notebook_duplicates = Duplikater settings_notebook_images = Lignende bilder settings_notebook_videos = Lignende video ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Viser forhåndsvisning på høyre side (når du velger en bildefil). settings_multiple_image_preview_checkbutton = Vis forhåndsvisning av bilde settings_multiple_clear_cache_button_tooltip = Manuelt tømmer hurtigbufferen av utdaterte oppføringer. Dette bør bare brukes hvis automatisk tømming er deaktivert. settings_multiple_clear_cache_button = Fjern utdaterte resultater fra mellomlager. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Skjuler alle filer unntatt en, hvis alle peker til samme data (knyttes sammen). 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. settings_duplicates_minimal_size_entry_tooltip = Angi minste filstørrelse som blir bufret i cachen. Hvis du velger en mindre verdi, genererer du flere poster. Dette vil fremskynde søk, men tregere hurtigbufferet lasting/lagring. settings_duplicates_prehash_checkbutton_tooltip = Aktiverer hurtigbufring av prehash (en hash beregnet fra en liten del av filen) som tillater tidligere fjerning av ikke-dupliserte resultater. Den er deaktivert som standard fordi den kan forårsake langsommere i noen situasjoner. Det anbefales å bruke det når det skannes hundre eller millioner filer, fordi det kan fremskynde søk med flere ganger. settings_duplicates_prehash_minimal_entry_tooltip = Minimal størrelse på mellomlagret oppføring. settings_duplicates_hide_hard_link_button = Skjul harde linker settings_duplicates_prehash_checkbutton = Bruk prehash cache settings_duplicates_minimal_size_cache_label = Minimal størrelse på filer (i byte) lagret i mellomlager settings_duplicates_minimal_size_cache_prehash_label = Minimal størrelse på filer (i byte) lagret i hurtigbuffer for prehash ## Saving/Loading settings settings_saving_button_tooltip = Lagre gjeldende innstillingskonfigurasjon til filen. settings_loading_button_tooltip = Last innstillinger fra fil og erstatt gjeldende konfigurasjon med dem. settings_reset_button_tooltip = Tilbakestill den gjeldende konfigurasjonen til standard. settings_saving_button = Lagre konfigurasjonen settings_loading_button = Last inn konfigurasjon settings_reset_button = Tilbakestill konfigurasjon ## Opening cache/config folders settings_folder_cache_open_tooltip = Åpner mappen der cache txt filene er lagret. Modifisere cachefilene kan forårsake ugyldige resultater. Imidlertid kan modifisere stien spare tid når du flytter store mengder filer til en annen posisjon. Du kan kopiere disse filene mellom datamaskiner for å spare tid ved skanning igjen for filer (hvis de har en lignende katalogstruktur). Hvis det oppstår problemer med cachen, kan disse filene fjernes. Appen vil automatisk regenerere dem. settings_folder_settings_open_tooltip = Åpner mappen der Czkawka konfigurasjonen er lagret. ADVARSEL: Manuelt endre konfigurasjonen kan ødelegge arbeidsflyten din. settings_folder_cache_open = Åpne mappe for hurtigbuffer settings_folder_settings_open = Åpne innstillingsmappen # Compute results compute_stopped_by_user = Søket ble stoppet av bruker compute_found_duplicates_hash_size = Fant { $number_files } duplikater i { $number_groups } grupper som tok { $size } i { $time } compute_found_duplicates_name = Fant { $number_files } duplikater i { $number_groups } grupper i { $time } compute_found_empty_folders = Fant { $number_files } tomme mapper i { $time } compute_found_empty_files = Fant { $number_files } tomme filer i { $time } compute_found_big_files = Fant { $number_files } store filer i { $time } compute_found_temporary_files = Fant { $number_files } midlertidige filer i { $time } compute_found_images = Fant { $number_files } lignende bilder i { $number_groups } grupper i { $time } compute_found_videos = Fant { $number_files } lignende videoer i { $number_groups } grupper i { $time } compute_found_music = Fant { $number_files } lignende musikkfiler i { $number_groups } grupper i { $time } compute_found_invalid_symlinks = Fant { $number_files } ugyldige symlinker i { $time } compute_found_broken_files = Fant { $number_files } ødelagte filer i { $time } compute_found_bad_extensions = Fant { $number_files } filer med ugyldige utvidelser i { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Skannet { $file_number } fil *[other] Skannet { $file_number } filer } progress_scanning_extension_of_files = Merket utvidelse av { $file_checked }/{ $all_files } -filen progress_scanning_broken_files = Merket { $file_checked }/{ $all_files } fil ({ $data_checked }/{ $all_data }) progress_scanning_video = Kastet av { $file_checked }/{ $all_files } video progress_creating_video_thumbnails = Lagede miniatyrbilder av { $file_checked }/{ $all_files } video progress_scanning_image = Kastet av { $file_checked }/{ $all_files } bilde ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Sammenlignet { $file_checked }/{ $all_files } bilde-hash progress_scanning_music_tags_end = Sammenligne tagger med { $file_checked }/{ $all_files } musikkfil progress_scanning_music_tags = Les tagger for { $file_checked }/{ $all_files } musikkfil progress_scanning_music_content_end = Sammenlignet fingeravtrykk av { $file_checked }/{ $all_files } musikkfil progress_scanning_music_content = Beregnet fingeravtrykk på { $file_checked }/{ $all_files } musikkfil ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Skannet { $folder_number } mappe *[other] Skannet { $folder_number } mapper } progress_scanning_size = Skannet størrelse på { $file_number } fil progress_scanning_size_name = { $file_number } fil skannet navn og størrelse progress_scanning_name = { $file_number } fil skannet navn progress_analyzed_partial_hash = Analyserte delvis hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Analysert full hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Laster prehash cache progress_prehash_cache_saving = Lagrer prehash-cache progress_hash_cache_loading = Laster hash-cache progress_hash_cache_saving = Lagrer hurtigbufferen for hash progress_cache_loading = Laster cache progress_cache_saving = Lagrer cachen progress_current_stage = Gjeldende trinn: { " " } progress_all_stages = Alle stadier:{ " " } # Saving loading saving_loading_saving_success = Lagret konfigurasjon til filen { $name }. saving_loading_saving_failure = Kunne ikke lagre konfigurasjonsdata som filen { $name }, grunn { $reason }. saving_loading_reset_configuration = Gjeldende konfigurasjon ble fjernet. saving_loading_loading_success = Godt lastet applikasjonskonfigurasjon. saving_loading_failed_to_create_config_file = Kunne ikke opprette konfigurasjonsfilen{ $path }", grunn "{ $reason }". saving_loading_failed_to_read_config_file = Kan ikke laste konfigurasjonen fra "{ $path }" fordi den ikke eksisterer eller ikke er en fil. saving_loading_failed_to_read_data_from_file = Kan ikke lese data fra filen{ $path }", grunn "{ $reason }". # Other selected_all_reference_folders = Kan ikke starte søk, når alle kataloger er angitt som referanselapper searching_for_data = Søker data, det kan ta en stund, vennligst vent... text_view_messages = MELDINGER text_view_warnings = ADVARSELSER text_view_errors = FEILSER about_window_motto = Dette programmet er gratis å bruke og vil alltid være. krokiet_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. # Various dialog dialogs_ask_next_time = Spør neste gang symlink_failed = Kan ikke symlink { $name } til { $target }, årsak { $reason } delete_title_dialog = Bekreft sletting delete_question_label = Er du sikker på at du vil slette filer? delete_all_files_in_group_title = Bekreftelse på sletting av alle filer i gruppen delete_all_files_in_group_label1 = For noen grupper er alle poster valgt. delete_all_files_in_group_label2 = Er du sikker på at du vil slette dem? delete_items_label = { $items } filer vil bli slettet. delete_items_groups_label = { $items } filer fra { $groups } grupper vil bli slettet. hardlink_failed = Kunne ikke koble { $name } til { $target }, grunn { $reason } hard_sym_invalid_selection_title_dialog = Ugyldig valg med noen grupper hard_sym_invalid_selection_label_1 = I noen grupper er det bare én post valgt og det vil bli ignorert. hard_sym_invalid_selection_label_2 = For å kunne feste koblingen til disse filene, må minst to resultater i gruppen velges. hard_sym_invalid_selection_label_3 = Først i gruppen gjenkjennes som originalen og endres ikke, men sekund og senere endres. hard_sym_link_title_dialog = Lenke bekreftelse hard_sym_link_label = Er du sikker på at du vil koble disse filene? move_folder_failed = Kunne ikke flytte mappen { $name }, årsak { $reason } move_file_failed = Kunne ikke flytte filen { $name }, årsak { $reason } move_files_title_dialog = Velg mappen du vil flytte dupliserte filer til move_files_choose_more_than_1_path = Bare én sti kan velges for å kunne kopiere sine dupliserte filer, valgt { $path_number }. move_stats = Flott flyttet { $num_files }/{ $all_files } elementer save_results_to_file = Lagrede resultater både i txt og json filer inn i "{ $name }"-mappen. search_not_choosing_any_music = FEIL: Du må velge minst en avkrysningsboks med musikk som søker. search_not_choosing_any_broken_files = FEIL: Du må velge minst en avkrysningsboks med sjekket ødelagte filer. include_folders_dialog_title = Mapper å inkludere exclude_folders_dialog_title = Mapper som skal ekskluderes include_manually_directories_dialog_title = Legg til mappe manuelt cache_properly_cleared = Riktig tømt cache cache_clear_duplicates_title = Tømmer duplikatene cache_clear_similar_images_title = Fjerner lignende bilde-mellomlager cache_clear_similar_videos_title = Tømmer hurtigbufferen for videoer cache_clear_message_label_1 = Vil du slette cachen med utdaterte oppføringer? cache_clear_message_label_2 = Denne operasjonen vil fjerne alle cacheoppføringer som peker til ugyldige filer. cache_clear_message_label_3 = Dette kan øke lasting og lagring på cache. cache_clear_message_label_4 = ADVARSEL: Operasjonen vil fjerne alle bufrede data fra eksterne stasjoner som ikke er koblet til. Så hver hash må regenereres. # Show preview preview_image_resize_failure = Kunne ikke endre størrelse på bildet { $name }. preview_image_opening_failure = Klarte ikke å åpne bilde { $name }, årsak { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Gruppe { $current_group }/{ $all_groups } ({ $images_in_group } bilder) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/pl/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Ustawienia window_main_title = Czkawka window_progress_title = Skanowanie window_compare_images = Porównywanie Obrazów # General general_ok_button = Ok general_close_button = Zamknij # Krokiet info dialog krokiet_info_title = Wprowadzamy Krokiet - Nowa wersja Czkawki krokiet_info_message = Krokiet to nowa, ulepszona, szybsza i bardziej niezawodna wersja Czkawki GTK! 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. 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. Wypróbuj go i zobacz różnicę! 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. PS: Ta wiadomość powinna pojawić się tylko raz. Jeśli pojawia się ponownie, ustaw zmienną środowiskową CZKAWKA_DONT_ANNOY_ME na dowolną niepustą wartość. # Main window music_title_checkbox = Tytuł music_artist_checkbox = Wykonawca music_year_checkbox = Rok music_bitrate_checkbox = Przepływność bitów music_genre_checkbox = Gatunek music_length_checkbox = Długość music_comparison_checkbox = Przybliżone Porównywanie music_checking_by_tags = Tagi music_checking_by_content = Zawartość same_music_seconds_label = Minimalny czas trwania podobnego fragmentu same_music_similarity_label = Maksymalna różnica music_compare_only_in_title_group = Porównaj w grupach o podobnych tytułach music_compare_only_in_title_group_tooltip = Gdy włączone, pliki są pogrupowane według tytułu, a następnie porównywane do siebie. z 10000 plików, zamiast prawie 100 milionów porównań, zwykle będzie sprawdzone ~20000 porównań. same_music_tooltip = Wyszukiwanie podobnych plików muzycznych przez jego zawartość można skonfigurować przez ustawienie: - Minimalny czas fragmentu, po którym pliki muzyczne mogą być zidentyfikowane jako podobne - Maksymalna różnica między dwoma testowanymi fragmentami Kluczem do dobrych wyników jest znalezienie rozsądnych kombinacji tych parametrów, do dostarczania. Ustawianie minimalnego czasu na 5s i maksymalnej różnicy na 1.0, będzie szukać prawie identycznych fragmentów w plikach. Czas 20s i maksymalna różnica 6.0, z drugiej strony, dobrze działa w poszukiwaniu remiksów/wersji na żywo itp. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Uwzględnij Wielkość Liter duplicate_case_sensitive_name_tooltip = Gdy włączone, grupowe rekordy tylko wtedy, gdy mają dokładnie taką samą nazwę, np. Żołd <-> Żołd Wyłączenie tej opcji spowoduje grupowanie nazw bez sprawdzania, czy każda litera ma ten sam rozmiar, np. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Rozmiar i nazwa duplicate_mode_name_combo_box = Nazwa duplicate_mode_size_combo_box = Rozmiar duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka oferuje 3 rodzaje hashów: Blake3 - kryptograficzna funkcja skrótu. Jest to wartość domyślna, ponieważ jest bardzo szybka. CRC32 - prosta funkcja haszująca. Powinno to być szybsze od Blake3, ale bardzo rzadko może to prowadzić do kolizji. 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. duplicate_check_method_tooltip = Na razie Czkawka oferuje trzy typy metod do znalezienia duplikatów przez: Nazwa - Znajduje pliki o tej samej nazwie. Rozmiar - Znajduje pliki o tym samym rozmiarze. 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. image_hash_size_tooltip = 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. 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. 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. 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). image_resize_filter_tooltip = Aby obliczyć skrót obrazu, biblioteka musi najpierw zmienić jego rozmiar. Dołącz do wybranego algorytmu, wynikowy obraz użyty do obliczenia skrótu będzie wyglądał nieco inaczej. 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. Przy rozmiarze skrótu 8x8 zaleca się użycie innego algorytmu niż najbliższy, aby mieć lepsze grupy obrazów. image_hash_alg_tooltip = Użytkownicy mogą wybrać jeden z wielu algorytmów obliczania hashu. Każdy ma zarówno mocniejsze jak i słabsze punkty i czasami daje lepsze a czasami gorsze wyniki dla różnych obrazów. Najlepiej jest samemu potestować jaki algorytm ma najlepsze wyniki(może to nie być zawsze dobrze widoczne). big_files_mode_combobox_tooltip = Pozwala na wyszukiwanie najmniejszych lub największych plików big_files_mode_label = Sprawdzane pliki big_files_mode_smallest_combo_box = Najmniejsze big_files_mode_biggest_combo_box = Największe main_notebook_duplicates = Duplikaty main_notebook_empty_directories = Puste Katalogi main_notebook_big_files = Duże Pliki main_notebook_empty_files = Puste Pliki main_notebook_temporary = Pliki Tymczasowe main_notebook_similar_images = Podobne Obrazy main_notebook_similar_videos = Podobne Wideo main_notebook_same_music = Podobna Muzyka main_notebook_symlinks = Niepoprawne Symlinki main_notebook_broken_files = Zepsute Pliki main_notebook_bad_extensions = Błędne rozszerzenia main_tree_view_column_file_name = Nazwa main_tree_view_column_folder_name = Nazwa main_tree_view_column_path = Ścieżka main_tree_view_column_modification = Data Modyfikacji main_tree_view_column_size = Rozmiar main_tree_view_column_similarity = Podobieństwo main_tree_view_column_dimensions = Wymiary main_tree_view_column_title = Tytuł main_tree_view_column_artist = Wykonawca main_tree_view_column_year = Rok main_tree_view_column_bitrate = Przepływność bitów main_tree_view_column_length = Długość main_tree_view_column_genre = Gatunek main_tree_view_column_symlink_file_name = Nazwa Symlinka main_tree_view_column_symlink_folder = Folder Symlinka main_tree_view_column_destination_path = Docelowa Ścieżka main_tree_view_column_type_of_error = Typ Błędu main_tree_view_column_current_extension = Aktualne rozszerzenie main_tree_view_column_proper_extensions = Poprawne rozszerzenia main_tree_view_column_fps = FPS main_tree_view_column_codec = Kodek main_label_check_method = Metoda sprawdzania main_label_hash_type = Typ hashu main_label_hash_size = Rozmiar hashu main_label_size_bytes = Rozmiar (bajty) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Liczba wyświetlanych plików main_label_resize_algorithm = Algorytm zmiany rozmiaru main_label_similarity = Podobieństwo{ " " } main_check_box_broken_files_audio = Dźwięk main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Archiwa main_check_box_broken_files_image = Obraz main_check_box_broken_files_video = Wideo main_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. check_button_general_same_size = Ignoruj identyczny rozmiar check_button_general_same_size_tooltip = Ignoruj pliki o identycznym rozmiarze w wynikach - zazwyczaj są to duplikaty 1:1 main_label_size_bytes_tooltip = Rozmiar plików które będą zawarte przy przeszukiwaniu # Upper window upper_tree_view_included_folder_column_title = Foldery do Przeszukania upper_tree_view_included_reference_column_title = Foldery Referencyjne upper_recursive_button = Rekursywnie upper_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. upper_manual_add_included_button = Ręcznie Dodaj upper_add_included_button = Dodaj upper_remove_included_button = Usuń upper_manual_add_excluded_button = Ręcznie Dodaj upper_add_excluded_button = Dodaj upper_remove_excluded_button = Usuń upper_manual_add_included_button_tooltip = Dodaj nazwę katalogu do ręcznego wyszukiwania. Aby dodać wiele ścieżek na raz, należy je oddzielić za pomocą średnika ; /home/roman;/home/rozkaz doda dwa katalogi /home/roman i /home/rozkaz upper_add_included_button_tooltip = Dodaje wybrany folder do przeskanowania. upper_remove_included_button_tooltip = Usuwa zaznaczony folder z listy do skanowania. upper_manual_add_excluded_button_tooltip = Dodaj ręcznie katalog do listy wykluczonych. Aby dodać wiele ścieżek na raz, oddziel je średnikiem ; /home/roman;/home/krokiet doda dwa katalogi /home/roman i /home/keokiet upper_add_excluded_button_tooltip = Dodaje wybrany folder do ignorowanych. upper_remove_excluded_button_tooltip = Usuwa zaznaczony folder z ignorowanych. upper_notebook_items_configuration = Konfiguracja Skanowania upper_notebook_excluded_directories = Wykluczone Ścieżki upper_notebook_included_directories = Dołączone ścieżki upper_allowed_extensions_tooltip = Dozwolone rozszerzenia muszą być oddzielone przecinkami (domyślnie wszystkie są dostępne). Istnieją makra, które umożliwiają dołączenie za jednym razem określonych typów plików IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Lista wyłączonych plików, które zostaną zignorowane w skanowaniu. Gdy używasz zarówno dozwolonych, jak i wyłączonych rozszerzeń, ten ma wyższy priorytet, więc plik nie zostanie sprawdzony. upper_excluded_items_tooltip = Wykluczone elementy muszą zawierać znak * i powinny być oddzielone przecinkami. To działa wolniej niż ustawianie wykluczonych katalogow i plików, więc używaj tego z rozwagą. upper_excluded_items = Ignorowane Obiekty: upper_allowed_extensions = Dozwolone Rozszerzenia: upper_excluded_extensions = Wyłączone rozszerzenia: # Popovers popover_select_all = Zaznacz wszystko popover_unselect_all = Odznacz wszystko popover_reverse = Odwróć zaznaczenie popover_select_all_except_shortest_path = Zaznacz wszystkie oprócz najkrótszej ścieżki popover_select_all_except_longest_path = Zaznacz wszystkie oprócz najdłuższego ścieżki popover_select_all_except_oldest = Zaznacz wszystkie oprócz najstarszego popover_select_all_except_newest = Zaznacz wszystkie oprócz najnowszego popover_select_one_oldest = Zaznacz jedno najstarsze popover_select_one_newest = Zaznacz jedno najnowsze popover_select_custom = Własne zaznaczanie popover_unselect_custom = Własne odznaczanie popover_select_all_images_except_biggest = Zaznacz wszystkie oprócz największego popover_select_all_images_except_smallest = Zaznacz wszystkie oprócz najmniejszego popover_custom_path_check_button_entry_tooltip = Zaznacza rekordy według ścieżki. Przykładowe użycie: /home/pimpek/rzecz.txt można znaleźć używając /home/pim* popover_custom_name_check_button_entry_tooltip = Zaznacza rekordy według nazw plików. Przykładowe użycie: /usr/ping/pong.txt można znaleźć za pomocą *ong* popover_custom_regex_check_button_entry_tooltip = Wybierz rekordy według określonego Regexa. W tym trybie wyszukiwanym tekstem jest pełna ścieżka(wraz z nazwą). Przykładowe użycie: /usr/bin/ziemniak. xt można znaleźć za pomocą /ziem[a-z]+ Używana jest tutaj domyślnej implementacja Regexa w Rust. Więcej informacji na ten temat można znaleźć tutaj: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Umożliwia wykrywanie wielkości liter. Wykluczenie /home/* znajdzie zarówno /HoMe/roman, jak i /home/roman. popover_custom_not_all_check_button_tooltip = Zapobiega wybraniu wszystkich rekordów w grupie. 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. OSTRZEŻENIE: To ustawienie nie działa jeśli wcześniej ręcznie zostały wybrane wszystkie rekordy w grupie. popover_custom_regex_path_label = Ścieżka popover_custom_regex_name_label = Nazwa popover_custom_regex_regex_label = Regex - Pełna ścieżka popover_custom_case_sensitive_check_button = Rozróżniaj wielkość liter popover_custom_all_in_group_label = Nie zaznaczaj wszystkich rekordów w grupie popover_custom_mode_unselect = Własne odznaczanie popover_custom_mode_select = Własne zaznaczanie popover_sort_file_name = Nazwa pliku popover_sort_folder_name = Nazwa katalogu popover_sort_full_name = Pełna nazwa popover_sort_size = Rozmiar popover_sort_selection = Zaznaczanie popover_invalid_regex = Regex jest niepoprawny popover_valid_regex = Regex jest poprawny # Bottom buttons bottom_search_button = Szukaj bottom_select_button = Zaznacz bottom_delete_button = Usuń bottom_save_button = Zapisz bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Przenieś bottom_sort_button = Sortuj bottom_compare_button = Porównaj bottom_search_button_tooltip = Rozpocznij wyszukiwanie bottom_select_button_tooltip = Wybierz rekordy. Tylko wybrane pliki/foldery mogą być później przetwarzane. bottom_delete_button_tooltip = Usuń zaznaczone elementy. bottom_save_button_tooltip = Zapisz informacje o skanowaniu bottom_symlink_button_tooltip = Utwórz linki symboliczne. Działa tylko wtedy, gdy co najmniej dwa wyniki w grupie są zaznaczone. Pierwszy jest niezmieniony, drugi i następny jest powiązywany z pierwszym. bottom_hardlink_button_tooltip = Tworzenie twardych linków. Działa tylko wtedy, gdy wybrano co najmniej dwa rekordy w grupie. Pierwszy jest niezmieniony, drugi i następny jest dowiązywany z pierwszym. bottom_hardlink_button_not_available_tooltip = Tworzenie twardych dowiązań. Przycisk jest zablokowany, gdyż stworzenie twardego dowiązania nie jest możliwe. 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. Jeśli aplikacja działa z nimi, należy przeszukać issues w Githubie celem znalezienia możliwych rozwiązań danego problemu. bottom_move_button_tooltip = Przenosi pliki do wybranego katalogu. Kopiuje wszystkie pliki do katalogu bez zachowania struktury plików. Podczas próby przeniesienia dwóch plików o identycznej nazwie do folderu, drugi plik nie zostanie przeniesiony i pojawi się błąd. bottom_sort_button_tooltip = Sortuje pliki/foldery zgodnie z wybraną metodą. bottom_compare_button_tooltip = Porównaj obrazy w grupie. bottom_show_errors_tooltip = Pokaż/Ukryj dolny panel tekstowy. bottom_show_upper_notebook_tooltip = Pokazuje/ukrywa górny panel. # Progress Window progress_stop_button = Zatrzymaj progress_stop_additional_message = Przerywanie skanowania # About Window about_repository_button_tooltip = Link do repozytorium z kodem źródłowym. about_donation_button_tooltip = Link do strony z dotacjami. about_instruction_button_tooltip = Link do strony z instrukcją. about_translation_button_tooltip = Link do strony Crowdin z tłumaczeniami aplikacji. Oficialnie wspierany jest język polski i angielski. about_repository_button = Repozytorium about_donation_button = Dotacje about_instruction_button = Instrukcja(ENG) about_translation_button = Tłumaczenie # Header header_setting_button_tooltip = Otwórz okno z ustawieniami programu. header_about_button_tooltip = Otwórz okno z informacjami o programie. # Settings ## General settings_number_of_threads = Liczba używanych wątków settings_number_of_threads_tooltip = Liczba używanych wątków, 0 oznacza, że zostaną użyte wszystkie dostępne wątki. settings_use_rust_preview = Użyj zewnętrznych bibliotek zamiast gtk, aby załadować podgląd settings_use_rust_preview_tooltip = 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. Jeśli masz problemy z ładowaniem podglądów, możesz spróbować zmienić to ustawienie. 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. settings_label_restart = Musisz ponownie uruchomić aplikację, aby aplikacja zaciągnęła nowe ustawienia! settings_ignore_other_filesystems = Ignoruj inne systemy plików (tylko Linux) settings_ignore_other_filesystems_tooltip = ignoruje pliki, które nie są w tym samym systemie plików co przeszukiwane katalogi. Działa tak samo jak opcja -xdev w komendzie find na Linux settings_save_at_exit_button_tooltip = Zapisz konfigurację do pliku podczas zamykania aplikacji. settings_load_at_start_button_tooltip = Wczytaj konfigurację z pliku podczas otwierania aplikacji. Jeśli nieaktywny, zostaną użyte domyślne ustawienia. settings_confirm_deletion_button_tooltip = Pokaż okno dialogowe potwierdzające usuwanie przy próbie usunięcia rekordu. settings_confirm_link_button_tooltip = Pokaż dodatkowe okno dialogowe przy próbie utworzenia hard/symlinków. settings_confirm_group_deletion_button_tooltip = Pokaż okno dialogowe ostrzegające przy próbie usunięcia wszystkich rekordów z grupy. settings_show_text_view_button_tooltip = Pokaż dolny panel tekstowy. settings_use_cache_button_tooltip = Użyj pamięci podręcznej plików. settings_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). settings_use_trash_button_tooltip = Przenosi pliki do kosza zamiast usuwać je na stałe. settings_language_label_tooltip = Język interfejsu użytkownika. settings_save_at_exit_button = Zapisz konfigurację podczas zamykania aplikacji settings_load_at_start_button = Załaduj konfigurację z pliku podczas otwierania aplikacji settings_confirm_deletion_button = Pokazuj okno potwierdzające usuwanie plików settings_confirm_link_button = Pokazuj potwierdzenie usuwania hard/symlinków settings_confirm_group_deletion_button = Pokazuj okno potwierdzające usuwanie wszystkich obiektów w grupie settings_show_text_view_button = Pokazuj panel tekstowy na dole settings_use_cache_button = Używaj pamięci podręcznej settings_save_also_as_json_button = Zapisz pamięć podręczną również do pliku JSON settings_use_trash_button = Przenoś pliki do kosza settings_language_label = Język settings_multiple_delete_outdated_cache_checkbutton = Usuwaj automatycznie nieaktualne rekordy z pamięci podręcznej settings_multiple_delete_outdated_cache_checkbutton_tooltip = Usuń nieaktualne rekordy z pamięci podręcznej, które wskazują na nieistniejące pliki. Po włączeniu aplikacja upewnia się, że podczas ładowania rekordów wszystkie wskazują na prawidłowe pliki (uszkodzone czy zmienione pliki są ignorowane). 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. 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. settings_notebook_general = Ogólne settings_notebook_duplicates = Duplikaty settings_notebook_images = Podobne Obrazy settings_notebook_videos = Podobne Wideo ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Pokazuje podgląd po prawej stronie (podczas zaznaczania obrazu). settings_multiple_image_preview_checkbutton = Pokazuj podgląd obrazów settings_multiple_clear_cache_button_tooltip = Ręcznie wyczyść pamięć podręczną przestarzałych wpisów. To powinno być używane tylko wtedy, gdy automatyczne czyszczenie zostało wyłączone. settings_multiple_clear_cache_button = Usuń nieaktualne wyniki z pamięci podręcznej. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Ukrywa wszystkie pliki z wyjątkiem jednego, jeśli wszystkie wskazują na te same dane (są połączone twardym dowiązaniem). 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. settings_duplicates_minimal_size_entry_tooltip = Ustaw minimalny rozmiar pliku, który zapisywany będzie do pliku z pamięcią podręcznej. 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. settings_duplicates_prehash_checkbutton_tooltip = 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. Jest domyślnie wyłączona opcja, ponieważ może spowodować spowolnienie w niektórych sytuacjach. 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. settings_duplicates_prehash_minimal_entry_tooltip = Minimalny rozmiar pliku, którego cząstkowy hash będzie zapisywany do pamięci podręcznej. settings_duplicates_hide_hard_link_button = Ukryj twarde dowiązania settings_duplicates_prehash_checkbutton = Używaj pamięci podręcznej dla hashy cząstkowych settings_duplicates_minimal_size_cache_label = Minimalny rozmiar plików (w bajtach) zapisywanych do pamięci podręcznej settings_duplicates_minimal_size_cache_prehash_label = Minimalny rozmiar plików (w bajtach) przy zapisywaniu ich częściowego haszu do pamięci podręcznej ## Saving/Loading settings settings_saving_button_tooltip = Zapisz aktualną konfigurację ustawień do pliku. settings_loading_button_tooltip = Załaduj ustawienia z pliku i nadpisz bieżącą konfigurację. settings_reset_button_tooltip = Przywróć domyślną konfigurację. settings_saving_button = Zapisanie ustawień settings_loading_button = Załadowanie ustawień settings_reset_button = Reset ustawień ## Opening cache/config folders settings_folder_cache_open_tooltip = Otwiera folder gdzie przechowywana jest pamięć podręczna aplikacji. 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. 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). W razie problemów z pamięcią podręczną, pliki mogą zostać usunięte. Aplikacja automatycznie je zregeneuje. settings_folder_settings_open_tooltip = Otwiera folder, w którym konfiguracja Czkawki jest przechowywana. OSTRZEŻENIE: ręczna modyfikacja konfiguracji może zakłócić przepływ twojej pracy. settings_folder_cache_open = Otwórz folder pamięci podręcznej settings_folder_settings_open = Otwórz folder ustawień # Compute results compute_stopped_by_user = Przeszukiwanie zostało zatrzymane przez użytkownika compute_found_duplicates_hash_size = Znaleziono { $number_files } duplikatów w { $number_groups } grupach w { $time }, które zajęły { $size } compute_found_duplicates_name = Znaleziono { $number_files } duplikatów w { $number_groups } grupach w { $time } compute_found_empty_folders = Znaleziono { $number_files } pustych folderów w { $time } compute_found_empty_files = Znaleziono { $number_files } pustych plików w { $time } compute_found_big_files = Znaleziono { $number_files } dużych plików w { $time } compute_found_temporary_files = Znaleziono { $number_files } plików tymczasowych w { $time } compute_found_images = Znaleziono { $number_files } podobnych obrazów w grupach { $number_groups } w { $time } compute_found_videos = Znaleziono { $number_files } podobnych plików wideo w { $number_groups } grupach w { $time } compute_found_music = Znaleziono { $number_files } podobnych plików muzycznych w { $number_groups } grupach w { $time } compute_found_invalid_symlinks = Znaleziono { $number_files } niepoprawnych symlinków w { $time } compute_found_broken_files = Znaleziono { $number_files } uszkodzonych plików w { $time } compute_found_bad_extensions = Znaleziono { $number_files } plików z nieprawidłowymi rozszerzeniami w { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Przeskanowano { $file_number } plik *[other] Przeskanowano { $file_number } plików } progress_scanning_extension_of_files = Sprawdzono rozszerzenie { $file_checked }/{ $all_files } plików progress_scanning_broken_files = Sprawdzono plik { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_video = Obliczono hashe dla { $file_checked }/{ $all_files } plików video progress_creating_video_thumbnails = Utworzone miniatury filmu { $file_checked }/{ $all_files } progress_scanning_image = Obliczono hashe dla { $file_checked }/{ $all_files } obrazów ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Porównano { $file_checked }/{ $all_files } hashy obrazów progress_scanning_music_tags_end = Porównano { $file_checked }/{ $all_files } tagów plików audio progress_scanning_music_tags = Odczytano tagi { $file_checked }/{ $all_files } plików audio progress_scanning_music_content_end = Porównano hashe { $file_checked }/{ $all_files } plików audio progress_scanning_music_content = Obliczono hash { $file_checked }/{ $all_files } plików audio ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Przeskanowano { $folder_number } folder *[other] Przeskanowano { $folder_number } folderów } progress_scanning_size = Przeskano rozmiar { $file_number } plików progress_scanning_size_name = Sprawdzono rozmiar i nazwę { $file_number } plików progress_scanning_name = Sprawdzono nazwę { $file_number } plików progress_analyzed_partial_hash = Obliczono częściowy hash { $file_checked }/{ $all_files } plików ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Obliczono pełny hash { $file_checked }/{ $all_files } plików ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Ładowanie pamięci podręcznej częściowego hashu progress_prehash_cache_saving = Zapisywanie pamięci podręcznej częściowego hashu progress_hash_cache_loading = Ładowanie pamięci podręcznej hash progress_hash_cache_saving = Zapisywanie pamięci podręcznej skrótu progress_cache_loading = Ładowanie pamięci podręcznej progress_cache_saving = Zapisywanie pamięci podręcznej progress_current_stage = Aktualny Etap:{ " " } progress_all_stages = Wszystkie Etapy:{ " " } # Saving loading saving_loading_saving_success = Zapisano konfigurację do pliku { $name }. saving_loading_saving_failure = Nie udało się zapisać danych konfiguracyjnych do pliku { $name }, powód { $reason }. saving_loading_reset_configuration = Przywrócono domyślą konfigurację. saving_loading_loading_success = Poprawnie załadowano ustawienia aplikacji. saving_loading_failed_to_create_config_file = Nie udało się utworzyć pliku konfiguracyjnego "{ $path }", powód "{ $reason }". saving_loading_failed_to_read_config_file = Nie można załadować konfiguracji z "{ $path }" ponieważ nie istnieje lub nie jest plikiem. saving_loading_failed_to_read_data_from_file = Nie można odczytać danych z pliku "{ $path }", powód "{ $reason }". # Other selected_all_reference_folders = Nie można rozpocząć wyszukiwania, gdy wszystkie katalogi są ustawione jako foldery źródłowe (referencyjne) searching_for_data = Przeszukiwanie dysku, może to potrwać chwilę, proszę czekać... text_view_messages = WIADOMOŚCI text_view_warnings = OSTRZEŻENIA text_view_errors = BŁĘDY about_window_motto = Program jest i będzie zawsze darmowy do użytku. Może interfejs programu nie jest ergonomiczny, ale za to przynajmniej kod jest nieczytelny. krokiet_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. # Various dialog dialogs_ask_next_time = Pytaj następnym razem symlink_failed = Nie udało się stworzyć symlinka { $name } do { $target }, powód { $reason } delete_title_dialog = Potwierdzenie usunięcia delete_question_label = Czy na pewno usunąć te pliki? delete_all_files_in_group_title = Potwierdzenie usunięcia wszystkich plików w grupie delete_all_files_in_group_label1 = W niektórych grupach wszystkie rekordy są zaznaczone. delete_all_files_in_group_label2 = Czy na pewno je usunąć? delete_items_label = { $items } plików będzie usuniętych. delete_items_groups_label = { $items } plików z { $groups } grup zostanie usuniętych. hardlink_failed = Nie udało się połączyć z { $name } do { $target }, powód { $reason } hard_sym_invalid_selection_title_dialog = Niepoprawne zaznaczenie w niektórych grupach hard_sym_invalid_selection_label_1 = W niektórych grupach jest zaznaczony tylko jeden rekord i zostanie zignorowany. hard_sym_invalid_selection_label_2 = Aby móc mocno połączyć te pliki, należy wybrać co najmniej dwa rekordy w grupie. hard_sym_invalid_selection_label_3 = Pierwszy pozostaje nienaruszony a drugi i kolejne są dowiązywane do tego pierwszego. hard_sym_link_title_dialog = Potwierdzenie dowiązania hard_sym_link_label = Czy na pewno chcesz dowiązać te pliki? move_folder_failed = Nie można przenieść folderu { $name }, powód { $reason } move_file_failed = Nie można przenieść pliku { $name }, powód { $reason } move_files_title_dialog = Wybierz folder, do którego zostaną przeniesione pliki move_files_choose_more_than_1_path = Tylko jedna ścieżka może być wybrana, aby móc skopiować zduplikowane pliki, wybrano { $path_number }. move_stats = Poprawnie przeniesiono { $num_files }/{ $all_files } elementów save_results_to_file = Zapisano wyniki zarówno do plików txt, jak i json w folderze "{ $name }". search_not_choosing_any_music = BŁĄD: Musisz zaznaczyć przynajmniej jeden pole, według którego będą wyszukiwane podobne pliki muzyczne. search_not_choosing_any_broken_files = BŁĄD: Musisz wybrać co najmniej jedno pole wyboru z rodzajem uszkodzonych plików. include_folders_dialog_title = Foldery do przeszukiwania exclude_folders_dialog_title = Foldery do ignorowania include_manually_directories_dialog_title = Dodaj katalogi ręcznie cache_properly_cleared = Poprawnie wyczyszczono pamięć podręczną cache_clear_duplicates_title = Czyszczenie pamięci podręcznej duplikatów cache_clear_similar_images_title = Czyszczenie pamięci podręcznej podobnych obrazów cache_clear_similar_videos_title = Czyszczenie pamięci podręcznej podobnych plików wideo cache_clear_message_label_1 = Czy na pewno chcesz oczyścić pamięć podręczną z przestarzałych wpisów? cache_clear_message_label_2 = Ta operacja usunie wszystkie rekordy, które wskazują na nieistniejące pliki. cache_clear_message_label_3 = Może to nieznacznie przyspieszyć ładowanie/oszczędzanie pamięci podręcznej. cache_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. # Show preview preview_image_resize_failure = Nie udało się zmienić rozmiaru obrazu { $name }. preview_image_opening_failure = Nie udało się otworzyć obrazu { $name }, powód { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Grupa { $current_group }/{ $all_groups } ({ $images_in_group } obrazów) compare_move_left_button = L compare_move_right_button = P ================================================ FILE: czkawka_gui/i18n/pt-BR/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Configurações window_main_title = Czkawka (Soluço) window_progress_title = Verificando window_compare_images = Comparar as imagens # General general_ok_button = Ok general_close_button = Fechar # Krokiet info dialog krokiet_info_title = Apresento a você o Krokiet, a nova versão do Czkawka krokiet_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. # Main window music_title_checkbox = Título music_artist_checkbox = Artista music_year_checkbox = Ano music_bitrate_checkbox = Taxa de bits music_genre_checkbox = Gênero music_length_checkbox = Comprimento music_comparison_checkbox = Comparação aproximada music_checking_by_tags = Informações do arquivo music_checking_by_content = Conteúdo same_music_seconds_label = Duração mínima em segundos do fragmento same_music_similarity_label = Diferença máxima music_compare_only_in_title_group = Comparar dentro dos grupos de títulos similares music_compare_only_in_title_group_tooltip = Quando esta opção está ativada, os arquivos são agrupados por título, em seguida, são comparados entre si. 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. same_music_tooltip = A pesquisa dos arquivos de música equivalentes por seu conteúdo pode ser defininda por meio das configurações: - O tempo mínimo do fragmento após o qual os arquivos de música podem ser identificados como equivalentes - A diferença máxima entre os dois fragmentos dos testes Para obter bons resultados forneça combinações razoáveis destes parâmetros em cada teste. Definir o tempo mínimo para 5s e a diferença máxima para 1.0, irá pesquisar fragmentos quase idênticos nos arquivos. 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. 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. music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) (Santuário --- O santuário foi remixado no verão de 2021) duplicate_case_sensitive_name = Diferenciar as letras maiúsculas das minúsculas duplicate_case_sensitive_name_tooltip = Quando esta opção está ativada, agrupa apenas os registros se eles tiverem exatamente o mesmo nome. Por exemplo, pagar <-> pagar. 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 duplicate_mode_size_name_combo_box = Tamanho e nome duplicate_mode_name_combo_box = Nome duplicate_mode_size_combo_box = Tamanho duplicate_mode_hash_combo_box = Integridade do arquivo duplicate_hash_type_tooltip = O Czkawka oferece três tipos de identificação pela integridade do arquivo por meio do código ‘hash’: Blake3 - Esta opção possui o recurso de criptografia e está definida como padrão porque é muito rápida. 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. XXH3 - Esta opção não possui o recurso de criptografia por ser muito similar em desempenho e qualidade do Blake3. Estes modos podem ser facilmente alternados. duplicate_check_method_tooltip = Por enquanto, o Czkawka oferece três tipos de métodos para localizar os arquivos duplicados: Por nome - Esta opção permite localizar os arquivos que têm o mesmo nome. Por tamanho - Esta opção permite localizar os arquivos que têm o mesmo tamanho. 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. image_hash_size_tooltip = 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. 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. 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’. 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. image_resize_filter_tooltip = Para calcular o ‘hash’ de uma imagem, a biblioteca deve ser primeiro dimensionada. 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. 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. O ‘hash’ com o tamanho de 8x8, recomenda-se utilizar um algoritmo diferente do mais próximo para obter melhores resultados para as imagens. image_hash_alg_tooltip = É possível escolher um dos vários algoritmos para o cálculo da criação do ‘hash’. 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. É 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. big_files_mode_combobox_tooltip = Permite pesquisar os arquivos menores ou maiores big_files_mode_label = Arquivos a serem verificados big_files_mode_smallest_combo_box = O arquivo menor big_files_mode_biggest_combo_box = O arquivo maior main_notebook_duplicates = Arquivos duplicados main_notebook_empty_directories = Diretórios vazios main_notebook_big_files = Arquivos grandes main_notebook_empty_files = Arquivos vazios main_notebook_temporary = Arquivos temporários main_notebook_similar_images = Imagens equivalentes main_notebook_similar_videos = Vídeos equivalentes main_notebook_same_music = Músicas duplicadas main_notebook_symlinks = Ligações simbólicas não válidas main_notebook_broken_files = Arquivos corrompidos main_notebook_bad_extensions = Extensões inválidas main_tree_view_column_file_name = Nome do arquivo main_tree_view_column_folder_name = Nome da pasta main_tree_view_column_path = Caminho main_tree_view_column_modification = Data da modificação main_tree_view_column_size = Tamanho main_tree_view_column_similarity = Equivalentes main_tree_view_column_dimensions = Dimensões main_tree_view_column_title = Título main_tree_view_column_artist = Artista main_tree_view_column_year = Ano main_tree_view_column_bitrate = Taxa de bits main_tree_view_column_length = Comprimento main_tree_view_column_genre = Gênero main_tree_view_column_symlink_file_name = Nome do arquivo da ligação simbólica main_tree_view_column_symlink_folder = Pasta da ligação simbólica main_tree_view_column_destination_path = Caminho do destino main_tree_view_column_type_of_error = Tipo do erro main_tree_view_column_current_extension = Extensão atual main_tree_view_column_proper_extensions = Extensões válidas main_tree_view_column_fps = FPS main_tree_view_column_codec = Codec main_label_check_method = Método de verificação main_label_hash_type = Tipo do ‘hash’ main_label_hash_size = Tamanho do ‘hash’ main_label_size_bytes = Tamanho (em bytes) main_label_min_size = Mínimo main_label_max_size = Máximo main_label_shown_files = Quantidade de arquivos exibidos main_label_resize_algorithm = Redimensionar o algoritmo main_label_similarity = Equivalentes { " " } main_check_box_broken_files_audio = Áudio main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = Arquivo main_check_box_broken_files_image = Imagem main_check_box_broken_files_video = Vídeo main_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. check_button_general_same_size = Ignorar os arquivos do mesmo tamanho check_button_general_same_size_tooltip = Ignorar os arquivos com o mesmo tamanho nos resultados, geralmente estes são os arquivos duplicados (1:1) main_label_size_bytes_tooltip = Tamanho dos arquivos que serão utilizados na pesquisa # Upper window upper_tree_view_included_folder_column_title = Pastas para serem pesquisadas upper_tree_view_included_reference_column_title = Pastas de referência upper_recursive_button = Pesquisa recursiva upper_recursive_button_tooltip = Quando esta opção está ativada, a pesquisa por arquivos ocorre também nas pastas que não foram escolhidas. upper_manual_add_included_button = Adicionar manualmente upper_add_included_button = Adicionar upper_remove_included_button = Remover upper_manual_add_excluded_button = Adicionar manualmente upper_add_excluded_button = Adicionar upper_remove_excluded_button = Remover upper_manual_add_included_button_tooltip = Adicionar manualmente os nomes dos diretórios ou das pastas para serem pesquisadas. Para adicionar vários caminhos de uma vez, separe-os com o ponto e vírgula ‘ ; ’. Por exemplo, ao utilizar ‘/home/roman;/home/rozkaz’ irá adicionar os dois diretórios ‘/home/roman’ e ‘/home/rozkaz’ upper_add_included_button_tooltip = Adicionar um novo diretório para ser pesquisado. upper_remove_included_button_tooltip = Remover o diretório da pesquisa. upper_manual_add_excluded_button_tooltip = Adicionar manualmente um diretório à lista das exceções. Para adicionar vários caminhos de uma vez, separe-os com o ponto e vírgula ‘ ; ’. Por exemplo, ao utilizar ‘/home/roman;/home/krokiet’ irá adicionar os dois diretórios ‘/home/roman’ e ‘/home/keokiet’ upper_add_excluded_button_tooltip = Selecionar o diretório que não será incluído na pesquisa. upper_remove_excluded_button_tooltip = Selecionar o diretório na lista das exceções. upper_notebook_items_configuration = Configurações dos itens upper_notebook_excluded_directories = Caminho dos diretórios não incluídos upper_notebook_included_directories = Caminho dos diretórios incluídos upper_allowed_extensions_tooltip = As extensões que são permitidas devem ser separadas por vírgulas, por padrão, todas as extensões estão disponíveis. 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. 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. upper_excluded_extensions_tooltip = Lista dos arquivos que serão ignorados na verificação. Quando você utiliza as extensões permitidas, estas tem maior prioridade em relação as outras, então o arquivo não será verificado. upper_excluded_items_tooltip = Itens excluídos devem conter * wildcard e devem ser separados por vírgulas. Isto é mais lento que Excluídas Caminhos, portanto use-o com cuidado. upper_excluded_items = Itens ignorados: upper_allowed_extensions = Extensões permitidas: upper_excluded_extensions = Extensões ignoradas: # Popovers popover_select_all = Selecionar todos popover_unselect_all = Desselecionar todos popover_reverse = Inverter a seleção popover_select_all_except_shortest_path = Selecionar todas as opções, exceto o caminho mais curto popover_select_all_except_longest_path = Selecionar todas as opções, exceto o caminho mais longo popover_select_all_except_oldest = Selecionar todos, exceto os mais antigos popover_select_all_except_newest = Selecionar todos, exceto os mais recentes popover_select_one_oldest = Selecionar o mais antigo popover_select_one_newest = Selecionar o mais recente popover_select_custom = Selecionar personalizado popover_unselect_custom = Desselecionar personalizado popover_select_all_images_except_biggest = Selecionar todos, exceto o maior popover_select_all_images_except_smallest = Selecionar todos, exceto o menor popover_custom_path_check_button_entry_tooltip = Selecionar os registros por caminho. Por exemplo: O caminho ‘/home/pimpek/rzecz.txt’ pode ser encontrado com ‘/home/pim*’ popover_custom_name_check_button_entry_tooltip = Selecionar os registros por nome de arquivo. Por exemplo: O caminho ‘/usr/ping/pong.txt’ pode ser encontrado com ‘*ong*’ popover_custom_regex_check_button_entry_tooltip = Selecionar os registros por meio das expressões regulares. Com o uso das expressões regulares (ou o modo ‘Regex’) o texto da pesquisa é o caminho completo (incluindo o nome do arquivo). Por exemplo: O caminho ‘/usr/bin/ziemniak.txt’ pode ser encontrado com ‘/ziem[a-z]+’ 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. popover_custom_case_sensitive_check_button_tooltip = Ativar a detecção da distinção entre as letras maiúsculas e minúsculas. Quando esta opção está ativada, o caminho ‘/home/*’ encontra ambos ‘HoMe/roman’ e ‘/home/roman’. popover_custom_not_all_check_button_tooltip = Impedir que todos os registros de um grupo sejam selecionados. 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. Atente-se ao seguinte detalhe, esta configuração não funcionará se você tiver selecionado manualmente todos os registros de um grupo. popover_custom_regex_path_label = Caminho popover_custom_regex_name_label = Nome popover_custom_regex_regex_label = Expressão regular junto com o nome popover_custom_case_sensitive_check_button = Diferenciar entre maiúsculas e minúsculas popover_custom_all_in_group_label = Não selecionar todos os registros em um grupo popover_custom_mode_unselect = Desselecionar personalizado popover_custom_mode_select = Selecionar o personalizado popover_sort_file_name = Nome do arquivo popover_sort_folder_name = Nome da pasta popover_sort_full_name = Nome completo popover_sort_size = Tamanho popover_sort_selection = Seleção popover_invalid_regex = A expressão regular não é válida popover_valid_regex = A expressão regular é válida # Bottom buttons bottom_search_button = Pesquisar bottom_select_button = Selecionar bottom_delete_button = Excluir bottom_save_button = Salvar bottom_symlink_button = Ligação simbólica bottom_hardlink_button = Ligação simbólica rígida bottom_move_button = Mover bottom_sort_button = Ordenar bottom_compare_button = Comparar bottom_search_button_tooltip = Iniciar a pesquisa bottom_select_button_tooltip = Ao selecionar os registros, apenas os arquivos e as pastas que foram selecionadas poderão ser processadas posteriormente. bottom_delete_button_tooltip = Excluir os arquivos e as pastas que foram selecionadas. bottom_save_button_tooltip = Salvar as informações da pesquisa em um arquivo bottom_symlink_button_tooltip = 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). Esta opção só funciona se pelo menos dois resultados do grupo estiverem selecionados. O primeiro permanece inalterado, o segundo e os subsequentes estão vinculados ou ligados simbolicamente ao primeiro. bottom_hardlink_button_tooltip = 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). Esta opção só funciona se pelo menos dois resultados do grupo estiverem selecionados. O primeiro permanece inalterado, o segundo e os subsequentes estão vinculados ou ligados simbolicamente ao primeiro. bottom_hardlink_button_not_available_tooltip = 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. O botão está desativado, porque as ligações simbólicas rígidas não podem ser criadas. 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. 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). bottom_move_button_tooltip = Mover os arquivos para o diretório que foi selecionado. Esta opção permite copiar todos os arquivos para o diretório sem preservar a estrutura dos diretórios e dos arquivos. Ao tentar mover dois arquivos com nomes idênticos para um diretório, o segundo arquivo não será movido e ocorrerá um erro. bottom_sort_button_tooltip = Ordenar os arquivos ou os diretórios de acordo com o método que foi selecionado. bottom_compare_button_tooltip = Comparar os arquivos e os diretórios nos grupos. bottom_show_errors_tooltip = Exibir ou ocultar o painel de texto inferior. bottom_show_upper_notebook_tooltip = Exibir ou ocultar o painel de texto superior. # Progress Window progress_stop_button = Parar progress_stop_additional_message = Parar a pesquisa # About Window about_repository_button_tooltip = Endereço da página eletrônica do repositório com o código-fonte dos programas Czkawka e Krokiet. about_donation_button_tooltip = Endereço da página eletrônica para fazer uma doação ao programador do Czkawka e Krokiet. about_instruction_button_tooltip = Endereço da página eletrônica para obter ajuda. about_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. about_repository_button = Repositório about_donation_button = Faça uma doação about_instruction_button = Ajuda about_translation_button = Tradução # Header header_setting_button_tooltip = Abrir a janela das configurações do programa Czkawka. header_about_button_tooltip = Abrir a janela das informações sobre o programa Czkawka. # Settings ## General settings_number_of_threads = Quantidade de tópicos utilizados settings_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. settings_use_rust_preview = Utilizar as bibliotecas externas em vez do GTK para carregar a pré-visualização settings_use_rust_preview_tooltip = 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. Se você tiver problemas para carregar a pré-visualização, pode tentar alterar esta configuração. 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. settings_label_restart = Você tem que reiniciar o programa para aplicar as novas configurações settings_ignore_other_filesystems = Ignorar outros sistemas de arquivos (somente para o GNU/Linux) settings_ignore_other_filesystems_tooltip = Ignorar os arquivos que não estão no mesmo sistema de arquivos dos diretórios que estão sendo pesquisados. Funciona da mesma forma que a opção ‘-xdev’ no comando ‘find’ (localizar) no GNU/Linux settings_save_at_exit_button_tooltip = Salvar as configurações em arquivo ao fechar o programa. settings_load_at_start_button_tooltip = Carregar as configurações a partir de um arquivo ao abrir o programa. Se esta opção não estiver ativada, as configurações padrão serão utilizadas. settings_confirm_deletion_button_tooltip = Exibir a janela de confirmação de exclusão ao clicar no botão ‘Excluir’. settings_confirm_link_button_tooltip = Exibir a janela de confirmação ao clicar no botão da ‘Ligação simbólica’. settings_confirm_group_deletion_button_tooltip = Exibir a janela de confirmação de exclusão ao tentar excluir todos os registros de um grupo. settings_show_text_view_button_tooltip = Exibir o painel de texto na parte inferior da interface gráfica do usuário. settings_use_cache_button_tooltip = Utilizar o arquivo de ‘cache’. settings_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. settings_use_trash_button_tooltip = Mover os arquivos para a lixeira em vez de excluí-los permanentemente. settings_language_label_tooltip = Idioma da interface gráfica do usuário. settings_save_at_exit_button = Salvar as configurações ao fechar o programa settings_load_at_start_button = Carregar as configurações ao abrir o programa settings_confirm_deletion_button = Exibir a janela de confirmação quando for excluir qualquer arquivo settings_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 settings_confirm_group_deletion_button = Exibir a janela de confirmação quando for excluir todos os arquivos do grupo settings_show_text_view_button = Exibir o painel de texto inferior settings_use_cache_button = Utilizar o arquivo de ‘cache’ settings_save_also_as_json_button = Salvar o arquivo de ‘cache’ com o formato JSON settings_use_trash_button = Mover os arquivos excluídos para a lixeira settings_language_label = Configurações do idioma settings_multiple_delete_outdated_cache_checkbutton = Excluir automaticamente os registros que estejam desatualizados no arquivo de ‘cache’ settings_multiple_delete_outdated_cache_checkbutton_tooltip = Excluir os registros que estejam desatualizados no arquivo de ‘cache’. 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. 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. 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. settings_notebook_general = Configurações gerais settings_notebook_duplicates = Arquivos duplicados settings_notebook_images = Imagens equivalentes settings_notebook_videos = Vídeos equivalentes ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Exibir a pré-visualização no lado direito ao selecionar um arquivo de imagem. settings_multiple_image_preview_checkbutton = Exibir a pré-visualização das imagens settings_multiple_clear_cache_button_tooltip = Excluir manualmente as entradas que estão desatualizadas no arquivo de ‘cache’. Esta opção só deve ser utilizada se a limpeza automática estiver desativada. settings_multiple_clear_cache_button = Remover os resultados que estejam desatualizados no arquivo de ‘cache’. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = 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’). 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. settings_duplicates_minimal_size_entry_tooltip = Configurar o tamanho mínimo do arquivo de ‘cache’ que será salvo no dispositivo de armazenamento. 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’. settings_duplicates_prehash_checkbutton_tooltip = 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. Esta opção está ativada por padrão, pois pode causar lentidão em algumas situações. 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. settings_duplicates_prehash_minimal_entry_tooltip = Tamanho mínimo do código ‘hash’ parcial que será gravado no arquivo de ‘cache’. settings_duplicates_hide_hard_link_button = Ocultar as ligações rígidas settings_duplicates_prehash_checkbutton = Utilizar o ‘hash’ parcial dos arquivo do ‘cache’ settings_duplicates_minimal_size_cache_label = Tamanho mínimo dos arquivos (em bytes) ao salvar o arquivo de ‘cache’ settings_duplicates_minimal_size_cache_prehash_label = Tamanho mínimo dos arquivos (em bytes) ao salvar o ‘hash’ parcial no arquivo de ‘cache’ ## Saving/Loading settings settings_saving_button_tooltip = Salvar as configurações atuais no arquivo. settings_loading_button_tooltip = Carregar as configurações do arquivo e substituir as configurações atuais. settings_reset_button_tooltip = Restaurar as configurações padrão. settings_saving_button = Salvar as configurações settings_loading_button = Carregar as configurações settings_reset_button = Restaurar as configurações ## Opening cache/config folders settings_folder_cache_open_tooltip = Abrir a pasta onde estão armazenados os arquivos ‘.txt’ do ‘cache’ do programa. 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. 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. Se ocorrer problemas nos arquivos de ‘cache’, os arquivos podem ser excluídos permanentemente. O programa irá criar novos arquivos de ‘cache’ automaticamente. settings_folder_settings_open_tooltip = Abrir a pasta onde está armazenada as configurações do Czkawka. Tenha muito cuidado, a modificação manual das configurações pode interromper o seu fluxo de trabalho. settings_folder_cache_open = Abrir a pasta do ‘cache’ settings_folder_settings_open = Abrir a pasta das configurações # Compute results compute_stopped_by_user = A pesquisa foi interrompida pelo usuário compute_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 }’ compute_found_duplicates_name = Foram encontrados ‘{ $number_files }’ arquivos duplicados nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’ compute_found_empty_folders = Foram encontradas ‘{ $number_files }’ pastas vazias. A verificação durou ‘{ $time }’ compute_found_empty_files = Foram encontrados ‘{ $number_files }’ arquivos vazios. A verificação durou ‘{ $time }’ compute_found_big_files = Foram encontrados ‘{ $number_files }’ arquivos grandes. A verificação durou ‘{ $time }’ compute_found_temporary_files = Foram encontrados ‘{ $number_files }’ arquivos temporários. A verificação durou ‘{ $time }’ compute_found_images = Foram encontrados ‘{ $number_files }’ arquivos de imagens equivalentes nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’ compute_found_videos = Foram encontrados ‘{ $number_files }’ arquivos de vídeos equivalentes nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’ compute_found_music = Foram encontrados ‘{ $number_files }’ arquivos de músicas equivalentes nos ‘{ $number_groups }’ grupos. A verificação durou ‘{ $time }’ compute_found_invalid_symlinks = Foram encontradas ‘{ $number_files }’ ligações simbólicas que não são válidas. A verificação durou ‘{ $time }’ compute_found_broken_files = Foram encontrados ‘{ $number_files }’ arquivos corrompidos. A verificação durou ‘{ $time }’ compute_found_bad_extensions = Foram encontrados ‘{ $number_files }’ arquivos com extensões que não são válidas. A verificação durou ‘{ $time }’ # Progress window progress_scanning_general_file = { $file_number -> [one] Foi verificado ‘{ $file_number }’ arquivo *[other] Foram verificados ‘{ $file_number }’ arquivos } progress_scanning_extension_of_files = Verificando ‘{ $file_checked }’ de ‘{ $all_files }’ por tipo da extensão dos arquivos progress_scanning_broken_files = Verificando ‘{ $file_checked }’ de ‘{ $all_files }’ arquivos ‘{ $data_checked }’ de ‘{ $all_data }’ progress_scanning_video = Foram criados ‘{ $file_checked }’ de ‘{ $all_files }’ código do ‘hash’ dos arquivos de vídeo progress_creating_video_thumbnails = Foram criadas ‘{ $file_checked }’ de ‘{ $all_files }’ miniaturas de vídeo progress_scanning_image = Foram criados ‘{ $file_checked }’ de ‘{ $all_files }’ código do ‘hash’ dos arquivos de imagem ‘{ $data_checked }’ de ‘{ $all_data }’ progress_comparing_image_hashes = Comparando ‘{ $file_checked }’ de ‘{ $all_files }’ código ‘hash’ dos arquivos de imagem progress_scanning_music_tags_end = Comparando ‘{ $file_checked }’ de ‘{ $all_files }’ informações dos arquivos de música progress_scanning_music_tags = Lendo ‘{ $file_checked }’ de ‘{ $all_files }’ informações dos arquivos de música progress_scanning_music_content_end = Comparando ‘{ $file_checked }’ de ‘{ $all_files }’ impressões digitais dos arquivos de música progress_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 }’ progress_scanning_size = Pesquisando por tamanho do arquivo nos ‘{ $file_number }’ progress_scanning_size_name = Pesquisando por nome e por tamanho do arquivo nos ‘{ $file_number }’ progress_scanning_name = Pesquisando por nome do arquivo nos ‘{ $file_number }’ progress_analyzed_partial_hash = O ‘hash’ parcial foi analisado nos arquivos ‘{ $file_checked }’ de ‘{ $all_files }’ e foi verificado ‘{ $data_checked }’ de ‘{ $all_data }’ progress_analyzed_full_hash = O ‘hash’ completo foi analisado nos arquivos ‘{ $file_checked }’ de ‘{ $all_files }’ e foi verificado ‘{ $data_checked }’ de ‘{ $all_data }’ progress_prehash_cache_loading = Carregando o ‘hash’ parcial dos arquivos do ‘cache’ progress_prehash_cache_saving = Salvando o ‘hash’ parcial dos arquivos no ‘cache’ progress_hash_cache_loading = Carregando o ‘hash’ dos arquivos do ‘cache’ progress_hash_cache_saving = Salvando o ‘hash’ dos arquivos no ‘cache’ progress_cache_loading = Carregando as informações do ‘cache’ progress_cache_saving = Salvando as informações no ‘cache’ progress_current_stage = Estágio atual: { " " } progress_all_stages = Todos os estágios: { " " } # Saving loading saving_loading_saving_success = As configurações foram salvas no arquivo ‘{ $name }’. saving_loading_saving_failure = Ocorreu uma falha ao salvar os dados no arquivo de configurações ‘{ $name }’, por causa de ‘{ $reason }’. saving_loading_reset_configuration = As configurações padrão foram restauradas. saving_loading_loading_success = As configurações do programa foram carregadas com sucesso. saving_loading_failed_to_create_config_file = Ocorreu uma falha ao criar o arquivo de configurações no caminho ‘{ $path }’, por causa de ‘{ $reason }’. saving_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. saving_loading_failed_to_read_data_from_file = Não foi possível ler os dados do arquivo do caminho ‘{ $path }’, por causa de ‘{ $reason }’. # Other selected_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) searching_for_data = Os dados estão sendo pesquisados. Esta ação pode demorar bastante tempo. Por favor, aguarde a finalização. text_view_messages = Exibir as mensagens text_view_warnings = Exibir os avisos text_view_errors = Exibir os erros about_window_motto = Este programa é e sempre será de código aberto e de uso gratuito. krokiet_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. # Various dialog dialogs_ask_next_time = Perguntar novamente na próxima vez que a janela for exibida symlink_failed = Ocorreu uma falha na ligação simbólica ‘{ $name }’ para ‘{ $target }’, por causa de ‘{ $reason }’ delete_title_dialog = Confirmação da exclusão delete_question_label = Você tem certeza de que quer excluir os arquivos? delete_all_files_in_group_title = Confirmação da exclusão de todos os arquivos do grupo delete_all_files_in_group_label1 = Em alguns grupos, todos os registros estão selecionados. delete_all_files_in_group_label2 = Você tem certeza de que quer excluí-los? delete_items_label = Os ‘{ $items }’ arquivos serão excluídos. delete_items_groups_label = Os ‘{ $items }’ arquivos dos ‘{ $groups }’ grupos serão excluídos. hardlink_failed = Ocorreu uma falha na ligação rígida ‘{ $name }’ para ‘{ $target }’, por causa de ‘{ $reason }’ hard_sym_invalid_selection_title_dialog = Alguns grupos não são válidos para serem selecionados hard_sym_invalid_selection_label_1 = Em alguns grupos, existe apenas um registro que foi selecionado e será ignorado. hard_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. hard_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. hard_sym_link_title_dialog = Confirmação da ligação simbólica hard_sym_link_label = Você tem certeza de que quer criar a ligação simbólica para estes arquivos? move_folder_failed = Ocorreu uma falha ao mover a pasta ‘{ $name }’, por causa de ‘{ $reason }’ move_file_failed = Ocorreu uma falha ao mover o arquivo ‘{ $name }’, por causa de ‘{ $reason }’ move_files_title_dialog = Escolha a pasta que você quer mover os arquivos duplicados move_files_choose_more_than_1_path = Somente um caminho pode ser selecionado para copiar os arquivos duplicados. A pasta ‘{ $path_number }’ foi selecionada. move_stats = Os arquivos ‘{ $num_files }’ de ‘{ $all_files }’ foram movidos corretamente save_results_to_file = Os resultados foram salvos tanto nos arquivos no formato ‘.txt’ quanto no formato ‘.json’ na pasta ‘{ $name }’. search_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. search_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. include_folders_dialog_title = Pastas a serem pesquisadas exclude_folders_dialog_title = Pastas a serem ignoradas include_manually_directories_dialog_title = Adicionar as pastas manualmente cache_properly_cleared = O ‘cache’ foi limpo com sucesso cache_clear_duplicates_title = Removendo os arquivos duplicados do ‘cache’ cache_clear_similar_images_title = Removendo as imagens equivalentes do ‘cache’ cache_clear_similar_videos_title = Removendo os vídeos equivalentes do ‘cache’ cache_clear_message_label_1 = Você quer remover as entradas que estão desatualizadas no ‘cache’? cache_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. cache_clear_message_label_3 = Esta opção pode acelerar um pouco o carregamento ou o salvamento do ‘cache’. cache_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. # Show preview preview_image_resize_failure = Ocorreu uma falha ao redimensionar a imagem ‘{ $name }’. preview_image_opening_failure = Ocorreu uma falha ao abrir a imagem ‘{ $name }’, por causa de ‘{ $reason }’ # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Os ‘{ $current_group }’ de ‘{ $all_groups }’ grupos possuem ‘{ $images_in_group }’ imagens compare_move_left_button = E compare_move_right_button = D progress_scanning_empty_folders = {$pasta_numero -> [um] Pasta {$folder_number} escaneada *[outro] Pastas {$folder_number} escaneadas} ================================================ FILE: czkawka_gui/i18n/pt-PT/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Configurações window_main_title = Czkawka (Soluço) window_progress_title = Escaneando window_compare_images = Comparar Imagens # General general_ok_button = Ok general_close_button = Fechar # Krokiet info dialog krokiet_info_title = Apresentando Krokiet - Nova versão do Czkawka krokiet_info_message = Krokiet é a nova, melhorada, mais rápida e mais confiável versão da interface gráfica Czkawka GTK! É 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. 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. Experimente e veja a diferença! 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. 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. # Main window music_title_checkbox = Título music_artist_checkbox = Artista music_year_checkbox = Ano music_bitrate_checkbox = Taxa de Bits music_genre_checkbox = Gênero music_length_checkbox = Comprimento music_comparison_checkbox = Comparação Aproximada music_checking_by_tags = Etiquetas music_checking_by_content = Conteúdo same_music_seconds_label = Duração mínima de segundos do fragmento same_music_similarity_label = Diferença máxima music_compare_only_in_title_group = Comparar dentro de grupos de títulos similares music_compare_only_in_title_group_tooltip = Quando ativado, os ficheiros são agrupados por título e então comparados entre si. Com 10000 ficheiros, em vez de quase 100 milhões de comparações, haverá geralmente cerca de 20 000. same_music_tooltip = Buscar por arquivos de música semelhantes por seu conteúdo pode ser configurado definindo: - O tempo mínimo de fragmento após o qual os arquivos de música podem ser identificados como semelhantes - A diferença máxima entre dois fragmentos testados A chave para bons resultados é achar combinações sensíveis desses parâmetros, para fornecido. Definir o tempo mínimo para 5s e a diferença máxima para 1.0 buscará fragmentos quase iguais nos arquivos. 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. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Sensível a Maiúsculas e Minúsculas duplicate_case_sensitive_name_tooltip = Quando ativado, o grupo só registra quando eles têm o mesmo nome, por exemplo, Żołd <-> Żołd Desativar esta opção agrupará os nomes sem verificar se cada letra é do mesmo tamanho, por exemplo, żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Tamanho e Nome duplicate_mode_name_combo_box = Nome duplicate_mode_size_combo_box = Tamanho duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Blake3 - função de hash criptográfico. Este é o padrão, por ser muito rápido. CRC32 - função de hash simples. Isto deve ser mais rápido que Blake3, mas pode muito raramente ter algumas colisões. XXH3 - muito semelhante em desempenho e qualidade de hash ao Blake3 (mas não criptográfico). Logo, tais modos podem ser facilmente intercambiáveis. duplicate_check_method_tooltip = Por ora, o Czkawka oferece três tipos de métodos para encontrar duplicatas: Nome - Acha arquivos que têm o mesmo nome. Tamanho - Acha arquivos que têm o mesmo tamanho. 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. image_hash_size_tooltip = 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. 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. 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. 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). image_resize_filter_tooltip = Para computar o hash da imagem, a biblioteca deve primeiro redimensioná-la. Dependendo do algoritmo escolhido, a imagem resultante usada para calcular o hash parecerá um pouco diferente. 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. Com o tamanho de hash 8x8, recomenda-se usar um algoritmo diferente do Mais Próximo para ter melhores grupos de imagens. image_hash_alg_tooltip = Os usuários podem escolher entre um dos muitos algoritmos de cálculo do hash. Cada um tem pontos fortes e fracos e por vezes darão resultados melhores e por vezes piores para imagens diferentes. Logo, para determinar o melhor para você, são precisos testes manuais. big_files_mode_combobox_tooltip = Permite a busca de arquivos menores/maiores big_files_mode_label = Arquivos verificados big_files_mode_smallest_combo_box = O Menor big_files_mode_biggest_combo_box = O Maior main_notebook_duplicates = Arquivos Duplicados main_notebook_empty_directories = Diretórios Vazios main_notebook_big_files = Arquivos Grandes main_notebook_empty_files = Arquivos Vazios main_notebook_temporary = Arquivos Temporários main_notebook_similar_images = Imagens Semelhantes main_notebook_similar_videos = Vídeos Similares main_notebook_same_music = Músicas Duplicadas main_notebook_symlinks = Ligações Simbólicas Inválidas main_notebook_broken_files = Arquivos Quebrados main_notebook_bad_extensions = Extensões Inválidas main_tree_view_column_file_name = Nome do arquivo main_tree_view_column_folder_name = Nome da Pasta main_tree_view_column_path = Caminho main_tree_view_column_modification = Data de Modificação main_tree_view_column_size = Tamanho main_tree_view_column_similarity = Similaridade main_tree_view_column_dimensions = Tamanho main_tree_view_column_title = Título main_tree_view_column_artist = Artista main_tree_view_column_year = Ano main_tree_view_column_bitrate = Taxa de Bits main_tree_view_column_length = Comprimento main_tree_view_column_genre = Género main_tree_view_column_symlink_file_name = Nome do Arquivo de Ligação Simbólica main_tree_view_column_symlink_folder = Pasta da Ligação Simbólica main_tree_view_column_destination_path = Caminho de Destino main_tree_view_column_type_of_error = Tipo de Erro main_tree_view_column_current_extension = Extensão Atual main_tree_view_column_proper_extensions = Extensão Adequada main_tree_view_column_fps = FPS main_tree_view_column_codec = Codificador main_label_check_method = Método de verificação main_label_hash_type = Tipo de hash main_label_hash_size = Tamanho do hash main_label_size_bytes = Tamanho (bytes) main_label_min_size = Mínimo main_label_max_size = Máximo main_label_shown_files = Número de arquivos exibidos main_label_resize_algorithm = Redimensionar algoritmo main_label_similarity = Similaridade{ " " } main_check_box_broken_files_audio = Áudio main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = Arquivar main_check_box_broken_files_image = Imagem main_check_box_broken_files_video = Vídeo main_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. check_button_general_same_size = Ignorar do mesmo tamanho check_button_general_same_size_tooltip = Ignorar arquivos com tamanho idêntico nos resultados — geralmente estes são duplicatas 1:1 main_label_size_bytes_tooltip = Tamanho dos arquivos usados na verificação # Upper window upper_tree_view_included_folder_column_title = Pastas para Buscar upper_tree_view_included_reference_column_title = Pastas de Referência upper_recursive_button = Recursiva upper_recursive_button_tooltip = Se selecionado, buscar também arquivos que não são postos diretamente nas pastas escolhidas. upper_manual_add_included_button = Adicionar Manual upper_add_included_button = Adicionar upper_remove_included_button = Excluir upper_manual_add_excluded_button = Adicionar Manual upper_add_excluded_button = Adicionar upper_remove_excluded_button = Excluir upper_manual_add_included_button_tooltip = Adicionar o nome do diretório à mão. Para adicionar vários caminhos de uma vez, separe-os por ; /home/roman;/home/rozkaz adicionará dois diretórios /home/roman e /home/rozkaz upper_add_included_button_tooltip = Adicionar novo diretório à busca. upper_remove_included_button_tooltip = Excluir diretório da busca. upper_manual_add_excluded_button_tooltip = Adicionar o nome de diretório excluído à mão. Para adicionar vários caminhos de uma vez, separe-os por ; /home/roman;/home/krokiet adicionará dois diretórios /home/roman e /home/keokiet upper_add_excluded_button_tooltip = Adicionar diretório a ser excluído na busca. upper_remove_excluded_button_tooltip = Excluir diretório da exclusão. upper_notebook_items_configuration = Configuração dos Itens upper_notebook_excluded_directories = Caminhos Excluídos upper_notebook_included_directories = Caminhos Incluídos upper_allowed_extensions_tooltip = Extensões permitidas devem ser separadas por vírgulas (por padrão todas estão disponíveis). Os seguintes Macros, que adicionam várias extensões de uma só vez, também estão disponíveis: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Lista de arquivos desabilitados que serão ignorados na verificação. Ao usar extensões permitidas e desativadas, este tem maior prioridade, então o arquivo não será marcado. upper_excluded_items_tooltip = Itens excluídos devem conter * wildcard e devem ser separados por vírgulas. Este é mais lento que Excluídas Caminhos, portanto use-o com cuidado. upper_excluded_items = Itens excluídos: upper_allowed_extensions = Extensões permitidas: upper_excluded_extensions = Extensões desabilitadas: # Popovers popover_select_all = Selecionar todos popover_unselect_all = Desmarcar todos popover_reverse = Seleção inversa popover_select_all_except_shortest_path = Selecione tudo exceto o caminho mais curto popover_select_all_except_longest_path = Selecione tudo exceto o caminho mais longo popover_select_all_except_oldest = Selecionar todos, exceto os mais antigos popover_select_all_except_newest = Selecionar todos, exceto os mais recentes popover_select_one_oldest = Selecionar um mais antigo popover_select_one_newest = Selecionar um mais recente popover_select_custom = Selecionar um customizado popover_unselect_custom = Desmarcar customizado popover_select_all_images_except_biggest = Selecionar tudo, exceto o maior popover_select_all_images_except_smallest = Selecionar tudo, exceto o menor popover_custom_path_check_button_entry_tooltip = Selecionar registros por caminho. Exemplo de uso: /home/pimpek/rzecz.txt pode ser achado com /home/pim* popover_custom_name_check_button_entry_tooltip = Selecionar registros por nomes de arquivos. Exemplo de uso: /usr/ping/pong.txt pode ser achado com *ong* popover_custom_regex_check_button_entry_tooltip = Selecionar registros por Regex especificada. Com este modo, o texto buscado é o caminho com o nome. Exemplo de uso: /usr/bin/ziemniak.txt pode ser achado com /ziem[a-z]+ Ele usa a implementação regex padrão do Rust. Você pode ler mais sobre isso aqui: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Ativa a deteção sensível a maiúsculas e minúsculas. Quando desativado, /home/* acha ambos /HoMe/roman e /home/roman. popover_custom_not_all_check_button_tooltip = Impede a seleção de todo registro no grupo. 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. AVISO: Esta configuração não funciona se você já selecionou manualmente todos os resultados num grupo. popover_custom_regex_path_label = Caminho popover_custom_regex_name_label = Nome popover_custom_regex_regex_label = Caminho da Regex + nome popover_custom_case_sensitive_check_button = Sensível a maiúsculas e minúsculas popover_custom_all_in_group_label = Não selecionar todo registro no grupo popover_custom_mode_unselect = Desmarcar customizado popover_custom_mode_select = Selecionar customizado popover_sort_file_name = Nome do arquivo popover_sort_folder_name = Nome da pasta popover_sort_full_name = Nome completo popover_sort_size = Tamanho popover_sort_selection = Seleção popover_invalid_regex = Regex inválido popover_valid_regex = Expressão regular é válida # Bottom buttons bottom_search_button = Buscar bottom_select_button = Selecionar bottom_delete_button = Excluir bottom_save_button = Guardar bottom_symlink_button = Ligação simbólica bottom_hardlink_button = Ligação hardlink bottom_move_button = Mover bottom_sort_button = Ordenar bottom_compare_button = Comparar bottom_search_button_tooltip = Iniciar busca bottom_select_button_tooltip = Selecionar registros. Só arquivos/diretórios selecionados podem ser processados posteriormente. bottom_delete_button_tooltip = Excluir arquivos/diretórios selecionados. bottom_save_button_tooltip = Guardar dados sobre a busca em arquivo bottom_symlink_button_tooltip = Criar ligações simbólicas. Só funciona quando ao menos dois resultados num grupo são selecionados. O primeiro é inalterado, e no segundo e mais tarde é feita a ligação simbólica para o primeiro. bottom_hardlink_button_tooltip = Criar ligações hardlinks. Só funciona quando ao menos dois resultados num grupo são selecionados. O primeiro é inalterado, e no segundo e posterior é feito o hardlink ao primeiro. bottom_hardlink_button_not_available_tooltip = Criar ligações hardlinks. O botão está desativado, pois ligações hardlinks não podem ser criadas. Hardlinks só funcionam com privilégios de administrador no Windows, logo, certifique-se de executar o aplicativo como administrador. Se o aplicativo já funciona com tais privilégios, verifique se há questões semelhantes no GitHub. bottom_move_button_tooltip = Move arquivos para o diretório escolhido. Ele copia todos os arquivos para o diretório sem preservar a árvore de diretório. Ao tentar mover dois arquivos com nome idêntico para o diretório, a segunda falhará e exibirá um erro. bottom_sort_button_tooltip = Ordena arquivos/pastas de acordo com o método selecionado. bottom_compare_button_tooltip = Compare as imagens do grupo. bottom_show_errors_tooltip = Exibir/ocultar painel de texto inferior. bottom_show_upper_notebook_tooltip = Exibir/ocultar painel superior do caderno. # Progress Window progress_stop_button = Parar progress_stop_additional_message = Parada pedida # About Window about_repository_button_tooltip = Link para a página do repositório com o código-fonte. about_donation_button_tooltip = Link para a página de doação. about_instruction_button_tooltip = Link para a página de instrução. about_translation_button_tooltip = Link para a página do Crowdin com traduções do aplicativo. Oficialmente, polonês e inglês são suportados. about_repository_button = Repositório about_donation_button = Doação about_instruction_button = Instrução about_translation_button = Tradução # Header header_setting_button_tooltip = Abre diálogo de configurações. header_about_button_tooltip = Abre diálogo com informações sobre o aplicativo. # Settings ## General settings_number_of_threads = Número de threads usadas settings_number_of_threads_tooltip = Numero de thread usadas. Zero significa que toda thread disponível será usada. settings_use_rust_preview = Usar bibliotecas externas em vez de gtk para carregar pré-visualizações settings_use_rust_preview_tooltip = 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. Se tiver problemas com o carregamento de pré-visualizações, tente alterar esta configuração. 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. settings_label_restart = Você tem de reiniciar o aplicativo para aplicar as configurações! settings_ignore_other_filesystems = Ignorar outros sistemas de arquivos (só Linux) settings_ignore_other_filesystems_tooltip = Ignora arquivos que não estão no mesmo sistema de arquivos que os diretórios buscados. Funciona como a opção -xdev no comando find no Linux settings_save_at_exit_button_tooltip = Guardar configuração em arquivo ao fechar o aplicativo. settings_load_at_start_button_tooltip = Carregar configuração do arquivo ao abrir aplicativo. Se não estiver ativado, as configurações padrão serão usadas. settings_confirm_deletion_button_tooltip = Exibir diálogo de confirmação ao clicar no botão excluir. settings_confirm_link_button_tooltip = Exibir diálogo de confirmação ao clicar no botão de ligação hardlink/simbólica. settings_confirm_group_deletion_button_tooltip = Exibir caixa de diálogo de aviso ao tentar excluir todo registro do grupo. settings_show_text_view_button_tooltip = Exibir painel de texto na parte inferior da interface do usuário. settings_use_cache_button_tooltip = Usar cache de arquivos. settings_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. settings_use_trash_button_tooltip = Move arquivos para a lixeira em vez de os excluir para sempre. settings_language_label_tooltip = Idioma para a interface de usuário. settings_save_at_exit_button = Guardar configuração ao fechar o aplicativo settings_load_at_start_button = Carregar configuração ao abrir o aplicativo settings_confirm_deletion_button = Exibir diálogo de confirmação ao excluir qualquer arquivo settings_confirm_link_button = Exibir a caixa de diálogo de confirmação ao fazer a ligação hardlink/simbólica de qualquer arquivo settings_confirm_group_deletion_button = Exibir diálogo de confirmação ao apagar todo arquivo no grupo settings_show_text_view_button = Exibir painel de texto inferior settings_use_cache_button = Usar cache settings_save_also_as_json_button = Também guardar o cache como arquivo JSON settings_use_trash_button = Mover os arquivos excluídos para a lixeira settings_language_label = Idioma settings_multiple_delete_outdated_cache_checkbutton = Excluir entradas de cache desatualizadas automaticamente settings_multiple_delete_outdated_cache_checkbutton_tooltip = Excluir resultados de cache desatualizados que apontam para arquivos inexistentes. Quando ativado, o aplicativo garante que ao carregar os registros, todos os registros apontem para arquivos válidos (aqueles com problemas são ignorados). 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. 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. settings_notebook_general = Geral settings_notebook_duplicates = Duplicatas settings_notebook_images = Imagens Semelhantes settings_notebook_videos = Vídeo Semelhante ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Exibe pré-visualização no lado direito (ao selecionar um arquivo de imagem). settings_multiple_image_preview_checkbutton = Exibir pré-visualização da imagem settings_multiple_clear_cache_button_tooltip = Limpar manualmente o cache de entradas desatualizadas. Isto só deve ser usado se a limpeza automática houver sido desativada. settings_multiple_clear_cache_button = Remover resultados desatualizados do cache. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Oculta todos os arquivos, exceto um, se todos apontarem para os mesmos dados (são ligados por hardlink). 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. settings_duplicates_minimal_size_entry_tooltip = Definir o tamanho mínimo do arquivo que será armazenado em cache. Escolher um valor menor gerará mais registros. Isto acelerará a busca, mas diminuirá o carregamento/armazenamento do cache. settings_duplicates_prehash_checkbutton_tooltip = 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. Está desativado por padrão, pois pode causar lentidões nalguns casos. É altamente recomendado o usar ao escanear centenas de milhares ou milhões de arquivos, pois pode acelerar a pesquisa em várias vezes. settings_duplicates_prehash_minimal_entry_tooltip = Tamanho mínimo da entrada em cache. settings_duplicates_hide_hard_link_button = Ocultar links físicos settings_duplicates_prehash_checkbutton = Usar cache de pré-hash settings_duplicates_minimal_size_cache_label = Tamanho mínimo dos arquivos (em bytes) guardados no cache settings_duplicates_minimal_size_cache_prehash_label = Tamanho mínimo dos arquivos (em bytes) guardados no cache de pré-hash ## Saving/Loading settings settings_saving_button_tooltip = Guardar as configurações atuais em arquivo. settings_loading_button_tooltip = Carregar configurações de arquivo e substituir a configuração atual por elas. settings_reset_button_tooltip = Redefinir a configuração atual para a padrão. settings_saving_button = Guardar configuração settings_loading_button = Carregar configuração settings_reset_button = Redefinir configuração ## Opening cache/config folders settings_folder_cache_open_tooltip = Abre o diretório onde os arquivos txt são armazenados. 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. 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). No caso de problemas com o cache, esses arquivos podem ser removidos. O aplicativo os regenerará automaticamente. settings_folder_settings_open_tooltip = Abre o diretório onde a configuração do Czkawka está armazenada. AVISO: Modificar manualmente a configuração pode quebrar seu fluxo de trabalho. settings_folder_cache_open = Abrir diretório do cache settings_folder_settings_open = Abrir diretório de configurações # Compute results compute_stopped_by_user = A busca foi parada pelo usuário compute_found_duplicates_hash_size = Encontradas { $number_files } duplicatas em { $number_groups } grupos que ocuparam { $size } em { $time } compute_found_duplicates_name = Encontradas { $number_files } duplicações em { $number_groups } grupos em { $time } compute_found_empty_folders = Encontradas pastas { $number_files } vazias em { $time } compute_found_empty_files = Encontrados { $number_files } arquivos vazios em { $time } compute_found_big_files = Encontrados { $number_files } arquivos grandes em { $time } compute_found_temporary_files = { $number_files } arquivos temporários encontrados em { $time } compute_found_images = Encontradas { $number_files } imagens similares em { $number_groups } grupos em { $time } compute_found_videos = Encontrados { $number_files } vídeos similares em { $number_groups } grupos em { $time } compute_found_music = Encontrados { $number_files } arquivos de música similares em { $number_groups } grupos em { $time } compute_found_invalid_symlinks = Encontrado { $number_files } links simbólicos inválidos em { $time } compute_found_broken_files = Encontrados { $number_files } arquivos quebrados em { $time } compute_found_bad_extensions = Encontrados { $number_files } arquivos com extensões inválidas em { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Verificado { $file_number } arquivo *[other] Escaneado { $file_number } arquivos } progress_scanning_extension_of_files = Extensão marcada do arquivo { $file_checked }/{ $all_files } progress_scanning_broken_files = Verificado { $file_checked }/{ $all_files } arquivo ({ $data_checked }/{ $all_data }) progress_scanning_video = Hash de { $file_checked }/{ $all_files } de vídeo progress_creating_video_thumbnails = Miniaturas criadas de { $file_checked }/{ $all_files } de vídeo progress_scanning_image = Hash de { $file_checked }/{ $all_files } imagem ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Comparado a { $file_checked }/{ $all_files } hash de imagem progress_scanning_music_tags_end = Etiquetas comparadas de { $file_checked }/{ $all_files } arquivo de música progress_scanning_music_tags = Ler etiquetas de { $file_checked }/{ $all_files } arquivo de música progress_scanning_music_content_end = Impressão digital comparada de { $file_checked }/{ $all_files } arquivo de música progress_scanning_music_content = Calculada impressão digital de { $file_checked }/ arquivo de música{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Pasta { $folder_number } escaneada *[other] Escaneado { $folder_number } pastas } progress_scanning_size = Tamanho digitalizado do arquivo { $file_number } progress_scanning_size_name = Nome digitalizado e tamanho do arquivo { $file_number } progress_scanning_name = Nome digitalizado do arquivo { $file_number } progress_analyzed_partial_hash = Hash parcial analisado de arquivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Hash completo analisado de arquivos { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Carregando cache de pré-hash progress_prehash_cache_saving = Salvando cache pré-hash progress_hash_cache_loading = Carregando cache de hash progress_hash_cache_saving = Salvando cache de hash progress_cache_loading = Carregando cache progress_cache_saving = Salvando cache progress_current_stage = Estágio atual:{ " " } progress_all_stages = Todo estágio:{ " " } # Saving loading saving_loading_saving_success = Configuração guardada no arquivo { $name }. saving_loading_saving_failure = Falhou na salvaguarda dos dados de configuração no arquivo { $name }, motivo { $reason }. saving_loading_reset_configuration = A configuração atual foi limpa. saving_loading_loading_success = Configuração de aplicativo devidamente carregada. saving_loading_failed_to_create_config_file = Falha ao criar o arquivo de configuração "{ $path }", razão "{ $reason }". saving_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. saving_loading_failed_to_read_data_from_file = Não se pode ler dados do arquivo "{ $path }", razão "{ $reason }". # Other selected_all_reference_folders = Não é possível iniciar a busca quando todo diretório está definido como pasta de referência searching_for_data = Buscando dados, pode demorar um pouco, aguarde... text_view_messages = MENSAGENS text_view_warnings = AVISOS text_view_errors = ERROS about_window_motto = Este programa é gratuito para o uso e sempre será. krokiet_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. # Various dialog dialogs_ask_next_time = Perguntar na próxima vez symlink_failed = Falha ao link simbólico { $name } para { $target }, motivo { $reason } delete_title_dialog = Confirmação de exclusão delete_question_label = Tem certeza de que quer excluir arquivos? delete_all_files_in_group_title = Confirmação da exclusão de todo arquivo no grupo delete_all_files_in_group_label1 = Em alguns grupos todo registro está selecionado. delete_all_files_in_group_label2 = Tem certeza de que quer os excluir? delete_items_label = { $items } arquivos serão excluídos. delete_items_groups_label = { $items } arquivos de { $groups } grupos serão excluídos. hardlink_failed = Falha ao hardlink { $name } para { $target }, motivo { $reason } hard_sym_invalid_selection_title_dialog = Seleção inválida com alguns grupos hard_sym_invalid_selection_label_1 = Em alguns grupos só há um registro selecionado e ele será ignorado. hard_sym_invalid_selection_label_2 = Para poder ligar estes arquivos, ao menos dois resultados no grupo têm de ser selecionados. hard_sym_invalid_selection_label_3 = O primeiro no grupo é reconhecido como original e não é mudado, mas o segundo e posterior são modificados. hard_sym_link_title_dialog = Link de confirmação hard_sym_link_label = Tem certeza de que quer vincular estes arquivos? move_folder_failed = Falha ao mover a pasta { $name }, razão { $reason } move_file_failed = Falha ao mover o arquivo { $name }, razão { $reason } move_files_title_dialog = Escolha a pasta para a qual você quer mover arquivos duplicados move_files_choose_more_than_1_path = Só um caminho pode ser selecionado para poder copiar seus arquivos duplicados, selecionado { $path_number }. move_stats = Devidamente movidos { $num_files }/{ $all_files } itens save_results_to_file = Resultados salvos tanto nos arquivos txt quanto json na pasta "{ $name }". search_not_choosing_any_music = ERRO: Você deve selecionar ao menos uma caixa de seleção com tipos de busca de música. search_not_choosing_any_broken_files = ERRO: Você deve selecionar ao menos uma caixa de seleção com tipo de arquivos quebrados. include_folders_dialog_title = Pastas para incluir exclude_folders_dialog_title = Pastas para excluir include_manually_directories_dialog_title = Adicionar diretório manualmente cache_properly_cleared = Cache devidamente limpo cache_clear_duplicates_title = Limpando o cache de duplicatas cache_clear_similar_images_title = Limpando o cache de imagens similares cache_clear_similar_videos_title = Limpando o cache de vídeos similares cache_clear_message_label_1 = Deseja limpar o cache de entradas desatualizadas? cache_clear_message_label_2 = Esta operação removerá toda entrada de cache que aponta para arquivos inválidos. cache_clear_message_label_3 = Isto pode acelerar um pouco o carregamento/salvamento para o cache. cache_clear_message_label_4 = AVISO: A operação removerá todo dado em cache de unidades externas desconectadas. Logo, cada hash terá de ser regenerado. # Show preview preview_image_resize_failure = Falha ao redimensionar a imagem { $name }. preview_image_opening_failure = Falha ao abrir a imagem { $name }, razão { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Grupo { $current_group }/{ $all_groups } ({ $images_in_group } imagens) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/ro/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Setări window_main_title = Czkawka (Sufletăciune) window_progress_title = Scanare window_compare_images = Compară imaginile # General general_ok_button = Ok general_close_button = Inchide # Krokiet info dialog krokiet_info_title = Introducerea lui Krokiet - Noua versiune a Czkawka krokiet_info_message = Krokiet este noua, îmbunătățită, mai rapidă și mai fiabilă versiune a Czkawka GTK GUI! 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. 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. Îl testează și vezi diferența! 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. 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ă. # Main window music_title_checkbox = Titlu music_artist_checkbox = Artist music_year_checkbox = An music_bitrate_checkbox = Rată de bitrate music_genre_checkbox = Gen music_length_checkbox = Lungime music_comparison_checkbox = Comparație aproximativă music_checking_by_tags = Etichete music_checking_by_content = Conținut same_music_seconds_label = Fragment minim a doua durată same_music_similarity_label = Diferența maximă music_compare_only_in_title_group = Compară în cadrul grupurilor de titluri similare music_compare_only_in_title_group_tooltip = Când este activat, fișierele sunt grupate după titlu și apoi comparate între ele. Cu 10000 de fişiere, în schimb aproape 100 de milioane de comparaţii vor fi de obicei aproximativ 20000 de comparaţii. same_music_tooltip = Căutarea fişierelor muzicale similare după conţinutul său poate fi configurată prin setarea: - Timpul minim de fragment după care fişierele muzicale pot fi identificate ca fiind similare - Diferenţa maximă între două fragmente testate Cheia pentru rezultate bune este de a găsi combinaţii rezonabile ale acestor parametri, pentru furnizare. Setarea timpului minim la 5 s și diferența maximă la 1.0, va căuta fragmente aproape identice în fișiere. 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 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ță). music_comparison_checkbox_tooltip = 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: Remix Lato 2021) duplicate_case_sensitive_name = Sensibil la caz duplicate_case_sensitive_name_tooltip = Când este activată, grupul înregistrează doar atunci când are exact același nume, de ex. Trunchiul <-> Z ołd Dezactivarea acestei opțiuni va grupa numele fără a verifica dacă fiecare literă are aceeași mărime, de ex. z oŁD <-> Z ołd duplicate_mode_size_name_combo_box = Dimensiune și nume duplicate_mode_name_combo_box = Nume duplicate_mode_size_combo_box = Dimensiune duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka oferă 3 tipuri de hash-uri: Blake3 - funcţie criptografică hash. Acesta este implicit pentru că este foarte rapid. CRC32 - funcţia simplă de hash. Acest lucru ar trebui să fie mai rapid decât Blake3, dar foarte rar poate avea unele coliziuni. 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. duplicate_check_method_tooltip = Deocamdată, Czkawka oferă trei tipuri de metode pentru a găsi duplicate după: Nume - Găseşte fişiere care au acelaşi nume. Dimensiune - Găseşte fişiere cu aceeaşi dimensiune. 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. image_hash_size_tooltip = 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. 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. 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. 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). image_resize_filter_tooltip = Pentru a calcula hash of imagine, biblioteca trebuie mai întâi să o redimensioneze. În funcție de algoritmul ales, imaginea rezultată folosită pentru a calcula hash va arăta puțin diferit. 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ă. Cu dimensiunea hash de 8x8 este recomandat să se folosească un algoritm diferit de Nearest, pentru a avea grupuri mai bune de imagini. image_hash_alg_tooltip = Utilizatorii pot alege unul dintre multele algoritmi de calculare a hash-ului. 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. Deci, pentru a determina cel mai bun dintre voi, este necesară testarea manuală. big_files_mode_combobox_tooltip = Permite căutarea celor mai mici/mai mari fişiere big_files_mode_label = Fișiere verificate big_files_mode_smallest_combo_box = Cel mai mic big_files_mode_biggest_combo_box = Miggest main_notebook_duplicates = Fișiere duplicate main_notebook_empty_directories = Dosare goale main_notebook_big_files = Fișiere mari main_notebook_empty_files = Fișiere goale main_notebook_temporary = Fișiere temporare main_notebook_similar_images = Imagini similare main_notebook_similar_videos = Video similare main_notebook_same_music = Duplicate Muzică main_notebook_symlinks = Simboluri invalide main_notebook_broken_files = Fișiere defecte main_notebook_bad_extensions = Extensii rele main_tree_view_column_file_name = Numele fișierului main_tree_view_column_folder_name = Nume folder main_tree_view_column_path = Cale main_tree_view_column_modification = Data modificării main_tree_view_column_size = Dimensiune main_tree_view_column_similarity = Similaritate main_tree_view_column_dimensions = Dimensiuni main_tree_view_column_title = Titlu main_tree_view_column_artist = Artist main_tree_view_column_year = An main_tree_view_column_bitrate = Rată de bitrate main_tree_view_column_length = Lungime main_tree_view_column_genre = Gen main_tree_view_column_symlink_file_name = Numele fișierului Symlink main_tree_view_column_symlink_folder = Dosar Symlink main_tree_view_column_destination_path = Calea destinației main_tree_view_column_type_of_error = Tip de eroare main_tree_view_column_current_extension = Extensia curentă main_tree_view_column_proper_extensions = Extensie corectă main_tree_view_column_fps = FPS main_tree_view_column_codec = Codecul main_label_check_method = Metoda de verificare main_label_hash_type = Tip hash main_label_hash_size = Dimensiune hash main_label_size_bytes = Dimensiune (octeți) main_label_min_size = Minim main_label_max_size = Maxim main_label_shown_files = Numărul de fișiere afișate main_label_resize_algorithm = Redimensionare algoritm main_label_similarity = Similarity{ " " } main_check_box_broken_files_audio = Audio main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Arhivează main_check_box_broken_files_image = Imagine main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignoră aceeași dimensiune check_button_general_same_size_tooltip = Ignoră fișierele cu rezultate de dimensiune identică - de obicei, acestea sunt de 1:1 duplicate main_label_size_bytes_tooltip = Dimensiunea fişierelor care vor fi utilizate în scanare # Upper window upper_tree_view_included_folder_column_title = Dosare de căutat upper_tree_view_included_reference_column_title = Dosare de referință upper_recursive_button = Recursiv upper_recursive_button_tooltip = Dacă este selectat, caută și fișiere care nu sunt plasate direct în dosarele alese. upper_manual_add_included_button = Adăugare manuală upper_add_included_button = Adăugare upper_remove_included_button = Elimină upper_manual_add_excluded_button = Adăugare manuală upper_add_excluded_button = Adăugare upper_remove_excluded_button = Elimină upper_manual_add_included_button_tooltip = Adăugați numele directorului pentru a căuta manual. Pentru a adăuga căi multiple simultan, separați-le de ; /home/roman;/home/rozkaz va adăuga două directoare /home/roman și /home/rozkaz upper_add_included_button_tooltip = Adăugați un nou director pentru căutare. upper_remove_included_button_tooltip = Ștergeți directorul de căutare. upper_manual_add_excluded_button_tooltip = Adaugă numele folderului exclus manual. Pentru a adăuga căi multiple simultan, separați-le de ; /home/roman;/home/krokiet va adăuga două directoare /home/roman și /home/keokiet upper_add_excluded_button_tooltip = Adauga directorul pentru a fi exclus in cautare. upper_remove_excluded_button_tooltip = Ştergeţi directorul din excludere. upper_notebook_items_configuration = Configurare articole upper_notebook_excluded_directories = Puteți exclude căile upper_notebook_included_directories = Include Puteți upper_allowed_extensions_tooltip = Extensiile permise trebuie separate prin virgulă (implicit toate sunt disponibile). Următoarele macro care adaugă simultan extensii multiple sunt de asemenea disponibile: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Lista fişierelor dezactivate care vor fi ignorate în scanare. La utilizarea extensiilor permise și dezactivate, aceasta are prioritate mai mare, deci fișierul nu va fi verificat. upper_excluded_items_tooltip = Elemente excluse trebuie să conțină * wildcard și să fie separate prin virgulă. Aceasta este mai lentă decât Excluded Paths, deci folosiți-o cu grijă. upper_excluded_items = Elemente excluse: upper_allowed_extensions = Extensii permise: upper_excluded_extensions = Extensii dezactivate: # Popovers popover_select_all = Selectează tot popover_unselect_all = Deselectează tot popover_reverse = Selectare inversă popover_select_all_except_shortest_path = Selectează toate, cu excepția celui mai scurt drum popover_select_all_except_longest_path = Selectează toate, cu excepția celui mai lung traseu popover_select_all_except_oldest = Selectează toate cu excepția celor mai vechi popover_select_all_except_newest = Selectează toate cu excepția celor noi popover_select_one_oldest = Selectează unul mai vechi popover_select_one_newest = Selectaţi unul dintre cele mai noi popover_select_custom = Selectare particularizată popover_unselect_custom = Deselectare particularizată popover_select_all_images_except_biggest = Selectează toate cu excepția celui mai mare popover_select_all_images_except_smallest = Selectează toate cu excepția celor mici popover_custom_path_check_button_entry_tooltip = Selectaţi înregistrările după cale. Exemplu de utilizare: /home/pimpek/rzecz.txt poate fi găsit cu /home/pim* popover_custom_name_check_button_entry_tooltip = Selectaţi înregistrările cu numele fişierelor. Exemplu de utilizare: /usr/ping/pong.txt poate fi găsit cu *ong* popover_custom_regex_check_button_entry_tooltip = Selectaţi înregistrările specificate de Regex. Cu acest mod, textul căutat este calea cu numele. Exemplu de utilizare: /usr/bin/ziemniak. xt poate fi găsit cu /ziem[a-z]+ Acest lucru folosește implementările implicite Rust regex. Puteți citi mai multe despre ele aici: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Activează detectarea cazurilor sensibile. Când este dezactivat /home/* găsește atât /HoMe/roman cât și /home/roman. popover_custom_not_all_check_button_tooltip = Previne selectarea tuturor înregistrărilor din grup. 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. ATENŢIE: Această setare nu funcţionează dacă aţi selectat deja manual toate rezultatele într-un grup. popover_custom_regex_path_label = Cale popover_custom_regex_name_label = Nume popover_custom_regex_regex_label = Cale Regex + Nume popover_custom_case_sensitive_check_button = Sensibil la caz popover_custom_all_in_group_label = Nu selectaţi toate înregistrările în grup popover_custom_mode_unselect = Deselectare particularizată popover_custom_mode_select = Selectare particularizată popover_sort_file_name = Nume fișier popover_sort_folder_name = Nume dosar popover_sort_full_name = Numele complet popover_sort_size = Dimensiune popover_sort_selection = Selecţie popover_invalid_regex = Regex nu este valid popover_valid_regex = Regex este valid # Bottom buttons bottom_search_button = Caută bottom_select_button = Selectare bottom_delete_button = Ștergere bottom_save_button = Salvează bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Mutare bottom_sort_button = Sortează bottom_compare_button = Compară bottom_search_button_tooltip = Începe căutarea bottom_select_button_tooltip = Selectaţi înregistrările. Numai fişierele/dosarele selectate pot fi procesate ulterior. bottom_delete_button_tooltip = Ştergeţi fişierele/dosarele selectate. bottom_save_button_tooltip = Salvează datele despre căutare în fișier bottom_symlink_button_tooltip = Creaţi link-uri simbolice. Funcţionează numai atunci când cel puţin două rezultate într-un grup sunt selectate. Prima este neschimbată, iar a doua și mai târziu simpatizează cu primul. bottom_hardlink_button_tooltip = Creează link-uri hardware. Funcţionează numai atunci când cel puţin două rezultate sunt selectate într-un grup. Prima este neschimbată, iar a doua și mai târziu sunt greu legate mai întâi. bottom_hardlink_button_not_available_tooltip = Creează link-uri hardware. Butonul este dezactivat, deoarece hardlink-urile nu pot fi create. Legăturile fizice funcționează doar cu privilegii de administrator pe Windows, așa că asigură-te că rulezi aplicația ca administrator. Dacă aplicația funcționează deja cu astfel de privilegii verificați pentru probleme similare pe Giwhere,. bottom_move_button_tooltip = Mută fișierele în directorul ales. Copiază toate fișierele în director fără a păstra directorul arborescent. Când se încearcă mutarea a două fișiere cu nume identic în folder, al doilea va eșua și va afișa eroarea. bottom_sort_button_tooltip = Sortează fișierele/dosarele în funcție de metoda selectată. bottom_compare_button_tooltip = Compară imaginile din grup. bottom_show_errors_tooltip = Arată/ascunde panoul de text de jos. bottom_show_upper_notebook_tooltip = Arată/Ascunde panoul de notebook-uri de sus. # Progress Window progress_stop_button = Oprește progress_stop_additional_message = Oprire solicitată # About Window about_repository_button_tooltip = Link către pagina de depozit cu codul sursă. about_donation_button_tooltip = Link la pagina de donare. about_instruction_button_tooltip = Link către pagina de instrucțiuni. about_translation_button_tooltip = Link catre pagina Crowdin cu traducerea aplicatiilor. Oficial, limba poloneza si engleza sunt suportate. about_repository_button = Depozit about_donation_button = Donație about_instruction_button = Instrucțiuni about_translation_button = Traducere # Header header_setting_button_tooltip = Deschide dialogul de setări. header_about_button_tooltip = Deschide dialogul cu informații despre aplicație. # Settings ## General settings_number_of_threads = Numar discutii folosite settings_number_of_threads_tooltip = Numărul de teme folosite, 0 înseamnă că vor fi folosite toate temele disponibile. settings_use_rust_preview = Folosește în schimb gtk librării externe pentru a încărca previzualizările settings_use_rust_preview_tooltip = Utilizarea de previzualizări gtk va fi uneori mai rapidă și va suporta mai multe formate, dar uneori aceasta ar putea fi exact opusul. Dacă aveţi probleme cu încărcarea previzualizărilor, puteţi încerca să schimbaţi această setare. 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. settings_label_restart = Trebuie să reporniți aplicația pentru a aplica setările! settings_ignore_other_filesystems = Ignorați alte sisteme de fișiere (doar Linux) settings_ignore_other_filesystems_tooltip = ignoră fişierele care nu se află în acelaşi sistem de fişiere ca şi directoarele căutate. Funcţionează la fel ca opţiunea -xdev în găsirea comenzii în Linux settings_save_at_exit_button_tooltip = Salvați configurația în fișier la închiderea aplicației. settings_load_at_start_button_tooltip = Încarcă configurația din fișier la deschiderea aplicației. Dacă nu este activată, se vor folosi setările implicite. settings_confirm_deletion_button_tooltip = Afișați caseta de confirmare când faceți clic pe butonul de ștergere. settings_confirm_link_button_tooltip = Afișați caseta de confirmare când faceți clic pe butonul hard/symlink. settings_confirm_group_deletion_button_tooltip = Arată dialogul de avertizare când se încearcă ștergerea tuturor înregistrărilor din grup. settings_show_text_view_button_tooltip = Arată panoul de text în partea de jos a interfeței utilizatorului. settings_use_cache_button_tooltip = Foloseşte cache-ul fişierelor. settings_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). settings_use_trash_button_tooltip = Mută fișierele la gunoi în loc să le ștergi definitiv. settings_language_label_tooltip = Limba interfeței utilizatorului. settings_save_at_exit_button = Salvați configurația la închiderea aplicației settings_load_at_start_button = Încarcă configurația la deschiderea aplicației settings_confirm_deletion_button = Arată dialog de confirmare la ștergerea oricăror fișiere settings_confirm_link_button = Arată dialog de confirmare atunci când fişierele hard/symlink settings_confirm_group_deletion_button = Arată dialog de confirmare la ștergerea tuturor fișierelor din grup settings_show_text_view_button = Arată panoul de text jos settings_use_cache_button = Utilizare geocutie settings_save_also_as_json_button = De asemenea, salvează cache-ul ca fișier JSON settings_use_trash_button = Mută fișierele șterse în gunoi settings_language_label = Limba settings_multiple_delete_outdated_cache_checkbutton = Şterge automat intrările învechite settings_multiple_delete_outdated_cache_checkbutton_tooltip = Ştergeţi rezultatele învechite ale geocutiei care indică fişierele inexistente. 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). 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 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. settings_notebook_general = Generalități settings_notebook_duplicates = Duplicate settings_notebook_images = Imagini similare settings_notebook_videos = Video similar ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Afișează previzualizarea în partea dreaptă (când se selectează un fișier imagine). settings_multiple_image_preview_checkbutton = Arată previzualizarea imaginii settings_multiple_clear_cache_button_tooltip = Curăță manual cache-ul intrărilor învechite. Acest lucru ar trebui folosit doar dacă curățarea automată a fost dezactivată. settings_multiple_clear_cache_button = Elimină rezultatele învechite din geocutie. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Ascunde toate fişierele, cu excepţia unuia, dacă toate arată spre aceleaşi date (sunt conectate). 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. settings_duplicates_minimal_size_entry_tooltip = Setaţi dimensiunea minimă a fişierului care va fi memorată în cache. 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. settings_duplicates_prehash_checkbutton_tooltip = Permite stocarea în cache a prehash (un hash calculat dintr-o mică parte a fișierului) care permite concedierea mai timpurie a rezultatelor nereplicate. este dezactivat implicit deoarece poate cauza încetiniri în unele situații. Este foarte recomandat sa il utilizezi cand scanezi sute de mii sau milioane de fisiere, pentru ca poate accelera cautarea de mai multe ori. settings_duplicates_prehash_minimal_entry_tooltip = Dimensiunea minimă a intrării în cache. settings_duplicates_hide_hard_link_button = Ascunde link-urile fizice settings_duplicates_prehash_checkbutton = Foloseste cache-ul prehash settings_duplicates_minimal_size_cache_label = Dimensiunea minimă a fişierelor (în octeţi) salvate în cache settings_duplicates_minimal_size_cache_prehash_label = Dimensiunea minimă a fişierelor (în octeţi) salvate în cache de prehash ## Saving/Loading settings settings_saving_button_tooltip = Salvați setările curente în fișier. settings_loading_button_tooltip = Încarcă setările din fișier și înlocuiește configurația curentă cu ele. settings_reset_button_tooltip = Resetați configurația curentă la cea implicită. settings_saving_button = Salvează configurația settings_loading_button = Încarcă configurația settings_reset_button = Resetare configurație ## Opening cache/config folders settings_folder_cache_open_tooltip = Deschide folderul unde sunt stocate fișierele txt cache-ul. 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ă. 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 caz de probleme cu geocutia, aceste fişiere pot fi şterse. Aplicaţia le va regenera automat. settings_folder_settings_open_tooltip = Deschide folderul unde este stocată configurația Czkawka. AVERTISMENT: Modificarea manuală a configurației poate rupe fluxul de lucru. settings_folder_cache_open = Deschide dosarul cache settings_folder_settings_open = Deschide folderul de setări # Compute results compute_stopped_by_user = Căutarea a fost oprită de utilizator compute_found_duplicates_hash_size = Am găsit { $number_files } duplicate în { $number_groups } grupuri care au luat { $size } în { $time } compute_found_duplicates_name = Am găsit { $number_files } duplicate în grupurile { $number_groups } în { $time } compute_found_empty_folders = Folderele goale { $number_files } au fost găsite în { $time } compute_found_empty_files = Fișiere goale { $number_files } găsite în { $time } compute_found_big_files = Fișiere mari { $number_files } găsite în { $time } compute_found_temporary_files = Fișiere temporare { $number_files } găsite în { $time } compute_found_images = S-au găsit imagini similare { $number_files } în grupurile { $number_groups } în { $time } compute_found_videos = S-au găsit videoclipuri similare { $number_files } în grupurile { $number_groups } în { $time } compute_found_music = Am găsit { $number_files } fişiere muzicale similare în { $number_groups } grupuri { $time } compute_found_invalid_symlinks = { $number_files } link-uri simboluri invalide găsite în { $time } compute_found_broken_files = Fișiere defecte { $number_files } găsite în { $time } compute_found_bad_extensions = Fișiere { $number_files } cu extensii invalide în { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] a scanat { $file_number } fişierul *[other] Scanat { $file_number } fişiere } progress_scanning_extension_of_files = S-a verificat extensia fișierului { $file_checked }/{ $all_files } progress_scanning_broken_files = Fişier verificat { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_video = Hashed of { $file_checked }/{ $all_files } video progress_creating_video_thumbnails = Pictograme video create de { $file_checked }/{ $all_files } progress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Imaginea a fost comparată { $file_checked }/{ $all_files } progress_scanning_music_tags_end = Tag-uri comparate ale fișierului de muzică { $file_checked }/{ $all_files } progress_scanning_music_tags = Citește etichetele fișierului de muzică { $file_checked }/{ $all_files } progress_scanning_music_content_end = Față de amprenta fișierului de muzică { $file_checked }/{ $all_files } progress_scanning_music_content = Amprenta calculată a fișierului de muzică { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Dosar Scanat { $folder_number } *[other] Scanat { $folder_number } dosare } progress_scanning_size = Dimensiune scanată pentru fişierul { $file_number } progress_scanning_size_name = Numele scanat şi dimensiunea fişierului { $file_number } progress_scanning_name = Numele scanat al fişierului { $file_number } progress_analyzed_partial_hash = S-a analizat hash parțial al fișierelor { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = S-a analizat hash complet al fişierelor { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Se încarcă cache-ul prehash progress_prehash_cache_saving = Salvare cache prehash progress_hash_cache_loading = Încărcare cache hash progress_hash_cache_saving = Salvare cache hash progress_cache_loading = Se încarcă geocutia progress_cache_saving = Salvare geocutie progress_current_stage = Current Stage:{ " " } progress_all_stages = All Stages:{ " " } # Saving loading saving_loading_saving_success = Configurație salvată în fișierul { $name }. saving_loading_saving_failure = Salvarea datelor de configurare a eșuat în fișierul { $name }, motivul { $reason }. saving_loading_reset_configuration = Configurația curentă a fost ștearsă. saving_loading_loading_success = Configurare aplicație încărcată corespunzător. saving_loading_failed_to_create_config_file = Nu s-a putut crea fișierul de configurare "{ $path }", motivul "{ $reason }". saving_loading_failed_to_read_config_file = Nu se poate încărca configurația din "{ $path }" deoarece nu există sau nu este un fișier. saving_loading_failed_to_read_data_from_file = Datele din fişierul "{ $path }", motivul "{ $reason }". # Other selected_all_reference_folders = Nu se poate începe căutarea, atunci când toate directoarele sunt setate ca dosare de referință searching_for_data = Se caută date, poate dura ceva timp, vă rugăm așteptați... text_view_messages = MESAJE text_view_warnings = ATENȚIONĂRI text_view_errors = EROARE about_window_motto = Acest program este liber de utilizat și va fi întotdeauna. krokiet_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ă. # Various dialog dialogs_ask_next_time = Întreabă data viitoare symlink_failed = Esuare simlink { $name } la { $target }, motivul { $reason } delete_title_dialog = Ștergeți confirmarea delete_question_label = Sunteţi sigur că doriţi să ştergeţi fişierele? delete_all_files_in_group_title = Confirmarea ștergerii tuturor fișierelor din grup delete_all_files_in_group_label1 = In unele grupuri, toate inregistrarile sunt selectate. delete_all_files_in_group_label2 = Sunteţi sigur că doriţi să le ştergeţi? delete_items_label = { $items } fișiere vor fi șterse. delete_items_groups_label = { $items } fișiere din grupurile { $groups } vor fi șterse. hardlink_failed = Eșuare conectare { $name } la { $target }, motiv { $reason } hard_sym_invalid_selection_title_dialog = Selecţie invalidă cu unele grupuri hard_sym_invalid_selection_label_1 = În unele grupuri există doar o înregistrare selectată și va fi ignorată. hard_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. hard_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. hard_sym_link_title_dialog = Confirmare link hard_sym_link_label = Sunteţi sigur că doriţi să conectaţi aceste fişiere? move_folder_failed = Nu s-a reușit mutarea dosarului { $name }, motivul { $reason } move_file_failed = Nu s-a reușit mutarea fișierului { $name }, motivul { $reason } move_files_title_dialog = Alegeți directorul în care doriți să mutați fișierele duplicate move_files_choose_more_than_1_path = Poate fi selectată doar o singură cale pentru a putea copia fişierele duplicate, selectate { $path_number }. move_stats = Elemente corect mutate { $num_files }/{ $all_files } save_results_to_file = Rezultate salvate atât pentru fişierele txt cât şi pentru fişierele json în folderul "{ $name }". search_not_choosing_any_music = EROARE: Trebuie să selectaţi cel puţin o casetă cu tipuri de căutare de muzică. search_not_choosing_any_broken_files = EROARE: Trebuie să selectaţi cel puţin o casetă de selectare cu tipul de fişiere bifate. include_folders_dialog_title = Dosare de inclus exclude_folders_dialog_title = Dosare de exclus include_manually_directories_dialog_title = Adaugă director manual cache_properly_cleared = Geocutie golită corect cache_clear_duplicates_title = Golire duplicate cache cache_clear_similar_images_title = Curăță cache imagini similare cache_clear_similar_videos_title = Curățare cache video similar cache_clear_message_label_1 = Vrei să ştergi memoria cache a intrărilor învechite? cache_clear_message_label_2 = Această operaţie va elimina toate intrările din cache-ul care indică fişiere invalide. cache_clear_message_label_3 = Aceasta poate încărca/salva uşor accelerat în cache. cache_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. # Show preview preview_image_resize_failure = Redimensionarea imaginii { $name } a eșuat. preview_image_opening_failure = Nu s-a reușit deschiderea imaginii { $name }, motivul { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Grup { $current_group }/{ $all_groups } ({ $images_in_group } imagini) compare_move_left_button = l compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/ru/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Настройки window_main_title = Czkawka («Икота») window_progress_title = Сканирование window_compare_images = Сравнить изображения # General general_ok_button = ОК general_close_button = Закрыть # Krokiet info dialog krokiet_info_title = Представляем Krokiet - новая версия Czkawka krokiet_info_message = Крокиет – это новая, улучшенная, более быстрая и надежная версия Czkawka GTK GUI! Его проще запускать и он более устойчив к изменениям системы, так как он зависит только от основных библиотек, доступных по умолчанию на большинстве систем. Крокиет также предоставляет функции, которых нет в Czkawka, включая миниатюры в режиме сравнения видео, EXIF-очиститель, прогресс перемещения/копирования/удаления файлов или расширенные возможности сортировки. Попробуйте сами и посмотрите разницу! Czkawka продолжит получать исправления ошибок и небольшие обновления от меня, но все новые функции будут разрабатываться исключительно для Крокиета, и любой может внести свой вклад, добавив новые функции, расширив режимы или дополнительно развив Czkawka. P.S.: Это сообщение должно появиться только один раз. Если оно снова появляется, установите переменную CZKAWKA_DONT_ANNOY_ME в любое непустое значение. # Main window music_title_checkbox = Заголовок music_artist_checkbox = Исполнитель music_year_checkbox = Год music_bitrate_checkbox = Битрейт music_genre_checkbox = Жанр music_length_checkbox = Длительность music_comparison_checkbox = Приблизительное сравнение music_checking_by_tags = Теги music_checking_by_content = Содержание same_music_seconds_label = Минимальная длительность второго фрагмента same_music_similarity_label = Максимальная разница music_compare_only_in_title_group = Сравнить внутри групп с одинаковыми названиями music_compare_only_in_title_group_tooltip = Когда включено, файлы сгруппируются по заголовку, а затем сравниваются друг с другом. С 10000 файлов, вместо этого почти 100 миллионов сравнений обычно будет около 20000 сравнений. same_music_tooltip = Поиск похожих музыкальных файлов по его содержимому может быть настроен с помощью настройки: - Минимальное время фрагмента, после которого музыкальные файлы можно определить как похожие - Максимальная разница между двумя проверенными фрагментами Ключ к хорошим результатам - найти разумные комбинации этих параметров, для предоставленных. Установка минимального времени на 5 секунд, а максимальная разница в 1.0, будет искать практически идентичные фрагменты файлов. Время 20 секунд и максимальная разница в 6,0, с другой стороны, хорошо подходит для поиска ремиксов/версий и т.д. По умолчанию, каждый музыкальный файл сравнивается друг с другом, и это может занять много времени при тестировании множества файлов, поэтому обычно лучше использовать справочные папки и указать, какие файлы следует сравнивать друг с другом (одинаковое количество файлов), сравнение отпечатков пальцев будет быстрее по крайней мере на 4х, чем без ссылочных папок). music_comparison_checkbox_tooltip = Ищет похожие музыкальные файлы с помощью ИИ, использующего машинное обучение для удаления скобок из фраз. Например, если эта опция включена, следующие файлы будут считаться дубликатами: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = С учётом регистра duplicate_case_sensitive_name_tooltip = При включённой опции записи группируются, только если у них полностью совпадают имена с точностью до каждого символа. Например, «ХИТ Дискотека» не совпадёт с «хит дискотека». При отключённой опции записи группируются вне зависимости от того, заглавные или строчные буквы использовались при написании. Например, «ХИТ Дискотека», «хит дискотека», «хИт ДиСкОтЕКа» будут эквивалентны duplicate_mode_size_name_combo_box = Размер и имя duplicate_mode_name_combo_box = Имя duplicate_mode_size_combo_box = Размер duplicate_mode_hash_combo_box = Хэш duplicate_hash_type_tooltip = В программе Czkawka можно использовать один из трёх алгоритмов хэширования: Blake3 — криптографическая хэш-функция. Используется по умолчанию, поскольку очень быстрый. CRC32 — простая хэш-функция. Ещё быстрее, чем Blake3, но возможны очень редкие совпадения хэшей неидентичных файлов. XXH3 — функция, похожая по производительности и надёжности хэша на Blake3, но не являющаяся криптографической, поэтому её можно использовать вместо Blake3. duplicate_check_method_tooltip = На данный момент Czkawka предлагает три метода поиска дубликатов: Имя — ищет файлы с одинаковыми именами. Размер — ищет файлы одинакового размера. Хэш — ищет файлы с одинаковым содержимым. Этот режим хэширует файл, а затем сравнивает хэш для поиска дубликатов. Этот режим является самым надёжным способом поиска. Приложение активно использует кэш, поэтому второе и последующие сканирования одних и тех же данных должны быть намного быстрее, чем первое. image_hash_size_tooltip = Каждое проверяемое изображение производит специальный хэш, который можно сравнить друг с другом, и небольшая разница между ними означает, что эти изображения аналогичны. 8 размер хэша достаточно хорош, чтобы найти изображения, которые немного похожи на оригинал. С большим набором изображений (>1000), это приведет к большому количеству ложных срабатываний, поэтому в данном случае я рекомендую использовать больший размер хэша. 16 - это размер хэша по умолчанию, который является хорошим компромиссом между нахождением даже немного похожих изображений и наличием лишь небольшого количества хэш-коллизий. 32 и 64 хэши находят только очень похожие изображения, но не должны иметь ложных срабатываний (может быть, за исключением некоторых изображений с альфа-каналом). image_resize_filter_tooltip = Чтобы вычислить хэш изображения, библиотека должна сначала его перемасштабировать. В зависимости от выбранного алгоритма полученное изображение, используемое при хэшировании, может выглядеть немного другим. Самый быстрый алгоритм с низким качеством — это метод ближайших соседей, Nearest. Он включён по умолчанию, потому при размере хэша 16x16 плохое качество не замечается. Если размер хэша 8x8, рекомендуется любой алгоритм, кроме Nearest, чтобы лучше отличать похожие изображения в группах. image_hash_alg_tooltip = Пользователи могут выбрать один из многих алгоритмов вычисления хэша. Каждый имеет сильные и слабые точки и иногда даёт более качественные и иногда хуже результаты для разных изображений. Поэтому для определения наилучшего из вас, требуется ручное тестирование. big_files_mode_combobox_tooltip = Поиск наименьших/наибольших файлов big_files_mode_label = Проверенные файлы big_files_mode_smallest_combo_box = Самый маленький big_files_mode_biggest_combo_box = Крупнейший main_notebook_duplicates = Файлы-дубликаты main_notebook_empty_directories = Пустые папки main_notebook_big_files = Большие файлы main_notebook_empty_files = Пустые файлы main_notebook_temporary = Временные файлы main_notebook_similar_images = Похожие изображения main_notebook_similar_videos = Похожие видео main_notebook_same_music = Музыкальные дубликаты main_notebook_symlinks = Битые симв. ссылки main_notebook_broken_files = Битые файлы main_notebook_bad_extensions = Плохие расширения main_tree_view_column_file_name = Имя файла main_tree_view_column_folder_name = Имя папки main_tree_view_column_path = Путь main_tree_view_column_modification = Дата изменения main_tree_view_column_size = Размер main_tree_view_column_similarity = Сходство main_tree_view_column_dimensions = Размеры main_tree_view_column_title = Заголовок main_tree_view_column_artist = Исполнитель main_tree_view_column_year = Год main_tree_view_column_bitrate = Битрейт main_tree_view_column_length = Длительность main_tree_view_column_genre = Жанр main_tree_view_column_symlink_file_name = Имя файла символьной ссылки main_tree_view_column_symlink_folder = Папка Symlink main_tree_view_column_destination_path = Путь назначения main_tree_view_column_type_of_error = Тип ошибки main_tree_view_column_current_extension = Текущее расширение main_tree_view_column_proper_extensions = Правильное расширение main_tree_view_column_fps = FPS main_tree_view_column_codec = Кодек main_label_check_method = Метод проверки main_label_hash_type = Тип хэша main_label_hash_size = Размер хэша main_label_size_bytes = Размер (байт) main_label_min_size = Мин main_label_max_size = Макс main_label_shown_files = Количество отображаемых файлов main_label_resize_algorithm = Алгоритм масштабирования main_label_similarity = Сходство{ " " } main_check_box_broken_files_audio = Звук main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Архивировать main_check_box_broken_files_image = Изображение main_check_box_broken_files_video = Видео main_check_box_broken_files_video_tooltip = Использует ffmpeg/ffprobe для проверки видеофайлов. Очень медленно и может обнаруживать педантичные ошибки, даже если файл воспроизводится нормально. check_button_general_same_size = Игнорировать одинаковый размер check_button_general_same_size_tooltip = Игнорировать файлы с одинаковым размером в результатах - обычно это 1:1 дубликаты main_label_size_bytes_tooltip = Размер файлов, которые будут просканированы # Upper window upper_tree_view_included_folder_column_title = Папки для поиска upper_tree_view_included_reference_column_title = Содержит оригиналы upper_recursive_button = В подпапках upper_recursive_button_tooltip = При включённой опции будут также искаться файлы, не находящиеся непосредственно в корне выбранной папки, т. е. в других подпапках данной папки и их подпапках. upper_manual_add_included_button = Прописать вручную upper_add_included_button = Добавить upper_remove_included_button = Удалить upper_manual_add_excluded_button = Ручное добавление upper_add_excluded_button = Добавить upper_remove_excluded_button = Удалить upper_manual_add_included_button_tooltip = Добавьте имя каталога для поиска вручную. Чтобы добавить несколько путей одновременно, разделите их на ; /home/roman;/home/rozkaz добавит два каталога /home/roman и /home/rozkaz upper_add_included_button_tooltip = Добавить новый каталог для поиска. upper_remove_included_button_tooltip = Исключить каталог из поиска. upper_manual_add_excluded_button_tooltip = Добавьте вручную исключенное имя каталога. Чтобы добавить несколько путей одновременно, разделите их на ; /home/roman;/home/krokiet добавит два каталога /home/roman и /home/keokiet upper_add_excluded_button_tooltip = Добавить каталог, исключаемый из поиска. upper_remove_excluded_button_tooltip = Убрать каталог из исключенных. upper_notebook_items_configuration = Параметры поиска upper_notebook_excluded_directories = Исключенные пути upper_notebook_included_directories = Включенные пути upper_allowed_extensions_tooltip = Включаемые расширения должны быть разделены запятыми (по умолчанию ищутся файлы с любыми расширениями). Макросы IMAGE, VIDEO, MUSIC, TEXT добавляют сразу несколько расширений. Пример использования: «.exe, IMAGE, VIDEO, .rar, 7z» — это означает, что будут сканироваться файлы изображений (напр. jpg, png), видео (напр. avi, mp4), exe, rar и 7z. upper_excluded_extensions_tooltip = Список отключенных файлов, которые будут игнорироваться в сканировании. При использовании разрешенных и отключенных расширений этот файл имеет более высокий приоритет, поэтому файл не будет проверяться. upper_excluded_items_tooltip = Исключенные элементы должны содержать * wildcard и должны быть разделены запятыми. Это медленнее, чем Excluded Paths, поэтому используйте его осторожно. upper_excluded_items = Исключённые элементы: upper_allowed_extensions = Допустимые расширения: upper_excluded_extensions = Отключенные расширения: # Popovers popover_select_all = Выбрать все popover_unselect_all = Снять выделение popover_reverse = Обратить выделение popover_select_all_except_shortest_path = Выбрать все, кроме кратчайшего пути popover_select_all_except_longest_path = Выбрать все, кроме самого длинного пути popover_select_all_except_oldest = Выделить все, кроме старых popover_select_all_except_newest = Выделить все, кроме новых popover_select_one_oldest = Выбрать один старый popover_select_one_newest = Выбрать один новый popover_select_custom = Выбрать произвольный popover_unselect_custom = Снять выбор popover_select_all_images_except_biggest = Выделить все, кроме наибольшего popover_select_all_images_except_smallest = Выделить все, кроме наименьшего popover_custom_path_check_button_entry_tooltip = Выбор записей на основе пути. Пример: /home/pimpek/rzecz.txt можно найти с помощью /home/pim* popover_custom_name_check_button_entry_tooltip = Выбор записей по именам файлов. Пример: /usr/ping/pong.txt можно найти с помощью *ong* popover_custom_regex_check_button_entry_tooltip = Выбор записей с помощью регулярного выражения. В этом режиме искомый текст представляет собой путь с именем. Пример: /usr/bin/ziemniak.txt можно найти с помощью выражения /ziem[a-z]+ По умолчанию используется синтаксис регулярных выражений Rust. Подробнее об этом можно прочитать здесь: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Включает регистрозависимый поиск. При отключённой опции «/home/*» будет соответствовать как «/home/roman», так и «/HoMe/roman». popover_custom_not_all_check_button_tooltip = Запрет выбора всех записей в группе. Эта опция включена по умолчанию, потому что в большинстве ситуаций вам не надо удалять и оригиналы, и дубликаты — обычно оставляют хотя бы один файл. ВНИМАНИЕ. Этот параметр не работает, если вы уже вручную выбрали все результаты в группе. popover_custom_regex_path_label = Путь popover_custom_regex_name_label = Имя popover_custom_regex_regex_label = Путь с рег. выраж. + имя popover_custom_case_sensitive_check_button = С учётом регистра popover_custom_all_in_group_label = Не выбирать все записи в группе popover_custom_mode_unselect = Снять выбор popover_custom_mode_select = Выбрать произвольный popover_sort_file_name = Имя файла popover_sort_folder_name = Название папки popover_sort_full_name = Полное имя popover_sort_size = Размер popover_sort_selection = Выбранные объекты popover_invalid_regex = Некорректное регулярное выражение popover_valid_regex = Корректное регулярное выражение # Bottom buttons bottom_search_button = Искать bottom_select_button = Выбрать bottom_delete_button = Удалить bottom_save_button = Сохранить bottom_symlink_button = Симв. ссылка bottom_hardlink_button = Жёст. ссылка bottom_move_button = Переместить bottom_sort_button = Сортировать bottom_compare_button = Сравнить bottom_search_button_tooltip = Начать поиск bottom_select_button_tooltip = Выберите записи. Только выбранные файлы/папки будут доступны для последующей обработки. bottom_delete_button_tooltip = Удалить выбранные файлы/папки. bottom_save_button_tooltip = Сохранить данные о поиске в файл bottom_symlink_button_tooltip = Создать символьные ссылки. Работает, только когда выбрано не менее двух результатов в группе. Первый результат оставляется, а второй и последующие делаются символьными ссылками на первый. bottom_hardlink_button_tooltip = Создать жёсткие ссылки. Работает, только когда выбрано не менее двух результатов в группе. Первый результат оставляется, а второй и последующие делаются жёсткими ссылками на первый. bottom_hardlink_button_not_available_tooltip = Создание жестких ссылок. Кнопка отключена, так как невозможно создать жёсткие ссылки. Связи работают только с правами администратора в Windows, поэтому не забудьте запустить приложение от имени администратора. Если приложение уже работает с такими привилегиями, проверьте аналогичные проблемы на Github. bottom_move_button_tooltip = Перемещение файлов в выбранный каталог. Копирует все файлы в папку без сохранения структуры дерева каталогов. При попытке переместить два файла с одинаковым именем в одну и ту же папку второй не будет перемещён и появится сообщение об ошибке. bottom_sort_button_tooltip = Сортировка файлов/папок по выбранному методу. bottom_compare_button_tooltip = Сравнить изображения в группе. bottom_show_errors_tooltip = Показать/скрыть нижнюю текстовую панель. bottom_show_upper_notebook_tooltip = Показать/скрыть верхнюю панель блокнота. # Progress Window progress_stop_button = Остановить progress_stop_additional_message = Стоп запрошен # About Window about_repository_button_tooltip = Ссылка на страницу репозитория с исходным кодом. about_donation_button_tooltip = Ссылка на страницу пожертвований. about_instruction_button_tooltip = Ссылка на страницу инструкций. about_translation_button_tooltip = Ссылка на страницу Crowdin с переводами приложений. Официально поддерживаются английский и польский языки. about_repository_button = Репозиторий about_donation_button = Пожертвование about_instruction_button = Инструкция about_translation_button = Перевод # Header header_setting_button_tooltip = Открыть окно настроек. header_about_button_tooltip = Открыть окно с информацией о приложении. # Settings ## General settings_number_of_threads = Количество использованных потоков settings_number_of_threads_tooltip = Количество используемых потоков. Установите 0, чтобы использовать все доступные потоки. settings_use_rust_preview = Использовать внешние библиотеки вместо gtk для загрузки предпросмотра settings_use_rust_preview_tooltip = Использование превью gtk иногда будет быстрее и поддерживать больше форматов, но иногда это может быть и наоборот. Если у вас возникли проблемы с загрузкой предпросмотра, вы можете попробовать изменить эту настройку. На не-linux системах рекомендуется использовать эту опцию, потому что gtk-pixbuf не всегда доступен там, поэтому отключение этой опции не будет загружать превью некоторых изображений. settings_label_restart = Вам нужно перезапустить приложение, чтобы применить настройки! settings_ignore_other_filesystems = Игнорировать другие файловые системы (только Linux) settings_ignore_other_filesystems_tooltip = игнорирует файлы, которые находятся в той же файловой системе, что и поисковые директории. Работает так же, как и команда 'xdev' в команде 'находить' settings_save_at_exit_button_tooltip = Сохранить конфигурацию в файл при закрытии приложения. settings_load_at_start_button_tooltip = Загрузить конфигурацию из файла при открытии приложения. Если не включено, будут использоваться настройки по умолчанию. settings_confirm_deletion_button_tooltip = Показать окно подтверждения при нажатии на кнопку удаления. settings_confirm_link_button_tooltip = Показывать окно подтверждения при нажатии кнопки жесткой/символической ссылки. settings_confirm_group_deletion_button_tooltip = Показывать окно предупреждения при попытке удалить все записи из группы. settings_show_text_view_button_tooltip = Показать текстовую панель в нижней части интерфейса. settings_use_cache_button_tooltip = Использовать файловый кэш. settings_save_also_as_json_button_tooltip = Сохранять кэш в формат JSON (человекочитаемый). Его содержимое можно изменять. Кэш из этого файла будет автоматически прочитан приложением, если бинарный кэш (с расширением bin) отсутствует. settings_use_trash_button_tooltip = Перемещать файлы в корзину вместо их безвозвратного удаления. settings_language_label_tooltip = Язык пользовательского интерфейса. settings_save_at_exit_button = Сохранять конфигурацию при закрытии приложения settings_load_at_start_button = Загружать конфигурацию при открытии приложения settings_confirm_deletion_button = Показывать подтверждение при удалении любых файлов settings_confirm_link_button = Показывать окно подтверждения при создании жёстких или символьных ссылок на файлы settings_confirm_group_deletion_button = Показывать подтверждение при удалении всех файлов в группе settings_show_text_view_button = Показывать нижнюю текстовую панель settings_use_cache_button = Использовать кэш settings_save_also_as_json_button = Также сохранять кэш в файл JSON settings_use_trash_button = Перемещать удаляемые файлы в корзину settings_language_label = Язык settings_multiple_delete_outdated_cache_checkbutton = Автоматически удалять устаревшие записи кэша settings_multiple_delete_outdated_cache_checkbutton_tooltip = Удалить устаревшие результаты кеша, указывающие на несуществующие файлы. Когда опция включена, приложение проверяет при загрузке записей, указывают ли они на доступные файлы (недостающие файлы игнорируются). Отключение этой опции помогает при сканировании файлов на внешних носителях, чтобы информация о них не была очищена при следующем сканировании. При наличии сотен тысяч записей в кэше рекомендуется включить эту опцию, чтобы ускорить загрузку и сохранение кэша в начале и конце сканирования. settings_notebook_general = Общие настройки settings_notebook_duplicates = Дубликаты settings_notebook_images = Похожие изображения settings_notebook_videos = Похожие видео ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Показывать предварительный просмотр справа (при выборе файла изображения). settings_multiple_image_preview_checkbutton = Показывать предпросмотр изображения settings_multiple_clear_cache_button_tooltip = Очистка устаревших записей кэша вручную. Следует использовать только в том случае, если автоматическая очистка отключена. settings_multiple_clear_cache_button = Удалить устаревшие результаты из кэша. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Скрыть все файлы, кроме первого, если все они указывают на одни и те же данные (связаны жёсткой ссылкой). Пример: если (на диске) семь файлов связаны жёсткой ссылкой с определёнными данными, а ещё один файл содержат те же данные, но на другом inode, то в средстве поиска дубликатов будут показаны только этот последний уникальный файл и один файл из являющихся жёсткой ссылкой. settings_duplicates_minimal_size_entry_tooltip = Установить минимальный размер кэшируемого файла. Выбор меньшего значения приведёт к созданию большего количества записей. Это ускорит поиск, но замедлит загрузку/сохранение кэша. settings_duplicates_prehash_checkbutton_tooltip = Включает кэширование предварительного хэша (предхэша), вычисляемого из небольшой части файла, что позволяет быстрее исключать из анализа отличающиеся файлы. По умолчанию отключено, так как в некоторых ситуациях может замедлять работу. Настоятельно рекомендуется использовать его при сканировании сотен тысяч или миллионов файлов, так как это может ускорить поиск в разы. settings_duplicates_prehash_minimal_entry_tooltip = Минимальный размер кэшируемого элемента. settings_duplicates_hide_hard_link_button = Скрыть жесткие ссылки settings_duplicates_prehash_checkbutton = Кэшировать предхэш settings_duplicates_minimal_size_cache_label = Минимальный размер (байт) кэшируемых файлов settings_duplicates_minimal_size_cache_prehash_label = Минимальный размер (байт) файлов для кэша предхэша ## Saving/Loading settings settings_saving_button_tooltip = Сохранить текущую конфигурацию настроек в файл. settings_loading_button_tooltip = Загрузить настройки из файла и заменить ими текущую конфигурацию. settings_reset_button_tooltip = Сбросить текущую конфигурацию на конфигурацию по умолчанию. settings_saving_button = Сохранить конфигурацию settings_loading_button = Загрузить конфигурацию settings_reset_button = Сбросить настройки ## Opening cache/config folders settings_folder_cache_open_tooltip = Открыть папку, в которой хранятся текстовые файлы кеша. Изменение файлов кэша может привести к отображению неверных результатов, однако изменение пути может сэкономить время при перемещении большого количества файлов в другое место. Вы можете копировать эти файлы между компьютерами, чтобы сэкономить время на повторном сканировании файлов (конечно, если они имеют схожую структуру каталогов). В случае возникновения проблем с кэшем эти файлы можно удалить. Приложение автоматически пересоздаст их. settings_folder_settings_open_tooltip = Открывает папку, в которой хранится конфигурация Czkawka. ВНИМАНИЕ. Ручное изменение конфигурации может нарушить функционирование программы. settings_folder_cache_open = Открыть папку кэша settings_folder_settings_open = Открыть папку настроек # Compute results compute_stopped_by_user = Поиск был остановлен пользователем compute_found_duplicates_hash_size = Найдено { $number_files } дубликатов в { $number_groups } группах, которые заняли { $size } за { $time } compute_found_duplicates_name = Найдено { $number_files } дубликатов в { $number_groups } группах за { $time } compute_found_empty_folders = Найдено { $number_files } пустых папки в { $time } compute_found_empty_files = Найдено { $number_files } пустых файла в { $time } compute_found_big_files = Найдено { $number_files } больших файлов в { $time } compute_found_temporary_files = Найдено { $number_files } временных файла в { $time } compute_found_images = Найдено { $number_files } подобных изображения в { $number_groups } группах за { $time } compute_found_videos = Найдено { $number_files } похожих видео в { $number_groups } группах за { $time } compute_found_music = Найдено { $number_files } схожих музыкальных файлов в { $number_groups } группах за { $time } compute_found_invalid_symlinks = Найдено { $number_files } невалидных symbolic ссылок за { $time } compute_found_broken_files = Найдено { $number_files } сломанных файлов в { $time } compute_found_bad_extensions = Найдено { $number_files } файлов с недопустимыми расширениями в { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Просканирован { $file_number } файл *[other] Просканированы { $file_number } файлов } progress_scanning_extension_of_files = Проверено расширение { $file_checked }/{ $all_files } файла progress_scanning_broken_files = Проверено { $file_checked }/{ $all_files } файл ({ $data_checked }/{ $all_data }) progress_scanning_video = Хэш { $file_checked }/{ $all_files } видео progress_creating_video_thumbnails = Созданы эскизы видео { $file_checked }/{ $all_files } progress_scanning_image = Хэш { $file_checked }/{ $all_files } изображения ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Хэш изображений по сравнению { $file_checked }/{ $all_files } progress_scanning_music_tags_end = По сравнению тегов музыкального файла { $file_checked }/{ $all_files } progress_scanning_music_tags = Чтение тегов { $file_checked }/{ $all_files } музыкального файла progress_scanning_music_content_end = По сравнению с музыкальным файлом { $file_checked }/{ $all_files } progress_scanning_music_content = Вычисляется отпечаток звука { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Просканирована { $folder_number } папка *[other] Просканированы { $folder_number } папок } progress_scanning_size = Отсканированный размер файла { $file_number } progress_scanning_size_name = Отсканированное имя и размер файла { $file_number } progress_scanning_name = Отсканированное имя файла { $file_number } progress_analyzed_partial_hash = Частичный хэш { $file_checked }/{ $all_files } файлов ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Полный хэш { $file_checked }/{ $all_files } файлов ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Загрузка кэша prehash progress_prehash_cache_saving = Сохранение кэша prehash progress_hash_cache_loading = Загрузка хеш-кэша progress_hash_cache_saving = Сохранение хэша progress_cache_loading = Загрузка кэша progress_cache_saving = Сохранение кэша progress_current_stage = Текущий этап:{ " " } progress_all_stages = Все этапы:{ " " } # Saving loading saving_loading_saving_success = Конфигурация сохранена в файл { $name }. saving_loading_saving_failure = Не удалось сохранить данные конфигурации в файл { $name }, причина { $reason }. saving_loading_reset_configuration = Текущая конфигурация была удалена. saving_loading_loading_success = Настройки приложения корректно загружены. saving_loading_failed_to_create_config_file = Не удалось создать файл конфигурации «{ $path }». Причина: «{ $reason }». saving_loading_failed_to_read_config_file = Невозможно загрузить конфигурацию из «{ $path }», так как или такого файла не существует, или это не файл. saving_loading_failed_to_read_data_from_file = Невозможно прочитать данные из файла «{ $path }». Причина: «{ $reason }». # Other selected_all_reference_folders = Невозможно начать поиск, когда все каталоги установлены как папки со ссылками searching_for_data = Поиск данных может занять некоторое время — пожалуйста, подождите... text_view_messages = СООБЩЕНИЯ text_view_warnings = ПРЕДУПРЕЖДЕНИЯ text_view_errors = ОШИБКИ about_window_motto = Эта программа бесплатна для использования и всегда будет оставаться таковой. krokiet_new_app = Чкавка находится в режиме технического обслуживания, что означает, что будут исправлены только критические ошибки, и новые возможности не будут добавлены. Для новых функций ознакомьтесь с новым приложением Krokiet, которое является более стабильным и эффективным, и всё ещё находится в стадии активной разработки. # Various dialog dialogs_ask_next_time = Всегда спрашивать symlink_failed = Не удалось привязать { $name } к { $target }, причина { $reason } delete_title_dialog = Подтверждение удаления delete_question_label = Вы уверены, что хотите удалить файлы? delete_all_files_in_group_title = Подтверждение удаления всех файлов в группе delete_all_files_in_group_label1 = В некоторых группах были выбраны все записи. delete_all_files_in_group_label2 = Вы уверены, что хотите удалить их? delete_items_label = Будет удалено файлов: { $items }. delete_items_groups_label = Будет удалено файлов: { $items } (групп: { $groups }). hardlink_failed = Не удалось привязать { $name } к { $target }, причина { $reason } hard_sym_invalid_selection_title_dialog = Неверный выбор в некоторых группах hard_sym_invalid_selection_label_1 = В некоторых группах выбрана только одна запись — они будут проигнорированы. hard_sym_invalid_selection_label_2 = Чтобы жёстко или символьно связать эти файлы, необходимо выбрать как минимум два результата в группе. hard_sym_invalid_selection_label_3 = Первый в группе признан в качестве оригинала и не будет изменён, но второй и последующие модифицированы. hard_sym_link_title_dialog = Подтверждение связывания ссылкой hard_sym_link_label = Вы уверены, что хотите связать эти файлы? move_folder_failed = Не удалось переместить папку { $name }. Причина: { $reason } move_file_failed = Не удалось переместить файл { $name }. Причина: { $reason } move_files_title_dialog = Выберите папку, в которую вы хотите переместить дублирующиеся файлы move_files_choose_more_than_1_path = Можно выбрать только один путь для копирования дубликатов файлов, но выбрано { $path_number }. move_stats = Удалось переместить без ошибок элементов: { $num_files }/{ $all_files } save_results_to_file = Результаты сохранены в txt и json файлы в папку "{ $name }". search_not_choosing_any_music = ОШИБКА: Необходимо выбрать как минимум один флажок с типами поиска музыки. search_not_choosing_any_broken_files = ОШИБКА: Вы должны выбрать хотя бы один флажок с типом проверенных ошибочных файлов. include_folders_dialog_title = Папки для включения exclude_folders_dialog_title = Папки для исключения include_manually_directories_dialog_title = Добавить папку вручную cache_properly_cleared = Кэш успешно очищен cache_clear_duplicates_title = Очистка кэша дубликатов cache_clear_similar_images_title = Очистка кэша похожих изображений cache_clear_similar_videos_title = Очистка кэша похожих видео cache_clear_message_label_1 = Убрать из кэша устаревшие записи? cache_clear_message_label_2 = Это действие удалит все записи кэша, указывающие на недоступные файлы. cache_clear_message_label_3 = Это может немного ускорить загрузку/сохранение кэша. cache_clear_message_label_4 = ВНИМАНИЕ. Это действие удалит все кэшированные данные с отключённых внешних дисков. Хэши для файлов на этих носителях будет необходимо сгенерировать заново. # Show preview preview_image_resize_failure = Не удалось изменить размер изображения { $name }. preview_image_opening_failure = Не удалось открыть изображение { $name }. Причина: { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Группа { $current_group }/{ $all_groups } (изображений: { $images_in_group }) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/sv-SE/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Inställningar window_main_title = Czkawka (Pipade) window_progress_title = Scannar window_compare_images = Jämför bilder # General general_ok_button = Ok general_close_button = Stäng # Krokiet info dialog krokiet_info_title = Införande av Krokiet – Ny version av Czkawka krokiet_info_message = Stödet är den nya, förbättrade, snabbare och mer pålitliga versionen av Czkawka GTK GUI! 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. 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. Prova det och se skillnaden! 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. 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. # Main window music_title_checkbox = Titel music_artist_checkbox = Kunstnär music_year_checkbox = År music_bitrate_checkbox = Bitratemarkering music_genre_checkbox = Genrė music_length_checkbox = Längd music_comparison_checkbox = Ungefärlig jämförelse music_checking_by_tags = Taggar music_checking_by_content = Innehåll same_music_seconds_label = Minsta fragment sekund varaktighet same_music_similarity_label = Maximal skillnad music_compare_only_in_title_group = Jämför inom grupper med liknande titlar music_compare_only_in_title_group_tooltip = När den är aktiverad grupperas filerna efter titel och jämförs sedan med varandra. Med 10000 filer, i stället nästan 100 miljoner jämförelser brukar det finnas runt 20000 jämförelser. same_music_tooltip = Sökning efter liknande musikfiler genom dess innehåll kan konfigureras genom att ställa in: - Minsta fragmenttid efter vilken musikfiler kan identifieras som liknande - Maximal skillnad mellan två testade fragment Nyckeln till bra resultat är att hitta förnuftiga kombinationer av dessa parametrar, för tillhandahållen. 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. En tid på 20-talet och en maximal skillnad på 6,0, å andra sidan, fungerar bra för att hitta remixer/live-versioner etc. 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). music_comparison_checkbox_tooltip = 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: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = Skiftlägeskänslig duplicate_case_sensitive_name_tooltip = När detta är aktiverat spelar gruppen bara in när de har exakt samma namn t.ex. Żołd <-> Żołd Inaktivera sådana alternativ kommer gruppnamn utan att kontrollera om varje bokstav är samma storlek t.ex. żoŁD <-> Żołd duplicate_mode_size_name_combo_box = Storlek och namn duplicate_mode_name_combo_box = Namn duplicate_mode_size_combo_box = Storlek duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka erbjuder 3 typer av hash: Blake3 - kryptografisk hash-funktion. Detta är standard eftersom det är mycket snabbt. CRC32 - enkel hash-funktion. Detta bör vara snabbare än Blake3, men kan mycket sällan ha några kollisioner. XXH3 - mycket lik i prestanda och hashkvalitet till Blake3 (men icke-kryptografisk). Så, sådana lägen kan lätt bytas ut. duplicate_check_method_tooltip = För tillfället erbjuder Czkawka tre typer av metoder för att hitta dubbletter av: Namn - Hittar filer som har samma namn. Storlek - Hittar filer som har samma storlek. 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. image_hash_size_tooltip = 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. 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. 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. 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). image_resize_filter_tooltip = För att beräkna hash av bilden, måste biblioteket först ändra storlek på den. Beroende på vald algoritm kommer den resulterande bilden som används för att beräkna hash att se lite annorlunda ut. 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. Med 8x8 hashstorlek rekommenderas att använda en annan algoritm än Närmaste för att få bättre grupper av bilder. image_hash_alg_tooltip = Användare kan välja mellan en av många algoritmer för att beräkna hash. Var och en har både starka och svagare punkter och ger ibland bättre och ibland sämre resultat för olika bilder. Så, för att bestämma den bästa för dig krävs manuell testning. big_files_mode_combobox_tooltip = Gör det möjligt att söka efter minsta/största filer big_files_mode_label = Markerade filer big_files_mode_smallest_combo_box = Den minsta big_files_mode_biggest_combo_box = Den största main_notebook_duplicates = Duplicera filer main_notebook_empty_directories = Tomma kataloger main_notebook_big_files = Stora filer main_notebook_empty_files = Tomma filer main_notebook_temporary = Tillfälliga filer main_notebook_similar_images = Liknande bilder main_notebook_similar_videos = Liknande videor main_notebook_same_music = Musik Duplicerar main_notebook_symlinks = Ogiltiga Symlinks main_notebook_broken_files = Trasiga filer main_notebook_bad_extensions = Dåliga tillägg main_tree_view_column_file_name = Filnamn main_tree_view_column_folder_name = Mappens namn main_tree_view_column_path = Sökväg main_tree_view_column_modification = Senast ändrad main_tree_view_column_size = Storlek main_tree_view_column_similarity = Likhet main_tree_view_column_dimensions = Dimensioner main_tree_view_column_title = Titel main_tree_view_column_artist = Künstler main_tree_view_column_year = År main_tree_view_column_bitrate = Bitratencion main_tree_view_column_length = Längd main_tree_view_column_genre = Genrer main_tree_view_column_symlink_file_name = Symlink filnamn main_tree_view_column_symlink_folder = Symlink mapp main_tree_view_column_destination_path = Målsökvägen main_tree_view_column_type_of_error = Typ av fel main_tree_view_column_current_extension = Nuvarande tillägg main_tree_view_column_proper_extensions = Rätt tillägg main_tree_view_column_fps = FPS main_tree_view_column_codec = Kodek main_label_check_method = Kontrollera metod main_label_hash_type = Hash typ main_label_hash_size = Hashstorlek main_label_size_bytes = Storlek (bytes) main_label_min_size = Min main_label_max_size = Max main_label_shown_files = Antal visade filer main_label_resize_algorithm = Ändra storlek på algoritm main_label_similarity = Similarity{ " " } main_check_box_broken_files_audio = Ljud main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Arkiv main_check_box_broken_files_image = Bild main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Ignorera samma storlek check_button_general_same_size_tooltip = Ignorera filer med samma storlek i resultat - vanligtvis är dessa 1:1 dubbletter main_label_size_bytes_tooltip = Storlek på filer som kommer att användas vid skanning # Upper window upper_tree_view_included_folder_column_title = Mappar att söka upper_tree_view_included_reference_column_title = Referens mappar upper_recursive_button = Rekursiv upper_recursive_button_tooltip = Om vald, sök även efter filer som inte placeras direkt under valda mappar. upper_manual_add_included_button = Manuell Lägg till upper_add_included_button = Lägg till upper_remove_included_button = Ta bort upper_manual_add_excluded_button = Manuell Lägg till upper_add_excluded_button = Lägg till upper_remove_excluded_button = Ta bort upper_manual_add_included_button_tooltip = Lägg till katalognamn för att söka för hand. För att lägga till flera sökvägar samtidigt, separera dem med ; /home/roman;/home/rozkaz lägger till två kataloger /home/roman och /home/rozkaz upper_add_included_button_tooltip = Lägg till ny katalog att söka. upper_remove_included_button_tooltip = Ta bort katalog från sökning. upper_manual_add_excluded_button_tooltip = Lägg till exkluderat katalognamn för hand. För att lägga till flera sökvägar samtidigt, separera dem med ; /home/roman;/home/krokiet kommer att lägga till två kataloger /home/roman och /home/keokiet upper_add_excluded_button_tooltip = Lägg till katalog som ska exkluderas i sökningen. upper_remove_excluded_button_tooltip = Ta bort katalog från utesluten. upper_notebook_items_configuration = Objekt konfiguration upper_notebook_excluded_directories = Exkluderade Sökvägar upper_notebook_included_directories = Inkluderade Sökvägar upper_allowed_extensions_tooltip = Tillåtna tillägg måste separeras med kommatecken (som standard alla är tillgängliga). Följande makron som lägger till flera tillägg samtidigt, finns också: IMAGE, VIDEO, MUSIC, TEXT. 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. upper_excluded_extensions_tooltip = Lista över inaktiverade filer som kommer att ignoreras i skanning. Vid användning av både tillåtna och inaktiverade tillägg har denna högre prioritet, så filen kommer inte att kontrolleras. upper_excluded_items_tooltip = Uteslutna objekt måste innehålla * wildcard och ska separeras med komma. Detta är långsammare än Exkluderade Sökvägar, så använd det försiktigt. upper_excluded_items = Exkluderade objekt: upper_allowed_extensions = Tillåtna tillägg: upper_excluded_extensions = Inaktiverade tillägg: # Popovers popover_select_all = Radera popover_unselect_all = Avmarkera alla popover_reverse = Omvänd markering popover_select_all_except_shortest_path = Välj allt förutom den kortaste vägen popover_select_all_except_longest_path = Välj allt undantaget längst väg popover_select_all_except_oldest = Välj alla utom äldsta popover_select_all_except_newest = Välj alla utom nyaste popover_select_one_oldest = Välj en äldsta popover_select_one_newest = Välj en nyaste popover_select_custom = Välj anpassad popover_unselect_custom = Avmarkera anpassade popover_select_all_images_except_biggest = Välj alla utom största popover_select_all_images_except_smallest = Välj alla utom minsta popover_custom_path_check_button_entry_tooltip = Välj poster efter sökväg. Exempel användning: /home/pimpek/rzecz.txt hittas med /home/pim* popover_custom_name_check_button_entry_tooltip = Välj poster efter filnamn. Exempel användning: /usr/ping/pong.txt finns med *ong* popover_custom_regex_check_button_entry_tooltip = Välj poster efter specificerad Regex. Med detta läge är sökord sökväg med namn. Exempel användning: /usr/bin/ziemniak. xt kan hittas med /ziem[a-z]+ Detta använder Rust regex-implementationen. Du kan läsa mer om det här: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Aktiverar skiftlägeskänslig detektion. När du inaktiverat /home/* hittar du både /HoMe/roman och /home/roman. popover_custom_not_all_check_button_tooltip = Förhindrar att alla poster väljs i grupp. 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. VARNING: Den här inställningen fungerar inte om du redan manuellt har valt alla resultat i en grupp. popover_custom_regex_path_label = Sökväg popover_custom_regex_name_label = Namn popover_custom_regex_regex_label = Regex sökväg + namn popover_custom_case_sensitive_check_button = Skiftlägeskänslighet popover_custom_all_in_group_label = Välj inte alla poster i gruppen popover_custom_mode_unselect = Avmarkera anpassad popover_custom_mode_select = Välj anpassad popover_sort_file_name = Filnamn popover_sort_folder_name = Mapp namn popover_sort_full_name = Fullständigt namn popover_sort_size = Storlek popover_sort_selection = Markerat popover_invalid_regex = Regex är ogiltigt popover_valid_regex = Regex är giltigt # Bottom buttons bottom_search_button = Sökning bottom_select_button = Välj bottom_delete_button = Radera bottom_save_button = Spara bottom_symlink_button = Symlink bottom_hardlink_button = Hardlink bottom_move_button = Flytta bottom_sort_button = Sortera bottom_compare_button = Jämför bottom_search_button_tooltip = Starta sökning bottom_select_button_tooltip = Välj poster. Endast valda filer/mappar kan senare bearbetas. bottom_delete_button_tooltip = Ta bort markerade filer/mappar. bottom_save_button_tooltip = Spara data om sökning till fil bottom_symlink_button_tooltip = Skapa symboliska länkar. Fungerar endast när minst två resultat i en grupp väljs. Först är oförändrad och andra och senare är symanknutna till först. bottom_hardlink_button_tooltip = Skapa hardlinks. Fungerar endast när minst två resultat i en grupp är valda. Först är oförändrad och andra och senare är hårt länkade till först. bottom_hardlink_button_not_available_tooltip = Skapa hardlinks. Knappen är inaktiverad, eftersom hardlinks inte kan skapas. Hårdlänkar fungerar bara med administratörsrättigheter i Windows, så se till att köra appen som administratör. Om appen redan fungerar med sådana rättigheter kontrollera liknande problem på Github. bottom_move_button_tooltip = Flyttar filer till vald katalog. Det kopierar alla filer till katalogen utan att bevara katalogträdet. När du försöker flytta två filer med identiskt namn till mappen kommer det andra att misslyckas och visa fel. bottom_sort_button_tooltip = Sortera filer/mappar enligt vald metod. bottom_compare_button_tooltip = Jämför bilder i gruppen. bottom_show_errors_tooltip = Visa/Dölj undertextpanelen. bottom_show_upper_notebook_tooltip = Visa/Dölj övre anteckningsbokspanelen. # Progress Window progress_stop_button = Stoppa progress_stop_additional_message = Stoppa begärd # About Window about_repository_button_tooltip = Länk till utvecklingskatalogen med källkod. about_donation_button_tooltip = Länk till donationssidan. about_instruction_button_tooltip = Länk till instruktionssidan. about_translation_button_tooltip = Länk till Crowdin sida med appöversättningar. Officiellt stöds polska och engelska. about_repository_button = Filförråd about_donation_button = Donationer about_instruction_button = Instruktion about_translation_button = Översättning # Header header_setting_button_tooltip = Öppnar dialogrutan för inställningar. header_about_button_tooltip = Öppnar dialog med info om app. # Settings ## General settings_number_of_threads = Antal använda trådar settings_number_of_threads_tooltip = Antal gängor, 0 betyder att alla gängor kommer att användas. settings_use_rust_preview = Använd externa bibliotek istället gtk för att ladda förhandsvisningar settings_use_rust_preview_tooltip = Att använda gtk-förhandsvisningar kommer ibland att vara snabbare och stödja fler format, men ibland kan det vara precis tvärtom. Om du har problem med att ladda förhandsvisningar, kan du försöka ändra den här inställningen. 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. settings_label_restart = Du måste starta om appen för att tillämpa inställningar! settings_ignore_other_filesystems = Ignorera andra filsystem (endast Linux) settings_ignore_other_filesystems_tooltip = ignorerar filer som inte finns i samma filsystem som sökta kataloger. Fungerar samma som -xdev alternativ för att hitta kommandot på Linux settings_save_at_exit_button_tooltip = Spara konfigurationen till fil när appen stängs. settings_load_at_start_button_tooltip = Ladda konfigurationen från filen när appen öppnas. Om den inte är aktiverad kommer standardinställningarna att användas. settings_confirm_deletion_button_tooltip = Visa bekräftelsedialog när du klickar på knappen ta bort. settings_confirm_link_button_tooltip = Visa bekräftelsedialog när du klickar på den hårda/symboliska länkknappen. settings_confirm_group_deletion_button_tooltip = Visa varningsdialog när du försöker ta bort alla poster från gruppen. settings_show_text_view_button_tooltip = Visa textpanelen längst ner i användargränssnittet. settings_use_cache_button_tooltip = Använd filcache. settings_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. settings_use_trash_button_tooltip = Flyttar filer till papperskorgen istället ta bort dem permanent. settings_language_label_tooltip = Språk för användargränssnitt. settings_save_at_exit_button = Spara konfiguration när appen stängs settings_load_at_start_button = Ladda konfiguration när appen öppnas settings_confirm_deletion_button = Visa bekräftelsedialog vid borttagning av filer settings_confirm_link_button = Visa bekräftelsedialog när hårda/symboliska länkar filer settings_confirm_group_deletion_button = Visa bekräftelsedialog när alla filer tas bort i grupp settings_show_text_view_button = Visa längst ned textpanel settings_use_cache_button = Använd cache settings_save_also_as_json_button = Spara även cache som JSON-fil settings_use_trash_button = Flytta raderade filer till papperskorgen settings_language_label = Språk settings_multiple_delete_outdated_cache_checkbutton = Ta bort föråldrade cache-poster automatiskt settings_multiple_delete_outdated_cache_checkbutton_tooltip = Ta bort föråldrade cacheresultat som pekar på obefintliga filer. När den är aktiverad, se till att appen när du laddar poster, att alla poster pekar på giltiga filer (trasiga dem ignoreras). 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ä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. settings_notebook_general = Info settings_notebook_duplicates = Dubbletter settings_notebook_images = Liknande bilder settings_notebook_videos = Liknande video ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Visar förhandsgranskning på höger sida (vid val av bildfil). settings_multiple_image_preview_checkbutton = Visa förhandsgranskning av bild settings_multiple_clear_cache_button_tooltip = Rensa cache manuellt för föråldrade poster. Detta bör endast användas om automatisk rensning har inaktiverats. settings_multiple_clear_cache_button = Ta bort föråldrade resultat från cachen. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Döljer alla filer utom en, om alla pekar på samma data (är hardlinked). 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. settings_duplicates_minimal_size_entry_tooltip = Ange minimal filstorlek som kommer att cachelagras. Att välja ett mindre värde kommer att generera fler poster. Detta kommer att snabba upp sökningen, men bromsa cache-laddning/spara. settings_duplicates_prehash_checkbutton_tooltip = 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. Det är inaktiverat som standard eftersom det kan orsaka nedgångar i vissa situationer. 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. settings_duplicates_prehash_minimal_entry_tooltip = Minimal storlek på cachad post. settings_duplicates_hide_hard_link_button = Dölj hårda länkar settings_duplicates_prehash_checkbutton = Använd prehash cache settings_duplicates_minimal_size_cache_label = Minimal storlek på filer (i bytes) sparade i cache settings_duplicates_minimal_size_cache_prehash_label = Minimal storlek på filer (i bytes) sparade för att kunna använda cache ## Saving/Loading settings settings_saving_button_tooltip = Spara konfigurationen för nuvarande inställningar till filen. settings_loading_button_tooltip = Ladda inställningar från fil och ersätta den aktuella konfigurationen med dem. settings_reset_button_tooltip = Återställ den aktuella konfigurationen till standardkonfigurationen. settings_saving_button = Spara konfiguration settings_loading_button = Ladda konfiguration settings_reset_button = Återställ konfiguration ## Opening cache/config folders settings_folder_cache_open_tooltip = Öppnar mappen där cache-txt-filer lagras. Ä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. Du kan kopiera dessa filer mellan datorer för att spara tid på skanning igen för filer (naturligtvis om de har liknande katalogstruktur). Vid problem med cachen kan dessa filer tas bort. Appen kommer automatiskt att regenerera dem. settings_folder_settings_open_tooltip = Öppnar mappen där Czkawka-konfigurationen lagras. VARNING: Manuellt modifierande av konfigurationen kan bryta ditt arbetsflöde. settings_folder_cache_open = Öppna cachemapp settings_folder_settings_open = Öppna inställningsmapp # Compute results compute_stopped_by_user = Sökandet stoppades av användaren compute_found_duplicates_hash_size = Hittade { $number_files } dubbletter i { $number_groups } grupper som tog { $size } i { $time } compute_found_duplicates_name = Hittade { $number_files } dubbletter i { $number_groups } grupper i { $time } compute_found_empty_folders = Hittade { $number_files } tomma mappar i { $time } compute_found_empty_files = Hittade { $number_files } tomma filer i { $time } compute_found_big_files = Hittade { $number_files } stora filer i { $time } compute_found_temporary_files = Hittade { $number_files } temporära filer i { $time } compute_found_images = Hittade { $number_files } liknande bilder i { $number_groups } grupper i { $time } compute_found_videos = Hittade { $number_files } liknande videor i { $number_groups } grupper i { $time } compute_found_music = Hittade { $number_files } liknande musikfiler i { $number_groups } grupper i { $time } compute_found_invalid_symlinks = Hittade { $number_files } ogiltiga symlänkar i { $time } compute_found_broken_files = Hittade { $number_files } trasiga filer i { $time } compute_found_bad_extensions = Hittade { $number_files } filer med ogiltiga tillägg i { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] skannade { $file_number } fil *[other] skannade { $file_number } filer } progress_scanning_extension_of_files = Kontrollerad förlängning av { $file_checked }/{ $all_files } fil progress_scanning_broken_files = Kontrollerade { $file_checked }/{ $all_files } fil ({ $data_checked }/{ $all_data }) progress_scanning_video = Hashad av { $file_checked }/{ $all_files } video progress_creating_video_thumbnails = Skapade miniatyrbilder av { $file_checked }/{ $all_files } video progress_scanning_image = Hashad av { $file_checked }/{ $all_files } bild ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Jämfört { $file_checked }/{ $all_files } bildhash progress_scanning_music_tags_end = Jämförda taggar av { $file_checked }/{ $all_files } musikfil progress_scanning_music_tags = Läs taggar för { $file_checked }/{ $all_files } musikfil progress_scanning_music_content_end = Jämfört fingeravtryck av { $file_checked }/{ $all_files } musikfil progress_scanning_music_content = Beräknat fingeravtryck av { $file_checked }/{ $all_files } musikfil ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] skannade { $folder_number } mapp *[other] skannade { $folder_number } mappar } progress_scanning_size = Skannad storlek på { $file_number } fil progress_scanning_size_name = Skannat namn och storlek på { $file_number } fil progress_scanning_name = Skannat namn på { $file_number } fil progress_analyzed_partial_hash = Analyserad partiell hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Analyserad full hash av { $file_checked }/{ $all_files } filer ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Laddar prehash cache progress_prehash_cache_saving = Sparar Omfattande cache progress_hash_cache_loading = Laddar hash-cache progress_hash_cache_saving = Sparar hash-cache progress_cache_loading = Laddar cache progress_cache_saving = Sparar cache progress_current_stage = Nuvarande steg:{ " " } progress_all_stages = Alla etapper:{ " " } # Saving loading saving_loading_saving_success = Sparad konfiguration till filen { $name }. saving_loading_saving_failure = Det gick inte att spara konfigurationsdata till filen { $name }, anledningen { $reason }. saving_loading_reset_configuration = Aktuell konfiguration har rensats. saving_loading_loading_success = Korrekt laddad app-konfiguration. saving_loading_failed_to_create_config_file = Det gick inte att skapa konfigurationsfil "{ $path }", orsak "{ $reason }". saving_loading_failed_to_read_config_file = Kan inte ladda konfiguration från "{ $path }" eftersom den inte finns eller inte är en fil. saving_loading_failed_to_read_data_from_file = Kan inte läsa data från fil "{ $path }", anledning "{ $reason }". # Other selected_all_reference_folders = Kan inte börja söka, när alla kataloger är inställda som referensmappar searching_for_data = Söker data, det kan ta en stund, vänta... text_view_messages = MEDDELANDEN text_view_warnings = VARNINGAR text_view_errors = FEL about_window_motto = Detta program är gratis att använda och kommer alltid att vara. krokiet_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. # Various dialog dialogs_ask_next_time = Fråga nästa gång symlink_failed = Misslyckades att symbolisk länk { $name } till { $target }, anledning { $reason } delete_title_dialog = Ta bort bekräftelse delete_question_label = Är du säker på att du vill ta bort filer? delete_all_files_in_group_title = Bekräftelse av att ta bort alla filer i grupp delete_all_files_in_group_label1 = I vissa grupper är alla poster valda. delete_all_files_in_group_label2 = Är du säker på att du vill radera dem? delete_items_label = { $items } filer kommer att tas bort. delete_items_groups_label = { $items } filer från { $groups } grupper kommer att raderas. hardlink_failed = Det gick inte att hardlink { $name } till { $target }, varför { $reason } hard_sym_invalid_selection_title_dialog = Ogiltigt val med vissa grupper hard_sym_invalid_selection_label_1 = I vissa grupper finns det bara en post vald och den kommer att ignoreras. hard_sym_invalid_selection_label_2 = För att kunna länka dessa filer måste minst två resultat i gruppen väljas. hard_sym_invalid_selection_label_3 = Först i grupp känns igen som original och ändras inte, men andra och senare ändras. hard_sym_link_title_dialog = Länkbekräftelse hard_sym_link_label = Är du säker på att du vill länka dessa filer? move_folder_failed = Det gick inte att flytta mappen { $name } anledning { $reason } move_file_failed = Det gick inte att flytta filen { $name } anledning { $reason } move_files_title_dialog = Välj mapp som du vill flytta duplicerade filer till move_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 }. move_stats = Korrekt flyttad { $num_files }/{ $all_files } objekt save_results_to_file = Sparade resultat både till txt och json filer i "{ $name }" mapp. search_not_choosing_any_music = FEL: Du måste välja minst en kryssruta med söktyper för musik. search_not_choosing_any_broken_files = FEL: Du måste välja minst en kryssruta med typ av markerade trasiga filer. include_folders_dialog_title = Mappar att inkludera exclude_folders_dialog_title = Mappar att exkludera include_manually_directories_dialog_title = Lägg till katalog manuellt cache_properly_cleared = Rensad cache cache_clear_duplicates_title = Rensar dubbletter cache cache_clear_similar_images_title = Rensar liknande bildcache cache_clear_similar_videos_title = Rensar liknande videoklipp cache cache_clear_message_label_1 = Vill du rensa cachen för föråldrade inlägg? cache_clear_message_label_2 = Denna åtgärd kommer att ta bort alla cache-poster som pekar på ogiltiga filer. cache_clear_message_label_3 = Detta kan något speedup ladda/spara till cache. cache_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. # Show preview preview_image_resize_failure = Kunde inte ändra storlek på bild { $name }. preview_image_opening_failure = Det gick inte att öppna bilden { $name } skäl { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Grupp { $current_group }/{ $all_groups } ({ $images_in_group } bilder) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/tr/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Ayarlar window_main_title = Czkawka (Hıçkırık) window_progress_title = Taranıyor window_compare_images = Resimleri Karşılaştır # General general_ok_button = Tamam general_close_button = Kapat # Krokiet info dialog krokiet_info_title = Krokiet - Yeni versiyon Czkawka krokiet_info_message = Krokiet, Czkawka GTK GUI’nin yeni, geliştirilmiş, daha hızlı ve daha güvenilir versiyonudur! Ç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. 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. Deneyin ve farkı görü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. 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. # Main window music_title_checkbox = Başlık music_artist_checkbox = Sanatçı music_year_checkbox = Yıl music_bitrate_checkbox = Bit-hızı music_genre_checkbox = Müzik Türü music_length_checkbox = Uzunluk music_comparison_checkbox = Yaklaşık Karşılaştırma music_checking_by_tags = Etiketler music_checking_by_content = İçerik same_music_seconds_label = Minimal parça saniyesel süresi same_music_similarity_label = Maksimum fark music_compare_only_in_title_group = Benzer başlıklı gruplar içinde karşılaştırın music_compare_only_in_title_group_tooltip = Etkinleştirildiğinde dosyalar başlığa göre gruplandırılır ve ardından birbirleriyle karşılaştırılır. 10.000 dosya ile neredeyse 100 milyon karşılaştırma yerine genellikle 20.000 civarında karşılaştırma olacaktır. same_music_tooltip = İçeriğine göre benzer müzik dosyalarının aranması ayarlanarak yapılandırılabilir: - Müzik dosyalarının benzer olarak tanımlanabileceği minimum parça süresi - Test edilen iki parça arasındaki maksimum fark İyi sonuçlar elde etmenin anahtarı, bu parametrelerin mantıklı kombinasyonlarını bulmaktır. Minimum süreyi 5 saniye ve maksimum farkı 1.0 olarak ayarlamak, dosyalarda neredeyse aynı parçaları arayacaktır. Ö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. 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). music_comparison_checkbox_tooltip = Yapay zeka kullanarak benzer müzik dosyalarını arar. Örneğin, bir tümcenin parantezlerini kaldırmak için makine öğrenimini kullanır. Bu seçenek etkinleştirildiğinde, söz konusu dosyalar kopya olarak kabul edilecektir: Geççek <--> Geççek (Tarkan 2022) duplicate_case_sensitive_name = Büyük/Küçük harfe Duyarlı duplicate_case_sensitive_name_tooltip = Etkinleştirilse, dosya adları tam olarak aynı olduğunda eşleştirilir ve bir grup oluşturulur. fatih.kavalci <--> fatih.kavalci Etkisizleştirilirse, her bir harfin büyük/küçük yazılıp yazılmadığını denetlemeden aynı adları eşleyip grup oluşturur. fatih.kavalci <--> FatiH.KaVaLCi duplicate_mode_size_name_combo_box = Boyut ve Ad Karşılaştırma duplicate_mode_name_combo_box = Ad Karşılaştırma duplicate_mode_size_combo_box = Boyut Karşılaştırma duplicate_mode_hash_combo_box = Hash duplicate_hash_type_tooltip = Czkawka, 3 tür Sabit Uzunlukta Çıktı (SUÇ) üretimi sunar: Blake3 - kriptografik SUÇ üretim işlevi. Bu varsayılandır çünkü çok hızlıdır. CRC32 - basit SUÇ üretim işlevi. Bu, Blake3'ten daha hızlı olmalıdır, ancak kimi zaman çakışmalar olabilir. XXH3 - performans ve benzersiz SUÇ üretim kalitesi açısından Blake3'e çok benzer (ancak kriptografik değildir). Böylece, bu tür modlar kolayca değiştirilebilir. duplicate_check_method_tooltip = Czkawka, eş dosyaları bulmak için şimdilik üç tür yöntem sunar: Ad Karşılaştırma - Aynı ada sahip dosyaları bulur. Boyut Karşılaştırma - Aynı boyuta sahip dosyaları bulur. Hash (SUÇ) Karşılaştırma - Aynı içeriğe sahip dosyaları bulur. Bu mod her dosya için veri analizi sonucu sabit uzunlukta benzersiz birer çıktı üretir ve daha sonra eş doşyaları bulmak için bu çıktıları karşılaştırır. Bu mod, eş dosyaları bulmanın en güvenli yoludur. Czkawka, önbelleği yoğun olarak kullanır. Bu nedenle aynı verilerin ikinci ve sonraki taramaları ilkinden çok daha hızlı olmalıdır. image_hash_size_tooltip = 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. 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. 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. 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ç). image_resize_filter_tooltip = Görüntünün hash'ini hesaplamak için kütüphanenin önce görüntüyü yeniden boyutlandırması gerekir. Seçilen algoritmaya bağlı olarak, hash hesaplamak için kullanılan sonuç görüntüsü biraz farklı görünecektir. 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. 8x8 karma boyutunda, daha iyi görüntü grupları elde etmek için Nearest'ten farklı bir algoritma kullanılması önerilir. image_hash_alg_tooltip = Kullanıcılar, SUÇ oluşturmanın birçok algoritmasından birini seçebilir. Her birinin hem güçlü hem de zayıf noktaları vardır ve farklı görüntüler için bazen daha iyi, bazen daha kötü sonuçlar verir. Bu nedenle, size göre en iyisini belirlemek için elle test gereklidir. big_files_mode_combobox_tooltip = Boyut bakımından En Büyük/En Küçük dosyaları aramaya izin verir big_files_mode_label = Denetim şekli big_files_mode_smallest_combo_box = En Küçük big_files_mode_biggest_combo_box = En Büyük main_notebook_duplicates = Eş Dosyalar main_notebook_empty_directories = Boş Dizinler main_notebook_big_files = Büyük/Küçük Dosyalar main_notebook_empty_files = Boş Dosyalar main_notebook_temporary = Geçici Dosyalar main_notebook_similar_images = Benzer Resimler main_notebook_similar_videos = Benzer Videolar main_notebook_same_music = Müzik Kopyaları main_notebook_symlinks = Geçersiz Sembolik Bağlar main_notebook_broken_files = Bozuk Dosyalar main_notebook_bad_extensions = Hatalı Uzantılar main_tree_view_column_file_name = Dosya Adı main_tree_view_column_folder_name = Klasör Adı main_tree_view_column_path = Yol main_tree_view_column_modification = Düzenleme Tarihi main_tree_view_column_size = Boyut main_tree_view_column_similarity = Benzerlik main_tree_view_column_dimensions = En x Boy main_tree_view_column_title = Başlık main_tree_view_column_artist = Sanatçı main_tree_view_column_year = Yıl main_tree_view_column_bitrate = Bit-hızı main_tree_view_column_length = Uzunluk main_tree_view_column_genre = Tür main_tree_view_column_symlink_file_name = Sembolik Bağ Dosyası Adı main_tree_view_column_symlink_folder = Sembolik Bağlantı Klasörü main_tree_view_column_destination_path = Hedef Yol main_tree_view_column_type_of_error = Hata türü main_tree_view_column_current_extension = Geçerli Uzantı main_tree_view_column_proper_extensions = Uygun Uzantı main_tree_view_column_fps = FPS main_tree_view_column_codec = KodçTürkçe: Kodç main_label_check_method = Denetim yöntemi: main_label_hash_type = SUÇ türü: main_label_hash_size = SURÇ boyutu: main_label_size_bytes = Boyut (bayt): main_label_min_size = Min main_label_max_size = Maks main_label_shown_files = Gösterilecek Dosya Sayısı: main_label_resize_algorithm = Yeniden boyutlandırma algoritması: main_label_similarity = Benzerlik: { " " } main_check_box_broken_files_audio = Ses main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Arşiv main_check_box_broken_files_image = Resim main_check_box_broken_files_video = Video main_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. check_button_general_same_size = Aynı boyutu yok say check_button_general_same_size_tooltip = Sonuçlarda aynı boyutta olan dosyaları yoksay - genellikle bunlar bire bir kopyalardır main_label_size_bytes_tooltip = Taramada kullanılacak dosyaların boyutu # Upper window upper_tree_view_included_folder_column_title = Aranacak Klasörler upper_tree_view_included_reference_column_title = Başvuru Klasörleri upper_recursive_button = Özyinelemeli upper_recursive_button_tooltip = Seçilirse, doğrudan "Aranacak Klasörler" listesindeki dizin altında yer almayan (alt dizinlerdeki dosyaları da) arar. upper_manual_add_included_button = Dizin Gir upper_add_included_button = Ekle upper_remove_included_button = Kaldır upper_manual_add_excluded_button = Dizin Gir upper_add_excluded_button = Ekle upper_remove_excluded_button = Kaldır upper_manual_add_included_button_tooltip = Arama yapılacak dizin yolunu doğrudan yazın. Aynı anda birden fazla girdi eklemek için bunları ";" ile ayırın. /home/fatih;/home/kavalci girdisi biri /home/fatih öteki /home/kavalci olmak üzere iki dizin ekleyecektir upper_add_included_button_tooltip = "Aranacak Klasörler" listesine yeni bir dizin ekler. upper_remove_included_button_tooltip = Seçili dizini "Aranacak Klasörler" listesinden kaldırır. upper_manual_add_excluded_button_tooltip = Hariç tutulacak dizin yolunu doğrudan yazın. Aynı anda birden fazla girdi eklemek için bunları ";" ile ayırın. /home/fatih;/home/kavalci girdisi biri /home/fatih öteki /home/kavalci olmak üzere iki dizin ekleyecektir upper_add_excluded_button_tooltip = "Hariç Tutulacak Klasörler" listesine yeni bir dizin ekler. upper_remove_excluded_button_tooltip = Seçili dizini "Hariç Tutulacak Klasörler" listesinden kaldırır. upper_notebook_items_configuration = Öğe Yapılandırması upper_notebook_excluded_directories = Hariç Tutulan Yollar upper_notebook_included_directories = Dahil Edilen Yollar upper_allowed_extensions_tooltip = İzin verilen uzantılar virgülle ayrılmalıdır (varsayılan olarak her uzantı kullanılır). Aynı anda birden fazla (aynı tür) uzantı ekleyen makrolar da kullanılabilir: IMAGE, VIDEO, MUSIC, TEXT. Kullanım örneği: ".exe, IMAGE, VIDEO, .rar, .7z" -- Bu girdi, resimlerin (ör. jpg, png ...), videoların (ör. avi, mp4 ...), exe, rar ve 7z dosyalarının taranacağı anlamına gelir. upper_excluded_extensions_tooltip = Taramada göz ardı edilecek devre dışı bırakılmış dosyaların listesi. İ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. upper_excluded_items_tooltip = Hariçlanan öğeler * joker karakterini içermeli ve virgülle ayrılmalıdır. Bu, Hariç Yollar'dan daha yavaştır, bu nedenle dikkatli kullanılmalıdır. upper_excluded_items = Hariç Tutulan Öğeler: upper_allowed_extensions = İzin Verilen Uzantılar: upper_excluded_extensions = Devre Dışı Uzantılar: # Popovers popover_select_all = Tümünü seç popover_unselect_all = Tümünün seçimini kaldır popover_reverse = Seçimi Ters Çevir popover_select_all_except_shortest_path = Tümünü seç hariç en kısa yolu seç popover_select_all_except_longest_path = Tümünü seç hariç en uzun yolu seç popover_select_all_except_oldest = En eski olan hariç hepsini seç popover_select_all_except_newest = En yeni olan hariç hepsini seç popover_select_one_oldest = En eski olanı seç popover_select_one_newest = En yeni olanı seç popover_select_custom = Özel girdi ile seçim yap popover_unselect_custom = Özel girdi ile seçimi kaldır popover_select_all_images_except_biggest = En büyük olan hariç hepsini seç popover_select_all_images_except_smallest = En küçük olan hariç hepsini seç popover_custom_path_check_button_entry_tooltip = Kayıtları, kısmi yol girdisine göre seçer. Örnek kullanım: /home/fatih/kavalci.txt dosyası, /home/fat* girdisi ile bulunabilir popover_custom_name_check_button_entry_tooltip = Kayıtları, kısmi dosya adı girdisine göre seçer. Örnek kullanım: /home/fatih/kavalci.txt dosyası, *val* girdisi ile bulunabilir popover_custom_regex_check_button_entry_tooltip = Kayıtları, belirtilen Regex girdisine göre seçer. Bu mod ile aranan metin, tam yol dosya adıdır. Örnek kullanım: /home/fatih/kavalcı.txt dosyası, h/ka[a-z]+ ile bulunabilir Bu işlev, varsayılan Rust regex uygulamasını kullanır. Daha fazla bilgi için bakınız: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Büyük/Küçük harfe duyarlı algılamayı etkinleştirir. Etkisizleştirilir ise; /home/fatih/* girdisi, hem /home/fatih/ hem de /home/FaTiH dizinlerini algılar. popover_custom_not_all_check_button_tooltip = Gruptaki tüm kayıtların seçilmesini engeller. Bu varsayılan olarak etkindir. Çünkü, çoğu durumda hem asıl dosyayı hem de kopyaları silmek istemezsiniz. En az bir dosya bırakmak istersiniz. UYARI: Bir gruptaki tüm sonuçlar zaten elle seçilmiş ise bu ayar çalışmaz. popover_custom_regex_path_label = Yol popover_custom_regex_name_label = Ad popover_custom_regex_regex_label = Regex Yolu + Adı popover_custom_case_sensitive_check_button = Büyük/Küçük harfe duyarlı popover_custom_all_in_group_label = Gruptaki tüm kayıtları seçme popover_custom_mode_unselect = Özel Girdi ile Seçimi Kaldır popover_custom_mode_select = Özel Girdi ile Seç popover_sort_file_name = Dosya adı popover_sort_folder_name = Klasör adı popover_sort_full_name = Tam ad popover_sort_size = Boyut popover_sort_selection = Seçim popover_invalid_regex = Regex geçersiz (hatalı) popover_valid_regex = Regex geçerli (doğru) # Bottom buttons bottom_search_button = Ara bottom_select_button = Seç bottom_delete_button = Sil bottom_save_button = Kaydet bottom_symlink_button = Sembolik bağlantı bottom_hardlink_button = Sabit bağlantı bottom_move_button = Taşı bottom_sort_button = Sırala bottom_compare_button = Karşılaştır bottom_search_button_tooltip = Aramayı başlatır bottom_select_button_tooltip = Kayıtları seçer. Yalnızca seçilen dosyalara/klasörlere işlem uygulanabilir. bottom_delete_button_tooltip = Seçili dosyaları/klasörleri siler. bottom_save_button_tooltip = Aramayla ilgili verileri dosyaya kaydeder bottom_symlink_button_tooltip = Sembolik bağlantılar oluşturur. Yalnızca bir gruptaki en az iki sonuç seçildiğinde çalışır. Birincisi değişmez, ikincisi ve sonrası birinciye sembolik olarak bağlanır. bottom_hardlink_button_tooltip = Sabit bağlantılar oluşturur. Yalnızca bir gruptaki en az iki sonuç seçildiğinde çalışır. Birincisi değişmez, ikincisi ve sonrası birinciye sabit olarak bağlanır. bottom_hardlink_button_not_available_tooltip = Hardlinkler oluştur. Düğme devre dışı, çünkü hardlinkler oluşturulamaz. 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. Eğer uygulama zaten yeterli ayrıcalıklarla çalışıyorsa Github üzerindeki benzer sorunları gözden geçirin. bottom_move_button_tooltip = Dosyaları seçilen dizine taşır. Dizin ağacını korumadan tüm dosyaları dizine taşır. Aynı ada sahip iki dosyayı klasöre taşımaya çalışırken, ikincisi başarısız olur ve hata gösterir. bottom_sort_button_tooltip = Dosyaları/Dizinleri seçilen metoda göre sırala. bottom_compare_button_tooltip = Gruptaki görüntüleri karşılaştır. bottom_show_errors_tooltip = Alt çıktı panelini göster/gizle. bottom_show_upper_notebook_tooltip = Üst denetim panelini göster/gizle. # Progress Window progress_stop_button = Durdur progress_stop_additional_message = İşlem durduruldu # About Window about_repository_button_tooltip = Kaynak kodu depo sayfasına bağlanır. about_donation_button_tooltip = Bağış sayfasına bağlanır. about_instruction_button_tooltip = Kullanım yönergeleri sayfasına bağlanır. about_translation_button_tooltip = Czkawka çevirileriyle Crowdin sayfasına bağlanır. Resmi olarak Lehçe ve İngilizce desteklenmektedir. about_repository_button = Depo about_donation_button = Bağış about_instruction_button = Yönerge about_translation_button = Çeviri # Header header_setting_button_tooltip = Ayarlar iletişim kutusunu açar. header_about_button_tooltip = Czkawka hakkında bilgi içeren iletişim kutusunu açar. # Settings ## General settings_number_of_threads = Kullanılan iş parçacığı sayısı settings_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. settings_use_rust_preview = Ön izlemeleri yüklemek için gtk yerine harici kitaplıkları kullanın settings_use_rust_preview_tooltip = Gtk ön izlemelerini kullanmak bazen daha hızlı olabilir ve daha fazla biçimi destekler, ancak bazen bu tam tersi de olabilir. Ön izlemeleri yüklemede sorun yaşıyorsanız bu ayarı değiştirmeyi deneyebilirsiniz. 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. settings_label_restart = Ayarları uygulamak için uygulamayı yeniden başlatmanız gerekir! settings_ignore_other_filesystems = Öteki dosya sistemlerini yoksay (sadece Linux) settings_ignore_other_filesystems_tooltip = Aranan dizinlerle aynı dosya sisteminde olmayan dosyaları yoksayar. Linux'ta find komutundaki -xdev seçeneği ile aynı şekilde çalışır settings_save_at_exit_button_tooltip = Uygulamayı kapatırken yapılandırmayı dosyaya kaydeder. settings_load_at_start_button_tooltip = Uygulamayı açarken yapılandırmayı dosyadan yükler. Etkinleştirilmezse, varsayılan ayarlar kullanılır. settings_confirm_deletion_button_tooltip = Sil düğmesine tıklandığında onay iletişim kutusunu gösterir. settings_confirm_link_button_tooltip = Sabit/sembolik bağlantı düğmesine tıklandığında onay iletişim kutusunu göster. settings_confirm_group_deletion_button_tooltip = Gruptan tüm kayıtları silmeye çalışırken uyarı iletişim kutusunu gösterir. settings_show_text_view_button_tooltip = Kullanıcı arayüzünün altında çıktı panelini gösterir. settings_use_cache_button_tooltip = Dosya önbelleğini kullanır. settings_save_also_as_json_button_tooltip = Önbelleği (kullanıcı tarafından okunabilir) JSON biçiminde kaydeder. İçeriğini değiştirmek mümkündür. İkili biçim önbelleği (bin uzantılı) eksikse, bu dosyadaki önbellek uygulama tarafından otomatik olarak okunacaktır. settings_use_trash_button_tooltip = Dosyaları kalıcı olarak silmek yerine çöp kutusuna taşır. settings_language_label_tooltip = Kullanıcı arayüzü dilini değiştirir. settings_save_at_exit_button = Uygulamayı kapatırken yapılandırmayı kaydet settings_load_at_start_button = Uygulamayı açarken yapılandırmayı yükle settings_confirm_deletion_button = Herhangi bir dosyayı silerken onay iletişim kutusunu göster settings_confirm_link_button = Herhangi bir dosyaya sabit/sembolik bağlantı yapıldığında onay iletişim kutusunu göster settings_confirm_group_deletion_button = Gruptaki tüm dosyaları silerken onay iletişim kutusunu göster settings_show_text_view_button = Alt çıktı panelini göster settings_use_cache_button = Önbelleği kullan settings_save_also_as_json_button = Önbelleği JSON dosyası olarak da kaydet settings_use_trash_button = Silinen dosyaları çöp kutusuna taşı settings_language_label = Dil settings_multiple_delete_outdated_cache_checkbutton = Güncel olmayan önbellek girişlerini otomatik olarak sil settings_multiple_delete_outdated_cache_checkbutton_tooltip = Var olmayan dosyalara işaret eden eski önbellek girdilerini siler. Etkinleştirildiğinde, uygulama kayıtları yüklerken tüm kayıtların geçerli dosyalara işaret etmesini sağlar (bozuk olanlar yoksayılır). Bunu devre dışı bırakmak, harici sürücülerdeki dosyaları tararken yardımcı olacaktır, bu nedenle bunlarla ilgili önbellek girdileri bir sonraki taramada temizlenmez. Önbellekte yüzbinlerce kayıt olması durumunda, taramanın başlangıcında/sonunda önbellek yükleme/kaydetme işlemini hızlandıracak olan bu özelliği etkinleştirmeniz önerilir. settings_notebook_general = Genel settings_notebook_duplicates = Eş Dosyalar settings_notebook_images = Benzer Resimler settings_notebook_videos = Benzer Videolar ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Sağ tarafta önizlemeyi gösterir (bir resim dosyası seçiliyken). settings_multiple_image_preview_checkbutton = Resim önizlemesini göster settings_multiple_clear_cache_button_tooltip = Güncel olmayan girişlerin önbelleğini el ile temizleyin. Bu, yalnızca otomatik temizleme devre dışı bırakılmışsa kullanılmalıdır. settings_multiple_clear_cache_button = Güncel olmayan girdileri önbellekten kaldır. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Hepsi aynı verilere işaret ediyorsa (sabit bağlantılıysa), biri dışındaki tüm dosyaları gizler. Örnek: (Diskte) belirli verilere sabit bağlantılı yedi dosya ve aynı veriye ancak farklı bir düğüme sahip bir farklı dosya olması durumunda, yinelenen bulucuda yalnızca bir benzersiz dosya ve sabit bağlantılı dosyalardan bir dosya gösterilecektir. settings_duplicates_minimal_size_entry_tooltip = Önbelleğe alınacak minimum dosya boyutunu ayarlayın. Daha küçük bir değer seçmek daha fazla kayıt üretecektir. Bu, aramayı hızlandıracak, ancak önbellek yüklemeyi/kaydetmeyi yavaşlatacaktır. settings_duplicates_prehash_checkbutton_tooltip = Yinelenmeyen sonuçların daha önce reddedilmesine izin veren kısmi-SUÇ (dosyanın küçük bir bölümünden hesaplanan bir SUÇ) değerinin önbelleğe alınmasını sağlar. Bazı durumlarda yavaşlamaya neden olabileceğinden varsayılan olarak devre dışıdır. Aramayı birden çok kez hızlandırabileceğinden, yüz binlerce veya milyonlarca dosyayı tararken kullanılması şiddetle tavsiye edilir. settings_duplicates_prehash_minimal_entry_tooltip = Önbelleğe alınacak girişlerin minimum boyutu. settings_duplicates_hide_hard_link_button = Zor bağlantıları gizle settings_duplicates_prehash_checkbutton = kısmi-SUÇ önbelleği kullan settings_duplicates_minimal_size_cache_label = Önbelleğe kaydedilen minimum dosya boyutu (bayt cinsinden): settings_duplicates_minimal_size_cache_prehash_label = kısmi-SUÇ önbelleğine kaydedilen minimum dosya boyutu (bayt cinsinden): ## Saving/Loading settings settings_saving_button_tooltip = Geçerli ayar yapılandırmasını dosyaya kaydeder. settings_loading_button_tooltip = Dosyadan ayarları yükler ve geçerli yapılandırmayı bunlarla değiştirir. settings_reset_button_tooltip = Geçerli yapılandırmayı varsayılana sıfırlar. settings_saving_button = Yapılandırmayı kaydet settings_loading_button = Yapılandırma yükle settings_reset_button = Yapılandırmayı sıfırla ## Opening cache/config folders settings_folder_cache_open_tooltip = Önbellek txt dosyalarının depolandığı klasörü açar. Önbellek dosyalarının değiştirilmesi geçersiz sonuçların gösterilmesine neden olabilir. Ancak, büyük miktarda dosyayı farklı bir konuma taşırken yolu değiştirmek zaman kazandırabilir. Dosyaları tekrar taramaktan zaman kazanmak için bu dosyaları bilgisayarlar arasında kopyalayabilirsiniz (tabii ki benzer dizin yapısına sahiplerse). Önbellekte sorun olması durumunda bu dosyalar kaldırılabilir. Uygulama onları otomatik olarak yeniden oluşturacaktır. settings_folder_settings_open_tooltip = Czkawka yapılandırmasının depolandığı klasörü açar. UYARI: Yapılandırmayı elle değiştirmek iş akışınızı bozabilir. settings_folder_cache_open = Önbellek klasörünü aç settings_folder_settings_open = Ayarlar klasörünü aç # Compute results compute_stopped_by_user = Arama, kullanıcı tarafından durduruldu compute_found_duplicates_hash_size = { $number_files } tane ekleme { $number_groups } grupta bulunmuştur ve bu,{ $size }'ye { $time } sürede kadardır compute_found_duplicates_name = { $number_files } kopya, { $number_groups } grubunda { $time } süresi içinde bulunmuştur compute_found_empty_folders = { $number_files } boş klasörünü { $time } buldum compute_found_empty_files = { $number_files } adet dosya { $time } içinde boş bulundu compute_found_big_files = { $number_files } büyük dosya { $time } içinde bulundu compute_found_temporary_files = { $number_files } geçici dosya { $time } içinde bulundu compute_found_images = { $number_files } benzer görüntüyü { $number_groups } grupta { $time } süre içinde buldum compute_found_videos = { $number_files } benzer videoyu { $number_groups } grupta { $time } içinde buldum compute_found_music = { $number_files } benzer müzik dosyası { $number_groups } grup içinde { $time } bulunmuştur compute_found_invalid_symlinks = { $number_files } geçerli olmayan simge bağlantısı { $time } içinde bulunuldu compute_found_broken_files = { $number_files } bozuk dosya bulundu { $time } içinde compute_found_bad_extensions = Geçersiz uzantılarla { $number_files } dosya { $time } içinde bulundu # Progress window progress_scanning_general_file = { $file_number -> [one] { $file_number } dosya tarandı *[other] { $file_number } dosya tarandı } progress_scanning_extension_of_files = { $file_checked }/{ $all_files } dosyasını kontrol edildi progress_scanning_broken_files = Kontrol edilen { $file_checked }/{ $all_files } dosya ({ $data_checked }/{ $all_data }) progress_scanning_video = Hash işlemi uygulanmış { $file_checked }/{ $all_files } video progress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video progress_scanning_image = Hash işlemi uygulanmış { $file_checked }/{ $all_files } görsel ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = { $file_checked }/{ $all_files } görsel hash kaydı karşılaştırıldı progress_scanning_music_tags_end = Compared tags of { $file_checked }/{ $all_files } music file progress_scanning_music_tags = Read tags of { $file_checked }/{ $all_files } music file progress_scanning_music_content_end = Compared fingerprint of { $file_checked }/{ $all_files } music file progress_scanning_music_content = Calculated fingerprint of { $file_checked }/{ $all_files } music file ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] { $folder_number } klasör tarandı *[other] { $folder_number } klasör tarandı } progress_scanning_size = Taranan { $file_number } dosyasının boyutu progress_scanning_size_name = Scanned name and size of { $file_number } file progress_scanning_name = Scanned name of { $file_number } file progress_analyzed_partial_hash = Analyzed partial hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Analyzed full hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Prehash önbelleği yükleniyor progress_prehash_cache_saving = Prehash önbelleği kaydediliyor progress_hash_cache_loading = Hash önbelleği yükleniyor progress_hash_cache_saving = Hash önbelleği kaydediliyor progress_cache_loading = Önbellek yükleniyor progress_cache_saving = Önbellek kaydediliyor progress_current_stage = Geçerli Aşama: { " " } progress_all_stages = Tüm Aşamalar: { " " } # Saving loading saving_loading_saving_success = Yapılandırma { $name } dosyasına kaydedildi. saving_loading_saving_failure = Konfigürasyon verilerini dosya { $name }'a kaydetme başarısız oldu, sebep { $reason }. saving_loading_reset_configuration = Geçerli yapılandırma temizlendi. saving_loading_loading_success = Uygulama yapılandırması düzgünce yüklendi. saving_loading_failed_to_create_config_file = "{ $path }" dizininde yapılandırma dosyası oluşturulamadı, nedeni: "{ $reason }". saving_loading_failed_to_read_config_file = "{ $path }" dizininden yapılandırma dosyası yüklenemiyor, böyle dosya yok ya da bir dosya değil. saving_loading_failed_to_read_data_from_file = "{ $path }" dosyasından veri okunamıyor, nedeni: "{ $reason }". # Other selected_all_reference_folders = Tüm dizinler, "Başvuru Klasörü" olarak ayarlandığında arama başlatılamaz searching_for_data = İşleminiz yürütülüyor, bu biraz zaman alabilir, lütfen bekleyin... text_view_messages = MESAJLAR text_view_warnings = UYARILAR text_view_errors = HATALAR about_window_motto = Bu programın kullanımı ücretsizdir ve her zaman öyle kalacaktır. krokiet_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. # Various dialog dialogs_ask_next_time = Bir dahaki sefere sor symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason } delete_title_dialog = Silmeyi onaylayın delete_question_label = Dosyaları silmek istediğinizden emin misiniz? delete_all_files_in_group_title = Gruptaki tüm dosyaları silmeyi onaylayın delete_all_files_in_group_label1 = Kimi gruplarda tüm kayıtlar seçilir. delete_all_files_in_group_label2 = Bunları silmek istediğinizden emin misiniz? delete_items_label = { $items } dosya silinecek. delete_items_groups_label = { $groups } gruptan { $items } dosya silinecek. hardlink_failed = { $name }'yi { $target }'a hafıza.linklemek başarısız oldu, sebep { $reason } hard_sym_invalid_selection_title_dialog = Kimi gruplarda geçersiz seçim hard_sym_invalid_selection_label_1 = Bazı gruplarda sadece bir kayıt seçilmiştir ve bu kayıt yok sayılacaktır. hard_sym_invalid_selection_label_2 = Bu dosyaları sabit/sembolik bağlayabilmek için gruptaki en az iki sonucun seçilmesi gerekir. hard_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. hard_sym_link_title_dialog = Bağlantı vermeyi onaylayın hard_sym_link_label = Bu dosyaları bağlamak istediğinizden emin misiniz? move_folder_failed = { $name } klasörü taşınamadı, nedeni: { $reason } move_file_failed = { $name } dosyası taşınamadı, nedeni: { $reason } move_files_title_dialog = Eş dosyaları taşımak istediğiniz klasörü seçin move_files_choose_more_than_1_path = Eş dosyaları taşıyabilmek için yalnızca bir yol seçilebilir, { $path_number } seçildi. move_stats = { $num_files }/{ $all_files } öğe düzgün şekilde taşındı save_results_to_file = Saved results both to txt and json files into "{ $name }" folder. search_not_choosing_any_music = HATA: Müzik araması için en az bir onay kutusu seçmelisiniz. search_not_choosing_any_broken_files = HATA: Bozuk dosya araması için en az bir onay kutusu seçmelisiniz. include_folders_dialog_title = Aranacak Klasörler exclude_folders_dialog_title = Hariç Tutulan Klasörler include_manually_directories_dialog_title = Dizini elle ekle cache_properly_cleared = Önbellek, uygun şekilde temizlendi cache_clear_duplicates_title = Eş dosyalar önbelleğini temizle cache_clear_similar_images_title = Benzer resimler önbelleğini temizle cache_clear_similar_videos_title = Benzer videolar önbelleğini temizle cache_clear_message_label_1 = Güncel olmayan girişleri önbellekten temizlemek istiyor musunuz? cache_clear_message_label_2 = Bu işlem, geçersiz dosyalara işaret eden tüm önbellek girişlerini kaldıracak. cache_clear_message_label_3 = Bu, önbelleğe yükleme/kaydetme işlemini biraz hızlandırabilir. cache_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. # Show preview preview_image_resize_failure = { $name } adlı resim yeniden boyutlandırılamadı. preview_image_opening_failure = { $name } adlı resim dosyası açılamadı, nedeni: { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Grup: { $current_group }/{ $all_groups } ({ $images_in_group } resim) compare_move_left_button = <- compare_move_right_button = -> ================================================ FILE: czkawka_gui/i18n/uk/czkawka_gui.ftl ================================================ # Window titles window_settings_title = Налаштування window_main_title = Czkawka («Гикавка») window_progress_title = Сканування window_compare_images = Порівняння зображень # General general_ok_button = Гаразд general_close_button = Закрити # Krokiet info dialog krokiet_info_title = Представляємо Krokiet - Нова версія Czkawka krokiet_info_message = Крокієт – це нова, покращена, швидша та надійніша версія Czkawka GTK GUI! Він легше запускається та більш стійкий до змін у системі, оскільки покладається лише на основні бібліотеки, які за замовчуванням доступні на більшості систем. Крокієт також приносить функції, яких немає в Czkawka, включаючи мініатюри в режимі порівняння відео, EXIF очищувач, прогрес переміщення/копіювання/видалення файлів або розширені опції сортування. Спробуйте його та подивіться на різницю! Czkawka продовжуватиме отримувати виправлення помилок та невеликі оновлення від мене, але всі нові функції будуть розроблені виключно для Крокієта, і будь-хто вільний пропонувати нові функції, додавати відсутні режими або розширювати Czkawka далі. P.S.: Це повідомлення має з’явитися лише один раз. Якщо воно з’являється знову, встановіть змінну середовища CZKAWKA_DONT_ANNOY_ME на будь-яке не порожнє значення. # Main window music_title_checkbox = Найменування music_artist_checkbox = Виконавець music_year_checkbox = Рік music_bitrate_checkbox = Бітрейт music_genre_checkbox = Жанр music_length_checkbox = Тривалість music_comparison_checkbox = Приблизне порівняння music_checking_by_tags = Мітки music_checking_by_content = Зміст same_music_seconds_label = Мінімальна тривалість фрагменту same_music_similarity_label = Максимальна різниця music_compare_only_in_title_group = Порівняйте у групах подібних назвах music_compare_only_in_title_group_tooltip = При активованому стані файли групуються за назвою і потім порівнюються між собою. З 10 000 файлів, замість близько 100 зіріх порівнянь, вище є ймовірністю побути близько 20 000 порівнянь. same_music_tooltip = Пошук подібних музичних файлів за його вмістом може бути налаштований за налаштуванням: - Мінімальний час фрагменту, після якого музичні файли можна визначити як схожий - Максимальна різниця між двома тестовими фрагментами —Що ключові з хороших результатів - знайти розумні комбінації цих параметрів, за умов. Встановлення мінімального часу на 5 сек і максимальна різниця складає 1.0, буде шукати майже однакові фрагменти у файлах. Час 20 і максимальна різниця в 6.0, з іншого боку, добре працює для пошуку реміксиксів/живу версії і т. д. За замовчуванням, кожен музичний файл порівнюється один з одним, і це може зайняти багато часу при тестуванні багатьох файлів, так що використовувати референтні папки і вказати, які файли слід порівнювати один з одним (з тією ж кількістю файлів, порівняння відбитків пальців буде швидше 4x, ніж без стандартних папок). music_comparison_checkbox_tooltip = Шукає схожі музичні файли за допомогою ШІ, що використовує машинне навчання для видалення дужок із фраз. Наприклад, якщо ця опція увімкнена, наступні файли будуть вважатися дублікатами: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = З урахуванням регістру duplicate_case_sensitive_name_tooltip = Коли увімкнено, записи групуються, тільки якщо вони повністю збігаються імена з точністю до кожного символу. Наприклад, «ХІТ Дискотека» не збігається з "хіт дискотека". Коли вимкнено, записи групуються незалежно від того, великі або малі літери використовувалися при написанні. Наприклад, «ХІТ Дискотека», «хіт дискотека», «хІт ДиСкОтЕКа» будуть еквівалентні duplicate_mode_size_name_combo_box = Розмір і ім'я duplicate_mode_name_combo_box = Ім'я duplicate_mode_size_combo_box = Розмір duplicate_mode_hash_combo_box = Хеш duplicate_hash_type_tooltip = У програмі Czkawka можна використовувати один із трьох алгоритмів хешування: Blake3 — криптографічна хеш-функція. Використовується за замовчуванням, оскільки дуже швидка. CRC32 — проста хеш-функція. Ще швидше, ніж Blake3, але можливі рідкісні колізії хешів різних файлів. XXH3 — функція, схожа за продуктивністю і надійністю хеша на Blake3 (але вона не криптографічна), тому її можна використовувати замість Blake3. duplicate_check_method_tooltip = На цей час Czkawka пропонує три методи пошуку дублікатів: Ім'я – шукає файли з однаковими іменами. Розмір – шукає файли однакового розміру. Хеш – шукає файли з однаковим вмістом. Цей режим хешує файл, а потім порівнює хеш для пошуку дублікатів. Цей режим є найнадійнішим способом пошуку. Додаток активно використовує кеш, тому друге та подальші сканування одних і тих же даних повинні бути набагато швидшими, ніж перше. image_hash_size_tooltip = Кожне перевірене зображення видає спеціальний хеш, який можна порівнювати один з одним, і невелика різниця між ними означає, що ці зображення є схожими. 8 хешів дуже добре знайти зображення, які є трохи схожими на оригінал. При великому наборі зображень (>1000) значення дає велику кількість хибних позитивних результатів, так що я рекомендую використовувати більший розмір хешу у цьому випадку. 16 - це хеш за замовчуванням, який є досить хороший компроміс між пошуком навіть мало схожих зображень і маючи тільки невелику кількість хеш-зіткнень. 32 і 64 пеші знайдуть лише дуже схожі зображення, але не повинні мати практично неправильних позитивних результатів (можливо, окрім деяких зображень з альфа каналом). image_resize_filter_tooltip = Щоб обчислити хеш зображення, бібліотека має спочатку його перемасштабувати. Залежно від обраного алгоритму отримане зображення, яке використовується при хешуванні, може виглядати трохи інакше. Найшвидший алгоритм з низькою якістю — це метод найближчого сусіда, Nearest. Він увімкнений за замовчуванням, тому при розмірі хешу 16×16 погана якість не помітна. Якщо розмір хешу 8×8, рекомендується будь-який алгоритм, крім Nearest, щоб краще відрізняти подібні зображення в групах. image_hash_alg_tooltip = Користувачі можуть вибирати з одного з багатьох алгоритмів обчислення хешу. Кожен з них має і сильні і слабкі точки, і іноді може призвести до кращого і іноді гірших результатів для різних зображень. Таким чином, щоб визначити найкращу для вас, потрібно ручне тестування. big_files_mode_combobox_tooltip = Дозволяє шукати найменші або найбільші файли big_files_mode_label = Перевірені файли big_files_mode_smallest_combo_box = Найменший big_files_mode_biggest_combo_box = Найбільший main_notebook_duplicates = Файли-дублікати main_notebook_empty_directories = Порожні каталоги main_notebook_big_files = Великі файли main_notebook_empty_files = Порожні файли main_notebook_temporary = Тимчасові файли main_notebook_similar_images = Схожі зображення main_notebook_similar_videos = Схожі відео main_notebook_same_music = Музичні дублікати main_notebook_symlinks = Пошкоджені симв. посилання main_notebook_broken_files = Пошкоджені файли main_notebook_bad_extensions = Помилкові розширення main_tree_view_column_file_name = Ім'я файлу main_tree_view_column_folder_name = Ім'я теки main_tree_view_column_path = Шлях main_tree_view_column_modification = Дата зміни main_tree_view_column_size = Розмір main_tree_view_column_similarity = Подібність main_tree_view_column_dimensions = Розміри main_tree_view_column_title = Найменування main_tree_view_column_artist = Виконавець main_tree_view_column_year = Рік main_tree_view_column_bitrate = Бітрейт main_tree_view_column_length = Тривалість main_tree_view_column_genre = Жанр main_tree_view_column_symlink_file_name = Ім'я файла символьного посилання main_tree_view_column_symlink_folder = Тека символічного посилання main_tree_view_column_destination_path = Шлях призначення main_tree_view_column_type_of_error = Тип помилки main_tree_view_column_current_extension = Поточне розширення main_tree_view_column_proper_extensions = Належне розширення main_tree_view_column_fps = Кадрів в секунду main_tree_view_column_codec = Кодек main_label_check_method = Метод перевірки main_label_hash_type = Тип хешу main_label_hash_size = Розмір хешу main_label_size_bytes = Розмір (байт) main_label_min_size = Мін main_label_max_size = Макс main_label_shown_files = Кількість показаних файлів main_label_resize_algorithm = Алгоритм масштабування main_label_similarity = Подібність{ " " } main_check_box_broken_files_audio = Звук main_check_box_broken_files_pdf = Pdf main_check_box_broken_files_archive = Архів main_check_box_broken_files_image = Зображення main_check_box_broken_files_video = Відео main_check_box_broken_files_video_tooltip = Використовує ffmpeg/ffprobe для валідації відеофайлів. Дуже повільно і може виявляти педантичні помилки, навіть якщо файл відтворюється нормально. check_button_general_same_size = Ігнорувати однаковий розмір check_button_general_same_size_tooltip = Ігнорувати файли з однаковим розміром у результаті - зазвичай це 1:1 дублікатів main_label_size_bytes_tooltip = Розмір файлів, які будуть проскановані # Upper window upper_tree_view_included_folder_column_title = Папки для пошуку upper_tree_view_included_reference_column_title = Містить оригінали upper_recursive_button = Рекурсивний upper_recursive_button_tooltip = Коли увімкнено, будуть шукатися файли, що не знаходяться безпосередньо в корені вибраної папки, тобто в інших підпапках даної папки та їх підпапках. upper_manual_add_included_button = Прописати вручну upper_add_included_button = Додати upper_remove_included_button = Видалити upper_manual_add_excluded_button = Ручне додавання upper_add_excluded_button = Додати upper_remove_excluded_button = Видалити upper_manual_add_included_button_tooltip = Додавати назву теки для пошуку вручну. Додайте декілька шляхів відразу, розділіть їх /home/roman;/home/rozkaz буде додано дві папки/home/roman and /home/rozkaz upper_add_included_button_tooltip = Додати нову директорію для пошуку. upper_remove_included_button_tooltip = Видалити директорію з пошуку. upper_manual_add_excluded_button_tooltip = Додавати виключені назви директорії. Додайте декілька шляхів одночасно, відокремте їх ; /home/roman;/home/krokiet додасть дві папки/home/roman та /home/keokiet upper_add_excluded_button_tooltip = Додати каталог, який виключається з пошуку. upper_remove_excluded_button_tooltip = Видалити каталог з виключених. upper_notebook_items_configuration = Параметри пошуку upper_notebook_excluded_directories = Виключені шляхи upper_notebook_included_directories = Включені шляхи upper_allowed_extensions_tooltip = Розширення, що включаються, повинні бути розділені комами (за замовчуванням шукаються файли з будь-якими розширеннями). Макроси IMAGE, VIDEO, MUSIC, TEXT додають одразу кілька розширень. Приклад використання: «.exe, IMAGE, VIDEO, .rar, 7z» — це означає, що будуть скануватися файли зображень (напр. jpg, png), відео (напр. avi, mp4), exe, rar і 7z. upper_excluded_extensions_tooltip = Список вимкнених файлів, які будуть ігноруватися при скануванні. При використанні дозволених і вимкнених розширень, цей файл має більший пріоритет, тому файл не буде відмітити. upper_excluded_items_tooltip = Виключені елементи повинні містити * wildcard і повинні бути розділені комами. Це повільніше, ніж Excluded Paths, тому використовуйте його обережно. upper_excluded_items = Виключені елементи: upper_allowed_extensions = Дозволені розширення: upper_excluded_extensions = Вимкнені розширення: # Popovers popover_select_all = Виділити все popover_unselect_all = Прибрати всі popover_reverse = Зворотній вибір popover_select_all_except_shortest_path = Виберіть все, крім найкоротшого шляху popover_select_all_except_longest_path = Виберіть все, крім найдовшого шляху popover_select_all_except_oldest = Вибрати всі, крім старіших popover_select_all_except_newest = Вибрати всі, крім найновіших popover_select_one_oldest = Вибрати один найстаріший popover_select_one_newest = Вибрати один найновіший popover_select_custom = Вибрати власний popover_unselect_custom = Скасувати вибір popover_select_all_images_except_biggest = Вибрати всі, крім найбільшого popover_select_all_images_except_smallest = Вибрати всі, крім найменших popover_custom_path_check_button_entry_tooltip = Вибір записів з урахуванням шляху. Приклад: /home/pimpek/rzecz.txt можна знайти за допомогою /home/pim* popover_custom_name_check_button_entry_tooltip = Вибір записів на ім'я файлів. Приклад: /usr/ping/pong.txt можна знайти за допомогою *ong* popover_custom_regex_check_button_entry_tooltip = Вибір записів за допомогою регулярних виразів. У цьому режимі шуканий текст є шлях з ім'ям. Приклад: /usr/bin/ziemniak.txt можна знайти за допомогою виразу /ziem[a-z]+ За замовчуванням використається синтаксис регулярних виразів Rust. Докладніше про це можна прочитати тут: https://docs.rs/regex. popover_custom_case_sensitive_check_button_tooltip = Вмикає пошук з урахуванням регістру. При відключеній опції «/home/*» буде відповідати як «/home/roman», так і «/HoMe/roman». popover_custom_not_all_check_button_tooltip = Заборона вибору всіх записів у групі. Ця опція включена за замовчуванням, тому що в більшості ситуацій вам не треба видаляти і оригінали, і дублікати — зазвичай залишають хоча б один файл. УВАГА. Цей параметр не працює, якщо ви вже вручну вибрали всі результати групи. popover_custom_regex_path_label = Шлях popover_custom_regex_name_label = Ім'я popover_custom_regex_regex_label = Шлях із рег. виразом + ім'я popover_custom_case_sensitive_check_button = З урахуванням регістру popover_custom_all_in_group_label = Не вибирати усі записи в групі popover_custom_mode_unselect = Зняти вибір popover_custom_mode_select = Вибрати власний popover_sort_file_name = Ім'я файлу popover_sort_folder_name = Назва папки popover_sort_full_name = Повне ім'я popover_sort_size = Розмір popover_sort_selection = Вибрані об'єкти popover_invalid_regex = Неприпустимий регулярний вираз popover_valid_regex = Коректний регулярний вираз # Bottom buttons bottom_search_button = Пошук bottom_select_button = Вибрати bottom_delete_button = Видалити bottom_save_button = Зберегти bottom_symlink_button = Симв. посилання bottom_hardlink_button = Жорст. посилання bottom_move_button = Перемістити bottom_sort_button = Сортування bottom_compare_button = Порівняти bottom_search_button_tooltip = Почати пошук bottom_select_button_tooltip = Виберіть запис. Тільки вибрані файли/папки будуть доступні для подальшої обробки. bottom_delete_button_tooltip = Видалити вибрані файли/папки. bottom_save_button_tooltip = Зберегти дані про пошук у файл bottom_symlink_button_tooltip = Створити символьні посилання. Працює лише тоді, коли вибрано не менше двох результатів у групі. Перший результат залишається, а другий та наступні робляться символьними посиланнями на перший. bottom_hardlink_button_tooltip = Створити жорсткі посилання. Працює лише тоді, коли вибрано не менше двох результатів у групі. Перший результат залишається, а другий та наступні робляться жорсткими посиланнями на перший. bottom_hardlink_button_not_available_tooltip = Створити Жорсткі посилання. кнопка вимкнена, тому що не може бути створена. Жорсткі посилання працюють тільки з правами адміністратора на Windows, тому не забудьте запустити додаток як адміністратор. Якщо додаток вже працює з такими привілеями для подібних проблем на GitHub. bottom_move_button_tooltip = Переміщення файлів до вибраного каталогу. Копіює всі файли в теку без збереження структури дерева каталогів. При спробі перемістити два файли з однаковим ім'ям в ту саму теку другий не буде переміщений, і з'явиться повідомлення про помилку. bottom_sort_button_tooltip = Сортує файли/теки відповідно до вибраного методу. bottom_compare_button_tooltip = Порівняйте зображення в групі. bottom_show_errors_tooltip = Показати/приховати нижню текстову панель. bottom_show_upper_notebook_tooltip = Показати/приховати верхню панель блокнота. # Progress Window progress_stop_button = Зупинити progress_stop_additional_message = Припинити запит # About Window about_repository_button_tooltip = Посилання на сторінку репозиторію з вихідним кодом. about_donation_button_tooltip = Посилання на сторінку пожертви. about_instruction_button_tooltip = Посилання на сторінку інструкцій. about_translation_button_tooltip = Посилання на сторінку Crowdin із перекладами додатків. Офіційно підтримуються англійська та польська мови. about_repository_button = Репозиторій about_donation_button = Пожертва about_instruction_button = Інструкція about_translation_button = Переклад # Header header_setting_button_tooltip = Відкриває вікно налаштувань. header_about_button_tooltip = Відкриває діалогове вікно з інформацією про додаток. # Settings ## General settings_number_of_threads = Кількість використаних тем settings_number_of_threads_tooltip = Кількість використаних потоків; встановіть 0, щоб використовувати всі доступні потоки. settings_use_rust_preview = Використовувати зовнішні бібліотеки gtk для завантаження прев'ю settings_use_rust_preview_tooltip = Використання gtk прев'ю іноді може бути швидшим і підтримувати більше форматів, але іноді це може бути зовсім навпаки. Якщо у вас виникли проблеми з завантаженням превью, можна змінити цей параметр. У нелінійних системах рекомендується використовувати цей параметр, тому, що gtk-pixbuf не завжди доступні, тому вимкнення цього параметру не буде завантажувати попередній перегляд деяких зображень. settings_label_restart = Щоб застосувати параметри, необхідно перезапустити додаток! settings_ignore_other_filesystems = Ігнорувати інші файлові системи (лише Linux) settings_ignore_other_filesystems_tooltip = ігнорує файли, які не знаходяться в одній файловій системі, як пошукові каталоги. Працює те саме, що і параметр -xdev у пошуку команди на Linux settings_save_at_exit_button_tooltip = Зберегти конфігурацію в файл під час закриття додатку. settings_load_at_start_button_tooltip = Завантажити конфігурацію з файлу під час відкриття програми. Якщо не ввімкнено, будуть використовуватися налаштування за замовчуванням. settings_confirm_deletion_button_tooltip = Показувати вікно підтвердження при натисканні на кнопку видалення. settings_confirm_link_button_tooltip = Показувати діалогове вікно підтвердження при натисканні на кнопку твердого/символічного посилання. settings_confirm_group_deletion_button_tooltip = Показувати діалогове вікно при спробі видалення всіх записів з групи. settings_show_text_view_button_tooltip = Показувати панель тексту в нижній частині інтерфейсу користувача. settings_use_cache_button_tooltip = Використовувати кеш файлів. settings_save_also_as_json_button_tooltip = Зберігати кеш у формат JSON (читабельний). Його вміст можна змінювати. Кеш із цього файлу буде автоматично прочитаний програмою, якщо бінарний кеш (з розширенням bin) відсутній. settings_use_trash_button_tooltip = Перемістити файли в смітник, а не видаляти їх назавжди. settings_language_label_tooltip = Мова інтерфейсу користувача. settings_save_at_exit_button = Зберегти конфігурацію під час закриття додатку settings_load_at_start_button = Завантажити конфігурацію при відкритті програми settings_confirm_deletion_button = Показувати вікно підтвердження при видаленні будь-якого файлу settings_confirm_link_button = Показувати вікно підтвердження при складній/символьних посилань будь-які файли settings_confirm_group_deletion_button = Показувати вікно підтвердження при видаленні всіх файлів групи settings_show_text_view_button = Показувати нижню текстову панель settings_use_cache_button = Використовувати кеш settings_save_also_as_json_button = Також зберегти кеш у файл JSON settings_use_trash_button = Перемістити видалені файли в смітник settings_language_label = Мова settings_multiple_delete_outdated_cache_checkbutton = Автоматично видаляти застарілі записи кешу settings_multiple_delete_outdated_cache_checkbutton_tooltip = Видалити застарілі результати кеша, що вказують на неіснуючі файли. Коли опція увімкнена, програма перевіряє під час завантаження записів, чи вказують вони на доступні файли (недостачі файли ігноруються). Вимкнення цієї опції допомагає при скануванні файлів на зовнішніх носіях, щоб інформація про них не була очищена під час наступного сканування. За наявності сотень тисяч записів у кеші рекомендується увімкнути цю опцію, щоб прискорити завантаження та збереження кешу на початку та в кінці сканування. settings_notebook_general = Загальні налаштування settings_notebook_duplicates = Дублікати settings_notebook_images = Схожі зображення settings_notebook_videos = Схожий відео ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = Показувати попередній перегляд праворуч (якщо вибрано файл зображення). settings_multiple_image_preview_checkbutton = Показати попередній перегляд зображення settings_multiple_clear_cache_button_tooltip = Очищення застарілих записів кешу вручну. Слід використовувати лише в тому випадку, якщо автоматичне очищення вимкнено. settings_multiple_clear_cache_button = Видалити застарілі результати з кешу. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = Приховати всі файли, крім першого, якщо всі вони вказують на ті самі дані (пов'язані жорстким посиланням). Приклад: якщо (на диску) сім файлів пов'язані жорстким посиланням з певними даними, а ще один файл містить ті ж дані, але на іншому inode, то в засобі пошуку дублікатів будуть показані тільки цей останній унікальний файл і один файл, що є жорстким посиланням. settings_duplicates_minimal_size_entry_tooltip = Встановити мінімальний розмір файлу, що кешується. Вибір меншого значення призведе до створення більшої кількості записів. Це прискорить пошук, але сповільнить завантаження/збереження кешу. settings_duplicates_prehash_checkbutton_tooltip = Включає кешування попереднього хеша (prehash), що обчислюється з невеликої частини файлу, що дозволяє швидше виключати з аналізу файли, що відрізняються. За замовчуванням вимкнено, оскільки в деяких ситуаціях може сповільнюватися. Рекомендується використовувати його при скануванні сотень тисяч або мільйонів файлів, оскільки це може прискорити пошук в рази. settings_duplicates_prehash_minimal_entry_tooltip = Мінімальний розмір кешованого запису. settings_duplicates_hide_hard_link_button = Приховати жорсткі посилання settings_duplicates_prehash_checkbutton = Кешувати попередній хеш settings_duplicates_minimal_size_cache_label = Мінімальний розмір (байт) файлів, що кешуються settings_duplicates_minimal_size_cache_prehash_label = Мінімальний розмір (байт) файлів для кешування попереднього хешу ## Saving/Loading settings settings_saving_button_tooltip = Зберегти поточні налаштування у файл. settings_loading_button_tooltip = Завантажити параметри з файлу і замінити поточні налаштування. settings_reset_button_tooltip = Скинути поточні налаштування до стандартних значень. settings_saving_button = Зберегти конфігурацію settings_loading_button = Завантажити конфігурацію settings_reset_button = Скидання налаштувань ## Opening cache/config folders settings_folder_cache_open_tooltip = Відкрити папку, де зберігаються текстові файли кеша. Зміна файлів кешу може призвести до відображення неправильних результатів, однак зміна шляху може заощадити час, коли переміщується велика кількість файлів в інше місце. Ви можете копіювати ці файли між комп'ютерами, щоб заощадити час на повторному скануванні файлів (звичайно якщо вони мають схожу структуру каталогів). У разі виникнення проблем із кешем ці файли можна видалити. Програма автоматично перестворить їх. settings_folder_settings_open_tooltip = Відкриває теку, де зберігається конфігурація Czkawka. УВАГА. Ручна зміна конфігурації може порушити функціонування програми. settings_folder_cache_open = Відкрити теку кешу settings_folder_settings_open = Відкрити папку налаштувань # Compute results compute_stopped_by_user = Пошук був зупинений користувачем compute_found_duplicates_hash_size = Знайдено { $number_files } дублікати в { $number_groups } групах, які зайняли { $size } за { $time } compute_found_duplicates_name = Знайдено { $number_files } дублікати в { $number_groups } групах за { $time } compute_found_empty_folders = Знайдено { $number_files } порожніх папок в { $time } compute_found_empty_files = Знайдено { $number_files } порожніх файлів за { $time } compute_found_big_files = Знайдено { $number_files } великих файлів за { $time } compute_found_temporary_files = Знайдено { $number_files } тимчасових файлів в { $time } compute_found_images = Знайдено { $number_files } схожих зображень в { $number_groups } групах на { $time } compute_found_videos = Знайдено { $number_files } подібних відео у { $number_groups } групах за { $time } compute_found_music = Знайдено { $number_files } подібних музичних файлів в { $number_groups } групах за { $time } compute_found_invalid_symlinks = Знайдено { $number_files } неприпустимі символьні посилання в { $time } compute_found_broken_files = Знарядно { $number_files } пошкоджених файлів за час { $time } compute_found_bad_extensions = Знайдено { $number_files } файли з недійсними розширеннями в { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Відсканований { $file_number } файл *[other] Відсканований { $file_number } файли } progress_scanning_extension_of_files = Перевірено розширення { $file_checked }/{ $all_files } файлу progress_scanning_broken_files = Перевірено { $file_checked }/{ $all_files } файл ({ $data_checked }/{ $all_data }) progress_scanning_video = Створено відео з { $file_checked }/{ $all_files } progress_creating_video_thumbnails = Створені мініатюри { $file_checked }/{ $all_files } відео progress_scanning_image = Створено зображення { $file_checked }/{ $all_files } ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Порівняо { $file_checked }/{ $all_files } хеш зображення progress_scanning_music_tags_end = Порівняті теґи { $file_checked }/{ $all_files } музичний файл progress_scanning_music_tags = Read tags of { $file_checked }/{ $all_files } music file progress_scanning_music_content_end = Відбиток порівняльного відбитка { $file_checked }/{ $all_files } музичного файлу progress_scanning_music_content = Розраховано відбиток пальця { $file_checked }/{ $all_files } музичного файлу ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Скановано { $folder_number } папку *[other] Скановано { $folder_number } папки } progress_scanning_size = Відсканований розмір файлу { $file_number } progress_scanning_size_name = Відскановане ім'я і розмір файлу { $file_number } progress_scanning_name = Відскановано ім'я файлу { $file_number } progress_analyzed_partial_hash = Проаналізовано частковий хеш { $file_checked }/{ $all_files } файлів ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Проаналізовано повний хеш { $file_checked }/{ $all_files } файлів ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = Завантаження цільового кешу progress_prehash_cache_saving = Збереження цілковитого кешу progress_hash_cache_loading = Завантаження схованки progress_hash_cache_saving = Збереження кешу кешу progress_cache_loading = Завантаження кешу progress_cache_saving = Збереження кешу progress_current_stage = Поточний етап: { " " } progress_all_stages = Усі етапи: { " " } # Saving loading saving_loading_saving_success = Збережено конфігурацію в файл { $name }. saving_loading_saving_failure = Не вдалося зберегти дані конфігурації у файл { $name }, причина { $reason }. saving_loading_reset_configuration = Поточна конфігурація була очищена. saving_loading_loading_success = Установки програми коректно завантажені. saving_loading_failed_to_create_config_file = Не вдалося створити файл налаштувань "{ $path }", причина "{ $reason }". saving_loading_failed_to_read_config_file = Неможливо завантажити конфігурацію з «{ $path }», тому що або такого файлу не існує, або це не файл. saving_loading_failed_to_read_data_from_file = Не вдалося прочитати дані з файлу "{ $path }", причина "{ $reason }". # Other selected_all_reference_folders = Неможливо почати пошук, коли всі каталоги встановлені як папки з орієнтирами searching_for_data = Пошук даних може зайняти деякий час — будь ласка, зачекайте... text_view_messages = ПОВІДОМЛЕННЯ text_view_warnings = ПОПЕРЕДЖЕННЯ text_view_errors = ПОМИЛКИ about_window_motto = Ця програма безкоштовна для використання і завжди буде такою. krokiet_new_app = Чкавка перебуває у режимі технічного обслуговування, а це означає, що фіксуються лише важливі помилки, і жодна нова функція не буде додана. Для нових функцій, будь ласка, ознайомтеся з новим додатком Krokiet (більш стабільним та ефективним та активним додатком). # Various dialog dialogs_ask_next_time = Завжди запитувати symlink_failed = Не вдалося прив'язати { $name } до { $target }, причина { $reason } delete_title_dialog = Підтвердження видалення delete_question_label = Ви впевнені, що бажаєте видалити файли? delete_all_files_in_group_title = Підтвердження видалення всіх файлів у групі delete_all_files_in_group_label1 = У деяких групах обрані всі записи. delete_all_files_in_group_label2 = Ви впевнені, що хочете видалити їх? delete_items_label = Буде видалено файлів: { $items }. delete_items_groups_label = Буде видалено файлів: { $items } (груп: { $groups }). hardlink_failed = Не вдалося жорстке посилання { $name } на { $target }, причина { $reason } hard_sym_invalid_selection_title_dialog = Невірний вибір у деяких групах hard_sym_invalid_selection_label_1 = У деяких групах вибрано лише один запис — вони будуть проігноровані. hard_sym_invalid_selection_label_2 = Щоб жорстко або символьно зв'язати ці файли, необхідно вибрати щонайменше два результати групи. hard_sym_invalid_selection_label_3 = Перший у групі визнаний як оригінал і не буде змінено, але другий та наступні модифіковані. hard_sym_link_title_dialog = Підтвердження посилання hard_sym_link_label = Ви впевнені, що хочете зв'язати ці файли? move_folder_failed = Не вдалося перемістити папку { $name }. Причина: { $reason } move_file_failed = Не вдалося перемістити файл { $name }, причина { $reason } move_files_title_dialog = Виберіть теку, в яку хочете перемістити дубльовані файли move_files_choose_more_than_1_path = Можна вибрати лише один шлях для копіювання дублікатів файлів, але вибрано { $path_number }. move_stats = Вдалося перемістити без помилок елементів: { $num_files }/{ $all_files } save_results_to_file = Збережено результати як з txt, так і з json файлів в папку "{ $name }". search_not_choosing_any_music = Помилка: Ви повинні вибрати принаймні один прапорець з типами пошуку музики. search_not_choosing_any_broken_files = ПОМИЛКА: Ви повинні вибрати принаймні один прапорець з типом пошкоджених файлів. include_folders_dialog_title = Теки для включення exclude_folders_dialog_title = Теки для виключення include_manually_directories_dialog_title = Додати теку вручну cache_properly_cleared = Кеш успішно очищений cache_clear_duplicates_title = Очищення кешу дублікатів cache_clear_similar_images_title = Очищення кешу подібних зображень cache_clear_similar_videos_title = Очищення кеша схожих відео cache_clear_message_label_1 = Ви хочете очистити кеш від застарілих записів? cache_clear_message_label_2 = Ця дія видаляє всі записи кешу, що вказують на недоступні файли. cache_clear_message_label_3 = Це може трохи прискорити завантаження/збереження кешу. cache_clear_message_label_4 = УВАГА. Ця дія видаляє всі кешовані дані від вимкнених зовнішніх дисків. Хеші для файлів на цих носіях потрібно буде згенерувати заново. # Show preview preview_image_resize_failure = Не вдалося змінити розмір зображення { $name }. preview_image_opening_failure = Не вдалося відкрити зображення { $name }. Причина: { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = Група { $current_group }/{ $all_groups } (зображень: { $images_in_group }) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/zh-CN/czkawka_gui.ftl ================================================ # Window titles window_settings_title = 设定 window_main_title = Czkawka - (Hiccup) window_progress_title = 正在扫描 window_compare_images = 比较图像 # General general_ok_button = 确定 general_close_button = 关闭 # Krokiet info dialog krokiet_info_title = Introducing Krokiet - 新版本 Czkawka krokiet_info_message = 克罗基特是 Czkawka GTK GUI 的新、改进、更快、更可靠的版本! 它更易于运行,并且对系统更改更具弹性,因为它只依赖于大多数系统默认可用的核心库。 克罗基特还带来 Czkawka 缺乏的功能,包括视频比较模式下的缩略图、EXIF 清理器、文件移动/复制/删除进度或扩展排序选项。 尝试一下并看看区别! Czkawka 将继续收到我提供的错误修复和小更新,但所有新功能都将专门为克罗基特开发,任何人都可以在添加新功能、添加缺失模式或进一步扩展 Czkawka 时自由贡献。 PS:此消息只应出现一次。如果它再次出现,请将 CZKAWKA_DONT_ANNOY_ME 环境变量设置为任何非空值。. # Main window music_title_checkbox = 标题 music_artist_checkbox = 艺人 music_year_checkbox = 年份 music_bitrate_checkbox = 码率 music_genre_checkbox = 流派 music_length_checkbox = 长度 music_comparison_checkbox = 近似比较 music_checking_by_tags = 标签 music_checking_by_content = 内容 same_music_seconds_label = 最小分片秒持续时间 same_music_similarity_label = 最大差异 music_compare_only_in_title_group = 在相似标题的组中比较 music_compare_only_in_title_group_tooltip = 如果启用,文件按标题分类,然后相互比较。 如果有一亿文件,而不是几乎1亿文件的比较,通常就会有大约2000万份比较。. same_music_tooltip = 通过设置以下内容,可以配置按其内容搜索类似的音乐文件: - 可以识别音乐文件为类似文件的最小碎片时间 - 两个测试片段之间的最大差异度 找到这些参数的合理组合是获得好结果的关键。 将最小时间设置为5秒,最大差异度设置为1.0,将寻找几乎相同的文件碎片。 另一方面,20秒的时间和6.0的最大差异度可以很好地找到混音/现场版本等内容。 默认情况下,每个音乐文件都会与其他音乐文件进行比较,当测试许多文件时,这可能需要很长时间,因此通常最好使用参考文件夹并指定要相互比较的文件(如有相同数量的文件,则比较指纹至少比不使用参考文件夹快4倍)。. music_comparison_checkbox_tooltip = 它使用AI搜索类似的音乐文件,利用机器学习从短语中移除括号。例如,启用此选项后,相关的文件将被视为重复文件: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = 区分大小写 duplicate_case_sensitive_name_tooltip = 启用时,仅当记录具有完全相同的名称时才进行分组,例如 Żołd <-> Żołd 禁用此选项将不检查每个字母是否具有相同的大小写,例如 żoŁD <-> Żołd duplicate_mode_size_name_combo_box = 大小和名称 duplicate_mode_name_combo_box = 名称 duplicate_mode_size_combo_box = 大小 duplicate_mode_hash_combo_box = 哈希 duplicate_hash_type_tooltip = Czkawka提供3种哈希类型: Blake3 - 加密散列函数。这是默认选项,因为它非常快。 CRC32 - 简单的散列函数。这应该比Blake3更快,但极少情况下可能会有一些冲突。 XXH3 - 与Blake3非常相似,性能和哈希质量也很高 (但不是加密的)。因此,这些模式可以很容易地互换使用。. duplicate_check_method_tooltip = 目前,Czkawka提供三种方法来查找重复: 名称 - 查找名称相同的文件。 大小 - 查找大小相同的文件。 哈希 - 查找内容相同的文件。 这种模式会对文件进行哈希计算,然后将哈希值进行比较以查找重复项。这种模式是查找重复项的最安全方法。应用程序大量使用缓存,因此对相同数据进行的第二次及更多次扫描应比第一次更快。. image_hash_size_tooltip = 每张选中的图像都产生了一个可相互比较的特殊哈希。 两者之间的小差别意味着这些图像是相似的。 8 散列尺寸非常适合于找到仅略类似于原始图像的图像。 有了更大的一组图像 (>1000),这将产生大量虚假的正数, 所以我建议在这种情况下使用更大的散列尺寸。 16是默认的散列尺寸,它是一个很好的折衷,既使找到了一些小相似的图像,又仅有少量的散列碰撞。 32和64 散列只找到非常相似的图像,但是几乎不应该有假正数 (可能只有一些带着Alpha 通道的图像)。. image_resize_filter_tooltip = 要计算图像散列,库必须首先调整大小。 在选定的算法上花费, 用来计算散列的结果图像看起来有点不同。 最快使用的算法,也是结果最差的算法,是Nearest。 默认情况下启用它,因为16x16散列大小较低的质量并不真正可见。 使用 8x8 散列大小,建议使用不同于Nearest的算法来拥有更好的图像组。. image_hash_alg_tooltip = 用户可以从许多计算哈希值的算法中选择一种。 每种算法都有强项和弱项,对于不同的图像,有时结果更好,有时结果更差。 因此,为了确定最适合你的算法,需要进行人工测试。. big_files_mode_combobox_tooltip = 允许搜索最小/最大的文件 big_files_mode_label = 已检查的文件 big_files_mode_smallest_combo_box = 最小的 big_files_mode_biggest_combo_box = 最大的 main_notebook_duplicates = 重复文件 main_notebook_empty_directories = 空目录 main_notebook_big_files = 大文件 main_notebook_empty_files = 空文件 main_notebook_temporary = 临时文件 main_notebook_similar_images = 相似图像 main_notebook_similar_videos = 相似视频 main_notebook_same_music = 重复音乐 main_notebook_symlinks = 无效的符号链接 main_notebook_broken_files = 损坏的文件 main_notebook_bad_extensions = 错误的扩展 main_tree_view_column_file_name = 文件名称 main_tree_view_column_folder_name = 文件夹名称 main_tree_view_column_path = 路径 main_tree_view_column_modification = 修改日期 main_tree_view_column_size = 大小 main_tree_view_column_similarity = 相似度 main_tree_view_column_dimensions = 尺寸 main_tree_view_column_title = 标题 main_tree_view_column_artist = 艺人 main_tree_view_column_year = 年份 main_tree_view_column_bitrate = 码率 main_tree_view_column_length = 长度 main_tree_view_column_genre = 流派 main_tree_view_column_symlink_file_name = 符号链接文件名 main_tree_view_column_symlink_folder = 符号链接文件夹 main_tree_view_column_destination_path = 目标路径 main_tree_view_column_type_of_error = 错误类型 main_tree_view_column_current_extension = 当前扩展 main_tree_view_column_proper_extensions = 合适的扩展 main_tree_view_column_fps = FPS main_tree_view_column_codec = 编解码器 main_label_check_method = 检查方法 main_label_hash_type = 哈希类型 main_label_hash_size = 哈希大小 main_label_size_bytes = 大小 (字节) main_label_min_size = 最小值 main_label_max_size = 最大值 main_label_shown_files = 显示的文件数 main_label_resize_algorithm = 调整算法 main_label_similarity = 相似性:{ " " } main_check_box_broken_files_audio = 音频 main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = 归档 main_check_box_broken_files_image = 图像 main_check_box_broken_files_video = 视频 main_check_box_broken_files_video_tooltip = 使用 ffmpeg/ffprobe 验证视频文件。 相当慢,并且可能检测到刻板的错误,即使文件播放正常。. check_button_general_same_size = 忽略相同的大小 check_button_general_same_size_tooltip = 忽略结果中相同大小的文件 - 通常是 1:1 重复 main_label_size_bytes_tooltip = 将用于扫描的文件大小 # Upper window upper_tree_view_included_folder_column_title = 要搜索的文件夹 upper_tree_view_included_reference_column_title = 参考文件夹 upper_recursive_button = 递归 upper_recursive_button_tooltip = 如果选中,也可以搜索未直接置于选定文件夹下的文件。. upper_manual_add_included_button = 手动添加 upper_add_included_button = 添加 upper_remove_included_button = 删除 upper_manual_add_excluded_button = 手动添加 upper_add_excluded_button = 添加 upper_remove_excluded_button = 删除 upper_manual_add_included_button_tooltip = 手动添加目录名。 如需一次性添加多个路径,请用分号;分隔它们 填写 /home/roman;/home/krokiet 将添加 /home/roman 和 /home/kookiet 两个目录 upper_add_included_button_tooltip = 添加新目录进行搜索。. upper_remove_included_button_tooltip = 从搜索中删除目录。. upper_manual_add_excluded_button_tooltip = 手动添加要排除的目录名称。 如需一次性添加多个路径,请用分号;分隔它们 填写 /home/roman;/home/krokiet 将添加 /home/roman 和 /home/kookiet 两个目录 upper_add_excluded_button_tooltip = 添加在搜索中排除的目录。. upper_remove_excluded_button_tooltip = 从排除中删除目录。. upper_notebook_items_configuration = 项目配置 upper_notebook_excluded_directories = 排除路径 upper_notebook_included_directories = 包含路径 upper_allowed_extensions_tooltip = 允许的扩展名必须用逗号分隔(默认情况下所有扩展名都可用)。 还可以使用以下可一次添加多个扩展名的宏:IMAGE、VIDEO、MUSIC、TEXT。 填写 /home/roman;/home/krokiet 将添加 /home/roman 和 /home/kookiet 两个目录 用法示例“.exe、IMAGE、VIDEO、.rar、7z” - 这意味着将扫描图像(例如 jpg、png)、视频(例如 avi、mp4)、exe、rar 和 7z 文件。. upper_excluded_extensions_tooltip = 在扫描中忽略的已禁用文件列表。 在使用允许和禁用的扩展时,这个扩展具有更高的优先级,所以文件将不会被检查。. upper_excluded_items_tooltip = 排除项目必须包含 * 并且应以逗号分隔。 这比排除路径慢,因此请谨慎使用。. upper_excluded_items = 排除的项目: upper_allowed_extensions = 允许的扩展: upper_excluded_extensions = 已禁用扩展: # Popovers popover_select_all = 全部选择 popover_unselect_all = 取消全选 popover_reverse = 反向选择 popover_select_all_except_shortest_path = 选择所有,除了最短路径 popover_select_all_except_longest_path = 选择所有,不包括最长路径 popover_select_all_except_oldest = 选择除最旧以外的所有选项 popover_select_all_except_newest = 选择除最新以外的所有选项 popover_select_one_oldest = 选择一个最旧的 popover_select_one_newest = 选择一个最新的 popover_select_custom = 选择自定义 popover_unselect_custom = 取消选择自定义 popover_select_all_images_except_biggest = 选择除最大以外的所有选项 popover_select_all_images_except_smallest = 选择除最小以外的所有 popover_custom_path_check_button_entry_tooltip = 通过路径选择记录。 示例用法: /home/pimpek/rzecz.txt 可以通过 /home/pim* 找到 popover_custom_name_check_button_entry_tooltip = 按文件名选择记录。 示例用法: /usr/ping/pong.txt 可以通过 *ong* 找到。 popover_custom_regex_check_button_entry_tooltip = 按指定的正则表达式选择记录。 使用此模式,搜索的文本是带有名称的路径。 示例用法: 可以使用 /ziem[a-z]+ 查找 /usr/bin/ziemniak.txt 这使用默认的Rust正则表达式实现。 您可以在此处阅读有关它的更多信息: https://docs.rs/regex。. popover_custom_case_sensitive_check_button_tooltip = 启用大小写检测。 该选项禁用时,/home/* 将同时找到 /HoMe/roman 和 /home/roman。. popover_custom_not_all_check_button_tooltip = 禁止在分组中选择所有记录。 这是默认启用的,因为在大多数情况下, 您不想删除原始文件和重复文件,而是想留下至少一个文件。 警告:如果您已经手动选择了一个组中的所有结果,则此设置不起作用。. popover_custom_regex_path_label = 路径 popover_custom_regex_name_label = 名称 popover_custom_regex_regex_label = 正则表达式路径 + 名称 popover_custom_case_sensitive_check_button = 区分大小写 popover_custom_all_in_group_label = 不在组中选择所有记录 popover_custom_mode_unselect = 取消选择自定义 popover_custom_mode_select = 选择自定义 popover_sort_file_name = 文件名称 popover_sort_folder_name = 文件夹名称 popover_sort_full_name = 全名 popover_sort_size = 大小 popover_sort_selection = 选择 popover_invalid_regex = 正则表达式无效 popover_valid_regex = 正则表达式有效 # Bottom buttons bottom_search_button = 搜索 bottom_select_button = 选择 bottom_delete_button = 删除 bottom_save_button = 保存 bottom_symlink_button = 软链接 bottom_hardlink_button = 硬链接 bottom_move_button = 移动 bottom_sort_button = 排序 bottom_compare_button = 比较 bottom_search_button_tooltip = 开始搜索 bottom_select_button_tooltip = 选择记录。只能稍后处理选定的文件/文件夹。. bottom_delete_button_tooltip = 删除选中的文件/文件夹。. bottom_save_button_tooltip = 保存搜索数据到文件 bottom_symlink_button_tooltip = 创建软链接。 只有在至少选择了一组中的两个结果时才起作用。 第一个结果保持不变,第二个及后续结果都会被软链接到第一个结果上。. bottom_hardlink_button_tooltip = 创建硬链接。 只有在至少选择了一组中的两个结果时才起作用。 第一个结果保持不变,第二个及后续结果都会被硬链接到第一个结果上。. bottom_hardlink_button_not_available_tooltip = 创建硬链接。 按钮已禁用,因为无法创建硬链接。 在 Windows 上,只有使用管理员权限才能使用硬链接,所以请确保以管理员身份运行该应用程序。 如果应用程序已经具有管理员权限,请在 Github 上查找类似的问题。. bottom_move_button_tooltip = 移动文件到选定的目录。 它复制所有文件到目录,而不保留目录树。 试图将两个具有相同名称的文件移动到文件夹时,第二个将失败并显示错误。. bottom_sort_button_tooltip = 根据选定的方法排序文件/文件夹。. bottom_compare_button_tooltip = 比较群组中的图像。. bottom_show_errors_tooltip = 显示/隐藏底部文本面板。. bottom_show_upper_notebook_tooltip = 显示/隐藏主笔记本面板。. # Progress Window progress_stop_button = 停止 progress_stop_additional_message = 停止请求 # About Window about_repository_button_tooltip = 链接到源代码的仓库页面。. about_donation_button_tooltip = 链接到捐赠页面。. about_instruction_button_tooltip = 链接到指令页面。. about_translation_button_tooltip = 链接到带有应用程序翻译的 Crowdin 页面。官方支持波兰语和英语。. about_repository_button = 存储库 about_donation_button = 捐助 about_instruction_button = 说明 about_translation_button = 翻译 # Header header_setting_button_tooltip = 打开设置对话框。. header_about_button_tooltip = 打开包含应用程序信息的对话框。. # Settings ## General settings_number_of_threads = 使用的线程数 settings_number_of_threads_tooltip = 使用的线程数,0表示所有可用线程都将被使用。. settings_use_rust_preview = 使用外部库来加载预览 settings_use_rust_preview_tooltip = 使用 gtk 预览有时会更快,支持更多格式,但有时恰恰相反。 如果您在加载预览时遇到问题,您可以尝试更改此设置。 关于非Linux系统,建议使用此选项。 因为gtk-pixbuf 并不总是可用的,所以禁用此选项不会加载某些图像的预览。. settings_label_restart = 您需要重新启动应用才能应用设置! settings_ignore_other_filesystems = 忽略其它文件系统 (仅限Linux) settings_ignore_other_filesystems_tooltip = 忽略与搜索的目录不在同一个文件系统中的文件。 在 Linux 上查找命令时类似-xdev选项 settings_save_at_exit_button_tooltip = 关闭应用时将配置保存到文件。. settings_load_at_start_button_tooltip = 打开应用程序时从文件加载配置。 如果未启用,将使用默认设置。. settings_confirm_deletion_button_tooltip = 点击删除按钮时显示确认对话框。. settings_confirm_link_button_tooltip = 点击硬链接/符号链接按钮时显示确认对话框。. settings_confirm_group_deletion_button_tooltip = 尝试从群组中删除所有记录时显示警告对话框。. settings_show_text_view_button_tooltip = 在用户界面底部显示文本面板。. settings_use_cache_button_tooltip = 使用文件缓存。. settings_save_also_as_json_button_tooltip = 保存缓存为 (人类可读) JSON 格式。可以修改其内容。 如果缺少二进制格式缓存(带bin extensional),此文件的缓存将被应用自动读取。. settings_use_trash_button_tooltip = 将文件移至回收站,而不是将其永久删除。. settings_language_label_tooltip = 用户界面的语言。. settings_save_at_exit_button = 关闭应用时保存配置 settings_load_at_start_button = 打开应用程序时加载配置 settings_confirm_deletion_button = 删除任何文件时显示确认对话框 settings_confirm_link_button = 硬/符号链接任何文件时显示确认对话框 settings_confirm_group_deletion_button = 删除组中所有文件时显示确认对话框 settings_show_text_view_button = 显示底部文本面板 settings_use_cache_button = 使用缓存 settings_save_also_as_json_button = 同时将缓存保存为 JSON 文件 settings_use_trash_button = 移动已删除的文件到回收站 settings_language_label = 语言 settings_multiple_delete_outdated_cache_checkbutton = 自动删除过时的缓存条目 settings_multiple_delete_outdated_cache_checkbutton_tooltip = 删除指向不存在文件的过期缓存结果。 当启用时,应用程序确保在加载记录时所有记录都指向有效文件 (无法访问的文件将被忽略)。 禁用此功能将有助于扫描外部驱动器上的文件时,避免在下一次扫描时清除与其相关的缓存条目。 如果缓存中有数十万条记录,则建议启用此功能。这将加快扫描开始/结束时的缓存加载/保存速度。. settings_notebook_general = 概况 settings_notebook_duplicates = 重复项 settings_notebook_images = 相似图像 settings_notebook_videos = 相似视频 ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = 在右侧显示预览 (当选择图像文件时)。. settings_multiple_image_preview_checkbutton = 显示图像预览 settings_multiple_clear_cache_button_tooltip = 手动清除过时条目的缓存。 仅在禁用自动清除时才使用。. settings_multiple_clear_cache_button = 从缓存中删除过时的结果。. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = 隐藏除一个以外的所有文件,如果所有文件都指向同一数据(即为硬链接)。 示例:如果(磁盘上)有七个文件硬链接到特定数据,而一个不同文件具有相同数据但不同 inode,则在重复查找器中,将仅显示一个唯一文件和一个来自硬链接文件的文件。. settings_duplicates_minimal_size_entry_tooltip = 设置将被缓存的最小文件大小。 选择较小的值将会生成更多的记录。这将加快搜索速度,但会减慢缓存的加载/保存速度。. settings_duplicates_prehash_checkbutton_tooltip = 启用预散列缓存 (从文件的一小部分计算出的哈希),以便更早地排除非重复结果。 默认情况下禁用它,因为在某些情况下可能会导致减慢速度。 强烈建议在扫描数十万或数百万个文件时使用它,因为它可以使搜索速度提高数倍。. settings_duplicates_prehash_minimal_entry_tooltip = 缓存条目的最小尺寸。. settings_duplicates_hide_hard_link_button = 隐藏硬链接 settings_duplicates_prehash_checkbutton = 使用捕捉缓存 settings_duplicates_minimal_size_cache_label = 保存到缓存的最小文件大小 (字节) settings_duplicates_minimal_size_cache_prehash_label = 文件最小尺寸(字节) 保存到逮捕缓存 ## Saving/Loading settings settings_saving_button_tooltip = 保存当前设置配置到文件。. settings_loading_button_tooltip = 从文件加载设置并替换当前配置。. settings_reset_button_tooltip = 重置当前配置为默认设置。. settings_saving_button = 保存配置 settings_loading_button = 加载配置 settings_reset_button = 重置配置 ## Opening cache/config folders settings_folder_cache_open_tooltip = 打开存储缓存的txt文件的文件夹。 修改缓存文件可能会导致显示无效的结果。然而,当将大量文件移动到另一个位置时,修改路径可能会节省时间。 您可以在计算机之间复制这些文件,以节省再次扫描文件的时间 (当然,如果它们具有相似的目录结构)。 如果出现缓存问题,可以删除这些文件。该应用程序将自动重新生成它们。. settings_folder_settings_open_tooltip = 打开保存Czkawka配置的文件夹。 警告:手动修改配置可能会破坏您的工作流程。. settings_folder_cache_open = 打开缓存文件夹 settings_folder_settings_open = 打开设置文件夹 # Compute results compute_stopped_by_user = 搜索已被用户停止 compute_found_duplicates_hash_size = 找到 { $number_files } 重复的 { $number_groups } 个小组,这些小组在 { $size } 中占用了 { $time } compute_found_duplicates_name = 在 { $number_groups } 组中找到 { $number_files } 重复的 { $time } compute_found_empty_folders = 找到了 { $number_files } 个空文件夹在 { $time } compute_found_empty_files = 在{ $time }找到了{ $number_files }个空文件 compute_found_big_files = 在 { $number_files } 中找到 { $time } 大文件 compute_found_temporary_files = 找到了 { $number_files } 个临时文件在 { $time } compute_found_images = 在 { $number_groups } 组中找到 { $number_files } 相似的图像,于 { $time } compute_found_videos = 找到了{ $number_files }个相似视频,在{ $number_groups }组中,耗时{ $time } compute_found_music = 在 { $number_groups } 组中找到 { $number_files } 类似的音乐文件在 { $time } compute_found_invalid_symlinks = 找到了{ $number_files }个无效的符号链接在{ $time } compute_found_broken_files = 在 { $time } 中找到了 { $number_files } 个损坏文件 compute_found_bad_extensions = 在 { $number_files } 中发现无效扩展名的 { $time } # Progress window progress_scanning_general_file = { $file_number -> [one] 已掃描 { $file_number } 個文件 *[other] 已掃描 { $file_number } 個文件 } progress_scanning_extension_of_files = 检查了 { $file_checked }/{ $all_files } 文件的扩展 progress_scanning_broken_files = 签入 { $file_checked }/{ $all_files } 文件({ $data_checked }/{ $all_data }) progress_scanning_video = 对 { $file_checked }/{ $all_files } 视频的哈希值 progress_creating_video_thumbnails = 创建 { $file_checked }/{ $all_files } 视频的缩略图 progress_scanning_image = 对 { $file_checked }/{ $all_files } 图像的哈希值({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = 比较 { $file_checked }/{ $all_files } 图像哈希 progress_scanning_music_tags_end = 对比的 { $file_checked }/{ $all_files } 音乐文件标签 progress_scanning_music_tags = 阅读 { $file_checked }/{ $all_files } 音乐文件的标签 progress_scanning_music_content_end = 比较了 { $file_checked }/{ $all_files } 音乐文件的指纹 progress_scanning_music_content = 计算的 { $file_checked }/{ $all_files } 音乐文件 ({ $data_checked }/{ $all_data } ) 的指纹 progress_scanning_empty_folders = { $folder_number -> [one] 已掃描 { $folder_number } 個資料夾 *[other] 已掃描 { $folder_number } 個資料夾 } progress_scanning_size = 扫描的 { $file_number } 文件大小 progress_scanning_size_name = 扫描的 { $file_number } 文件的名称和大小 progress_scanning_name = 扫描的 { $file_number } 文件名称 progress_analyzed_partial_hash = 分析了 { $file_checked }/{ $all_files } 文件的部分哈希值({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = 分析了 { $file_checked }/{ $all_files } 文件的完整哈希值({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = 正在加载逮捕缓存 progress_prehash_cache_saving = 正在保存抓取缓存 progress_hash_cache_loading = 加载散列缓存 progress_hash_cache_saving = 保存哈希缓存 progress_cache_loading = 加载缓存 progress_cache_saving = 正在保存缓存 progress_current_stage = 当前阶段:{ " " } progress_all_stages = 所有阶段:{ " " } # Saving loading saving_loading_saving_success = 配置保存到文件 { $name }。. saving_loading_saving_failure = 无法将配置数据保存到文件 { $name }, 原因 { $reason }. saving_loading_reset_configuration = 当前配置已被清除。. saving_loading_loading_success = 正确加载应用程序配置。. saving_loading_failed_to_create_config_file = 无法创建配置文件 "{ $path }", 原因"{ $reason }". saving_loading_failed_to_read_config_file = 无法从 "{ $path }" 加载配置,因为它不存在或不是文件。. saving_loading_failed_to_read_data_from_file = 无法从文件读取数据"{ $path }", 原因"{ $reason }". # Other selected_all_reference_folders = 当所有目录被设置为参考文件夹时,无法开始搜索 searching_for_data = 正在搜索数据,可能需要一段时间,请稍候... text_view_messages = 消息 text_view_warnings = 警告 text_view_errors = 错误 about_window_motto = 本程序永久免费。. krokiet_new_app = Czkawka正处于维护模式,这意味着只能修复关键的bug并且不会添加新的功能。 关于新的功能,请查看新的 Krokiet 应用,它更加稳定和性能更强,仍在开发中。. # Various dialog dialogs_ask_next_time = 下次询问 symlink_failed = 无法符号链接 { $name } 到 { $target }, 原因 { $reason } delete_title_dialog = 删除确认 delete_question_label = 您确定要删除文件吗? delete_all_files_in_group_title = 确认删除组中的所有文件 delete_all_files_in_group_label1 = 在某些组中,所有记录都被选中。. delete_all_files_in_group_label2 = 您确定要删除它们吗? delete_items_label = { $items } 文件将被删除。. delete_items_groups_label = 来自 { $groups } 个组中的 { $items } 个文件将被删除。. hardlink_failed = 无法将 { $name } 到 { $target }链接,原因 { $reason } hard_sym_invalid_selection_title_dialog = 对某些组的选择无效 hard_sym_invalid_selection_label_1 = 在某些组中,只选择了一个记录,它将被忽略。. hard_sym_invalid_selection_label_2 = 要能够链接到这些文件,至少需要选择两个组的结果。. hard_sym_invalid_selection_label_3 = 第一个组被承认为原始组别,没有改变,但是第二个组别后来被修改。. hard_sym_link_title_dialog = 链接确认 hard_sym_link_label = 您确定要链接这些文件吗? move_folder_failed = 无法移动文件夹 { $name }, 原因 { $reason } move_file_failed = 移动文件 { $name } 失败,原因 { $reason } move_files_title_dialog = 选择要移动重复文件的文件夹 move_files_choose_more_than_1_path = 只能选择一个路径来复制重复的文件,选择 { $path_number }。. move_stats = 正确移动 { $num_files }/{ $all_files } 个项目 save_results_to_file = 将结果保存到 txt 和 json 文件到 "{ $name }" 文件夹。. search_not_choosing_any_music = 错误:您必须选择至少一个带有音乐搜索类型的复选框。. search_not_choosing_any_broken_files = 错误:您必须选择至少一个带有选中文件类型的复选框。. include_folders_dialog_title = 要包含的文件夹 exclude_folders_dialog_title = 要排除的文件夹 include_manually_directories_dialog_title = 手动添加目录 cache_properly_cleared = 已正确清除缓存 cache_clear_duplicates_title = 清除重复缓存 cache_clear_similar_images_title = 清除相似图像缓存 cache_clear_similar_videos_title = 正在清除类似视频缓存 cache_clear_message_label_1 = 您想要清除过时条目的缓存吗? cache_clear_message_label_2 = 此操作将删除所有指向无效文件的缓存项。. cache_clear_message_label_3 = 这可能会稍微加速加载/保存到缓存。. cache_clear_message_label_4 = 警告:操作将从未接入的外部驱动器中移除所有缓存数据。所以每个散列都需要重新生成。. # Show preview preview_image_resize_failure = 调整图像 { $name } 的大小失败. preview_image_opening_failure = 打开镜像 { $name } 失败,原因 { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = 组 { $current_group }/{ $all_groups } ({ $images_in_group } 图像) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n/zh-TW/czkawka_gui.ftl ================================================ # Window titles window_settings_title = 設定 window_main_title = Czkawka window_progress_title = 掃描中 window_compare_images = 比較影像 # General general_ok_button = 確定 general_close_button = 關閉 # Krokiet info dialog krokiet_info_title = 介紹 Krokiet - Czkawka 的新版本 krokiet_info_message = 考基特是新的、改良的、更快速且更可靠的Czkawka GTK GUI版本! 它更容易運行,並且更能抵抗系統變動,因為它僅依賴於大多數系統預設可用的核心函式庫。 考基特還帶來了Czkawka所沒有的功能,包括影片比較模式下的縮圖、EXIF清理器、檔案移動/複製/刪除進度或擴展的排序選項。 試試看,看看區別吧! Czkawka將繼續由我提供錯誤修復和輕微更新,但所有新功能都將專門為考基特開發,任何人都可以在自由地貢獻新的功能、添加缺失的模式或進一步擴展Czkawka。 PS:這則訊息只應該出現一次。如果它再次出現,請將CZKAWKA_DONT_ANNOY_ME環境變數設定為任何非空值。. # Main window music_title_checkbox = 標題 music_artist_checkbox = 藝人 music_year_checkbox = 年份 music_bitrate_checkbox = 位元率 music_genre_checkbox = 類型 music_length_checkbox = 長度 music_comparison_checkbox = 近似比較 music_checking_by_tags = 標籤 music_checking_by_content = 內容 same_music_seconds_label = 最小片段秒數 same_music_similarity_label = 最大差異 music_compare_only_in_title_group = 比較相同標題類群之間 music_compare_only_in_title_group_tooltip = 當啟用時,檔案會按照標題分組然後相互比較。 有萬個檔案,通常會有的近乎十億次比較,反之只會約有二萬次比較。. same_music_tooltip = 透過以下設定,可以根據內容搜尋相似的音樂檔案: - 音樂檔案在超過最小片段時間後可以被識別為相似 - 兩個測試片段之間允許的最大差異 要得到理想的結果,關鍵是找到這些參數的合適組合。 例如,將最小時間設定為 5 秒,最大差異設定為 1.0,會尋找檔案中幾乎相同的片段。 而設定時間為 20 秒和最大差異為 6.0,則適用於尋找混音版本或現場版本等。 預設情況下,每個音樂檔案都會與其他檔案彼此進行比較,這在測試大量檔案時會非常耗時。因此,通常更建議使用參考資料夾,並明確指定哪些檔案需要相互比較。如果檔案數量相同,使用參考資料夾進行指紋比較的速度至少會比不使用參考資料夾快 4 倍。. music_comparison_checkbox_tooltip = 它利用 AI 搜尋相似的音樂檔案,該 AI 使用機器學習來去除句子中的括號。例如,啟用這個選項後,以下的檔案將被視為重複檔案: Świędziżłób --- Świędziżłób (Remix Lato 2021) duplicate_case_sensitive_name = 區分大小寫 duplicate_case_sensitive_name_tooltip = 啟用後,只有在檔案名稱完全相同的情況下才會將其分組,例如 Żołd <-> Żołd。 停用這個選項則會在不檢查每個字母大小是否相同的情況下進行分組,例如 żoŁD <-> Żołd。 duplicate_mode_size_name_combo_box = 大小和名稱 duplicate_mode_name_combo_box = 名稱 duplicate_mode_size_combo_box = 大小 duplicate_mode_hash_combo_box = 雜湊 duplicate_hash_type_tooltip = Czkawka 提供三種類型的雜湊: Blake3 - 這是一種加密雜湊函式,也是預設選項,主要因為它的計算速度非常快。 CRC32 - 這是一種簡單的雜湊函式。理論上它比 Blake3 更快,雖然機率很低但有時可能會產生碰撞。 XXH3 - 在效能和雜湊品質上與 Blake3 非常相似,但它不是加密型的。因此,這兩種模式可以輕易地互換使用。. duplicate_check_method_tooltip = 目前,Czkawka 提供三種方法來找出重複檔案: 名稱 - 找出名稱相同的檔案。 大小 - 找出大小相同的檔案。 雜湊 - 找出內容相同的檔案。這個模式會先對檔案進行雜湊運算,然後比較這些雜湊值來識別重複檔案。這是找出重複檔案最安全的方式。由於應用程式大量使用快取,對同一組資料進行的第二次及後續掃描會比第一次快得多。. image_hash_size_tooltip = 每個檢查的圖片會產生一個可用來來互相比較的的特定的雜湊值,它們之間些微的差異則代表這些圖片是相似的。 8 雜湊大小是相當不錯用以尋找與原版僅些微相似的圖片。對於更大規模的圖片組(>1000),則會產生大量的誤報,所以在此情形中推薦使用更大的雜湊大小。 16 是預設的雜湊大小是相當不錯的折衷方案,對於尋找即使僅些微相似的圖片,並且只會有少量的雜湊衝突。 32 與 64 雜湊值用於尋找非常相似的圖片,但應該幾乎沒有誤報(也許除了一些具有 Alpha 通道的圖片)。. image_resize_filter_tooltip = 要計算圖片的雜湊值,函式庫必須先對它進行調整大小。 取決於選取的演算法,用於計算雜湊值的圖片將會看起來有些不同。 最快的演算法是 Nearest,但也許會給出最差結果。預設為啟用,因為 16x16 雜湊大小並不是明顯可見的較低品質。 對於 8x8 雜湊大小,建議使用不同於 Nearest 的演算法,以獲得更好的圖片分組。. image_hash_alg_tooltip = 使用者可以從許多計算雜湊值的演算法中選擇一種。 每種演算法都有強項和弱項,對於不同的圖片,有時會有更好的結果,有時會有更差的結果。 因此,為了確定最適合你的演算法,需要進行人工測試。. big_files_mode_combobox_tooltip = 允許搜尋最小/最大的檔案 big_files_mode_label = 已檢查的檔案 big_files_mode_smallest_combo_box = 最小的 big_files_mode_biggest_combo_box = 最大的 main_notebook_duplicates = 重複檔案 main_notebook_empty_directories = 空目錄 main_notebook_big_files = 大檔案 main_notebook_empty_files = 空檔案 main_notebook_temporary = 臨時檔案 main_notebook_similar_images = 相似影像 main_notebook_similar_videos = 相似影片 main_notebook_same_music = 音樂重複 main_notebook_symlinks = 無效的符號連結 main_notebook_broken_files = 損壞的檔案 main_notebook_bad_extensions = 錯誤的副檔名 main_tree_view_column_file_name = 檔案名稱 main_tree_view_column_folder_name = 資料夾名稱 main_tree_view_column_path = 路徑 main_tree_view_column_modification = 修改日期 main_tree_view_column_size = 大小 main_tree_view_column_similarity = 相似度 main_tree_view_column_dimensions = 尺寸 main_tree_view_column_title = 標題 main_tree_view_column_artist = 藝人 main_tree_view_column_year = 年份 main_tree_view_column_bitrate = 位元率 main_tree_view_column_length = 長度 main_tree_view_column_genre = 類型 main_tree_view_column_symlink_file_name = 符號連結檔案名稱 main_tree_view_column_symlink_folder = 符號連結資料夾 main_tree_view_column_destination_path = 目標路徑 main_tree_view_column_type_of_error = 錯誤類型 main_tree_view_column_current_extension = 現有副檔名 main_tree_view_column_proper_extensions = 適當的副檔名 main_tree_view_column_fps = 每秒幀數 main_tree_view_column_codec = 編碼解碼器 main_label_check_method = 檢查方法 main_label_hash_type = 雜湊類型 main_label_hash_size = 雜湊大小 main_label_size_bytes = 大小(位元組) main_label_min_size = 最小 main_label_max_size = 最大 main_label_shown_files = 顯示的檔案數 main_label_resize_algorithm = 調整大小的演算法 main_label_similarity = 相似度:{ " " } main_check_box_broken_files_audio = 音訊 main_check_box_broken_files_pdf = PDF main_check_box_broken_files_archive = 歸檔 main_check_box_broken_files_image = 影像 main_check_box_broken_files_video = 影片 main_check_box_broken_files_video_tooltip = 使用 ffmpeg/ffprobe 驗證影片檔案。相當慢,且可能偵測到刻板錯誤,即使檔案播放正常。. check_button_general_same_size = 忽略相同的大小 check_button_general_same_size_tooltip = 忽略在結果中具有完全相同大小的檔案 - 通常這些是 1:1 的重複 main_label_size_bytes_tooltip = 將用於掃描的檔案大小 # Upper window upper_tree_view_included_folder_column_title = 要搜尋的資料夾 upper_tree_view_included_reference_column_title = 參考資料夾 upper_recursive_button = 遞迴 upper_recursive_button_tooltip = 如果選取,也會搜尋未直接放在選定資料夾下的檔案。. upper_manual_add_included_button = 手動新增 upper_add_included_button = 新增 upper_remove_included_button = 移除 upper_manual_add_excluded_button = 手動新增 upper_add_excluded_button = 新增 upper_remove_excluded_button = 移除 upper_manual_add_included_button_tooltip = 手動新增目錄名稱。 一次新增多個路徑,用分號(;)分隔它們 /home/roman;/home/rozkaz 將新增兩個目錄 /home/roman 和 /home/rozkaz upper_add_included_button_tooltip = 新增新目錄進行搜尋。. upper_remove_included_button_tooltip = 從搜尋中移除目錄。. upper_manual_add_excluded_button_tooltip = 手動新增要排除的目錄名稱。 一次新增多個路徑,請用分號(;)分隔它們 /home/roman;/home/krokiet 將新增兩個目錄 /home/roman 和 /home/krokiet upper_add_excluded_button_tooltip = 新增要在搜尋中排除的目錄。. upper_remove_excluded_button_tooltip = 從排除中移除目錄。. upper_notebook_items_configuration = 項目設定 upper_notebook_excluded_directories = 排除路徑 upper_notebook_included_directories = 包含的路徑 upper_allowed_extensions_tooltip = 允許的副檔名必須用逗號分隔(預設所有可用)。 以下的巨集也可用,可以一次新增多個副檔名:IMAGE, VIDEO, MUSIC, TEXT。 使用範例 ".exe, IMAGE, VIDEO, .rar, 7z" - 這表示將影像檔案(例如 .jpg, .png)、影片檔案(例如 .avi, .mp4)、.exe、.rar 和 .7z 檔案。. upper_excluded_extensions_tooltip = 在掃描中將會被忽略的禁用檔案清單。 當使同時使用允許與禁用兩者時,此項擁有更高的優先等級,所以檔案將不會被檢查。. upper_excluded_items_tooltip = 排除的項目必須包含 * 萬位符號,並且用逗號分隔。 這比排除路徑慢,所以請謹慎使用。. upper_excluded_items = 排除的項目: upper_allowed_extensions = 允許的副檔名: upper_excluded_extensions = 禁用的副檔名: # Popovers popover_select_all = 選擇全部 popover_unselect_all = 取消選擇全部 popover_reverse = 反向選擇 popover_select_all_except_shortest_path = 選擇所有,除最短路徑 popover_select_all_except_longest_path = 選擇所有,不包括最長路徑 popover_select_all_except_oldest = 選擇除最舊以外的全部 popover_select_all_except_newest = 選擇除最新以外的全部 popover_select_one_oldest = 選擇一個最舊的 popover_select_one_newest = 選擇一個最新的 popover_select_custom = 選擇自訂 popover_unselect_custom = 取消選擇自訂 popover_select_all_images_except_biggest = 選擇除最大以外的全部影像 popover_select_all_images_except_smallest = 選擇除最小以外的全部影像 popover_custom_path_check_button_entry_tooltip = 透過路徑選擇記錄。 範例用法: /home/pimpek/rzecz.txt 可以透過 /home/pim* 找到 popover_custom_name_check_button_entry_tooltip = 透過檔名選擇記錄。 範例用法: /usr/ping/pong.txt 可以在 *ong* 中找到。 popover_custom_regex_check_button_entry_tooltip = 透過指定的正規表達式(Regex)來選擇記錄。 在這個模式下,被搜尋的文字是「路徑」加上「名稱」。 範例用法: 使用 /ziem[a-z]+ 可以找到 /usr/bin/ziemniak.txt。 這個功能使用的是 Rust 語言預設的正規表達式實作。更多相關資訊,您可以參考這個網址: https://docs.rs/regex。. popover_custom_case_sensitive_check_button_tooltip = 啟用區分大小寫的偵測。 當此選項停用時,「/home/*」會同時找到「/HoMe/roman」和「/home/roman」。. popover_custom_not_all_check_button_tooltip = 防止在同一群組中全選所有記錄。 這個選項預設是啟用的,主要是因為在多數情況下,您不會想要同時刪除原始檔案和其重複檔,而是會希望至少保留一個檔案。 警告:如果您已經手動全選了某一群組中的所有結果,這個設定將不會生效。. popover_custom_regex_path_label = 路徑 popover_custom_regex_name_label = 名稱 popover_custom_regex_regex_label = 正規表達式路徑 + 名稱 popover_custom_case_sensitive_check_button = 區分大小寫 popover_custom_all_in_group_label = 不要選取群組中的所有記錄 popover_custom_mode_unselect = 取消選擇自訂 popover_custom_mode_select = 選擇自訂 popover_sort_file_name = 檔案名稱 popover_sort_folder_name = 資料夾名稱 popover_sort_full_name = 完整名稱 popover_sort_size = 大小 popover_sort_selection = 選擇 popover_invalid_regex = 正規表達式無效 popover_valid_regex = 正規表達式有效 # Bottom buttons bottom_search_button = 搜尋 bottom_select_button = 選擇 bottom_delete_button = 刪除 bottom_save_button = 儲存 bottom_symlink_button = 符號連結 bottom_hardlink_button = 永久連結 bottom_move_button = 移動 bottom_sort_button = 排序 bottom_compare_button = 比較 bottom_search_button_tooltip = 開始搜尋 bottom_select_button_tooltip = 選擇記錄。只能稍後處理選定的檔案/資料夾。. bottom_delete_button_tooltip = 刪除選取的檔案/資料夾。. bottom_save_button_tooltip = 儲存搜尋資料到檔案 bottom_symlink_button_tooltip = 建立符號連結。 只有在一個群組中至少選擇了兩個結果時才會生效。 第一個檔案保持不變,第二個以及之後的檔案會建立為指向第一個檔案的符號連結。. bottom_hardlink_button_tooltip = 建立永久連結。 只有在一個群組中至少選擇了兩個結果時才會生效。 第一個檔案保持不變,第二個以及之後的檔案會建立為與第一個檔案的永久連結。. bottom_hardlink_button_not_available_tooltip = 建立永久連結。 此按鈕已被停用,因為無法建立永久連結。 在 Windows 上,只有擁有管理員權限才能建立永久連結,請確保以管理員身份執行應用程式。 如果應用程式已經具有對應的權限,請在 GitHub 上查詢相關問題。. bottom_move_button_tooltip = 將檔案移動到指定目錄。 會將所有檔案複製到目錄中,但不會保留原始的目錄結構。 如果試圖將兩個同名檔案移動到同一資料夾,第二個檔案將無法移動並會顯示錯誤。. bottom_sort_button_tooltip = 根據選定的方法排序檔案/資料夾。. bottom_compare_button_tooltip = 比較群組中的圖像。. bottom_show_errors_tooltip = 顯示/隱藏底部文字面板。. bottom_show_upper_notebook_tooltip = 顯示/隱藏主筆記本面板。. # Progress Window progress_stop_button = 停止 progress_stop_additional_message = 已請求停止 # About Window about_repository_button_tooltip = 連結到原始碼的專案。. about_donation_button_tooltip = 連結到贊助頁面。. about_instruction_button_tooltip = 連結到指令頁面。. about_translation_button_tooltip = 連結到帶有應用程式翻譯的 Crowdin 頁面。官方支援波蘭語和英語。. about_repository_button = 儲存庫 about_donation_button = 贊助 about_instruction_button = 說明 about_translation_button = 翻譯 # Header header_setting_button_tooltip = 開啟設定對話方塊。. header_about_button_tooltip = 開啟包含應用程式資訊的對話方塊。. # Settings ## General settings_number_of_threads = 使用的執行緒數 settings_number_of_threads_tooltip = 使用的執行緒數,0 表示所有可用執行緒都將被使用。. settings_use_rust_preview = 使用外部庫 Instead gtk 來加載預覽 settings_use_rust_preview_tooltip = 使用 gtk 預覽通常會較快且支援更多格式,但有時這可能會正好相反。 如果您在載入預覽時遇到問題,您可以嘗試更變這個設定。 於非 Linux 系統中,建議使用此選項,因為 gtk-pixbuf 不總是在這些系統中可用,因而禁用此選項將無法載入某些圖像的預覽。. settings_label_restart = 您需要重新啟動應用程式才能套用設定! settings_ignore_other_filesystems = 忽略其它檔案系統(僅限 Linux) settings_ignore_other_filesystems_tooltip = 忽略與搜尋的目錄不在同一個檔案系統中的檔案。 在 Linux 上查詢命令時類似 -xdev 選項 settings_save_at_exit_button_tooltip = 關閉應用程式時將設定儲存到檔案。. settings_load_at_start_button_tooltip = 開啟應用程式時從檔案載入設定。 如果未啟用,將使用預設設定。. settings_confirm_deletion_button_tooltip = 點選刪除按鈕時顯示確認對話方塊。. settings_confirm_link_button_tooltip = 點選永久連結/符號連結按鈕時顯示確認對話方塊。. settings_confirm_group_deletion_button_tooltip = 嘗試從群組中刪除所有記錄時顯示警告對話方塊。. settings_show_text_view_button_tooltip = 在使用者介面底部顯示文字面板。. settings_use_cache_button_tooltip = 使用檔案快取。. settings_save_also_as_json_button_tooltip = 儲存快取為(人類可讀)JSON 格式。可以修改其內容。 如果缺少二進位制格式快取(帶bin extensional),此檔案的快取將被應用程式自動讀取。. settings_use_trash_button_tooltip = 將檔案移至回收桶,而將其永久刪除。. settings_language_label_tooltip = 使用者介面的語言。. settings_save_at_exit_button = 關閉應用程式時儲存設定 settings_load_at_start_button = 開啟應用程式時載入設定 settings_confirm_deletion_button = 刪除任何檔案時顯示確認對話方塊 settings_confirm_link_button = 硬/符號連結任何檔案時顯示確認對話方塊 settings_confirm_group_deletion_button = 刪除群組中所有檔案時顯示確認對話方塊 settings_show_text_view_button = 顯示底部文字面板 settings_use_cache_button = 使用快取 settings_save_also_as_json_button = 同時將快取儲存為 JSON 檔案 settings_use_trash_button = 移動已刪除的檔案到回收桶 settings_language_label = 語言 settings_multiple_delete_outdated_cache_checkbutton = 自動刪除過時的快取項目 settings_multiple_delete_outdated_cache_checkbutton_tooltip = 刪除指向不存在檔案的過時快取結果。 啟用後,應用程式在載入記錄時會確保所有記錄都指向有效的檔案(無效的檔案會被忽略)。 停用此選項將有助於掃描外部硬碟上的檔案,這樣下次掃描時有關這些檔案的快取項目不會被清除。 若快取中有數十萬條記錄,建議啟用此選項,這將加速掃描開始和結束時的快取載入和儲存。. settings_notebook_general = 一般 settings_notebook_duplicates = 重複項目 settings_notebook_images = 相似影像 settings_notebook_videos = 相似影片 ## Multiple - settings used in multiple tabs settings_multiple_image_preview_checkbutton_tooltip = 在右側顯示預覽(當選擇影像檔案時)。. settings_multiple_image_preview_checkbutton = 顯示影像預覽 settings_multiple_clear_cache_button_tooltip = 手動清除過時項目的快取。 僅在停用自動清除時才應使用。. settings_multiple_clear_cache_button = 從快取中移除過時結果. ## Duplicates settings_duplicates_hide_hard_link_button_tooltip = 如果所有檔案都指向相同的資料(即為永久連結),則隱藏除一個以外的所有檔案。 例如:在有七個檔案與特定資料有永久連結,以及一個具有相同資料但不同 inode 的不同檔案的情況下,重複檔案檢查工具只會顯示一個獨特的檔案和一個來自永久連結的檔案。. settings_duplicates_minimal_size_entry_tooltip = 設定將被快取的最小檔案大小。 選擇較小的值會產生更多記錄。這會加速搜尋,但會減慢快取的載入和儲存。. settings_duplicates_prehash_checkbutton_tooltip = 啟用預先計算的雜湊(從檔案的一小部分計算出來)的快取,這允許更早地排除非重複的結果。 這個選項預設是停用的,因為在某些情況下它可能會造成減速。 當掃描數十萬或百萬個檔案時,強烈建議使用此選項,因為它可以多倍加速搜尋。. settings_duplicates_prehash_minimal_entry_tooltip = 快取項目的最小大小。. settings_duplicates_hide_hard_link_button = 隱藏硬連結 settings_duplicates_prehash_checkbutton = 使用捕捉快取 settings_duplicates_minimal_size_cache_label = 儲存到快取的檔案最小大小(位元組) settings_duplicates_minimal_size_cache_prehash_label = 檔案最小大小(位元組)儲存到逮捕快取 ## Saving/Loading settings settings_saving_button_tooltip = 儲存目前設定設定到檔案。. settings_loading_button_tooltip = 從檔案載入設定並替換目前設定。. settings_reset_button_tooltip = 重設目前設定為預設設定。. settings_saving_button = 儲存設定 settings_loading_button = 載入設定 settings_reset_button = 重設設定 ## Opening cache/config folders settings_folder_cache_open_tooltip = 開啟儲存快取 txt 檔案的資料夾。 修改快取檔案可能會導致顯示無效的結果。然而,如果需要將大量檔案移動到不同位置,修改路徑可能會節省時間。 如果兩台電腦有類似的目錄結構,您可以在它們之間複製這些檔案,以節省重新掃描檔案的時間。 如果快取有問題,這些檔案可以被移除。應用程式會自動重新產生它們。. settings_folder_settings_open_tooltip = 開啟儲存 Czkawka 設定的資料夾。 警告:手動修改設定可能會影響您的工作流程。. settings_folder_cache_open = 開啟快取資料夾 settings_folder_settings_open = 開啟設定資料夾 # Compute results compute_stopped_by_user = 搜尋已被使用者停止 compute_found_duplicates_hash_size = 找到{ $number_files }個重複檔案在{ $number_groups }組中,佔用了{ $size },在{ $time }內 compute_found_duplicates_name = 發現{ $number_files }個重複檔在{ $number_groups }組中於{ $time } compute_found_empty_folders = 找到{ $number_files }個空資料夾在{ $time } compute_found_empty_files = 找到{ $number_files }個空文件在{ $time } compute_found_big_files = 找到{ $number_files }個大檔案在{ $time } compute_found_temporary_files = 找到 { $number_files } 個臨時文件在{ $time } compute_found_images = 找到{ $number_files }張相似圖像在{ $number_groups }組中,在{ $time }內 compute_found_videos = 找到{ $number_files }個相似影片,在{ $number_groups }組中,耗時{ $time } compute_found_music = 找到{ $number_files }首相似音樂檔案在{ $number_groups }組中,在{ $time }內 compute_found_invalid_symlinks = 發現 { $number_files } 個無效的符徵鏈接在 { $time } compute_found_broken_files = 找到 { $number_files } 個損壞檔案在 { $time } compute_found_bad_extensions = 找到{ $number_files }個擴展名無效的檔案在{ $time } # Progress window progress_scanning_general_file = { $file_number -> [one] Scanned { $file_number } file *[other] Scanned { $file_number } files } progress_scanning_extension_of_files = Checked extension of { $file_checked }/{ $all_files } file progress_scanning_broken_files = Checked { $file_checked }/{ $all_files } file ({ $data_checked }/{ $all_data }) progress_scanning_video = Hashed of { $file_checked }/{ $all_files } video progress_creating_video_thumbnails = Created thumbnails of { $file_checked }/{ $all_files } video progress_scanning_image = Hashed of { $file_checked }/{ $all_files } image ({ $data_checked }/{ $all_data }) progress_comparing_image_hashes = Compared { $file_checked }/{ $all_files } image hash progress_scanning_music_tags_end = Compared tags of { $file_checked }/{ $all_files } music file progress_scanning_music_tags = Read tags of { $file_checked }/{ $all_files } music file progress_scanning_music_content_end = Compared fingerprint of { $file_checked }/{ $all_files } music file progress_scanning_music_content = Calculated fingerprint of { $file_checked }/{ $all_files } music file ({ $data_checked }/{ $all_data }) progress_scanning_empty_folders = { $folder_number -> [one] Scanned { $folder_number } folder *[other] Scanned { $folder_number } folders } progress_scanning_size = Scanned size of { $file_number } file progress_scanning_size_name = Scanned name and size of { $file_number } file progress_scanning_name = Scanned name of { $file_number } file progress_analyzed_partial_hash = Analyzed partial hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data }) progress_analyzed_full_hash = Analyzed full hash of { $file_checked }/{ $all_files } files ({ $data_checked }/{ $all_data }) progress_prehash_cache_loading = 正在載入 PreHash 快取 progress_prehash_cache_saving = 正在儲存 PreHash 快取 progress_hash_cache_loading = 正在載入雜湊快取 progress_hash_cache_saving = 正在儲存雜湊快取 progress_cache_loading = 正在載入快取 progress_cache_saving = 正在儲存快取 progress_current_stage = 目前階段:{ " " } progress_all_stages = 所有階段:{ " " } # Saving loading saving_loading_saving_success = 設定儲存到檔案 { $name }。. saving_loading_saving_failure = 失敗將配置資料存儲至檔案 { $name },原因 { $reason }。. saving_loading_reset_configuration = 目前設定已被清除。. saving_loading_loading_success = 正確載入應用程式設定。. saving_loading_failed_to_create_config_file = 無法建立設定檔案 "{ $path }", 原因"{ $reason }". saving_loading_failed_to_read_config_file = 無法從 "{ $path }" 載入設定,因為它不存在或不是檔案。. saving_loading_failed_to_read_data_from_file = 無法從檔案讀取資料"{ $path }", 原因"{ $reason }". # Other selected_all_reference_folders = 當所有目錄被設定為參考資料夾時,無法開始搜尋 searching_for_data = 正在搜尋資料,可能需要一段時間,請稍候... text_view_messages = 訊息 text_view_warnings = 警告 text_view_errors = 錯誤 about_window_motto = 這個程式可以永遠自由使用。. krokiet_new_app = Czkawka處於維護模式,這意味著 hanya 會修復關鍵錯誤而不會添加新功能。對於新功能,請檢視新的Krokietapp,該應用更為穩定且效能更好,並且仍然處於積極開發中。. # Various dialog dialogs_ask_next_time = 下次詢問 symlink_failed = Failed to symlink { $name } to { $target }, reason { $reason } delete_title_dialog = 刪除確認 delete_question_label = 您確定要刪除檔案嗎? delete_all_files_in_group_title = 確認刪除群組中的所有檔案 delete_all_files_in_group_label1 = 在某些群組中,所有記錄都被選取。. delete_all_files_in_group_label2 = 您確定要刪除它們嗎? delete_items_label = { $items } 檔案將被刪除。. delete_items_groups_label = { $items } 檔案來自 { $groups } 群組將被刪除。. hardlink_failed = 無法硬連結 { $name } 至 { $target },理由 { $reason } hard_sym_invalid_selection_title_dialog = 對某些群組的選擇無效 hard_sym_invalid_selection_label_1 = 在某些群組中,只選擇了一個記錄,它將被忽略。. hard_sym_invalid_selection_label_2 = 要能夠連結到這些檔案,至少需要選擇兩個群組的結果。. hard_sym_invalid_selection_label_3 = 第一個群組被承認為原始群組,沒有改變,但是第二個群組後來被修改。. hard_sym_link_title_dialog = 連結確認 hard_sym_link_label = 您確定要連結這些檔案嗎? move_folder_failed = 無法移動資料夾 { $name }, 原因 { $reason } move_file_failed = 移動檔案 { $name } 失敗,原因 { $reason } move_files_title_dialog = 選擇要移動重複檔案的資料夾 move_files_choose_more_than_1_path = 只能選擇一個路徑來複製重複的檔案,選擇 { $path_number }。. move_stats = 正確移動 { $num_files }/{ $all_files } 專案 save_results_to_file = Saved results both to txt and json files into "{ $name }" folder. search_not_choosing_any_music = 錯誤:您必須選擇至少一個帶有音樂搜尋類型的核取方塊。. search_not_choosing_any_broken_files = 錯誤:您必須選擇至少一個帶有選取檔案類型的核取方塊。. include_folders_dialog_title = 要包含的資料夾 exclude_folders_dialog_title = 要排除的資料夾 include_manually_directories_dialog_title = 手動新增目錄 cache_properly_cleared = 已正確清除快取 cache_clear_duplicates_title = 清除重複快取 cache_clear_similar_images_title = 清除相似影像快取 cache_clear_similar_videos_title = 正在清除相似影片快取 cache_clear_message_label_1 = 您想要清除過時項目的快取嗎? cache_clear_message_label_2 = 此操作將刪除所有指向無效檔案的快取項。. cache_clear_message_label_3 = 這可能會稍微加速載入/儲存到快取。. cache_clear_message_label_4 = 警告:操作將從未接入的外部硬碟中移除所有快取資料。所以每個雜湊都需要重新產生。. # Show preview preview_image_resize_failure = 調整影像大小失敗 { $name }. preview_image_opening_failure = 開啟影像 { $name } 失敗,原因 { $reason } # Compare images (L is short Left, R is short Right - they can't take too much space) compare_groups_number = 組 { $current_group }/{ $all_groups } ({ $images_in_group } 影像) compare_move_left_button = L compare_move_right_button = R ================================================ FILE: czkawka_gui/i18n.toml ================================================ # (Required) The language identifier of the language used in the # source code for gettext system, and the primary fallback language # (for which all strings must be present) when using the fluent # system. fallback_language = "en" # Use the fluent localization system. [fluent] # (Required) The path to the assets directory. # The paths inside the assets directory should be structured like so: # `assets_dir/{language}/{domain}.ftl` assets_dir = "i18n" ================================================ FILE: czkawka_gui/src/compute_results.rs ================================================ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; use chrono::DateTime; use crossbeam_channel::Receiver; use czkawka_core::common::model::CheckingMethod; use czkawka_core::common::tool_data::CommonData; use czkawka_core::common::traits::ResultEntry; use czkawka_core::common::{format_time, split_path, split_path_compare}; use czkawka_core::tools::bad_extensions::BadExtensions; use czkawka_core::tools::big_file::BigFile; use czkawka_core::tools::broken_files::BrokenFiles; use czkawka_core::tools::duplicate::DuplicateFinder; use czkawka_core::tools::empty_files::EmptyFiles; use czkawka_core::tools::empty_folder::EmptyFolder; use czkawka_core::tools::invalid_symlinks::InvalidSymlinks; use czkawka_core::tools::same_music::core::format_audio_duration; use czkawka_core::tools::same_music::{MusicSimilarity, SameMusic}; use czkawka_core::tools::similar_images::core::get_string_from_similarity; use czkawka_core::tools::similar_images::{ImagesEntry, SimilarImages}; use czkawka_core::tools::similar_videos::SimilarVideos; use czkawka_core::tools::similar_videos::core::{format_bitrate_opt, format_duration_opt}; use czkawka_core::tools::temporary::Temporary; use fun_time::fun_time; use gtk4::prelude::*; use gtk4::{Entry, ListStore, TextView}; use humansize::{BINARY, format_size}; use rayon::prelude::*; use crate::flg; use crate::gui_structs::common_tree_view::{SharedModelEnum, SubView, TreeViewListStoreTrait}; use crate::gui_structs::gui_data::GuiData; use crate::help_combo_box::IMAGES_HASH_SIZE_COMBO_BOX; use crate::help_functions::{HEADER_ROW_COLOR, MAIN_ROW_COLOR, TEXT_COLOR, print_text_messages_to_text_view, set_buttons}; use crate::helpers::enums::{ BottomButtonsEnum, ColumnsBadExtensions, ColumnsBigFiles, ColumnsBrokenFiles, ColumnsDuplicates, ColumnsEmptyFiles, ColumnsEmptyFolders, ColumnsInvalidSymlinks, ColumnsSameMusic, ColumnsSimilarImages, ColumnsSimilarVideos, ColumnsTemporaryFiles, Message, }; use crate::helpers::list_store_operations::append_row_to_list_store; use crate::notebook_enums::NotebookMainEnum; use crate::notebook_info::NOTEBOOKS_INFO; use crate::opening_selecting_records::{ select_function_always_true, select_function_duplicates, select_function_same_music, select_function_similar_images, select_function_similar_videos, }; // Helper functions for deduplication fn handle_stopped_search(tool: &T, entry_info: &Entry) -> bool { if tool.get_stopped_search() { entry_info.set_text(&flg!("compute_stopped_by_user")); true } else { false } } #[expect(clippy::unnecessary_wraps)] fn finalize_compute>(subview: &SubView, tool: T, items_found: usize) -> Option { subview.shared_model_enum.replace(tool.into()); Some(items_found > 0) } fn conditional_sort_vector(vector: &[T]) -> Vec where T: ResultEntry + Clone + Send, { if vector.len() >= 2 { let mut vector = vector.to_vec(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.get_path(), b.get_path())); vector } else { vector.to_vec() } } fn format_size_and_date(size: u64, modified_date: u64, is_header: bool, is_reference_folder: bool) -> (String, String) { if is_header && !is_reference_folder { (String::new(), String::new()) } else { (format_size(size, BINARY), get_dt_timestamp_string(modified_date)) } } fn get_row_color(is_header: bool) -> &'static str { if is_header { HEADER_ROW_COLOR } else { MAIN_ROW_COLOR } } pub(crate) fn connect_compute_results(gui_data: &GuiData, result_receiver: Receiver) { let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone(); let buttons_search = gui_data.bottom_buttons.buttons_search.clone(); let notebook_main = gui_data.main_notebook.notebook_main.clone(); let entry_info = gui_data.entry_info.clone(); let buttons_array = gui_data.bottom_buttons.buttons_array.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let shared_buttons = gui_data.shared_buttons.clone(); let buttons_names = gui_data.bottom_buttons.buttons_names; let window_progress = gui_data.progress_window.window_progress.clone(); let taskbar_state = gui_data.taskbar_state.clone(); let notebook_upper = gui_data.upper_notebook.notebook_upper.clone(); let button_settings = gui_data.header.button_settings.clone(); let button_app_info = gui_data.header.button_app_info.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); let main_context = glib::MainContext::default(); let _guard = main_context.acquire().expect("Failed to acquire main context"); glib::spawn_future_local(async move { loop { while let Ok(msg) = result_receiver.try_recv() { buttons_search.set_visible(true); notebook_main.set_sensitive(true); notebook_upper.set_sensitive(true); button_settings.set_sensitive(true); button_app_info.set_sensitive(true); window_progress.set_visible(false); taskbar_state.borrow().hide(); let hash_size_index = combo_box_image_hash_size.active().expect("Failed to get active item") as usize; let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index] as u8; let msg_type = msg.get_message_type(); let subview = common_tree_views.get_subview(msg_type); let found_duplicates: Option = match msg { Message::Duplicates(df) => compute_duplicate_finder(df, &entry_info, &text_view_errors, subview), Message::EmptyFolders(ef) => compute_empty_folders(ef, &entry_info, &text_view_errors, subview), Message::EmptyFiles(vf) => compute_empty_files(vf, &entry_info, &text_view_errors, subview), Message::BigFiles(bf) => compute_big_files(bf, &entry_info, &text_view_errors, subview), Message::Temporary(tf) => compute_temporary_files(tf, &entry_info, &text_view_errors, subview), Message::SimilarImages(sf) => compute_similar_images(sf, &entry_info, &text_view_errors, subview, hash_size), Message::SimilarVideos(ff) => compute_similar_videos(ff, &entry_info, &text_view_errors, subview), Message::SameMusic(mf) => compute_same_music(mf, &entry_info, &text_view_errors, subview), Message::InvalidSymlinks(ifs) => compute_invalid_symlinks(ifs, &entry_info, &text_view_errors, subview), Message::BrokenFiles(br) => compute_broken_files(br, &entry_info, &text_view_errors, subview), Message::BadExtensions(be) => compute_bad_extensions(be, &entry_info, &text_view_errors, subview), }; if let Some(found_duplicates) = found_duplicates { set_specific_buttons_as_active(&shared_buttons, msg_type, found_duplicates); set_buttons( &mut *shared_buttons.borrow_mut().get_mut(&msg_type).expect("Failed to borrow buttons"), &buttons_array, &buttons_names, ); } } glib::timeout_future(Duration::from_millis(300)).await; } }); } #[fun_time(message = "compute_bad_extensions", level = "debug")] fn compute_bad_extensions(be: BadExtensions, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&be, entry_info) { return None; } let information = be.get_information(); let text_messages = be.get_text_messages(); let bad_extensions_number = information.number_of_files_with_bad_extension; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(flg!("compute_found_bad_extensions", number_files = bad_extensions_number, time = scanning_time_str).as_str()); } let list_store = subview.tree_view.get_model(); let mut vector = be.get_bad_extensions_files().clone(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); for file_entry in vector { let (directory, file) = split_path(&file_entry.path); let values: [(u32, &dyn ToValue); 7] = [ (ColumnsBadExtensions::SelectionButton as u32, &false), (ColumnsBadExtensions::Name as u32, &file), (ColumnsBadExtensions::Path as u32, &directory), (ColumnsBadExtensions::CurrentExtension as u32, &file_entry.current_extension), (ColumnsBadExtensions::ValidExtensions as u32, &file_entry.proper_extensions_group), (ColumnsBadExtensions::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))), (ColumnsBadExtensions::ModificationAsSecs as u32, &(file_entry.modified_date as i64)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, be, bad_extensions_number) } #[fun_time(message = "compute_broken_files", level = "debug")] fn compute_broken_files(br: BrokenFiles, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&br, entry_info) { return None; } let information = br.get_information(); let text_messages = br.get_text_messages(); let broken_files_number = information.number_of_broken_files; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(flg!("compute_found_broken_files", number_files = broken_files_number, time = scanning_time_str).as_str()); } let list_store = subview.tree_view.get_model(); let mut vector = br.get_broken_files().clone(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); for file_entry in vector { let (directory, file) = split_path(&file_entry.path); let values: [(u32, &dyn ToValue); 6] = [ (ColumnsBrokenFiles::SelectionButton as u32, &false), (ColumnsBrokenFiles::Name as u32, &file), (ColumnsBrokenFiles::Path as u32, &directory), (ColumnsBrokenFiles::ErrorType as u32, &file_entry.error_string), (ColumnsBrokenFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))), (ColumnsBrokenFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, br, broken_files_number) } #[fun_time(message = "compute_invalid_symlinks", level = "debug")] fn compute_invalid_symlinks(ifs: InvalidSymlinks, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&ifs, entry_info) { return None; } let information = ifs.get_information(); let text_messages = ifs.get_text_messages(); let invalid_symlinks = information.number_of_invalid_symlinks; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!("compute_found_invalid_symlinks", number_files = invalid_symlinks, time = scanning_time_str)); } let list_store = subview.tree_view.get_model(); let vector = conditional_sort_vector(ifs.get_invalid_symlinks()); for file_entry in vector { let (directory, file) = split_path(&file_entry.path); let symlink_info = file_entry.symlink_info; let values: [(u32, &dyn ToValue); 7] = [ (ColumnsInvalidSymlinks::SelectionButton as u32, &false), (ColumnsInvalidSymlinks::Name as u32, &file), (ColumnsInvalidSymlinks::Path as u32, &directory), (ColumnsInvalidSymlinks::DestinationPath as u32, &symlink_info.destination_path.to_string_lossy().to_string()), (ColumnsInvalidSymlinks::TypeOfError as u32, &symlink_info.type_of_error.translate()), (ColumnsInvalidSymlinks::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))), (ColumnsInvalidSymlinks::ModificationAsSecs as u32, &(file_entry.modified_date as i64)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, ifs, invalid_symlinks) } #[fun_time(message = "compute_same_music", level = "debug")] fn compute_same_music(mf: SameMusic, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&mf, entry_info) { return None; } if mf.get_use_reference() { subview.tree_view.selection().set_select_function(select_function_always_true); } else { subview.tree_view.selection().set_select_function(select_function_same_music); } let information = mf.get_information(); let text_messages = mf.get_text_messages(); let same_music_number: usize = information.number_of_duplicates; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!( "compute_found_music", number_files = information.number_of_duplicates, number_groups = information.number_of_groups, time = scanning_time_str )); } // Create GUI { let list_store = subview.tree_view.get_model(); let music_similarity = mf.get_params().music_similarity; let is_track_title = (MusicSimilarity::TRACK_TITLE & music_similarity) != MusicSimilarity::NONE; let is_track_artist = (MusicSimilarity::TRACK_ARTIST & music_similarity) != MusicSimilarity::NONE; let is_year = (MusicSimilarity::YEAR & music_similarity) != MusicSimilarity::NONE; let is_bitrate = (MusicSimilarity::BITRATE & music_similarity) != MusicSimilarity::NONE; let is_length = (MusicSimilarity::LENGTH & music_similarity) != MusicSimilarity::NONE; let is_genre = (MusicSimilarity::GENRE & music_similarity) != MusicSimilarity::NONE; if mf.get_use_reference() { let vector = mf.get_similar_music_referenced(); for (base_file_entry, vec_file_entry) in vector { let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry); let (directory, file) = split_path(&base_file_entry.path); same_music_add_to_list_store( &list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, &base_file_entry.track_title, &base_file_entry.track_artist, &base_file_entry.year, base_file_entry.bitrate, &format!("{} kbps", base_file_entry.bitrate), &base_file_entry.genre, &format_audio_duration(base_file_entry.length), true, true, ); for file_entry in vec_file_entry { let (directory, file) = split_path(&file_entry.path); same_music_add_to_list_store( &list_store, &file, &directory, file_entry.size, file_entry.modified_date, &file_entry.track_title, &file_entry.track_artist, &file_entry.year, file_entry.bitrate, &format!("{} kbps", file_entry.bitrate), &file_entry.genre, &format_audio_duration(file_entry.length), false, true, ); } } } else { let vector = mf.get_duplicated_music_entries(); let text: &str = if mf.get_params().check_type == CheckingMethod::AudioTags { "-----" } else { "" }; for vec_file_entry in vector { let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry); same_music_add_to_list_store( &list_store, "", "", 0, 0, if is_track_title { text } else { "" }, if is_track_artist { text } else { "" }, if is_year { text } else { "" }, 0, if is_bitrate { text } else { "" }, if is_genre { text } else { "" }, if is_length { text } else { "" }, true, false, ); for file_entry in vec_file_entry { let (directory, file) = split_path(&file_entry.path); same_music_add_to_list_store( &list_store, &file, &directory, file_entry.size, file_entry.modified_date, &file_entry.track_title, &file_entry.track_artist, &file_entry.year, file_entry.bitrate, &format!("{} kbps", file_entry.bitrate), &file_entry.genre, &format_audio_duration(file_entry.length), false, false, ); } } } print_text_messages_to_text_view(text_messages, text_view_errors); } finalize_compute(subview, mf, same_music_number) } #[fun_time(message = "compute_similar_videos", level = "debug")] fn compute_similar_videos(ff: SimilarVideos, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&ff, entry_info) { return None; } if ff.get_use_reference() { subview.tree_view.selection().set_select_function(select_function_always_true); } else { subview.tree_view.selection().set_select_function(select_function_similar_videos); } let information = ff.get_information(); let text_messages = ff.get_text_messages(); let found_any_duplicates = information.number_of_duplicates > 0; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!( "compute_found_videos", number_files = information.number_of_duplicates, number_groups = information.number_of_groups, time = scanning_time_str )); } // Create GUI { let list_store = subview.tree_view.get_model(); if ff.get_use_reference() { let vec_struct_similar = ff.get_similar_videos_referenced(); for (base_file_entry, vec_file_entry) in vec_struct_similar { let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry); let (directory, file) = split_path(&base_file_entry.path); similar_videos_add_to_list_store( &list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true, base_file_entry.fps, base_file_entry.codec.as_deref(), base_file_entry.bitrate, base_file_entry.width, base_file_entry.height, base_file_entry.duration, ); for file_entry in &vec_file_entry { let (directory, file) = split_path(&file_entry.path); similar_videos_add_to_list_store( &list_store, &file, &directory, file_entry.size, file_entry.modified_date, false, true, file_entry.fps, file_entry.codec.as_deref(), file_entry.bitrate, file_entry.width, file_entry.height, file_entry.duration, ); } } } else { let vec_struct_similar = ff.get_similar_videos(); for vec_file_entry in vec_struct_similar { let vec_file_entry = vector_sort_unstable_entry_by_path(vec_file_entry); similar_videos_add_to_list_store(&list_store, "", "", 0, 0, true, false, None, None, None, None, None, None); for file_entry in &vec_file_entry { let (directory, file) = split_path(&file_entry.path); similar_videos_add_to_list_store( &list_store, &file, &directory, file_entry.size, file_entry.modified_date, false, false, file_entry.fps, file_entry.codec.as_deref(), file_entry.bitrate, file_entry.width, file_entry.height, file_entry.duration, ); } } } print_text_messages_to_text_view(text_messages, text_view_errors); } finalize_compute(subview, ff, found_any_duplicates as usize) } #[fun_time(message = "compute_similar_images", level = "debug")] fn compute_similar_images(sf: SimilarImages, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView, hash_size: u8) -> Option { if handle_stopped_search(&sf, entry_info) { return None; } if sf.get_use_reference() { subview.tree_view.selection().set_select_function(select_function_always_true); } else { subview.tree_view.selection().set_select_function(select_function_similar_images); } let information = sf.get_information(); let text_messages = sf.get_text_messages(); let found_any_duplicates = information.number_of_duplicates > 0; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!( "compute_found_images", number_files = information.number_of_duplicates, number_groups = information.number_of_groups, time = scanning_time_str )); } // Create GUI { let list_store = subview.tree_view.get_model(); if sf.get_use_reference() { let vec_struct_similar: Vec<(ImagesEntry, Vec)> = sf.get_similar_images_referenced().clone(); for (base_file_entry, mut vec_file_entry) in vec_struct_similar { vec_file_entry.sort_by_key(|e| e.difference); // Header let (directory, file) = split_path(&base_file_entry.path); similar_images_add_to_list_store( &list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, &format!("{}x{}", base_file_entry.width, base_file_entry.height), 0, hash_size, true, true, ); for file_entry in &vec_file_entry { let (directory, file) = split_path(&file_entry.path); similar_images_add_to_list_store( &list_store, &file, &directory, file_entry.size, file_entry.modified_date, &format!("{}x{}", file_entry.width, file_entry.height), file_entry.difference, hash_size, false, true, ); } } } else { let vec_struct_similar = sf.get_similar_images().clone(); for mut vec_file_entry in vec_struct_similar { vec_file_entry.sort_by_key(|e| e.difference); similar_images_add_to_list_store(&list_store, "", "", 0, 0, "", 0, 0, true, false); for file_entry in &vec_file_entry { let (directory, file) = split_path(&file_entry.path); similar_images_add_to_list_store( &list_store, &file, &directory, file_entry.size, file_entry.modified_date, &format!("{}x{}", file_entry.width, file_entry.height), file_entry.difference, hash_size, false, false, ); } } } print_text_messages_to_text_view(text_messages, text_view_errors); } finalize_compute(subview, sf, found_any_duplicates as usize) } #[fun_time(message = "compute_temporary_files", level = "debug")] fn compute_temporary_files(tf: Temporary, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&tf, entry_info) { return None; } let information = tf.get_information(); let text_messages = tf.get_text_messages(); let temporary_files_number = information.number_of_temporary_files; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!("compute_found_temporary_files", number_files = temporary_files_number, time = scanning_time_str)); } let list_store = subview.tree_view.get_model(); let mut vector = tf.get_temporary_files().clone(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); for file_entry in vector { let (directory, file) = split_path(&file_entry.path); let values: [(u32, &dyn ToValue); 5] = [ (ColumnsTemporaryFiles::SelectionButton as u32, &false), (ColumnsTemporaryFiles::Name as u32, &file), (ColumnsTemporaryFiles::Path as u32, &directory), (ColumnsTemporaryFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))), (ColumnsTemporaryFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, tf, temporary_files_number) } #[fun_time(message = "compute_big_files", level = "debug")] fn compute_big_files(bf: BigFile, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&bf, entry_info) { return None; } let information = bf.get_information(); let text_messages = bf.get_text_messages(); let biggest_files_number = information.number_of_real_files; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!("compute_found_big_files", number_files = biggest_files_number, time = scanning_time_str)); } let list_store = subview.tree_view.get_model(); let vector = bf.get_big_files(); for file_entry in vector { let (directory, file) = split_path(&file_entry.path); let values: [(u32, &dyn ToValue); 7] = [ (ColumnsBigFiles::SelectionButton as u32, &false), (ColumnsBigFiles::Size as u32, &(format_size(file_entry.size, BINARY))), (ColumnsBigFiles::Name as u32, &file), (ColumnsBigFiles::Path as u32, &directory), (ColumnsBigFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))), (ColumnsBigFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)), (ColumnsBigFiles::SizeAsBytes as u32, &(file_entry.size)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, bf, biggest_files_number) } #[fun_time(message = "compute_empty_files", level = "debug")] fn compute_empty_files(vf: EmptyFiles, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&vf, entry_info) { return None; } let information = vf.get_information(); let text_messages = vf.get_text_messages(); let empty_files_number = information.number_of_empty_files; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!("compute_found_empty_files", number_files = empty_files_number, time = scanning_time_str)); } let list_store = subview.tree_view.get_model(); let vector = conditional_sort_vector(vf.get_empty_files()); for file_entry in vector { let (directory, file) = split_path(&file_entry.path); let values: [(u32, &dyn ToValue); 5] = [ (ColumnsEmptyFiles::SelectionButton as u32, &false), (ColumnsEmptyFiles::Name as u32, &file), (ColumnsEmptyFiles::Path as u32, &directory), (ColumnsEmptyFiles::Modification as u32, &(get_dt_timestamp_string(file_entry.modified_date))), (ColumnsEmptyFiles::ModificationAsSecs as u32, &(file_entry.modified_date as i64)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, vf, empty_files_number) } #[fun_time(message = "compute_empty_folders", level = "debug")] fn compute_empty_folders(ef: EmptyFolder, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&ef, entry_info) { return None; } let information = ef.get_information(); let text_messages = ef.get_text_messages(); let empty_folder_number = information.number_of_empty_folders; let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { entry_info.set_text(&flg!("compute_found_empty_folders", number_files = empty_folder_number, time = scanning_time_str)); } let list_store = subview.tree_view.get_model(); let hashmap = ef.get_empty_folder_list(); let mut vector = hashmap.values().collect::>(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); for fe in vector { let (directory, file) = split_path(&fe.path); let values: [(u32, &dyn ToValue); 5] = [ (ColumnsEmptyFolders::SelectionButton as u32, &false), (ColumnsEmptyFolders::Name as u32, &file), (ColumnsEmptyFolders::Path as u32, &directory), (ColumnsEmptyFolders::Modification as u32, &(get_dt_timestamp_string(fe.modified_date))), (ColumnsEmptyFolders::ModificationAsSecs as u32, &(fe.modified_date)), ]; append_row_to_list_store(&list_store, &values); } print_text_messages_to_text_view(text_messages, text_view_errors); finalize_compute(subview, ef, empty_folder_number) } #[fun_time(message = "compute_duplicate_finder", level = "debug")] fn compute_duplicate_finder(df: DuplicateFinder, entry_info: &Entry, text_view_errors: &TextView, subview: &SubView) -> Option { if handle_stopped_search(&df, entry_info) { return None; } if df.get_use_reference() { subview.tree_view.selection().set_select_function(select_function_always_true); } else { subview.tree_view.selection().set_select_function(select_function_duplicates); } let information = df.get_information(); let text_messages = df.get_text_messages(); let duplicates_number: usize; let duplicates_size: u64; let duplicates_group: usize; match df.get_params().check_method { CheckingMethod::Name => { duplicates_number = information.number_of_duplicated_files_by_name; duplicates_size = 0; duplicates_group = information.number_of_groups_by_name; } CheckingMethod::Hash => { duplicates_number = information.number_of_duplicated_files_by_hash; duplicates_size = information.lost_space_by_hash; duplicates_group = information.number_of_groups_by_hash; } CheckingMethod::Size => { duplicates_number = information.number_of_duplicated_files_by_size; duplicates_size = information.lost_space_by_size; duplicates_group = information.number_of_groups_by_size; } CheckingMethod::SizeName => { duplicates_number = information.number_of_duplicated_files_by_size_name; duplicates_size = information.lost_space_by_size; duplicates_group = information.number_of_groups_by_size_name; } _ => unreachable!(), } let scanning_time_str = format_time(information.scanning_time); if let Some(critical) = text_messages.critical.clone() { entry_info.set_text(&critical); } else { if duplicates_size == 0 { entry_info.set_text( flg!( "compute_found_duplicates_name", number_files = duplicates_number, number_groups = duplicates_group, time = scanning_time_str ) .as_str(), ); } else { entry_info.set_text( flg!( "compute_found_duplicates_hash_size", number_files = duplicates_number, number_groups = duplicates_group, size = format_size(duplicates_size, BINARY), time = scanning_time_str ) .as_str(), ); } } // Create GUI { let list_store = subview.tree_view.get_model(); if df.get_use_reference() { match df.get_params().check_method { CheckingMethod::Name => { let btreemap = df.get_files_with_identical_name_referenced(); for (_name, (base_file_entry, vector)) in btreemap.iter().rev() { let vector = vector_sort_unstable_entry_by_path(vector); let (directory, file) = split_path(&base_file_entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true); } } } CheckingMethod::Hash => { let btreemap = df.get_files_with_identical_hashes_referenced(); for (_size, vectors_vector) in btreemap.iter().rev() { for (base_file_entry, vector) in vectors_vector { let vector = vector_sort_unstable_entry_by_path(vector); let (directory, file) = split_path(&base_file_entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true); } } } } CheckingMethod::Size => { let btreemap = df.get_files_with_identical_size_referenced(); for (_size, (base_file_entry, vector)) in btreemap.iter().rev() { let vector = vector_sort_unstable_entry_by_path(vector); let (directory, file) = split_path(&base_file_entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true); } } } CheckingMethod::SizeName => { let btreemap = df.get_files_with_identical_size_names_referenced(); for (_size, (base_file_entry, vector)) in btreemap.iter().rev() { let vector = vector_sort_unstable_entry_by_path(vector); let (directory, file) = split_path(&base_file_entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, base_file_entry.size, base_file_entry.modified_date, true, true); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, true); } } } _ => panic!(), } } else { match df.get_params().check_method { CheckingMethod::Name => { let btreemap = df.get_files_sorted_by_names(); for (_name, vector) in btreemap.iter().rev() { let vector = vector_sort_unstable_entry_by_path(vector); duplicates_add_to_list_store(&list_store, "", "", 0, 0, true, false); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false); } } } CheckingMethod::Hash => { let btreemap = df.get_files_sorted_by_hash(); for (_size, vectors_vector) in btreemap.iter().rev() { for vector in vectors_vector { let vector = vector_sort_unstable_entry_by_path(vector); duplicates_add_to_list_store(&list_store, "", "", 0, 0, true, false); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false); } } } } CheckingMethod::Size => { let btreemap = df.get_files_sorted_by_size(); for (_size, vector) in btreemap.iter().rev() { let vector = vector_sort_unstable_entry_by_path(vector); duplicates_add_to_list_store(&list_store, "", "", 0, 0, true, false); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false); } } } CheckingMethod::SizeName => { let btreemap = df.get_files_sorted_by_size_name(); for (_size, vector) in btreemap.iter().rev() { let vector = vector_sort_unstable_entry_by_path(vector); duplicates_add_to_list_store(&list_store, "", "", 0, 0, true, false); for entry in vector { let (directory, file) = split_path(&entry.path); duplicates_add_to_list_store(&list_store, &file, &directory, entry.size, entry.modified_date, false, false); } } } _ => panic!(), } } print_text_messages_to_text_view(text_messages, text_view_errors); } finalize_compute(subview, df, duplicates_number) } fn vector_sort_unstable_entry_by_path(vector: &[T]) -> Vec where T: ResultEntry + Clone + Send, { if vector.len() >= 2 { let mut vector = vector.to_vec(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.get_path(), b.get_path())); vector } else { vector.to_vec() } } fn duplicates_add_to_list_store(list_store: &ListStore, file: &str, directory: &str, size: u64, modified_date: u64, is_header: bool, is_reference_folder: bool) { const COLUMNS_NUMBER: usize = 11; let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder); let color = get_row_color(is_header); let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [ (ColumnsDuplicates::ActivatableSelectButton as u32, &(!is_header)), (ColumnsDuplicates::SelectionButton as u32, &false), (ColumnsDuplicates::Size as u32, &size_str), (ColumnsDuplicates::SizeAsBytes as u32, &size), (ColumnsDuplicates::Name as u32, &file), (ColumnsDuplicates::Path as u32, &directory), (ColumnsDuplicates::Modification as u32, &string_date), (ColumnsDuplicates::ModificationAsSecs as u32, &modified_date), (ColumnsDuplicates::Color as u32, &color), (ColumnsDuplicates::IsHeader as u32, &is_header), (ColumnsDuplicates::TextColor as u32, &TEXT_COLOR), ]; append_row_to_list_store(list_store, &values); } fn similar_images_add_to_list_store( list_store: &ListStore, file: &str, directory: &str, size: u64, modified_date: u64, dimensions: &str, similarity: u32, hash_size: u8, is_header: bool, is_reference_folder: bool, ) { const COLUMNS_NUMBER: usize = 13; let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder); let color = get_row_color(is_header); let similarity_string = if is_header { String::new() } else { get_string_from_similarity(similarity, hash_size) }; let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [ (ColumnsSimilarImages::ActivatableSelectButton as u32, &(!is_header)), (ColumnsSimilarImages::SelectionButton as u32, &false), (ColumnsSimilarImages::Similarity as u32, &similarity_string), (ColumnsSimilarImages::Size as u32, &size_str), (ColumnsSimilarImages::SizeAsBytes as u32, &size), (ColumnsSimilarImages::Dimensions as u32, &dimensions), (ColumnsSimilarImages::Name as u32, &file), (ColumnsSimilarImages::Path as u32, &directory), (ColumnsSimilarImages::Modification as u32, &string_date), (ColumnsSimilarImages::ModificationAsSecs as u32, &modified_date), (ColumnsSimilarImages::Color as u32, &color), (ColumnsSimilarImages::IsHeader as u32, &is_header), (ColumnsSimilarImages::TextColor as u32, &TEXT_COLOR), ]; append_row_to_list_store(list_store, &values); } fn similar_videos_add_to_list_store( list_store: &ListStore, file: &str, directory: &str, size: u64, modified_date: u64, is_header: bool, is_reference_folder: bool, fps: Option, codec: Option<&str>, bitrate: Option, width: Option, height: Option, duration: Option, ) { const COLUMNS_NUMBER: usize = 16; let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder); let color = get_row_color(is_header); let fps_str = fps.map(|f| format!("{f:.2}")).unwrap_or_default(); let bitrate_str = format_bitrate_opt(bitrate); let codec_str = codec.unwrap_or_default(); let dimensions = match (width, height) { (Some(w), Some(h)) => format!("{w}x{h}"), _ => "".to_string(), }; let duration_str = format_duration_opt(duration); let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [ (ColumnsSimilarVideos::ActivatableSelectButton as u32, &(!is_header)), (ColumnsSimilarVideos::SelectionButton as u32, &false), (ColumnsSimilarVideos::Size as u32, &size_str), (ColumnsSimilarVideos::SizeAsBytes as u32, &size), (ColumnsSimilarVideos::Fps as u32, &fps_str), (ColumnsSimilarVideos::Codec as u32, &codec_str), (ColumnsSimilarVideos::Bitrate as u32, &bitrate_str), (ColumnsSimilarVideos::Dimensions as u32, &dimensions), (ColumnsSimilarVideos::Duration as u32, &duration_str), (ColumnsSimilarVideos::Name as u32, &file), (ColumnsSimilarVideos::Path as u32, &directory), (ColumnsSimilarVideos::Modification as u32, &string_date), (ColumnsSimilarVideos::ModificationAsSecs as u32, &modified_date), (ColumnsSimilarVideos::Color as u32, &color), (ColumnsSimilarVideos::IsHeader as u32, &is_header), (ColumnsSimilarVideos::TextColor as u32, &TEXT_COLOR), ]; append_row_to_list_store(list_store, &values); } fn same_music_add_to_list_store( list_store: &ListStore, file: &str, directory: &str, size: u64, modified_date: u64, track_title: &str, track_artist: &str, track_year: &str, track_bitrate: u32, bitrate_string: &str, track_genre: &str, track_length: &str, is_header: bool, is_reference_folder: bool, ) { const COLUMNS_NUMBER: usize = 18; let (size_str, string_date) = format_size_and_date(size, modified_date, is_header, is_reference_folder); let color = get_row_color(is_header); let values: [(u32, &dyn ToValue); COLUMNS_NUMBER] = [ (ColumnsSameMusic::ActivatableSelectButton as u32, &(!is_header)), (ColumnsSameMusic::SelectionButton as u32, &false), (ColumnsSameMusic::Size as u32, &size_str), (ColumnsSameMusic::SizeAsBytes as u32, &size), (ColumnsSameMusic::Name as u32, &file), (ColumnsSameMusic::Path as u32, &directory), (ColumnsSameMusic::Title as u32, &track_title), (ColumnsSameMusic::Artist as u32, &track_artist), (ColumnsSameMusic::Year as u32, &track_year), (ColumnsSameMusic::Genre as u32, &track_genre), (ColumnsSameMusic::Bitrate as u32, &bitrate_string), (ColumnsSameMusic::BitrateAsNumber as u32, &track_bitrate), (ColumnsSameMusic::Length as u32, &track_length), (ColumnsSameMusic::Modification as u32, &string_date), (ColumnsSameMusic::ModificationAsSecs as u32, &modified_date), (ColumnsSameMusic::Color as u32, &color), (ColumnsSameMusic::IsHeader as u32, &is_header), (ColumnsSameMusic::TextColor as u32, &TEXT_COLOR), ]; append_row_to_list_store(list_store, &values); } fn get_dt_timestamp_string(timestamp: u64) -> String { DateTime::from_timestamp(timestamp as i64, 0) .expect("Modified date always should be in valid range") .to_string() } fn set_specific_buttons_as_active(buttons_array: &Rc>>>, notebook_enum: NotebookMainEnum, value_to_set: bool) { let mut b_mut = buttons_array.borrow_mut(); let butt = b_mut.get_mut(¬ebook_enum).expect("Failed to borrow buttons"); let allowed_buttons = NOTEBOOKS_INFO[notebook_enum as usize].bottom_buttons; for i in allowed_buttons { *butt.get_mut(i).expect("Failed to borrow buttons") = value_to_set; } } ================================================ FILE: czkawka_gui/src/connect_things/connect_about_buttons.rs ================================================ use gtk4::prelude::*; use log::error; use crate::gui_structs::gui_data::GuiData; const SPONSOR_SITE: &str = "https://github.com/sponsors/qarmin"; const REPOSITORY_SITE: &str = "https://github.com/qarmin/czkawka"; const INSTRUCTION_SITE: &str = "https://github.com/qarmin/czkawka/blob/master/instructions/Instruction.md"; const TRANSLATION_SITE: &str = "https://crwd.in/czkawka"; const KROKIET_SITE: &str = "https://github.com/qarmin/czkawka/tree/master/krokiet"; pub(crate) fn connect_about_buttons(gui_data: &GuiData) { let button_donation = gui_data.about.button_donation.clone(); button_donation.connect_clicked(move |_| { if let Err(e) = open::that(SPONSOR_SITE) { error!("Failed to open sponsor site: {SPONSOR_SITE}, reason {e}"); } }); let button_instruction = gui_data.about.button_instruction.clone(); button_instruction.connect_clicked(move |_| { if let Err(e) = open::that(INSTRUCTION_SITE) { error!("Failed to open instruction site: {INSTRUCTION_SITE}, reason {e}"); } }); let button_krokiet = gui_data.about.button_krokiet.clone(); button_krokiet.connect_clicked(move |_| { if let Err(e) = open::that(KROKIET_SITE) { error!("Failed to open Krokiet site: {KROKIET_SITE}, reason {e}"); } }); let button_repository = gui_data.about.button_repository.clone(); button_repository.connect_clicked(move |_| { if let Err(e) = open::that(REPOSITORY_SITE) { error!("Failed to open repository site: {REPOSITORY_SITE}, reason {e}"); } }); let button_translation = gui_data.about.button_translation.clone(); button_translation.connect_clicked(move |_| { if let Err(e) = open::that(TRANSLATION_SITE) { error!("Failed to open translation site: {TRANSLATION_SITE}, reason {e}"); } }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_compare.rs ================================================ use std::cell::RefCell; use std::rc::Rc; use czkawka_core::common::image::{ImgResizeOptions, get_dynamic_image_from_path}; use czkawka_core::re_exported::FirFilterType; use gdk4::gdk_pixbuf::{InterpType, Pixbuf}; use gtk4::prelude::*; use gtk4::{Align, CheckButton, Orientation, Picture, ScrolledWindow, TreeIter, TreeModel, TreePath, Widget}; use image::DynamicImage; use log::error; use crate::flg; use crate::gtk_traits::WidgetTraits; use crate::gui_structs::common_tree_view::SubView; use crate::gui_structs::gui_data::GuiData; use crate::help_functions::{get_full_name_from_path_name, get_max_file_name}; use crate::helpers::image_operations::{get_pixbuf_from_dynamic_image, resize_pixbuf_dimension}; use crate::helpers::list_store_operations::count_number_of_groups; use crate::notebook_info::NotebookObject; const BIG_PREVIEW_SIZE: i32 = 1024; const SMALL_PREVIEW_SIZE: i32 = 130; pub(crate) fn connect_button_compare(gui_data: &GuiData) { let button_compare = gui_data.bottom_buttons.buttons_compare.clone(); let window_compare = gui_data.compare_images.window_compare.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); let scrolled_window_compare_choose_images = gui_data.compare_images.scrolled_window_compare_choose_images.clone(); let label_group_info = gui_data.compare_images.label_group_info.clone(); let button_go_previous_compare_group = gui_data.compare_images.button_go_previous_compare_group.clone(); let button_go_next_compare_group = gui_data.compare_images.button_go_next_compare_group.clone(); let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone(); let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone(); let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone(); let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone(); let shared_current_path = gui_data.compare_images.shared_current_path.clone(); let shared_image_cache = gui_data.compare_images.shared_image_cache.clone(); let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone(); let image_compare_left = gui_data.compare_images.image_compare_left.clone(); let image_compare_right = gui_data.compare_images.image_compare_right.clone(); let check_button_settings_use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone(); window_compare.set_default_size(700, 700); button_compare.connect_clicked(move |_| { let subview = common_tree_views.get_current_subview(); let model = subview.tree_view.model().expect("Missing tree_view model"); let group_number = count_number_of_groups(subview); if group_number == 0 { return; } // Check selected items let (current_group, tree_path) = get_current_group_and_iter_from_selection(subview); *shared_current_of_groups.borrow_mut() = current_group; *shared_numbers_of_groups.borrow_mut() = group_number; populate_groups_at_start( &subview.nb_object, &model, &shared_current_path, tree_path, &image_compare_left, &image_compare_right, current_group, group_number, &check_button_left_preview_text, &check_button_right_preview_text, &scrolled_window_compare_choose_images, &label_group_info, &shared_image_cache, &shared_using_for_preview, &button_go_previous_compare_group, &button_go_next_compare_group, &check_button_settings_use_rust_preview, ); window_compare.set_visible(true); }); let shared_image_cache = gui_data.compare_images.shared_image_cache.clone(); let shared_current_path = gui_data.compare_images.shared_current_path.clone(); let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone(); let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone(); let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone(); let window_compare = gui_data.compare_images.window_compare.clone(); let image_compare_left = gui_data.compare_images.image_compare_left.clone(); let image_compare_right = gui_data.compare_images.image_compare_right.clone(); window_compare.connect_close_request(move |window_compare| { window_compare.set_visible(false); *shared_image_cache.borrow_mut() = Vec::new(); *shared_current_path.borrow_mut() = None; *shared_current_of_groups.borrow_mut() = 0; *shared_numbers_of_groups.borrow_mut() = 0; *shared_using_for_preview.borrow_mut() = (None, None); image_compare_left.set_pixbuf(None); image_compare_right.set_pixbuf(None); glib::Propagation::Stop }); let button_go_previous_compare_group = gui_data.compare_images.button_go_previous_compare_group.clone(); let button_go_next_compare_group = gui_data.compare_images.button_go_next_compare_group.clone(); let label_group_info = gui_data.compare_images.label_group_info.clone(); let scrolled_window_compare_choose_images = gui_data.compare_images.scrolled_window_compare_choose_images.clone(); let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone(); let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone(); let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone(); let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone(); let shared_current_path = gui_data.compare_images.shared_current_path.clone(); let shared_image_cache = gui_data.compare_images.shared_image_cache.clone(); let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone(); let image_compare_left = gui_data.compare_images.image_compare_left.clone(); let image_compare_right = gui_data.compare_images.image_compare_right.clone(); let check_button_settings_use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); button_go_previous_compare_group.connect_clicked(move |button_go_previous_compare_group| { let sv = common_tree_views.get_current_subview(); let model = sv.get_tree_model(); *shared_current_of_groups.borrow_mut() -= 1; let current_group = *shared_current_of_groups.borrow(); let group_number = *shared_numbers_of_groups.borrow(); let tree_path = move_iter( &model, shared_current_path.borrow().as_ref().expect("Missing current path"), sv.nb_object.column_header.expect("Missing column_header"), false, ); populate_groups_at_start( &sv.nb_object, &model, &shared_current_path, tree_path, &image_compare_left, &image_compare_right, current_group, group_number, &check_button_left_preview_text, &check_button_right_preview_text, &scrolled_window_compare_choose_images, &label_group_info, &shared_image_cache, &shared_using_for_preview, button_go_previous_compare_group, &button_go_next_compare_group, &check_button_settings_use_rust_preview, ); }); let button_go_previous_compare_group = gui_data.compare_images.button_go_previous_compare_group.clone(); let button_go_next_compare_group = gui_data.compare_images.button_go_next_compare_group.clone(); let label_group_info = gui_data.compare_images.label_group_info.clone(); let scrolled_window_compare_choose_images = gui_data.compare_images.scrolled_window_compare_choose_images.clone(); let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone(); let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone(); let shared_current_of_groups = gui_data.compare_images.shared_current_of_groups.clone(); let shared_numbers_of_groups = gui_data.compare_images.shared_numbers_of_groups.clone(); let shared_current_path = gui_data.compare_images.shared_current_path.clone(); let shared_image_cache = gui_data.compare_images.shared_image_cache.clone(); let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone(); let image_compare_left = gui_data.compare_images.image_compare_left.clone(); let image_compare_right = gui_data.compare_images.image_compare_right.clone(); let check_button_settings_use_rust_preview = gui_data.settings.check_button_settings_use_rust_preview.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); button_go_next_compare_group.connect_clicked(move |button_go_next_compare_group| { let sv = common_tree_views.get_current_subview(); let model = sv.get_tree_model(); *shared_current_of_groups.borrow_mut() += 1; let current_group = *shared_current_of_groups.borrow(); let group_number = *shared_numbers_of_groups.borrow(); let tree_path = move_iter( &model, shared_current_path.borrow().as_ref().expect("Missing current path"), sv.nb_object.column_header.expect("Missing column_header"), true, ); populate_groups_at_start( &sv.nb_object, &model, &shared_current_path, tree_path, &image_compare_left, &image_compare_right, current_group, group_number, &check_button_left_preview_text, &check_button_right_preview_text, &scrolled_window_compare_choose_images, &label_group_info, &shared_image_cache, &shared_using_for_preview, &button_go_previous_compare_group, button_go_next_compare_group, &check_button_settings_use_rust_preview, ); }); let button_replace_group = gui_data.compare_images.button_replace_group.clone(); let image_compare_right = gui_data.compare_images.image_compare_right.clone(); let image_compare_left = gui_data.compare_images.image_compare_left.clone(); button_replace_group.connect_clicked(move |_| { let tmp = image_compare_left.paintable(); image_compare_left.set_paintable(image_compare_right.paintable().as_ref()); image_compare_right.set_paintable(tmp.as_ref()); }); let check_button_left_preview_text = gui_data.compare_images.check_button_left_preview_text.clone(); let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone(); let shared_current_path = gui_data.compare_images.shared_current_path.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); check_button_left_preview_text.connect_toggled(move |check_button_left_preview_text| { let sv = common_tree_views.get_current_subview(); let model = sv.get_model(); let main_tree_path = shared_current_path.borrow().as_ref().expect("Missing current path").clone(); let this_tree_path = shared_using_for_preview.borrow().0.clone().expect("Missing left preview path"); if main_tree_path == this_tree_path { return; // Selected header, so we don't need to select result in treeview // TODO this should be handled by disabling entirely check box } let is_active = check_button_left_preview_text.is_active(); model.set_value( &model.iter(&this_tree_path).expect("Using invalid tree_path"), sv.nb_object.column_selection as u32, &is_active.to_value(), ); }); let check_button_right_preview_text = gui_data.compare_images.check_button_right_preview_text.clone(); let shared_using_for_preview = gui_data.compare_images.shared_using_for_preview.clone(); let shared_current_path = gui_data.compare_images.shared_current_path.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); check_button_right_preview_text.connect_toggled(move |check_button_right_preview_text| { let sv = common_tree_views.get_current_subview(); let model = sv.get_model(); let main_tree_path = shared_current_path.borrow().as_ref().expect("Missing current path").clone(); let this_tree_path = shared_using_for_preview.borrow().1.clone().expect("Missing right preview path"); if main_tree_path == this_tree_path { return; // Selected header, so we don't need to select result in treeview // TODO this should be handled by disabling entirely check box } let is_active = check_button_right_preview_text.is_active(); model.set_value( &model.iter(&this_tree_path).expect("Using invalid tree_path"), sv.nb_object.column_selection as u32, &is_active.to_value(), ); }); } fn populate_groups_at_start( nb_object: &NotebookObject, model: &TreeModel, shared_current_path: &Rc>>, tree_path: TreePath, image_compare_left: &Picture, image_compare_right: &Picture, current_group: u32, group_number: u32, check_button_left_preview_text: &CheckButton, check_button_right_preview_text: &CheckButton, scrolled_window_compare_choose_images: &ScrolledWindow, label_group_info: >k4::Label, shared_image_cache: &Rc>>, shared_using_for_preview: &Rc, Option)>>, button_go_previous_compare_group: >k4::Button, button_go_next_compare_group: >k4::Button, check_button_settings_use_rust_preview: &CheckButton, ) { if current_group == 1 { button_go_previous_compare_group.set_sensitive(false); } else { button_go_previous_compare_group.set_sensitive(true); } if current_group == group_number { button_go_next_compare_group.set_sensitive(false); } else { button_go_next_compare_group.set_sensitive(true); } let all_vec = get_all_path( model, &tree_path, nb_object.column_header.expect("Missing column_header"), nb_object.column_path, nb_object.column_name, ); *shared_current_path.borrow_mut() = Some(tree_path); let cache_all_images = generate_cache_for_results(all_vec, check_button_settings_use_rust_preview.is_active()); // This is safe, because cache have at least 2 results image_compare_left.set_paintable(cache_all_images[0].2.paintable().as_ref()); image_compare_right.set_paintable(cache_all_images[1].2.paintable().as_ref()); *shared_using_for_preview.borrow_mut() = (Some(cache_all_images[0].4.clone()), Some(cache_all_images[1].4.clone())); check_button_left_preview_text.set_label(Some(&format!("1. {}", get_max_file_name(&cache_all_images[0].0, 60)))); check_button_right_preview_text.set_label(Some(&format!("2. {}", get_max_file_name(&cache_all_images[1].0, 60)))); label_group_info.set_text( flg!( "compare_groups_number", current_group = current_group, all_groups = group_number, images_in_group = cache_all_images.len() ) .as_str(), ); populate_similar_scrolled_view( scrolled_window_compare_choose_images, &cache_all_images, image_compare_left, image_compare_right, shared_using_for_preview, shared_image_cache, check_button_left_preview_text, check_button_right_preview_text, model, nb_object.column_selection, ); *shared_image_cache.borrow_mut() = cache_all_images.clone(); let mut found = false; for i in scrolled_window_compare_choose_images .child() .expect("Failed to get child of scrolled_window_compare_choose_images") .downcast::() .expect("Failed to downcast to Viewport") .get_all_direct_children() { if i.widget_name() == "all_box" { let gtk_box = i.downcast::().expect("Failed to downcast to Box"); update_bottom_buttons(>k_box, shared_using_for_preview, shared_image_cache); found = true; break; } } assert!(found); let is_active = model.get::(&model.iter(&cache_all_images[0].4).expect("Using invalid tree_path"), nb_object.column_selection); check_button_left_preview_text.set_active(is_active); let is_active = model.get::(&model.iter(&cache_all_images[1].4).expect("Using invalid tree_path"), nb_object.column_selection); check_button_right_preview_text.set_active(is_active); } fn generate_cache_for_results(vector_with_path: Vec<(String, String, TreePath)>, use_rust_loader: bool) -> Vec<(String, String, Picture, Picture, TreePath)> { // TODO use here threads, // For now threads cannot be used because Image and TreeIter cannot be used in threads let mut cache_all_images = Vec::new(); for (full_path, name, tree_path) in vector_with_path { let small_img = Picture::new(); let big_img = Picture::new(); let mut pixbuf = get_pixbuf_from_dynamic_image(DynamicImage::new_rgb8(1, 1)).expect("Failed to create pixbuf"); if use_rust_loader { match get_dynamic_image_from_path( &full_path, Some(ImgResizeOptions { max_width: BIG_PREVIEW_SIZE as u32, max_height: BIG_PREVIEW_SIZE as u32, filter: FirFilterType::Bilinear, }), ) .and_then(|e| get_pixbuf_from_dynamic_image(e.image)) { Ok(t) => { pixbuf = t; } Err(e) => { error!("Failed to open image {full_path}, reason {e}"); } } } else { match Pixbuf::from_file(&full_path) { Ok(t) => { pixbuf = t; } Err(e) => { error!("Failed to open image {full_path}, reason {e}"); } } } #[expect(clippy::never_loop)] loop { let Some(pixbuf_big) = resize_pixbuf_dimension(&pixbuf, (BIG_PREVIEW_SIZE, BIG_PREVIEW_SIZE), InterpType::Bilinear) else { error!("Failed to resize image {full_path}."); break; }; let Some(pixbuf_small) = resize_pixbuf_dimension(&pixbuf_big, (SMALL_PREVIEW_SIZE, SMALL_PREVIEW_SIZE), InterpType::Bilinear) else { error!("Failed to resize image {full_path}."); break; }; big_img.set_pixbuf(Some(&pixbuf_big)); small_img.set_pixbuf(Some(&pixbuf_small)); break; } cache_all_images.push((full_path, name, big_img, small_img, tree_path)); } cache_all_images } fn get_all_path(model: &TreeModel, current_path: &TreePath, column_header: i32, column_path: i32, column_name: i32) -> Vec<(String, String, TreePath)> { let mut used_iter = model.iter(current_path).expect("Using invalid tree_path"); assert!(model.get::(&used_iter, column_header)); let using_reference = !model.get::(&used_iter, column_path).is_empty(); let mut returned_vector = Vec::new(); if using_reference { let name = model.get::(&used_iter, column_name); let path = model.get::(&used_iter, column_path); let full_name = get_full_name_from_path_name(&path, &name); returned_vector.push((full_name, name, model.path(&used_iter))); } assert!(model.iter_next(&mut used_iter), "Found only header!"); loop { let name = model.get::(&used_iter, column_name); let path = model.get::(&used_iter, column_path); let full_name = get_full_name_from_path_name(&path, &name); returned_vector.push((full_name, name, model.path(&used_iter))); if !model.iter_next(&mut used_iter) { break; } if model.get::(&used_iter, column_header) { break; } } assert!(returned_vector.len() > 1); returned_vector } fn move_iter(model: &TreeModel, tree_path: &TreePath, column_header: i32, go_next: bool) -> TreePath { let mut tree_iter = model.iter(tree_path).expect("Using invalid tree_path"); assert!(model.get::(&tree_iter, column_header)); if go_next { assert!(model.iter_next(&mut tree_iter), "Found only header!"); } else { assert!(model.iter_previous(&mut tree_iter), "Found only header!"); } loop { if go_next { if !model.iter_next(&mut tree_iter) { break; } } else if !model.iter_previous(&mut tree_iter) { break; } if model.get::(&tree_iter, column_header) { break; } } model.path(&tree_iter) } fn populate_similar_scrolled_view( scrolled_window: &ScrolledWindow, image_cache: &[(String, String, Picture, Picture, TreePath)], image_compare_left: &Picture, image_compare_right: &Picture, shared_using_for_preview: &Rc, Option)>>, shared_image_cache: &Rc>>, check_button_left_preview_text: &CheckButton, check_button_right_preview_text: &CheckButton, model: &TreeModel, column_selection: i32, ) { scrolled_window.set_child(None::<&Widget>); let all_gtk_box = gtk4::Box::new(Orientation::Horizontal, 5); all_gtk_box.set_widget_name("all_box"); all_gtk_box.set_halign(Align::Fill); all_gtk_box.set_valign(Align::Fill); for (number, (path, _name, big_thumbnail, small_thumbnail, tree_path)) in image_cache.iter().enumerate() { let small_box = gtk4::Box::new(Orientation::Vertical, 3); let smaller_box = gtk4::Box::new(Orientation::Horizontal, 2); let button_left = gtk4::Button::builder().label(&flg!("compare_move_left_button")).build(); let label = gtk4::Label::builder().label((number + 1).to_string()).build(); let button_right = gtk4::Button::builder().label(&flg!("compare_move_right_button")).build(); let image_compare_left = image_compare_left.clone(); let image_compare_right = image_compare_right.clone(); let big_thumbnail_clone = big_thumbnail.clone(); let tree_path_clone = tree_path.clone(); let all_gtk_box_clone = all_gtk_box.clone(); let shared_using_for_preview_clone = shared_using_for_preview.clone(); let shared_image_cache_clone = shared_image_cache.clone(); let check_button_left_preview_text_clone = check_button_left_preview_text.clone(); let model_clone = model.clone(); let path_clone = path.clone(); button_left.connect_clicked(move |_button_left| { shared_using_for_preview_clone.borrow_mut().0 = Some(tree_path_clone.clone()); update_bottom_buttons(&all_gtk_box_clone, &shared_using_for_preview_clone, &shared_image_cache_clone); image_compare_left.set_paintable(big_thumbnail_clone.paintable().as_ref()); let is_active = model_clone.get::(&model_clone.iter(&tree_path_clone).expect("Invalid tree_path"), column_selection); check_button_left_preview_text_clone.set_active(is_active); check_button_left_preview_text_clone.set_label(Some(&format!("{}. {}", number + 1, get_max_file_name(&path_clone, 60)))); }); let big_thumbnail_clone = big_thumbnail.clone(); let tree_path_clone = tree_path.clone(); let all_gtk_box_clone = all_gtk_box.clone(); let shared_using_for_preview_clone = shared_using_for_preview.clone(); let shared_image_cache_clone = shared_image_cache.clone(); let check_button_right_preview_text_clone = check_button_right_preview_text.clone(); let model_clone = model.clone(); let path_clone = path.clone(); button_right.connect_clicked(move |_button_right| { shared_using_for_preview_clone.borrow_mut().1 = Some(tree_path_clone.clone()); update_bottom_buttons(&all_gtk_box_clone, &shared_using_for_preview_clone, &shared_image_cache_clone); image_compare_right.set_paintable(big_thumbnail_clone.paintable().as_ref()); let is_active = model_clone.get::(&model_clone.iter(&tree_path_clone).expect("Invalid tree_path"), column_selection); check_button_right_preview_text_clone.set_active(is_active); check_button_right_preview_text_clone.set_label(Some(&format!("{}. {}", number + 1, get_max_file_name(&path_clone, 60)))); }); smaller_box.append(&button_left); smaller_box.append(&label); smaller_box.append(&button_right); small_box.append(&smaller_box); small_box.set_halign(Align::Fill); small_box.set_valign(Align::Fill); small_box.set_hexpand_set(true); small_box.set_vexpand_set(true); small_thumbnail.set_halign(Align::Fill); small_thumbnail.set_valign(Align::Fill); small_thumbnail.set_hexpand(true); small_thumbnail.set_hexpand_set(true); small_thumbnail.set_vexpand(true); small_thumbnail.set_vexpand_set(true); small_box.append(small_thumbnail); all_gtk_box.append(&small_box); } all_gtk_box.set_visible(true); scrolled_window.set_child(Some(&all_gtk_box)); } fn update_bottom_buttons( all_gtk_box: >k4::Box, shared_using_for_preview: &Rc, Option)>>, image_cache: &Rc>>, ) { let left_tree_view = shared_using_for_preview.borrow().0.clone().expect("Left tree_view not set"); let right_tree_view = shared_using_for_preview.borrow().1.clone().expect("Right tree_view not set"); for (number, i) in all_gtk_box.get_all_direct_children().into_iter().enumerate() { let cache_tree_path = (*image_cache.borrow())[number].4.clone(); let is_chosen = cache_tree_path != right_tree_view && cache_tree_path != left_tree_view; let bx = i.downcast::().expect("Not Box"); let smaller_bx = bx.first_child().expect("No first child").downcast::().expect("First child is not Box"); for items in smaller_bx.get_all_direct_children() { if let Ok(btn) = items.downcast::() { btn.set_sensitive(is_chosen); } } } } fn get_current_group_and_iter_from_selection(sv: &SubView) -> (u32, TreePath) { let mut current_group = 1; let mut possible_group = 1; let mut header_clone: TreeIter; let mut possible_header: TreeIter; let column_header = sv.nb_object.column_header.expect("Missing column_header"); let model = sv.get_tree_model(); let selection = sv.get_tree_selection(); let selected_records = selection.selected_rows().0; 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 header_clone = iter; // if nothing selected, use first group possible_header = iter; // if nothing selected, use first group assert!(model.get::(&iter, column_header)); // First element should be header if !selected_records.is_empty() { let first_selected_record = selected_records[0].clone(); loop { if !model.iter_next(&mut iter) { break; } if model.get::(&iter, column_header) { possible_group += 1; possible_header = iter; } if model.path(&iter) == first_selected_record { header_clone = possible_header; current_group = possible_group; } } } (current_group, model.path(&header_clone)) } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_delete.rs ================================================ use czkawka_core::common::{remove_folder_if_contains_only_empty_folders, remove_single_file}; use gtk4::prelude::*; use gtk4::{Align, CheckButton, Dialog, Orientation, ResponseType, TextView}; use log::debug; use rayon::prelude::*; use crate::flg; use crate::gui_structs::common_tree_view::SubView; use crate::gui_structs::gui_data::GuiData; use crate::help_functions::get_full_name_from_path_name; use crate::helpers::list_store_operations::{check_how_much_elements_is_selected, clean_invalid_headers}; use crate::helpers::model_iter::iter_list; use crate::notebook_enums::NotebookMainEnum; // TODO add support for checking if really symlink doesn't point to correct directory/file pub(crate) fn connect_button_delete(gui_data: &GuiData) { let buttons_delete = gui_data.bottom_buttons.buttons_delete.clone(); let gui_data = gui_data.clone(); // TODO this maybe can be replaced, not sure if worth to clone everything buttons_delete.connect_clicked(move |_| { glib::MainContext::default().spawn_local(delete_things(gui_data.clone())); }); } pub async fn delete_things(gui_data: GuiData) { let window_main = gui_data.window_main.clone(); let check_button_settings_confirm_deletion = gui_data.settings.check_button_settings_confirm_deletion.clone(); let check_button_settings_confirm_group_deletion = gui_data.settings.check_button_settings_confirm_group_deletion.clone(); let check_button_settings_use_trash = gui_data.settings.check_button_settings_use_trash.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); let sv = gui_data.main_notebook.common_tree_views.get_current_subview(); let (number_of_selected_items, number_of_selected_groups) = check_how_much_elements_is_selected(sv); // Nothing is selected if number_of_selected_items == 0 { return; } if !check_if_can_delete_files(&check_button_settings_confirm_deletion, &window_main, number_of_selected_items, number_of_selected_groups).await { return; } if let Some(column_header) = sv.nb_object.column_header { 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 { tree_remove(sv, column_header, &check_button_settings_use_trash, &text_view_errors); } } else if sv.nb_object.notebook_type == NotebookMainEnum::EmptyDirectories { empty_folder_remover(sv, &check_button_settings_use_trash, &text_view_errors); } else { basic_remove(sv, &check_button_settings_use_trash, &text_view_errors); } common_tree_views.hide_preview(); } pub async fn check_if_can_delete_files( check_button_settings_confirm_deletion: &CheckButton, window_main: >k4::Window, number_of_selected_items: u64, number_of_selected_groups: u64, ) -> bool { if check_button_settings_confirm_deletion.is_active() { let (confirmation_dialog_delete, check_button) = create_dialog_ask_for_deletion(window_main, number_of_selected_items, number_of_selected_groups); let response_type = confirmation_dialog_delete.run_future().await; if response_type == ResponseType::Ok { if !check_button.is_active() { check_button_settings_confirm_deletion.set_active(false); } confirmation_dialog_delete.set_visible(false); confirmation_dialog_delete.close(); } else { confirmation_dialog_delete.set_visible(false); confirmation_dialog_delete.close(); return false; } } true } fn create_dialog_ask_for_deletion(window_main: >k4::Window, number_of_selected_items: u64, number_of_selected_groups: u64) -> (Dialog, CheckButton) { let dialog = Dialog::builder().title(flg!("delete_title_dialog")).transient_for(window_main).modal(true).build(); let button_ok = dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); dialog.set_default_size(300, 0); let label: gtk4::Label = gtk4::Label::new(Some(&flg!("delete_question_label"))); let label2: gtk4::Label = match number_of_selected_groups { 0 => gtk4::Label::new(Some(&flg!("delete_items_label", items = number_of_selected_items))), _ => gtk4::Label::new(Some(&flg!( "delete_items_groups_label", items = number_of_selected_items, groups = number_of_selected_groups ))), }; let check_button: CheckButton = CheckButton::builder() .label(flg!("dialogs_ask_next_time")) .active(true) .halign(Align::Center) .margin_top(5) .build(); button_ok.grab_focus(); let parent = button_ok.parent().expect("Hack 1").parent().expect("Hack 2").downcast::().expect("Hack 3"); // TODO Hack, but not so ugly as before parent.set_orientation(Orientation::Vertical); parent.insert_child_after(&label, None::<>k4::Widget>); parent.insert_child_after(&label2, Some(&label)); parent.insert_child_after(&check_button, Some(&label2)); dialog.set_visible(true); (dialog, check_button) } fn create_dialog_group_deletion(window_main: >k4::Window) -> (Dialog, CheckButton) { let dialog = Dialog::builder() .title(flg!("delete_all_files_in_group_title")) .transient_for(window_main) .modal(true) .build(); let button_ok = dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); let label: gtk4::Label = gtk4::Label::new(Some(&flg!("delete_all_files_in_group_label1"))); let label2: gtk4::Label = gtk4::Label::new(Some(&flg!("delete_all_files_in_group_label2"))); let check_button: CheckButton = CheckButton::builder().label(flg!("dialogs_ask_next_time")).active(true).halign(Align::Center).build(); button_ok.grab_focus(); let parent = button_ok.parent().expect("Hack 1").parent().expect("Hack 2").downcast::().expect("Hack 3"); // TODO Hack, but not so ugly as before parent.set_orientation(Orientation::Vertical); parent.insert_child_after(&label, None::<>k4::Widget>); parent.insert_child_after(&label2, Some(&label)); parent.insert_child_after(&check_button, Some(&label2)); dialog.set_visible(true); (dialog, check_button) } pub async fn check_if_deleting_all_files_in_group(sv: &SubView, window_main: >k4::Window, check_button_settings_confirm_group_deletion: &CheckButton) -> bool { let column_header = sv.nb_object.column_header.expect("Column header must exist here"); let model = sv.get_model(); let mut selected_all_records: bool = true; if let Some(mut iter) = model.iter_first() { assert!(model.get::(&iter, column_header)); // First element should be header // It is safe to remove any number of files in reference mode if !model.get::(&iter, sv.nb_object.column_path).is_empty() { return false; } loop { if !model.iter_next(&mut iter) { break; } if model.get::(&iter, column_header) { if selected_all_records { break; } selected_all_records = true; } else if !model.get::(&iter, sv.nb_object.column_selection) { selected_all_records = false; } } } else { return false; } if !selected_all_records { return false; } let (confirmation_dialog_group_delete, check_button) = create_dialog_group_deletion(window_main); let response_type = confirmation_dialog_group_delete.run_future().await; if response_type == ResponseType::Ok { if !check_button.is_active() { check_button_settings_confirm_group_deletion.set_active(false); } } else { confirmation_dialog_group_delete.set_visible(false); confirmation_dialog_group_delete.close(); return true; } confirmation_dialog_group_delete.set_visible(false); confirmation_dialog_group_delete.close(); false } pub(crate) fn empty_folder_remover(sv: &SubView, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView) { common_file_remove(sv, check_button_settings_use_trash, text_view_errors, None, false); } pub(crate) fn basic_remove(sv: &SubView, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView) { common_file_remove(sv, check_button_settings_use_trash, text_view_errors, None, true); } pub(crate) fn tree_remove(sv: &SubView, column_header: i32, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView) { common_file_remove(sv, check_button_settings_use_trash, text_view_errors, Some(column_header), true); clean_invalid_headers(&sv.get_model(), column_header, sv.nb_object.column_path); } pub(crate) fn common_file_remove(sv: &SubView, check_button_settings_use_trash: &CheckButton, text_view_errors: &TextView, column_header: Option, file_remove: bool) { let use_trash = check_button_settings_use_trash.is_active(); let model = sv.get_model(); let mut messages: String = String::new(); let mut selected_rows = Vec::new(); iter_list(&model, |m, i| { if m.get::(i, sv.nb_object.column_selection) { if let Some(column_header) = column_header { if !m.get::(i, column_header) { selected_rows.push(m.path(i)); } else { panic!("Header row shouldn't be selected, please report bug."); } } else { selected_rows.push(m.path(i)); } } }); if selected_rows.is_empty() { return; // No selected rows } debug!("Starting to delete {} files", selected_rows.len()); let start_time = std::time::Instant::now(); let to_remove = selected_rows .iter() .enumerate() .map(|(idx, tree_path)| { let iter = model.iter(tree_path).expect("Using invalid tree_path"); let name = model.get::(&iter, sv.nb_object.column_name); let path = model.get::(&iter, sv.nb_object.column_path); (idx, get_full_name_from_path_name(&path, &name)) }) .collect::>(); let (mut removed, failed_to_remove): (Vec, Vec) = to_remove .into_par_iter() .map(|(idx, path)| { if file_remove { remove_single_file(&path, use_trash)?; } else { remove_folder_if_contains_only_empty_folders(&path, use_trash)?; } Ok(idx) }) .partition_map(|res| match res { Ok(entry) => itertools::Either::Left(entry), Err(err) => itertools::Either::Right(err), }); for failed in &failed_to_remove { messages += failed; messages += "\n"; } removed.sort_unstable(); removed.reverse(); // Must be deleted from end to start let deleted_files = removed.len(); for idx in removed { let iter = model.iter(&selected_rows[idx]).expect("Using invalid tree_path"); model.remove(&iter); } debug!( "Deleted {deleted_files}/{} items({} tab) in {:?}", selected_rows.len(), sv.nb_object.name, start_time.elapsed() ); text_view_errors.buffer().set_text(messages.as_str()); } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_hardlink.rs ================================================ use czkawka_core::common::{make_file_symlink, make_hard_link}; use gtk4::prelude::*; use gtk4::{Align, CheckButton, Dialog, Orientation, ResponseType, TextView, TreeIter, TreePath}; use rayon::prelude::*; use crate::flg; use crate::gui_structs::common_tree_view::SubView; use crate::gui_structs::gui_data::GuiData; use crate::help_functions::{add_text_to_text_view, get_full_name_from_path_name, reset_text_view}; use crate::helpers::list_store_operations::clean_invalid_headers; use crate::helpers::model_iter::{iter_list, iter_list_break_with_init}; #[derive(PartialEq, Eq, Copy, Clone)] enum TypeOfTool { Hardlinking, Symlinking, } #[derive(Debug)] struct SymHardlinkData { original_data: String, files_to_symhardlink: Vec, } pub(crate) fn connect_button_hardlink_symlink(gui_data: &GuiData) { // Hardlinking { let buttons_hardlink = gui_data.bottom_buttons.buttons_hardlink.clone(); let gui_data = gui_data.clone(); buttons_hardlink.connect_clicked(move |_| { glib::MainContext::default().spawn_local(sym_hard_link_things(gui_data.clone(), TypeOfTool::Hardlinking)); }); } // Symlinking { let buttons_symlink = gui_data.bottom_buttons.buttons_symlink.clone(); let gui_data = gui_data.clone(); buttons_symlink.connect_clicked(move |_| { glib::MainContext::default().spawn_local(sym_hard_link_things(gui_data.clone(), TypeOfTool::Symlinking)); }); } } async fn sym_hard_link_things(gui_data: GuiData, hardlinking: TypeOfTool) { let text_view_errors = gui_data.text_view_errors.clone(); let window_main = gui_data.window_main.clone(); let common_tree_views = &gui_data.main_notebook.common_tree_views.clone(); let sv = common_tree_views.get_current_subview(); let check_button_settings_confirm_link = gui_data.settings.check_button_settings_confirm_link.clone(); if !check_if_anything_is_selected_async(sv) { return; } if !check_if_can_link_files(&check_button_settings_confirm_link, &window_main).await { return; } if !check_if_changing_one_item_in_group_and_continue(sv, &window_main).await { return; } hardlink_symlink(sv, hardlinking, &text_view_errors); common_tree_views.hide_preview(); } fn hardlink_symlink(sv: &SubView, hardlinking: TypeOfTool, text_view_errors: &TextView) { reset_text_view(text_view_errors); let column_header = sv.nb_object.column_header.expect("Linking can be only used for tree views with grouped results"); let model = sv.get_model(); let mut vec_tree_path_to_remove: Vec = Vec::new(); // List of hardlinked files without its root let mut vec_symhardlink_data: Vec = Vec::new(); let mut current_iter: TreeIter = match model.iter_first() { Some(t) => t, None => return, // No records }; let mut selected_rows = Vec::new(); iter_list(&model, |m, i| { if m.get::(i, sv.nb_object.column_selection) { if !m.get::(i, column_header) { selected_rows.push(m.path(i)); } else { panic!("Header row shouldn't be selected, please report bug."); } } }); if selected_rows.is_empty() { return; // No selected rows } let mut current_symhardlink_data: Option = None; let mut current_selected_index = 0; loop { if model.get::(¤t_iter, column_header) { if let Some(current_symhardlink_data) = current_symhardlink_data && !current_symhardlink_data.files_to_symhardlink.is_empty() { vec_symhardlink_data.push(current_symhardlink_data); } current_symhardlink_data = None; assert!(model.iter_next(&mut current_iter), "HEADER, shouldn't be a last item."); continue; } if model.path(¤t_iter) == selected_rows[current_selected_index] { let file_name = model.get::(¤t_iter, sv.nb_object.column_name); let path = model.get::(¤t_iter, sv.nb_object.column_path); let full_file_path = get_full_name_from_path_name(&path, &file_name); if let Some(mut current_data) = current_symhardlink_data { vec_tree_path_to_remove.push(model.path(¤t_iter)); current_data.files_to_symhardlink.push(full_file_path); current_symhardlink_data = Some(current_data); } else { current_symhardlink_data = Some(SymHardlinkData { original_data: full_file_path, files_to_symhardlink: Vec::new(), }); } if current_selected_index != selected_rows.len() - 1 { current_selected_index += 1; } else { if let Some(current_symhardlink_data) = current_symhardlink_data && !current_symhardlink_data.files_to_symhardlink.is_empty() { vec_symhardlink_data.push(current_symhardlink_data); } break; // There is no more selected items, so we just end checking } } if !model.iter_next(&mut current_iter) { if let Some(current_symhardlink_data) = current_symhardlink_data && !current_symhardlink_data.files_to_symhardlink.is_empty() { vec_symhardlink_data.push(current_symhardlink_data); } break; } } let errors = vec_symhardlink_data .into_par_iter() .flat_map(|symhardlink_data| { let mut err = Vec::new(); for file_to_be_replaced in symhardlink_data.files_to_symhardlink { if hardlinking == TypeOfTool::Symlinking { if let Err(e) = make_file_symlink(&symhardlink_data.original_data, &file_to_be_replaced) { err.push(flg!( "symlink_failed", name = symhardlink_data.original_data.clone(), target = file_to_be_replaced, reason = e.to_string() )); } } else { if let Err(e) = make_hard_link(&symhardlink_data.original_data, &file_to_be_replaced) { err.push(flg!( "hardlink_failed", name = symhardlink_data.original_data.clone(), target = file_to_be_replaced, reason = e.to_string() )); } } } err }) .collect::>(); for error in errors { add_text_to_text_view(text_view_errors, &error); } for tree_path in vec_tree_path_to_remove.iter().rev() { model.remove(&model.iter(tree_path).expect("Using invalid tree_path")); } clean_invalid_headers(&model, column_header, sv.nb_object.column_path); } fn create_dialog_non_group(window_main: >k4::Window) -> Dialog { let dialog = Dialog::builder() .title(flg!("hard_sym_invalid_selection_title_dialog")) .transient_for(window_main) .modal(true) .build(); let button_ok = dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); let label: gtk4::Label = gtk4::Label::new(Some(&flg!("hard_sym_invalid_selection_label_1"))); let label2: gtk4::Label = gtk4::Label::new(Some(&flg!("hard_sym_invalid_selection_label_2"))); let label3: gtk4::Label = gtk4::Label::new(Some(&flg!("hard_sym_invalid_selection_label_3"))); button_ok.grab_focus(); let parent = button_ok.parent().expect("Hack 1").parent().expect("Hack 2").downcast::().expect("Hack 3"); // TODO Hack, but not so ugly as before parent.set_orientation(Orientation::Vertical); parent.insert_child_after(&label, None::<>k4::Widget>); parent.insert_child_after(&label2, Some(&label)); parent.insert_child_after(&label3, Some(&label2)); dialog.set_visible(true); dialog } pub async fn check_if_changing_one_item_in_group_and_continue(sv: &SubView, window_main: >k4::Window) -> bool { let model = sv.get_model(); let column_header = sv.nb_object.column_header.expect("Column header must exists for linking"); let mut selected_values_in_group = 0; if let Some(mut iter) = model.iter_first() { assert!(model.get::(&iter, column_header)); // First element should be header loop { if !model.iter_next(&mut iter) { break; } if model.get::(&iter, column_header) { if selected_values_in_group == 1 { break; } selected_values_in_group = 0; } else if model.get::(&iter, sv.nb_object.column_selection) { selected_values_in_group += 1; } } } else { return false; // No available records } if selected_values_in_group == 1 { let confirmation_dialog = create_dialog_non_group(window_main); let response_type = confirmation_dialog.run_future().await; if response_type != ResponseType::Ok { confirmation_dialog.set_visible(false); confirmation_dialog.close(); return false; } confirmation_dialog.set_visible(false); confirmation_dialog.close(); } true } pub(crate) fn check_if_anything_is_selected_async(sv: &SubView) -> bool { let model = sv.get_model(); let column_header = sv.nb_object.column_header.expect("Column header must exists for linking"); let mut non_header_selected = false; iter_list_break_with_init( &model, |m, i| { assert!(m.get::(i, column_header)); // First element should be header }, |m, i| { if !m.get::(i, column_header) && m.get::(i, sv.nb_object.column_selection) { non_header_selected = true; return false; } true }, ); non_header_selected } pub async fn check_if_can_link_files(check_button_settings_confirm_link: &CheckButton, window_main: >k4::Window) -> bool { if check_button_settings_confirm_link.is_active() { let (confirmation_dialog_link, check_button) = create_dialog_ask_for_linking(window_main); let response_type = confirmation_dialog_link.run_future().await; if response_type == ResponseType::Ok { if !check_button.is_active() { check_button_settings_confirm_link.set_active(false); } confirmation_dialog_link.set_visible(false); confirmation_dialog_link.close(); } else { confirmation_dialog_link.set_visible(false); confirmation_dialog_link.close(); return false; } } true } fn create_dialog_ask_for_linking(window_main: >k4::Window) -> (Dialog, CheckButton) { let dialog = Dialog::builder().title(flg!("hard_sym_link_title_dialog")).transient_for(window_main).modal(true).build(); let button_ok = dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); let label: gtk4::Label = gtk4::Label::new(Some(&flg!("hard_sym_link_label"))); let check_button: CheckButton = CheckButton::builder().label(flg!("dialogs_ask_next_time")).active(true).halign(Align::Center).build(); button_ok.grab_focus(); let parent = button_ok.parent().expect("Hack 1").parent().expect("Hack 2").downcast::().expect("Hack 3"); // TODO Hack, but not so ugly as before parent.set_orientation(Orientation::Vertical); parent.insert_child_after(&label, None::<>k4::Widget>); parent.insert_child_after(&check_button, Some(&label)); dialog.set_visible(true); (dialog, check_button) } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_move.rs ================================================ use std::path::Path; use fs_extra::dir::CopyOptions; use gtk4::prelude::*; use gtk4::{ResponseType, TreePath}; use log::debug; use crate::connect_things::file_chooser_helpers::extract_paths_from_file_chooser; use crate::flg; use crate::gui_structs::common_tree_view::SubView; use crate::gui_structs::gui_data::GuiData; use crate::help_functions::{add_text_to_text_view, get_full_name_from_path_name, reset_text_view}; use crate::helpers::list_store_operations::{check_how_much_elements_is_selected, clean_invalid_headers}; use crate::helpers::model_iter::iter_list; pub(crate) fn connect_button_move(gui_data: &GuiData) { let buttons_move = gui_data.bottom_buttons.buttons_move.clone(); let entry_info = gui_data.entry_info.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let file_dialog_move_to_folder = gui_data.file_dialog_move_to_folder.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); file_dialog_move_to_folder.connect_response(move |file_chooser, response_type| { let sv = common_tree_views.get_current_subview(); let (number_of_selected_items, _number_of_selected_groups) = check_how_much_elements_is_selected(sv); // Nothing is selected if number_of_selected_items == 0 { return; } reset_text_view(&text_view_errors); if response_type == ResponseType::Accept { let folders = extract_paths_from_file_chooser(file_chooser); if folders.len() != 1 { add_text_to_text_view(&text_view_errors, flg!("move_files_choose_more_than_1_path", path_number = folders.len()).as_str()); } else { let folder = folders[0].clone(); if sv.nb_object.column_header.is_some() { move_with_tree(sv, &folder, &entry_info, &text_view_errors); } else { move_with_list(sv, &folder, &entry_info, &text_view_errors); } } } common_tree_views.hide_preview(); }); buttons_move.connect_clicked(move |_| { file_dialog_move_to_folder.set_visible(true); }); } fn move_with_tree(sv: &SubView, destination_folder: &Path, entry_info: >k4::Entry, text_view_errors: >k4::TextView) { let model = sv.get_model(); let column_header = sv.nb_object.column_header.expect("Using move_with_tree without header column"); let mut selected_rows = Vec::new(); iter_list(&model, |m, i| { if m.get::(i, sv.nb_object.column_selection) { if !m.get::(i, column_header) { selected_rows.push(m.path(i)); } else { panic!("Header row shouldn't be selected, please report bug."); } } }); if selected_rows.is_empty() { return; // No selected rows } move_files_common( &selected_rows, &model, sv.nb_object.column_name, sv.nb_object.column_path, destination_folder, entry_info, text_view_errors, ); clean_invalid_headers(&model, column_header, sv.nb_object.column_path); } fn move_with_list(sv: &SubView, destination_folder: &Path, entry_info: >k4::Entry, text_view_errors: >k4::TextView) { let model = sv.get_model(); let mut selected_rows = Vec::new(); iter_list(&model, |m, i| { if m.get::(i, sv.nb_object.column_selection) { selected_rows.push(m.path(i)); } }); if selected_rows.is_empty() { return; // No selected rows } move_files_common( &selected_rows, &model, sv.nb_object.column_name, sv.nb_object.column_path, destination_folder, entry_info, text_view_errors, ); } fn move_files_common( selected_rows: &[TreePath], model: >k4::ListStore, column_file_name: i32, column_path: i32, destination_folder: &Path, entry_info: >k4::Entry, text_view_errors: >k4::TextView, ) { let mut messages: String = String::new(); let mut moved_files: u32 = 0; debug!("Starting to move {} files", selected_rows.len()); let start_time = std::time::Instant::now(); // Save to variable paths of files, and remove it when not removing all occurrences. 'next_result: for tree_path in selected_rows.iter().rev() { let iter = model.iter(tree_path).expect("Using invalid tree_path"); let file_name = model.get::(&iter, column_file_name); let path = model.get::(&iter, column_path); let thing = get_full_name_from_path_name(&path, &file_name); let destination_file = destination_folder.join(&file_name); if Path::new(&thing).is_dir() { if let Err(e) = fs_extra::dir::move_dir(&thing, &destination_file, &CopyOptions::new()) { messages += flg!("move_folder_failed", name = thing, reason = e.to_string()).as_str(); messages += "\n"; continue 'next_result; } } else if let Err(e) = fs_extra::file::move_file(&thing, &destination_file, &fs_extra::file::CopyOptions::new()) { messages += flg!("move_file_failed", name = thing, reason = e.to_string()).as_str(); messages += "\n"; continue 'next_result; } model.remove(&iter); moved_files += 1; } debug!("Moved {moved_files} files in {:?}", start_time.elapsed()); entry_info.set_text(flg!("move_stats", num_files = moved_files, all_files = selected_rows.len()).as_str()); text_view_errors.buffer().set_text(messages.as_str()); } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_save.rs ================================================ use std::cell::RefCell; use std::collections::HashMap; use std::env; use std::rc::Rc; use gtk4::prelude::*; use gtk4::{Button, Entry}; use crate::flg; use crate::gui_structs::gui_data::GuiData; use crate::helpers::enums::BottomButtonsEnum; use crate::notebook_enums::NotebookMainEnum; pub(crate) fn connect_button_save(gui_data: &GuiData) { let buttons_save = gui_data.bottom_buttons.buttons_save.clone(); let shared_buttons = gui_data.shared_buttons.clone(); let entry_info = gui_data.entry_info.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_save.connect_clicked(move |buttons_save| { let mut current_path = match env::current_dir() { Ok(t) => t.to_string_lossy().to_string(), Err(_) => "__unknown__".to_string(), }; if ["Windows"].iter().any(|item| current_path.contains(item)) { current_path = directories_next::UserDirs::new() .and_then(|d| d.desktop_dir().map(|d| d.to_string_lossy().to_string())) .unwrap_or_else(|| current_path.clone()); } let subview = common_tree_views.get_current_subview(); if let Err(e) = subview.shared_model_enum.save_all_in_one(¤t_path) { entry_info.set_text(&format!("Failed to save results to folder {current_path}, reason {e}")); return; } post_save_things(subview.enum_value, &shared_buttons, &entry_info, buttons_save, current_path); }); } fn post_save_things( type_of_tab: NotebookMainEnum, shared_buttons: &Rc>>>, entry_info: &Entry, buttons_save: &Button, current_path: String, ) { entry_info.set_text(&flg!("save_results_to_file", name = current_path)); // Set state { buttons_save.set_visible(false); *shared_buttons .borrow_mut() .get_mut(&type_of_tab) .expect("Failed to get current tab") .get_mut(&BottomButtonsEnum::Save) .expect("Failed to get save button") = false; } } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_search.rs ================================================ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use crossbeam_channel::Sender; use czkawka_core::common::consts::DEFAULT_THREAD_SIZE; use czkawka_core::common::model::CheckingMethod; use czkawka_core::common::progress_data::ProgressData; use czkawka_core::common::tool_data::CommonData; use czkawka_core::common::traits::Search; use czkawka_core::tools::bad_extensions::{BadExtensions, BadExtensionsParameters}; use czkawka_core::tools::big_file::{BigFile, BigFileParameters}; use czkawka_core::tools::broken_files::{BrokenFiles, BrokenFilesParameters, CheckedTypes}; use czkawka_core::tools::duplicate::{DuplicateFinder, DuplicateFinderParameters}; use czkawka_core::tools::empty_files::EmptyFiles; use czkawka_core::tools::empty_folder::EmptyFolder; use czkawka_core::tools::invalid_symlinks::InvalidSymlinks; use czkawka_core::tools::same_music::{MusicSimilarity, SameMusic, SameMusicParameters}; use czkawka_core::tools::similar_images::{SimilarImages, SimilarImagesParameters}; use czkawka_core::tools::similar_videos::{DEFAULT_CROP_DETECT, DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION, SimilarVideos, SimilarVideosParameters}; use czkawka_core::tools::temporary::Temporary; use fun_time::fun_time; use gtk4::Grid; use gtk4::prelude::*; use crate::gui_structs::common_tree_view::TreeViewListStoreTrait; use crate::gui_structs::common_upper_tree_view::UpperTreeViewEnum; use crate::gui_structs::gui_data::GuiData; use crate::help_combo_box::{ 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, IMAGES_HASH_TYPE_COMBO_BOX, IMAGES_RESIZE_ALGORITHM_COMBO_BOX, }; use crate::help_functions::{get_path_buf_from_vector_of_strings, hide_all_buttons, reset_text_view, set_buttons}; use crate::helpers::enums::{ColumnsExcludedDirectory, ColumnsIncludedDirectory, Message}; use crate::helpers::list_store_operations::{check_if_list_store_column_have_all_same_values, get_string_from_list_store}; use crate::helpers::model_iter::iter_list; use crate::notebook_enums::NotebookMainEnum; use crate::taskbar_progress::tbp_flags::TBPF_NOPROGRESS; use crate::{DEFAULT_MAXIMAL_FILE_SIZE, DEFAULT_MINIMAL_CACHE_SIZE, DEFAULT_MINIMAL_FILE_SIZE, flg}; pub(crate) fn connect_button_search(gui_data: &GuiData, result_sender: Sender, progress_sender: Sender) { let buttons_array = gui_data.bottom_buttons.buttons_array.clone(); let buttons_search_clone = gui_data.bottom_buttons.buttons_search.clone(); let grid_progress = gui_data.progress_window.grid_progress.clone(); let label_stage = gui_data.progress_window.label_stage.clone(); let notebook_main = gui_data.main_notebook.notebook_main.clone(); let notebook_upper = gui_data.upper_notebook.notebook_upper.clone(); let progress_bar_all_stages = gui_data.progress_window.progress_bar_all_stages.clone(); let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone(); let stop_flag = gui_data.stop_flag.clone(); let taskbar_state = gui_data.taskbar_state.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let tree_view_included_directories = gui_data .upper_notebook .common_upper_tree_views .get_tree_view(UpperTreeViewEnum::IncludedDirectories) .clone(); let window_progress = gui_data.progress_window.window_progress.clone(); let entry_info = gui_data.entry_info.clone(); let button_settings = gui_data.header.button_settings.clone(); let button_app_info = gui_data.header.button_app_info.clone(); let gui_data = gui_data.clone(); buttons_search_clone.connect_clicked(move |_| { let loaded_commons = LoadedCommonItems::load_items(&gui_data); // Check if user selected all referenced folders let list_store_included_directories = tree_view_included_directories.get_model(); if check_if_list_store_column_have_all_same_values(&list_store_included_directories, ColumnsIncludedDirectory::ReferenceButton as i32, true) { entry_info.set_text(&flg!("selected_all_reference_folders")); return; } let show_dialog = Arc::new(AtomicBool::new(true)); window_progress.set_title(Some(&flg!("window_progress_title"))); hide_all_buttons(&buttons_array); notebook_main.set_sensitive(false); notebook_upper.set_sensitive(false); button_settings.set_sensitive(false); button_app_info.set_sensitive(false); entry_info.set_text(&flg!("searching_for_data")); // Resets progress bars progress_bar_all_stages.set_fraction(0f64); progress_bar_current_stage.set_fraction(0f64); reset_text_view(&text_view_errors); let result_sender = result_sender.clone(); let stop_flag = stop_flag.clone(); // Clear stop flag stop_flag.store(false, Ordering::Relaxed); label_stage.set_visible(true); let progress_sender = progress_sender.clone(); let current_data = gui_data.main_notebook.common_tree_views.clone(); match current_data.get_current_page() { NotebookMainEnum::Duplicate => duplicate_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::EmptyFiles => empty_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::EmptyDirectories => empty_dirs_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::BigFiles => big_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::Temporary => temporary_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::SimilarImages => similar_image_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::SimilarVideos => similar_video_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::SameMusic => same_music_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender, &show_dialog), NotebookMainEnum::Symlinks => bad_symlinks_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), NotebookMainEnum::BrokenFiles => broken_files_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender, &show_dialog), NotebookMainEnum::BadExtensions => bad_extensions_search(&gui_data, loaded_commons, stop_flag, result_sender, &grid_progress, progress_sender), } window_progress.set_default_size(1, 1); // Show progress dialog if show_dialog.load(Ordering::Relaxed) { window_progress.show(); taskbar_state.borrow().show(); taskbar_state.borrow().set_progress_state(TBPF_NOPROGRESS); } }); } struct LoadedCommonItems { included_directories: Vec, excluded_directories: Vec, reference_directories: Vec, recursive_search: bool, excluded_items: Vec, allowed_extensions: String, excluded_extensions: String, hide_hard_links: bool, use_cache: bool, save_also_as_json: bool, minimal_cache_file_size: u64, minimal_file_size: u64, maximal_file_size: u64, ignore_other_filesystems: bool, } impl LoadedCommonItems { fn load_items(gui_data: &GuiData) -> Self { let check_button_settings_one_filesystem = gui_data.settings.check_button_settings_one_filesystem.clone(); let check_button_recursive = gui_data.upper_notebook.check_button_recursive.clone(); let check_button_settings_hide_hard_links = gui_data.settings.check_button_settings_hide_hard_links.clone(); let check_button_settings_use_cache = gui_data.settings.check_button_settings_use_cache.clone(); let entry_allowed_extensions = gui_data.upper_notebook.entry_allowed_extensions.clone(); let entry_excluded_extensions = gui_data.upper_notebook.entry_excluded_extensions.clone(); let entry_excluded_items = gui_data.upper_notebook.entry_excluded_items.clone(); let entry_general_maximal_size = gui_data.upper_notebook.entry_general_maximal_size.clone(); let entry_general_minimal_size = gui_data.upper_notebook.entry_general_minimal_size.clone(); let entry_settings_cache_file_minimal_size = gui_data.settings.entry_settings_cache_file_minimal_size.clone(); let tree_view_excluded_directories = gui_data .upper_notebook .common_upper_tree_views .get_tree_view(UpperTreeViewEnum::ExcludedDirectories) .clone(); let tree_view_included_directories = gui_data .upper_notebook .common_upper_tree_views .get_tree_view(UpperTreeViewEnum::IncludedDirectories) .clone(); let check_button_settings_save_also_json = gui_data.settings.check_button_settings_save_also_json.clone(); let included_directories = get_path_buf_from_vector_of_strings(&get_string_from_list_store(&tree_view_included_directories, ColumnsIncludedDirectory::Path as i32, None)); let excluded_directories = get_path_buf_from_vector_of_strings(&get_string_from_list_store(&tree_view_excluded_directories, ColumnsExcludedDirectory::Path as i32, None)); let reference_directories = get_path_buf_from_vector_of_strings(&get_string_from_list_store( &tree_view_included_directories, ColumnsIncludedDirectory::Path as i32, Some(ColumnsIncludedDirectory::ReferenceButton as i32), )); let recursive_search = check_button_recursive.is_active(); let excluded_items = entry_excluded_items.text().as_str().split(',').map(ToString::to_string).collect::>(); let allowed_extensions = entry_allowed_extensions.text().as_str().to_string(); let excluded_extensions = entry_excluded_extensions.text().as_str().to_string(); let hide_hard_links = check_button_settings_hide_hard_links.is_active(); let use_cache = check_button_settings_use_cache.is_active(); let save_also_as_json = check_button_settings_save_also_json.is_active(); let minimal_cache_file_size = entry_settings_cache_file_minimal_size .text() .as_str() .parse::() .unwrap_or_else(|_| DEFAULT_MINIMAL_CACHE_SIZE.parse::().expect("Failed to parse minimal_cache_file_size")); let minimal_file_size_txt = entry_general_minimal_size.text().trim().to_string(); let minimal_file_size = if minimal_file_size_txt.is_empty() { 0u64 } else { minimal_file_size_txt .parse::() .unwrap_or_else(|_| DEFAULT_MINIMAL_FILE_SIZE.parse::().expect("Failed to parse minimal_file_size")) }; let maximal_file_size = entry_general_maximal_size .text() .as_str() .parse::() .unwrap_or_else(|_| DEFAULT_MAXIMAL_FILE_SIZE.parse::().expect("Failed to parse maximal_file_size")); let ignore_other_filesystems = check_button_settings_one_filesystem.is_active(); Self { included_directories, excluded_directories, reference_directories, recursive_search, excluded_items, allowed_extensions, excluded_extensions, hide_hard_links, use_cache, save_also_as_json, minimal_cache_file_size, minimal_file_size, maximal_file_size, ignore_other_filesystems, } } } fn duplicate_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(true); let combo_box_duplicate_check_method = gui_data.main_notebook.combo_box_duplicate_check_method.clone(); let combo_box_duplicate_hash_type = gui_data.main_notebook.combo_box_duplicate_hash_type.clone(); let check_button_duplicates_use_prehash_cache = gui_data.settings.check_button_duplicates_use_prehash_cache.clone(); let check_button_duplicate_case_sensitive_name: gtk4::CheckButton = gui_data.main_notebook.check_button_duplicate_case_sensitive_name.clone(); let check_button_settings_duplicates_delete_outdated_cache = gui_data.settings.check_button_settings_duplicates_delete_outdated_cache.clone(); let entry_settings_prehash_cache_file_minimal_size = gui_data.settings.entry_settings_prehash_cache_file_minimal_size.clone(); let image_preview_duplicates = gui_data.main_notebook.image_preview_duplicates.clone(); image_preview_duplicates.set_visible(false); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); let check_method_index = combo_box_duplicate_check_method.active().expect("Failed to get active search") as usize; let check_method = DUPLICATES_CHECK_METHOD_COMBO_BOX[check_method_index].check_method; let hash_type_index = combo_box_duplicate_hash_type.active().expect("Failed to get active search") as usize; let hash_type = DUPLICATES_HASH_TYPE_COMBO_BOX[hash_type_index].hash_type; let use_prehash_cache = check_button_duplicates_use_prehash_cache.is_active(); let minimal_prehash_cache_file_size = entry_settings_prehash_cache_file_minimal_size.text().as_str().parse::().unwrap_or(0); let case_sensitive_name_comparison = check_button_duplicate_case_sensitive_name.is_active(); let delete_outdated_cache = check_button_settings_duplicates_delete_outdated_cache.is_active(); // Find duplicates thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = DuplicateFinderParameters::new( check_method, hash_type, use_prehash_cache, loaded_commons.minimal_cache_file_size, minimal_prehash_cache_file_size, case_sensitive_name_comparison, ); let mut tool = DuplicateFinder::new(params); set_common_settings(&mut tool, &loaded_commons); tool.set_delete_outdated_cache(delete_outdated_cache); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::Duplicates(tool)).expect("Failed to send Duplicates message"); }) .expect("Failed to spawn DuplicateFinder thread"); } fn empty_files_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(false); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); // Find empty files thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let mut tool = EmptyFiles::new(); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::EmptyFiles(tool)).expect("Failed to send EmptyFiles message"); }) .expect("Failed to spawn EmptyFiles thread"); } fn empty_dirs_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(false); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let mut tool = EmptyFolder::new(); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::EmptyFolders(tool)).expect("Failed to send EmptyFolders message"); }) .expect("Failed to spawn EmptyFolders thread"); } fn big_files_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(false); let combo_box_big_files_mode = gui_data.main_notebook.combo_box_big_files_mode.clone(); let entry_big_files_number = gui_data.main_notebook.entry_big_files_number.clone(); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); let big_files_mode_index = combo_box_big_files_mode.active().expect("Failed to get active search") as usize; let big_files_mode = BIG_FILES_CHECK_METHOD_COMBO_BOX[big_files_mode_index].check_method; let numbers_of_files_to_check = entry_big_files_number.text().as_str().parse::().unwrap_or(50); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = BigFileParameters::new(numbers_of_files_to_check, big_files_mode); let mut tool = BigFile::new(params); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::BigFiles(tool)).expect("Failed to send BigFiles message"); }) .expect("Failed to spawn BigFiles thread"); } fn temporary_files_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(false); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let mut tool = Temporary::new(); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::Temporary(tool)).expect("Failed to send Temporary message"); }) .expect("Failed to spawn Temporary thread"); } fn same_music_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, show_dialog: &Arc, ) { grid_progress.set_visible(true); let check_button_music_artist: gtk4::CheckButton = gui_data.main_notebook.check_button_music_artist.clone(); let check_button_music_title: gtk4::CheckButton = gui_data.main_notebook.check_button_music_title.clone(); let check_button_music_year: gtk4::CheckButton = gui_data.main_notebook.check_button_music_year.clone(); let check_button_music_genre: gtk4::CheckButton = gui_data.main_notebook.check_button_music_genre.clone(); let check_button_music_length: gtk4::CheckButton = gui_data.main_notebook.check_button_music_length.clone(); let check_button_music_bitrate: gtk4::CheckButton = gui_data.main_notebook.check_button_music_bitrate.clone(); let combo_box_audio_check_type = gui_data.main_notebook.combo_box_audio_check_type.clone(); let check_button_music_approximate_comparison = gui_data.main_notebook.check_button_music_approximate_comparison.clone(); let check_button_music_compare_only_in_title_group = gui_data.main_notebook.check_button_music_compare_only_in_title_group.clone(); let scale_seconds_same_music = gui_data.main_notebook.scale_seconds_same_music.clone(); let scale_similarity_same_music = gui_data.main_notebook.scale_similarity_same_music.clone(); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); let approximate_comparison = check_button_music_approximate_comparison.is_active(); let comparison_only_in_title_group = check_button_music_compare_only_in_title_group.is_active(); let mut music_similarity: MusicSimilarity = MusicSimilarity::NONE; if check_button_music_title.is_active() { music_similarity |= MusicSimilarity::TRACK_TITLE; } if check_button_music_artist.is_active() { music_similarity |= MusicSimilarity::TRACK_ARTIST; } if check_button_music_year.is_active() { music_similarity |= MusicSimilarity::YEAR; } if check_button_music_bitrate.is_active() { music_similarity |= MusicSimilarity::BITRATE; } if check_button_music_genre.is_active() { music_similarity |= MusicSimilarity::GENRE; } if check_button_music_length.is_active() { music_similarity |= MusicSimilarity::LENGTH; } let check_method_index = combo_box_audio_check_type.active().expect("Failed to get active search") as usize; let check_method = AUDIO_TYPE_CHECK_METHOD_COMBO_BOX[check_method_index].check_method; let maximum_difference = scale_similarity_same_music.value(); let minimum_segment_duration = scale_seconds_same_music.value() as f32; if music_similarity != MusicSimilarity::NONE || check_method == CheckingMethod::AudioContent { thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = SameMusicParameters::new( music_similarity, approximate_comparison, check_method, minimum_segment_duration, maximum_difference, comparison_only_in_title_group, ); let mut tool = SameMusic::new(params); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::SameMusic(tool)).expect("Failed to send SameMusic message"); }) .expect("Failed to spawn SameMusic thread"); } else { let shared_buttons = gui_data.shared_buttons.clone(); let buttons_array = gui_data.bottom_buttons.buttons_array.clone(); let buttons_names = gui_data.bottom_buttons.buttons_names; let entry_info = gui_data.entry_info.clone(); let notebook_main = gui_data.main_notebook.notebook_main.clone(); let notebook_upper = gui_data.upper_notebook.notebook_upper.clone(); let button_settings = gui_data.header.button_settings.clone(); let button_app_info = gui_data.header.button_app_info.clone(); set_buttons( &mut *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SameMusic).expect("Failed to get SameMusic button"), &buttons_array, &buttons_names, ); entry_info.set_text(&flg!("search_not_choosing_any_music")); show_dialog.store(false, Ordering::Relaxed); notebook_main.set_sensitive(true); notebook_upper.set_sensitive(true); button_settings.set_sensitive(true); button_app_info.set_sensitive(true); } } fn broken_files_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, show_dialog: &Arc, ) { grid_progress.set_visible(true); let check_button_broken_files_archive: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_archive.clone(); let check_button_broken_files_pdf: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_pdf.clone(); let check_button_broken_files_audio: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_audio.clone(); let check_button_broken_files_image: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_image.clone(); let check_button_broken_files_video: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_video.clone(); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); let mut checked_types: CheckedTypes = CheckedTypes::NONE; if check_button_broken_files_audio.is_active() { checked_types |= CheckedTypes::AUDIO; } if check_button_broken_files_pdf.is_active() { checked_types |= CheckedTypes::PDF; } if check_button_broken_files_image.is_active() { checked_types |= CheckedTypes::IMAGE; } if check_button_broken_files_archive.is_active() { checked_types |= CheckedTypes::ARCHIVE; } if check_button_broken_files_video.is_active() { checked_types |= CheckedTypes::VIDEO; } if checked_types != CheckedTypes::NONE { thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = BrokenFilesParameters::new(checked_types); let mut tool = BrokenFiles::new(params); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::BrokenFiles(tool)).expect("Failed to send BrokenFiles message"); }) .expect("Failed to spawn BrokenFiles thread"); } else { let shared_buttons = gui_data.shared_buttons.clone(); let buttons_array = gui_data.bottom_buttons.buttons_array.clone(); let buttons_names = gui_data.bottom_buttons.buttons_names; let entry_info = gui_data.entry_info.clone(); let notebook_main = gui_data.main_notebook.notebook_main.clone(); let notebook_upper = gui_data.upper_notebook.notebook_upper.clone(); let button_settings = gui_data.header.button_settings.clone(); let button_app_info = gui_data.header.button_app_info.clone(); set_buttons( &mut *shared_buttons .borrow_mut() .get_mut(&NotebookMainEnum::BrokenFiles) .expect("Failed to get BrokenFiles button"), &buttons_array, &buttons_names, ); entry_info.set_text(&flg!("search_not_choosing_any_broken_files")); show_dialog.store(false, Ordering::Relaxed); notebook_main.set_sensitive(true); notebook_upper.set_sensitive(true); button_settings.set_sensitive(true); button_app_info.set_sensitive(true); } } fn similar_image_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(true); let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone(); let combo_box_image_hash_algorithm = gui_data.main_notebook.combo_box_image_hash_algorithm.clone(); let combo_box_image_resize_algorithm = gui_data.main_notebook.combo_box_image_resize_algorithm.clone(); let check_button_image_ignore_same_size = gui_data.main_notebook.check_button_image_ignore_same_size.clone(); let check_button_settings_similar_images_delete_outdated_cache = gui_data.settings.check_button_settings_similar_images_delete_outdated_cache.clone(); let image_preview_similar_images = gui_data.main_notebook.image_preview_similar_images.clone(); let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone(); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); image_preview_similar_images.set_visible(false); let hash_size_index = combo_box_image_hash_size.active().expect("Failed to get active search") as usize; let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index] as u8; let image_filter_index = combo_box_image_resize_algorithm.active().expect("Failed to get active search") as usize; let image_filter = IMAGES_RESIZE_ALGORITHM_COMBO_BOX[image_filter_index].filter; let hash_alg_index = combo_box_image_hash_algorithm.active().expect("Failed to get active search") as usize; let hash_alg = IMAGES_HASH_TYPE_COMBO_BOX[hash_alg_index].hash_alg; let ignore_same_size = check_button_image_ignore_same_size.is_active(); let similarity = scale_similarity_similar_images.value() as u32; let delete_outdated_cache = check_button_settings_similar_images_delete_outdated_cache.is_active(); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = SimilarImagesParameters::new(similarity, hash_size, hash_alg, image_filter, ignore_same_size); let mut tool = SimilarImages::new(params); set_common_settings(&mut tool, &loaded_commons); tool.set_delete_outdated_cache(delete_outdated_cache); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::SimilarImages(tool)).expect("Failed to send SimilarImages message"); }) .expect("Failed to spawn SimilarImages thread"); } fn similar_video_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(true); let check_button_video_ignore_same_size = gui_data.main_notebook.check_button_video_ignore_same_size.clone(); let check_button_settings_similar_videos_delete_outdated_cache = gui_data.settings.check_button_settings_similar_videos_delete_outdated_cache.clone(); let scale_similarity_similar_videos = gui_data.main_notebook.scale_similarity_similar_videos.clone(); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); let tolerance = scale_similarity_similar_videos.value() as i32; let delete_outdated_cache = check_button_settings_similar_videos_delete_outdated_cache.is_active(); let ignore_same_size = check_button_video_ignore_same_size.is_active(); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = SimilarVideosParameters::new( tolerance, ignore_same_size, DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION, DEFAULT_CROP_DETECT, false, // Not implemented in gtk gui 10, // Not implemented in gtk gui false, // Not implemented in gtk gui 2, // Not implemented in gtk gui ); let mut tool = SimilarVideos::new(params); set_common_settings(&mut tool, &loaded_commons); tool.set_delete_outdated_cache(delete_outdated_cache); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::SimilarVideos(tool)).expect("Failed to send SimilarVideos message"); }) .expect("Failed to spawn SimilarVideos thread"); } fn bad_symlinks_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(false); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let mut tool = InvalidSymlinks::new(); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::InvalidSymlinks(tool)).expect("Failed to send InvalidSymlinks message"); }) .expect("Failed to spawn InvalidSymlinks thread"); } fn bad_extensions_search( gui_data: &GuiData, loaded_commons: LoadedCommonItems, stop_flag: Arc, result_sender: Sender, grid_progress: &Grid, progress_data_sender: Sender, ) { grid_progress.set_visible(true); clean_tree_view(&gui_data.main_notebook.common_tree_views.get_current_subview().tree_view); thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { let params = BadExtensionsParameters::new(); let mut tool = BadExtensions::new(params); set_common_settings(&mut tool, &loaded_commons); tool.search(&stop_flag, Some(&progress_data_sender)); result_sender.send(Message::BadExtensions(tool)).expect("Failed to send BadExtensions message"); }) .expect("Failed to spawn BadExtensions thread"); } fn set_common_settings(component: &mut T, loaded_commons: &LoadedCommonItems) where T: CommonData, { component.set_included_paths(loaded_commons.included_directories.clone()); component.set_excluded_paths(loaded_commons.excluded_directories.clone()); component.set_reference_paths(loaded_commons.reference_directories.clone()); component.set_recursive_search(loaded_commons.recursive_search); component.set_allowed_extensions(loaded_commons.allowed_extensions.split(',').map(str::to_string).collect()); component.set_excluded_extensions(loaded_commons.excluded_extensions.split(',').map(str::to_string).collect()); component.set_excluded_items(loaded_commons.excluded_items.clone()); component.set_exclude_other_filesystems(loaded_commons.ignore_other_filesystems); component.set_use_cache(loaded_commons.use_cache); component.set_save_also_as_json(loaded_commons.save_also_as_json); component.set_minimal_file_size(loaded_commons.minimal_file_size); component.set_maximal_file_size(loaded_commons.maximal_file_size); component.set_hide_hard_links(loaded_commons.hide_hard_links); } #[fun_time(message = "clean_tree_view", level = "debug")] fn clean_tree_view(tree_view: >k4::TreeView) { let list_store = tree_view.get_model(); let mut all_iters: Vec = Vec::new(); iter_list(&list_store, |_m, i| { all_iters.push(*i); }); all_iters.reverse(); for iter in all_iters { list_store.remove(&iter); } } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_select.rs ================================================ use gtk4::prelude::*; use crate::gui_structs::common_tree_view::SubView; use crate::gui_structs::gui_data::GuiData; use crate::gui_structs::gui_popovers_select::GuiSelectPopovers; use crate::helpers::enums::PopoverTypes; pub(crate) fn connect_button_select(gui_data: &GuiData) { let popovers_select = gui_data.popovers_select.clone(); let gc_buttons_select = gui_data.bottom_buttons.gc_buttons_select.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); gc_buttons_select.connect_pressed(move |_, _, _, _| { show_required_popovers(&popovers_select, common_tree_views.get_current_subview()); }); } fn show_required_popovers(popovers_select: &GuiSelectPopovers, sv: &SubView) { let buttons_popover_select_all = popovers_select.buttons_popover_select_all.clone(); let buttons_popover_unselect_all = popovers_select.buttons_popover_unselect_all.clone(); let buttons_popover_reverse = popovers_select.buttons_popover_reverse.clone(); let buttons_popover_select_all_except_shortest_path = popovers_select.buttons_popover_select_all_except_shortest_path.clone(); let buttons_popover_select_all_except_longest_path = popovers_select.buttons_popover_select_all_except_longest_path.clone(); let buttons_popover_select_all_except_oldest = popovers_select.buttons_popover_select_all_except_oldest.clone(); let buttons_popover_select_all_except_newest = popovers_select.buttons_popover_select_all_except_newest.clone(); let buttons_popover_select_one_oldest = popovers_select.buttons_popover_select_one_oldest.clone(); let buttons_popover_select_one_newest = popovers_select.buttons_popover_select_one_newest.clone(); let buttons_popover_select_custom = popovers_select.buttons_popover_select_custom.clone(); let buttons_popover_unselect_custom = popovers_select.buttons_popover_unselect_custom.clone(); let buttons_popover_select_all_images_except_biggest = popovers_select.buttons_popover_select_all_images_except_biggest.clone(); let buttons_popover_select_all_images_except_smallest = popovers_select.buttons_popover_select_all_images_except_smallest.clone(); let separator_select_shortest_path = popovers_select.separator_select_shortest_path.clone(); let separator_select_custom = popovers_select.separator_select_custom.clone(); let separator_select_date = popovers_select.separator_select_date.clone(); let separator_select_image_size = popovers_select.separator_select_image_size.clone(); let separator_select_reverse = popovers_select.separator_select_reverse.clone(); let arr = sv.nb_object.available_modes; if arr.contains(&PopoverTypes::All) { buttons_popover_select_all.set_visible(true); buttons_popover_unselect_all.set_visible(true); } else { buttons_popover_select_all.set_visible(false); buttons_popover_unselect_all.set_visible(false); } if arr.contains(&PopoverTypes::Size) { buttons_popover_select_all_images_except_biggest.set_visible(true); buttons_popover_select_all_images_except_smallest.set_visible(true); separator_select_image_size.set_visible(true); } else { buttons_popover_select_all_images_except_biggest.set_visible(false); buttons_popover_select_all_images_except_smallest.set_visible(false); separator_select_image_size.set_visible(false); } if arr.contains(&PopoverTypes::Reverse) { buttons_popover_reverse.set_visible(true); separator_select_reverse.set_visible(true); } else { buttons_popover_reverse.set_visible(false); separator_select_reverse.set_visible(false); } if arr.contains(&PopoverTypes::Custom) { buttons_popover_select_custom.set_visible(true); buttons_popover_unselect_custom.set_visible(true); separator_select_custom.set_visible(true); } else { buttons_popover_select_custom.set_visible(false); buttons_popover_unselect_custom.set_visible(false); separator_select_custom.set_visible(false); } if arr.contains(&PopoverTypes::Date) { buttons_popover_select_all_except_oldest.set_visible(true); buttons_popover_select_all_except_newest.set_visible(true); buttons_popover_select_one_oldest.set_visible(true); buttons_popover_select_one_newest.set_visible(true); separator_select_date.set_visible(true); } else { buttons_popover_select_all_except_oldest.set_visible(false); buttons_popover_select_all_except_newest.set_visible(false); buttons_popover_select_one_oldest.set_visible(false); buttons_popover_select_one_newest.set_visible(false); separator_select_date.set_visible(false); } if arr.contains(&PopoverTypes::PathLength) { buttons_popover_select_all_except_shortest_path.set_visible(true); buttons_popover_select_all_except_longest_path.set_visible(true); separator_select_shortest_path.set_visible(true); } else { buttons_popover_select_all_except_shortest_path.set_visible(false); buttons_popover_select_all_except_longest_path.set_visible(false); separator_select_shortest_path.set_visible(false); } } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_sort.rs ================================================ use gtk4::prelude::*; use crate::gui_structs::common_tree_view::SubView; use crate::gui_structs::gui_data::GuiData; use crate::gui_structs::gui_popovers_sort::GuiSortPopovers; use crate::helpers::enums::PopoverTypes; pub(crate) fn connect_button_sort(gui_data: &GuiData) { let popovers_sort = gui_data.popovers_sort.clone(); let gc_buttons_sort = gui_data.bottom_buttons.gc_buttons_sort.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); gc_buttons_sort.connect_pressed(move |_, _, _, _| { show_required_popovers(&popovers_sort, common_tree_views.get_current_subview()); }); } fn show_required_popovers(popovers_sort: &GuiSortPopovers, sv: &SubView) { let buttons_popover_sort_file_name = popovers_sort.buttons_popover_sort_file_name.clone(); let buttons_popover_sort_size = popovers_sort.buttons_popover_sort_size.clone(); let buttons_popover_sort_folder_name = popovers_sort.buttons_popover_sort_folder_name.clone(); let buttons_popover_sort_full_name = popovers_sort.buttons_popover_sort_full_name.clone(); let buttons_popover_sort_selection = popovers_sort.buttons_popover_sort_selection.clone(); let arr = sv.nb_object.available_modes; buttons_popover_sort_full_name.set_visible(false); if arr.contains(&PopoverTypes::All) { buttons_popover_sort_selection.set_visible(true); buttons_popover_sort_file_name.set_visible(true); buttons_popover_sort_folder_name.set_visible(true); // buttons_popover_sort_full_name.set_visible(true); // TODO, this needs to be handled a little different } else { buttons_popover_sort_selection.set_visible(false); buttons_popover_sort_file_name.set_visible(false); buttons_popover_sort_folder_name.set_visible(false); // buttons_popover_sort_full_name.set_visible(false); } if arr.contains(&PopoverTypes::Size) { buttons_popover_sort_size.set_visible(true); } else { buttons_popover_sort_size.set_visible(false); } } ================================================ FILE: czkawka_gui/src/connect_things/connect_button_stop.rs ================================================ use std::sync::Arc; use std::sync::atomic::AtomicBool; use gtk4::prelude::*; use crate::flg; use crate::gui_structs::gui_data::GuiData; use crate::help_functions::KEY_ENTER; fn send_stop_message(stop_flag: &Arc) { stop_flag.store(true, std::sync::atomic::Ordering::Relaxed); } pub(crate) fn connect_button_stop(gui_data: &GuiData) { let evk_button_stop_in_dialog = gui_data.progress_window.evk_button_stop_in_dialog.clone(); let stop_dialog = gui_data.progress_window.window_progress.clone(); let stop_flag = gui_data.stop_flag.clone(); evk_button_stop_in_dialog.connect_key_released(move |_, _, key_code, _| { if key_code == KEY_ENTER { stop_dialog.set_title(Some(&format!("{} ({})", flg!("window_progress_title"), flg!("progress_stop_additional_message")))); send_stop_message(&stop_flag); } }); let button_stop_in_dialog = gui_data.progress_window.button_stop_in_dialog.clone(); let stop_dialog = gui_data.progress_window.window_progress.clone(); let stop_flag = gui_data.stop_flag.clone(); button_stop_in_dialog.connect_clicked(move |_a| { stop_dialog.set_title(Some(&format!("{} ({})", flg!("window_progress_title"), flg!("progress_stop_additional_message")))); send_stop_message(&stop_flag); }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_change_language.rs ================================================ use gtk4::prelude::*; use i18n_embed::DesktopLanguageRequester; use i18n_embed::unic_langid::LanguageIdentifier; use log::error; use crate::language_functions::get_language_from_combo_box_text; use crate::{GuiData, LANGUAGES_ALL, localizer_gui}; // use i18n_embed::{DesktopLanguageRequester, Localizer}; pub(crate) fn connect_change_language(gui_data: &GuiData) { change_language(gui_data); let combo_box_settings_language = gui_data.settings.combo_box_settings_language.clone(); let gui_data = gui_data.clone(); combo_box_settings_language.connect_changed(move |_| { change_language(&gui_data); }); } fn change_language(gui_data: &GuiData) { let localizers = vec![ ("czkawka_core", czkawka_core::localizer_core::localizer_core()), ("czkawka_gui", localizer_gui::localizer_gui()), ]; let lang_short = get_language_from_combo_box_text(&gui_data.settings.combo_box_settings_language.active_text().expect("No active text")).short_text; let lang_identifier = vec![LanguageIdentifier::from_bytes(lang_short.as_bytes()).expect("Failed to create LanguageIdentifier")]; for (lib, localizer) in localizers { if let Err(error) = localizer.select(&lang_identifier) { error!("Error while loading languages for {lib} {error:?}"); } } gui_data.update_language(); } pub(crate) fn load_system_language(gui_data: &GuiData) { let requested_languages = DesktopLanguageRequester::requested_languages(); if let Some(language) = requested_languages.first() { let old_short_lang = language.to_string(); let mut short_lang = String::new(); // removes from e.g. en_zb, ending _zd since Czkawka doesn't support this (maybe could add this in future) for i in old_short_lang.chars() { if i.is_ascii_alphabetic() { short_lang.push(i); } else { break; } } for (index, lang) in LANGUAGES_ALL.iter().enumerate() { if lang.short_text == short_lang { gui_data.settings.combo_box_settings_language.set_active(Some(index as u32)); break; } } } } ================================================ FILE: czkawka_gui/src/connect_things/connect_duplicate_buttons.rs ================================================ use czkawka_core::common::model::CheckingMethod; use gtk4::prelude::*; use crate::gui_structs::gui_data::GuiData; use crate::help_combo_box::DUPLICATES_CHECK_METHOD_COMBO_BOX; pub(crate) fn connect_duplicate_combo_box(gui_data: &GuiData) { let combo_box_duplicate_check_method = gui_data.main_notebook.combo_box_duplicate_check_method.clone(); let combo_box_duplicate_hash_type = gui_data.main_notebook.combo_box_duplicate_hash_type.clone(); let label_duplicate_hash_type = gui_data.main_notebook.label_duplicate_hash_type.clone(); let check_button_duplicate_case_sensitive_name = gui_data.main_notebook.check_button_duplicate_case_sensitive_name.clone(); combo_box_duplicate_check_method.connect_changed(move |combo_box_duplicate_check_method| { // None active can be if when adding elements(this signal is activated when e.g. adding new fields or removing them) if let Some(chosen_index) = combo_box_duplicate_check_method.active() { if DUPLICATES_CHECK_METHOD_COMBO_BOX[chosen_index as usize].check_method == CheckingMethod::Hash { combo_box_duplicate_hash_type.set_visible(true); label_duplicate_hash_type.set_visible(true); } else { combo_box_duplicate_hash_type.set_visible(false); label_duplicate_hash_type.set_visible(false); } if [CheckingMethod::Name, CheckingMethod::SizeName].contains(&DUPLICATES_CHECK_METHOD_COMBO_BOX[chosen_index as usize].check_method) { check_button_duplicate_case_sensitive_name.set_visible(true); } else { check_button_duplicate_case_sensitive_name.set_visible(false); } } }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_header_buttons.rs ================================================ use gtk4::prelude::*; use crate::gui_structs::gui_data::GuiData; pub(crate) fn connect_button_about(gui_data: &GuiData) { let about_dialog = gui_data.about.about_dialog.clone(); let button_app_info = gui_data.header.button_app_info.clone(); button_app_info.connect_clicked(move |_| { about_dialog.set_visible(true); // Prevent from deleting dialog after close about_dialog.connect_close_request(|dialog| { dialog.set_visible(false); glib::Propagation::Stop }); }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_krokiet_info_dialog.rs ================================================ use gtk4::prelude::*; use gtk4::{Align, Dialog, Orientation, ResponseType}; use crate::flg; pub fn show_krokiet_info_dialog(window_main: >k4::Window) { let dialog = Dialog::builder().title(flg!("krokiet_info_title")).transient_for(window_main).modal(true).build(); let button_ok = dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.set_default_size(500, 0); let label = gtk4::Label::builder() .label(&flg!("krokiet_info_message")) .wrap(true) .justify(gtk4::Justification::Center) .halign(Align::Center) .margin_top(10) .margin_bottom(10) .margin_start(10) .margin_end(10) .build(); let link = gtk4::Label::builder() .label("https://github.com/qarmin/czkawka/tree/master/krokiet / https://github.com/qarmin/czkawka/releases") .use_markup(true) .halign(Align::Center) .margin_top(5) .margin_bottom(10) .build(); button_ok.grab_focus(); let parent = button_ok .parent() .expect("Button should have parent") .parent() .expect("Button parent should have parent") .downcast::() .expect("Should be a Box"); parent.set_orientation(Orientation::Vertical); parent.set_halign(Align::Fill); parent.set_margin_start(10); parent.set_margin_end(10); parent.set_margin_top(10); parent.set_margin_bottom(10); parent.insert_child_after(&label, None::<>k4::Widget>); parent.insert_child_after(&link, Some(&label)); if let Some(action_area) = button_ok.parent() { action_area.set_halign(Align::Center); } dialog.set_visible(true); dialog.connect_response(move |dialog, response_type| { if response_type == ResponseType::Ok { dialog.close(); } }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_notebook_tabs.rs ================================================ use crate::gui_structs::gui_data::GuiData; use crate::help_functions::set_buttons; use crate::notebook_enums::to_notebook_main_enum; pub(crate) fn connect_notebook_tabs(gui_data: &GuiData) { let shared_buttons = gui_data.shared_buttons.clone(); let buttons_array = gui_data.bottom_buttons.buttons_array.clone(); let notebook_main_clone = gui_data.main_notebook.notebook_main.clone(); let buttons_names = gui_data.bottom_buttons.buttons_names; notebook_main_clone.connect_switch_page(move |_, _, number| { let current_tab_in_main_notebook = to_notebook_main_enum(number); // Buttons set_buttons( &mut *shared_buttons.borrow_mut().get_mut(¤t_tab_in_main_notebook).expect("Failed to get current tab"), &buttons_array, &buttons_names, ); }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_popovers_select.rs ================================================ use czkawka_core::common::items::new_excluded_item; use czkawka_core::common::regex_check; use gtk4::prelude::*; use gtk4::{ResponseType, TreeIter, Window}; use log::error; use regex::Regex; use crate::flg; use crate::gtk_traits::DialogTraits; use crate::gui_structs::common_tree_view::{SubView, TreeViewListStoreTrait}; use crate::gui_structs::gui_data::GuiData; use crate::help_functions::{change_dimension_to_krotka, get_full_name_from_path_name}; use crate::helpers::model_iter::iter_list; // File length variable allows users to choose duplicates which have shorter file name // e.g. 'tar.gz' will be selected instead 'tar.gz (copy)' etc. fn popover_select_all(popover: >k4::Popover, tree_view: >k4::TreeView, column_button_selection: u32, column_header: Option) { let model = tree_view.get_model(); if let Some(mut iter) = model.iter_first() { if let Some(column_header) = column_header { loop { if !model.get::(&iter, column_header) { model.set_value(&iter, column_button_selection, &true.to_value()); } if !model.iter_next(&mut iter) { break; } } } else { loop { model.set_value(&iter, column_button_selection, &true.to_value()); if !model.iter_next(&mut iter) { break; } } } } popover.popdown(); } fn popover_unselect_all(popover: >k4::Popover, tree_view: >k4::TreeView, column_button_selection: u32) { let model = tree_view.get_model(); iter_list(&model, |m, i| { m.set_value(i, column_button_selection, &false.to_value()); }); popover.popdown(); } fn popover_reverse(popover: >k4::Popover, tree_view: >k4::TreeView, column_button_selection: u32, column_header: Option) { let model = tree_view.get_model(); if let Some(mut iter) = model.iter_first() { if let Some(column_header) = column_header { loop { if !model.get::(&iter, column_header) { let current_value: bool = model.get::(&iter, column_button_selection as i32); model.set_value(&iter, column_button_selection, &(!current_value).to_value()); } if !model.iter_next(&mut iter) { break; } } } else { loop { let current_value: bool = model.get::(&iter, column_button_selection as i32); model.set_value(&iter, column_button_selection, &(!current_value).to_value()); if !model.iter_next(&mut iter) { break; } } } } popover.popdown(); } fn popover_all_except_longest_shortest_path( popover: >k4::Popover, tree_view: >k4::TreeView, column_header: i32, column_path: i32, column_button_selection: u32, except_longest: bool, ) { let model = tree_view.get_model(); if let Some(mut iter) = model.iter_first() { let mut end: bool = false; loop { let mut tree_iter_array: Vec = Vec::new(); let mut used_index: Option = None; let mut current_index: usize = 0; let mut path_extreme: usize = if except_longest { usize::MAX } else { 0 }; loop { if model.get::(&iter, column_header) { if !model.iter_next(&mut iter) { end = true; } break; } tree_iter_array.push(iter); let path_length = model.get::(&iter, column_path).len(); if except_longest { if path_length < path_extreme { path_extreme = path_length; used_index = Some(current_index); } } else if path_length > path_extreme { path_extreme = path_length; used_index = Some(current_index); } current_index += 1; if !model.iter_next(&mut iter) { end = true; break; } } let Some(used_index) = used_index else { continue; }; for (index, tree_iter) in tree_iter_array.iter().enumerate() { if index != used_index { model.set_value(tree_iter, column_button_selection, &true.to_value()); } else { model.set_value(tree_iter, column_button_selection, &false.to_value()); } } if end { break; } } } popover.popdown(); } fn popover_all_except_oldest_newest( popover: >k4::Popover, tree_view: >k4::TreeView, column_header: i32, column_modification_as_secs: i32, column_file_name: i32, column_button_selection: u32, except_oldest: bool, ) { let model = tree_view.get_model(); if let Some(mut iter) = model.iter_first() { let mut end: bool = false; loop { let mut tree_iter_array: Vec = Vec::new(); let mut used_index: Option = None; let mut current_index: usize = 0; let mut modification_time_min_max: u64 = if except_oldest { u64::MAX } else { 0 }; let mut file_length: usize = 0; loop { if model.get::(&iter, column_header) { if !model.iter_next(&mut iter) { end = true; } break; } tree_iter_array.push(iter); let modification = model.get::(&iter, column_modification_as_secs); let current_file_length = model.get::(&iter, column_file_name).len(); if except_oldest { if modification < modification_time_min_max || (modification == modification_time_min_max && current_file_length < file_length) { file_length = current_file_length; modification_time_min_max = modification; used_index = Some(current_index); } } else if modification > modification_time_min_max || (modification == modification_time_min_max && current_file_length < file_length) { file_length = current_file_length; modification_time_min_max = modification; used_index = Some(current_index); } current_index += 1; if !model.iter_next(&mut iter) { end = true; break; } } let Some(used_index) = used_index else { continue; }; for (index, tree_iter) in tree_iter_array.iter().enumerate() { if index != used_index { model.set_value(tree_iter, column_button_selection, &true.to_value()); } else { model.set_value(tree_iter, column_button_selection, &false.to_value()); } } if end { break; } } } popover.popdown(); } fn popover_one_oldest_newest( popover: >k4::Popover, sv: &SubView, // tree_view: >k4::TreeView, // column_header: i32, // column_modification_as_secs: i32, // column_file_name: i32, // column_button_selection: u32, check_oldest: bool, ) { let model = sv.get_model(); let column_header = sv.nb_object.column_header.expect("OO/ON can't be used without headers"); let column_modification_as_secs = sv.nb_object.column_modification_as_secs.expect("OO/ON needs modification as secs column"); if let Some(mut iter) = model.iter_first() { let mut end: bool = false; loop { let mut tree_iter_array: Vec = Vec::new(); let mut used_index: Option = None; let mut current_index: usize = 0; let mut modification_time_min_max: u64 = if check_oldest { u64::MAX } else { 0 }; let mut file_length: usize = 0; loop { if model.get::(&iter, column_header) { if !model.iter_next(&mut iter) { end = true; } break; } tree_iter_array.push(iter); let modification = model.get::(&iter, column_modification_as_secs); let current_file_length = model.get::(&iter, sv.nb_object.column_name).len(); if check_oldest { if modification < modification_time_min_max || (modification == modification_time_min_max && current_file_length > file_length) { file_length = current_file_length; modification_time_min_max = modification; used_index = Some(current_index); } } else if modification > modification_time_min_max || (modification == modification_time_min_max && current_file_length > file_length) { file_length = current_file_length; modification_time_min_max = modification; used_index = Some(current_index); } current_index += 1; if !model.iter_next(&mut iter) { end = true; break; } } let Some(used_index) = used_index else { continue; }; for (index, tree_iter) in tree_iter_array.iter().enumerate() { if index == used_index { model.set_value(tree_iter, sv.nb_object.column_selection as u32, &true.to_value()); } else { model.set_value(tree_iter, sv.nb_object.column_selection as u32, &false.to_value()); } } if end { break; } } } popover.popdown(); } fn popover_custom_select_unselect( popover: >k4::Popover, window_main: &Window, sv: &SubView, // tree_view: >k4::TreeView, // column_header: Option, // column_file_name: i32, // column_path: i32, // column_button_selection: u32, select_things: bool, ) { popover.popdown(); let window_title = if select_things { flg!("popover_custom_mode_select") } else { flg!("popover_custom_mode_unselect") }; // Dialog for select/unselect items { let dialog = gtk4::Dialog::builder().title(window_title).transient_for(window_main).modal(true).build(); dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); let check_button_path = gtk4::CheckButton::builder() .label(flg!("popover_custom_regex_path_label")) .tooltip_text(flg!("popover_custom_path_check_button_entry_tooltip")) .build(); let check_button_name = gtk4::CheckButton::builder() .label(flg!("popover_custom_regex_name_label")) .tooltip_text(flg!("popover_custom_name_check_button_entry_tooltip")) .build(); let check_button_rust_regex = gtk4::CheckButton::builder() .label(flg!("popover_custom_regex_regex_label")) .tooltip_text(flg!("popover_custom_regex_check_button_entry_tooltip")) .build(); let check_button_case_sensitive = gtk4::CheckButton::builder() .label(flg!("popover_custom_case_sensitive_check_button")) .tooltip_text(flg!("popover_custom_case_sensitive_check_button_tooltip")) .active(false) .build(); let check_button_select_not_all_results = gtk4::CheckButton::builder() .label(flg!("popover_custom_all_in_group_label")) .tooltip_text(flg!("popover_custom_not_all_check_button_tooltip")) .active(true) .build(); let entry_path = gtk4::Entry::builder().tooltip_text(flg!("popover_custom_path_check_button_entry_tooltip")).build(); let entry_name = gtk4::Entry::builder().tooltip_text(flg!("popover_custom_name_check_button_entry_tooltip")).build(); let entry_rust_regex = gtk4::Entry::builder() .tooltip_text(flg!("popover_custom_regex_check_button_entry_tooltip")) .sensitive(false) .build(); // By default check button regex is disabled let label_regex_valid = gtk4::Label::new(None); { let label_regex_valid = label_regex_valid.clone(); entry_rust_regex.connect_changed(move |entry_rust_regex| { let message; let text_to_check = entry_rust_regex.text().to_string(); if text_to_check.is_empty() { message = String::new(); } else { match Regex::new(&text_to_check) { Ok(_) => message = flg!("popover_valid_regex"), Err(_) => message = flg!("popover_invalid_regex"), } } // TODO add red and green color to text // let attributes_list = AttrList::new(); // let p_a = PangoAttribute::init(); // let attribute = PangoAttrFontDesc { attr }; // attributes_list.insert(attribute); // label_regex_valid.set_attributes(Some(&attributes_list)); label_regex_valid.set_text(&message); }); } // Disable other modes when Rust Regex is enabled { let check_button_path = check_button_path.clone(); let check_button_name = check_button_name.clone(); let entry_path = entry_path.clone(); let entry_name = entry_name.clone(); let entry_rust_regex = entry_rust_regex.clone(); check_button_rust_regex.connect_toggled(move |check_button_rust_regex| { if check_button_rust_regex.is_active() { check_button_path.set_sensitive(false); check_button_name.set_sensitive(false); entry_path.set_sensitive(false); entry_name.set_sensitive(false); entry_rust_regex.set_sensitive(true); } else { check_button_path.set_sensitive(true); check_button_name.set_sensitive(true); entry_path.set_sensitive(true); entry_name.set_sensitive(true); entry_rust_regex.set_sensitive(false); } }); } // Configure look of things { // TODO Label should have const width, and rest should fill entry, but for now is 50%-50% let grid = gtk4::Grid::builder().row_homogeneous(true).column_homogeneous(true).build(); grid.attach(&check_button_name, 0, 1, 1, 1); grid.attach(&check_button_path, 0, 2, 1, 1); grid.attach(&check_button_rust_regex, 0, 3, 1, 1); grid.attach(&entry_name, 1, 1, 1, 1); grid.attach(&entry_path, 1, 2, 1, 1); grid.attach(&entry_rust_regex, 1, 3, 1, 1); grid.attach(&label_regex_valid, 0, 4, 2, 1); grid.attach(&check_button_case_sensitive, 0, 5, 2, 1); if select_things { grid.attach(&check_button_select_not_all_results, 0, 6, 2, 1); } let box_widget = dialog.get_box_child(); box_widget.append(&grid); dialog.set_visible(true); } let sv = sv.clone(); dialog.connect_response(move |confirmation_dialog_select_unselect, response_type| { let name_wildcard = entry_name.text().trim().to_string(); let path_wildcard = entry_path.text().trim().to_string(); let regex_wildcard = entry_rust_regex.text().trim().to_string(); #[cfg(target_family = "windows")] let name_wildcard = name_wildcard.replace("/", "\\"); #[cfg(target_family = "windows")] let path_wildcard = path_wildcard.replace("/", "\\"); let name_wildcard_excluded = new_excluded_item(&name_wildcard); let name_wildcard_lowercase_excluded = new_excluded_item(&name_wildcard.to_lowercase()); let path_wildcard_excluded = new_excluded_item(&path_wildcard); let path_wildcard_lowercase_excluded = new_excluded_item(&path_wildcard.to_lowercase()); if response_type == ResponseType::Ok { let check_path = check_button_path.is_active(); let check_name = check_button_name.is_active(); let check_regex = check_button_rust_regex.is_active(); let case_sensitive = check_button_case_sensitive.is_active(); let check_all_selected = check_button_select_not_all_results.is_active(); if check_button_path.is_active() || check_button_name.is_active() || check_button_rust_regex.is_active() { let compiled_regex = if check_regex { if let Ok(t) = Regex::new(®ex_wildcard) { t } else { error!("What? Regex should compile properly."); confirmation_dialog_select_unselect.close(); return; } } else { // Trivial regex is used, because I need here regex #[expect(clippy::trivial_regex)] Regex::new("").expect("Empty regex should compile properly.") }; let model = sv.get_model(); let Some(mut iter) = model.iter_first() else { confirmation_dialog_select_unselect.close(); return; }; let using_reference_folders = sv.nb_object.column_header.is_some_and(|e| model.get::(&iter, e)) && !model.get::(&iter, sv.nb_object.column_name).is_empty(); let mut number_of_all_things = 0; let mut number_of_already_selected_things = 0; let mut vec_of_iters: Vec = Vec::new(); loop { // If went to header and all previous items were selected, then deselect last item if let Some(column_header) = sv.nb_object.column_header && model.get::(&iter, column_header) { if select_things { if !using_reference_folders && check_all_selected && (number_of_all_things - number_of_already_selected_things == vec_of_iters.len()) { vec_of_iters.pop(); } for iter in vec_of_iters { model.set_value(&iter, sv.nb_object.column_selection as u32, &true.to_value()); } } else { for iter in vec_of_iters { model.set_value(&iter, sv.nb_object.column_selection as u32, &false.to_value()); } } if !model.iter_next(&mut iter) { break; } number_of_all_things = 0; number_of_already_selected_things = 0; vec_of_iters = Vec::new(); continue; } let is_selected = model.get::(&iter, sv.nb_object.column_selection); let path = model.get::(&iter, sv.nb_object.column_path); let name = model.get::(&iter, sv.nb_object.column_name); let path_and_name = get_full_name_from_path_name(&path, &name); let mut need_to_change_thing: bool = false; number_of_all_things += 1; if check_regex && compiled_regex.find(&path_and_name).is_some() { need_to_change_thing = true; } else { if check_name { if case_sensitive { if regex_check(&name_wildcard_excluded, &name) { need_to_change_thing = true; } } else if regex_check(&name_wildcard_lowercase_excluded, &name.to_lowercase()) { need_to_change_thing = true; } } if check_path { if case_sensitive { if regex_check(&path_wildcard_excluded, &path) { need_to_change_thing = true; } } else if regex_check(&path_wildcard_lowercase_excluded, &path.to_lowercase()) { need_to_change_thing = true; } } } if select_things { if is_selected { number_of_already_selected_things += 1; } else if need_to_change_thing { vec_of_iters.push(iter); } } else if need_to_change_thing { vec_of_iters.push(iter); } // If went to last item and all previous items were selected, then deselect last item if !model.iter_next(&mut iter) { if select_things { if !using_reference_folders && check_all_selected && (number_of_all_things - number_of_already_selected_things == vec_of_iters.len()) { vec_of_iters.pop(); } for iter in vec_of_iters { model.set_value(&iter, sv.nb_object.column_selection as u32, &true.to_value()); } } else { for iter in vec_of_iters { model.set_value(&iter, sv.nb_object.column_selection as u32, &false.to_value()); } } break; } } } } confirmation_dialog_select_unselect.close(); }); } } fn popover_all_except_biggest_smallest( popover: >k4::Popover, sv: &SubView, // tree_view: >k4::TreeView, // column_header: i32, // column_size_as_bytes: i32, // column_dimensions: Option, // column_button_selection: u32, except_biggest: bool, ) { let model = sv.get_model(); let column_header = sv.nb_object.column_header.expect("AEB/AES can't be used without headers"); let column_size_as_bytes = sv.nb_object.column_size_as_bytes.expect("AEB/AES needs size as bytes column"); if let Some(mut iter) = model.iter_first() { let mut end: bool = false; loop { let mut tree_iter_array: Vec = Vec::new(); let mut used_index: Option = None; let mut current_index: usize = 0; let mut size_as_bytes_min_max: u64 = if except_biggest { 0 } else { u64::MAX }; let mut number_of_pixels_min_max: u64 = if except_biggest { 0 } else { u64::MAX }; loop { if model.get::(&iter, column_header) { if !model.iter_next(&mut iter) { end = true; } break; } tree_iter_array.push(iter); let size_as_bytes = model.get::(&iter, column_size_as_bytes); // If dimension exists, then needs to be checked images if let Some(column_dimensions) = sv.nb_object.column_dimensions { let dimensions_string = model.get::(&iter, column_dimensions); let dimensions = change_dimension_to_krotka(&dimensions_string); let number_of_pixels = dimensions.0 * dimensions.1; if except_biggest { 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) { number_of_pixels_min_max = number_of_pixels; size_as_bytes_min_max = size_as_bytes; used_index = Some(current_index); } } 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) { number_of_pixels_min_max = number_of_pixels; size_as_bytes_min_max = size_as_bytes; used_index = Some(current_index); } } else if except_biggest { if size_as_bytes > size_as_bytes_min_max { size_as_bytes_min_max = size_as_bytes; used_index = Some(current_index); } } else if size_as_bytes < size_as_bytes_min_max { size_as_bytes_min_max = size_as_bytes; used_index = Some(current_index); } current_index += 1; if !model.iter_next(&mut iter) { end = true; break; } } let Some(used_index) = used_index else { continue; }; for (index, tree_iter) in tree_iter_array.iter().enumerate() { if index != used_index { model.set_value(tree_iter, sv.nb_object.column_selection as u32, &true.to_value()); } else { model.set_value(tree_iter, sv.nb_object.column_selection as u32, &false.to_value()); } } if end { break; } } } popover.popdown(); } pub(crate) fn connect_popover_select(gui_data: &GuiData) { let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all = gui_data.popovers_select.buttons_popover_select_all.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_select_all(&popover_select, &sv.tree_view, sv.nb_object.column_selection as u32, sv.nb_object.column_header); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_unselect_all = gui_data.popovers_select.buttons_popover_unselect_all.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_unselect_all.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_unselect_all(&popover_select, &sv.tree_view, sv.nb_object.column_selection as u32); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_reverse = gui_data.popovers_select.buttons_popover_reverse.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_reverse.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_reverse(&popover_select, &sv.tree_view, sv.nb_object.column_selection as u32, sv.nb_object.column_header); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all_except_oldest = gui_data.popovers_select.buttons_popover_select_all_except_oldest.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all_except_oldest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_all_except_oldest_newest( &popover_select, &sv.tree_view, sv.nb_object.column_header.expect("AEO can't be used without headers"), sv.nb_object.column_modification_as_secs.expect("AEO needs modification as secs column"), sv.nb_object.column_name, sv.nb_object.column_selection as u32, true, ); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all_except_newest = gui_data.popovers_select.buttons_popover_select_all_except_newest.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all_except_newest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_all_except_oldest_newest( &popover_select, &sv.tree_view, sv.nb_object.column_header.expect("AEN can't be used without headers"), sv.nb_object.column_modification_as_secs.expect("AEN needs modification as secs column"), sv.nb_object.column_name, sv.nb_object.column_selection as u32, false, ); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all_except_shortest = gui_data.popovers_select.buttons_popover_select_all_except_shortest_path.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all_except_shortest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_all_except_longest_shortest_path( &popover_select, &sv.tree_view, sv.nb_object.column_header.expect("AES can't be used without headers"), sv.nb_object.column_path, sv.nb_object.column_selection as u32, true, ); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all_except_longest = gui_data.popovers_select.buttons_popover_select_all_except_longest_path.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all_except_longest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_all_except_longest_shortest_path( &popover_select, &sv.tree_view, sv.nb_object.column_header.expect("AES can't be used without headers"), sv.nb_object.column_path, sv.nb_object.column_selection as u32, false, ); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_one_oldest = gui_data.popovers_select.buttons_popover_select_one_oldest.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_one_oldest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_one_oldest_newest(&popover_select, sv, true); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_one_newest = gui_data.popovers_select.buttons_popover_select_one_newest.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_one_newest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_one_oldest_newest(&popover_select, sv, false); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_custom = gui_data.popovers_select.buttons_popover_select_custom.clone(); let window_main = gui_data.window_main.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_custom.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_custom_select_unselect(&popover_select, &window_main, sv, true); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_unselect_custom = gui_data.popovers_select.buttons_popover_unselect_custom.clone(); let window_main = gui_data.window_main.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_unselect_custom.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_custom_select_unselect(&popover_select, &window_main, sv, false); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all_images_except_biggest = gui_data.popovers_select.buttons_popover_select_all_images_except_biggest.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all_images_except_biggest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_all_except_biggest_smallest(&popover_select, sv, true); }); let popover_select = gui_data.popovers_select.popover_select.clone(); let buttons_popover_select_all_images_except_smallest = gui_data.popovers_select.buttons_popover_select_all_images_except_smallest.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_select_all_images_except_smallest.connect_clicked(move |_| { let sv = common_tree_views.get_current_subview(); popover_all_except_biggest_smallest(&popover_select, sv, false); }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_popovers_sort.rs ================================================ use std::fmt::Debug; use gtk4::prelude::*; use gtk4::{ListStore, TreeIter}; use crate::gui_structs::common_tree_view::{SubView, TreeViewListStoreTrait}; use crate::gui_structs::gui_data::GuiData; fn popover_sort_general_abs(popover: >k4::Popover, sv: &SubView) where T: Ord + for<'b> glib::value::FromValue<'b> + 'static + Debug, { popover_sort_general::( popover, &sv.tree_view, sv.nb_object.column_size_as_bytes.expect("Failed to get size as bytes column"), sv.nb_object.column_header.expect("Failed to get header column"), ); } fn popover_sort_general(popover: >k4::Popover, tree_view: >k4::TreeView, column_sort: i32, column_header: i32) where T: Ord + for<'b> glib::value::FromValue<'b> + 'static + Debug, { let model = tree_view.get_model(); if let Some(mut curr_iter) = model.iter_first() { assert!(model.get::(&curr_iter, column_header)); assert!(model.iter_next(&mut curr_iter)); loop { let mut iters = Vec::new(); let mut all_have = false; let mut local_iter = curr_iter; loop { if model.get::(&local_iter, column_header) { if !model.iter_next(&mut local_iter) { all_have = true; } break; } iters.push(local_iter); if !model.iter_next(&mut local_iter) { all_have = true; break; } } if iters.len() == 1 { curr_iter = local_iter; if all_have { break; } continue; } sort_iters::(&model, iters, column_sort); curr_iter = local_iter; if all_have { break; } } } popover.popdown(); } fn sort_iters(model: &ListStore, mut iters: Vec, column_sort: i32) where T: Ord + for<'b> glib::value::FromValue<'b> + 'static + Debug, { assert!(iters.len() >= 2); loop { let mut changed_item = false; for idx in 0..(iters.len() - 1) { if model.get::(&iters[idx], column_sort) > model.get::(&iters[idx + 1], column_sort) { model.swap(&iters[idx], &iters[idx + 1]); iters.swap(idx, idx + 1); changed_item = true; } } if !changed_item { return; } } } pub(crate) fn connect_popover_sort(gui_data: &GuiData) { let popover_sort = gui_data.popovers_sort.popover_sort.clone(); let buttons_popover_file_name = gui_data.popovers_sort.buttons_popover_sort_file_name.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_file_name.connect_clicked(move |_| { popover_sort_general_abs::(&popover_sort, common_tree_views.get_current_subview()); }); let popover_sort = gui_data.popovers_sort.popover_sort.clone(); let buttons_popover_sort_folder_name = gui_data.popovers_sort.buttons_popover_sort_folder_name.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_sort_folder_name.connect_clicked(move |_| { popover_sort_general_abs::(&popover_sort, common_tree_views.get_current_subview()); }); let popover_sort = gui_data.popovers_sort.popover_sort.clone(); let buttons_popover_sort_selection = gui_data.popovers_sort.buttons_popover_sort_selection.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_sort_selection.connect_clicked(move |_| { popover_sort_general_abs::(&popover_sort, common_tree_views.get_current_subview()); }); let popover_sort = gui_data.popovers_sort.popover_sort.clone(); let buttons_popover_sort_size = gui_data.popovers_sort.buttons_popover_sort_size.clone(); let common_tree_views = gui_data.main_notebook.common_tree_views.clone(); buttons_popover_sort_size.connect_clicked(move |_| { popover_sort_general_abs::(&popover_sort, common_tree_views.get_current_subview()); }); } #[cfg(test)] mod test { use glib::types::Type; use gtk4::prelude::*; use gtk4::{Popover, TreeView}; use rand::random; use super::*; use crate::helpers::list_store_operations::append_row_to_list_store; #[gtk4::test] fn test_sort_iters() { let columns_types: &[Type] = &[Type::U32, Type::STRING]; let list_store = gtk4::ListStore::new(columns_types); let values_to_add: &[&[(u32, &dyn ToValue)]] = &[&[(0, &2), (1, &"AAA")], &[(0, &3), (1, &"CCC")], &[(0, &1), (1, &"BBB")]]; for i in values_to_add { append_row_to_list_store(&list_store, i); } let mut iters = Vec::new(); let mut iter = list_store.iter_first().expect("Failed to get first iter"); iters.push(iter); list_store.iter_next(&mut iter); iters.push(iter); list_store.iter_next(&mut iter); iters.push(iter); sort_iters::(&list_store, iters, 1); let expected = [(2, "AAA"), (1, "BBB"), (3, "CCC")]; let mut curr_iter = list_store.iter_first().expect("Failed to get first iter"); for exp in expected { let real_0 = list_store.get::(&curr_iter, 0); assert_eq!(real_0, exp.0); let real_1 = list_store.get::(&curr_iter, 1); assert_eq!(real_1, exp.1); list_store.iter_next(&mut curr_iter); } } #[gtk4::test] pub(crate) fn test_popover_sort_general_simple() { let columns_types: &[Type] = &[Type::BOOL, Type::STRING]; let list_store = gtk4::ListStore::new(columns_types); let tree_view = TreeView::builder().model(&list_store).build(); let popover = Popover::new(); let values_to_add: &[&[(u32, &dyn ToValue)]] = &[&[(0, &true), (1, &"DDD")], &[(0, &false), (1, &"CCC")], &[(0, &false), (1, &"BBB")]]; for i in values_to_add { append_row_to_list_store(&list_store, i); } popover_sort_general::(&popover, &tree_view, 1, 0); let expected = ["DDD", "BBB", "CCC"]; let mut curr_iter = list_store.iter_first().expect("Failed to get first iter"); for exp in expected { let real = list_store.get::(&curr_iter, 1); assert_eq!(real, exp); list_store.iter_next(&mut curr_iter); } } #[gtk4::test] pub(crate) fn test_popover_sort_general() { let columns_types: &[Type] = &[Type::BOOL, Type::STRING]; let list_store = gtk4::ListStore::new(columns_types); let tree_view = TreeView::builder().model(&list_store).build(); let popover = Popover::new(); let values_to_add: &[&[(u32, &dyn ToValue)]] = &[ &[(0, &true), (1, &"AAA")], &[(0, &false), (1, &"CCC")], &[(0, &false), (1, &"BBB")], &[(0, &true), (1, &"TTT")], &[(0, &false), (1, &"PPP")], &[(0, &false), (1, &"AAA")], &[(0, &true), (1, &"RRR")], &[(0, &false), (1, &"WWW")], &[(0, &false), (1, &"ZZZ")], ]; for i in values_to_add { append_row_to_list_store(&list_store, i); } popover_sort_general::(&popover, &tree_view, 1, 0); let expected = ["AAA", "BBB", "CCC", "TTT", "AAA", "PPP", "RRR", "WWW", "ZZZ"]; let mut curr_iter = list_store.iter_first().expect("Failed to get first iter"); for exp in expected { let real = list_store.get::(&curr_iter, 1); assert_eq!(real, exp); list_store.iter_next(&mut curr_iter); } } #[gtk4::test] pub(crate) fn fuzzer_test() { for _ in 0..1000 { let columns_types: &[Type] = &[Type::BOOL, Type::STRING]; let list_store = gtk4::ListStore::new(columns_types); let tree_view = TreeView::builder().model(&list_store).build(); let popover = Popover::new(); // Always start with a header let first_row: &[(u32, &dyn ToValue)] = &[(0, &true), (1, &"AAA")]; append_row_to_list_store(&list_store, first_row); let mut since_last_header = 0; let mut need_header = false; let num_rows = (random::() % 10 + 5) as usize; let mut i = 0; while i < num_rows { if need_header { // Insert a header only if last was not a header let a: Vec<(u32, &dyn ToValue)> = vec![(0, &true), (1, &"HEADER")]; append_row_to_list_store(&list_store, &a); since_last_header = 0; need_header = false; i += 1; continue; } // Insert a non-header row let string_val = rand::random::().to_string(); let a: Vec<(u32, &dyn ToValue)> = vec![(0, &false), (1, &string_val)]; append_row_to_list_store(&list_store, &a); since_last_header += 1; // After at least 2 non-header rows, randomly decide to insert a header next if since_last_header >= 2 && random::().is_multiple_of(3) { need_header = true; } i += 1; } // Ensure at least one non-header after the last header let mut last_iter = list_store.iter_first().expect("TEST"); let mut last_is_header; loop { last_is_header = list_store.get::(&last_iter, 0); if !list_store.iter_next(&mut last_iter) { break; } } if last_is_header { let a: Vec<(u32, &dyn ToValue)> = vec![(0, &false), (1, &"FINALROW")]; append_row_to_list_store(&list_store, &a); } popover_sort_general::(&popover, &tree_view, 1, 0); } } } ================================================ FILE: czkawka_gui/src/connect_things/connect_progress_window.rs ================================================ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; use crossbeam_channel::Receiver; use czkawka_core::common::model::ToolType; use czkawka_core::common::progress_data::{CurrentStage, ProgressData}; use glib::MainContext; use gtk4::ProgressBar; use gtk4::prelude::*; use humansize::{BINARY, format_size}; use crate::flg; use crate::gui_structs::gui_data::GuiData; use crate::localizer_core::generate_translation_hashmap; use crate::taskbar_progress::TaskbarProgress; use crate::taskbar_progress::tbp_flags::TBPF_INDETERMINATE; pub(crate) fn connect_progress_window(gui_data: &GuiData, progress_receiver: Receiver) { let main_context = MainContext::default(); let _guard = main_context.acquire().expect("Failed to acquire main context"); let gui_data = gui_data.clone(); let future = async move { loop { loop { let item = progress_receiver.try_recv(); if let Ok(item) = item { if item.current_stage_idx == 0 { progress_collect_items(&gui_data, &item, item.tool_type != ToolType::EmptyFolders); } else if item.sstage.check_if_loading_saving_cache() { progress_save_load_cache(&gui_data, &item); } else { progress_default(&gui_data, &item); } } else { break; } } glib::timeout_future(Duration::from_millis(300)).await; } }; main_context.spawn_local(future); } fn progress_save_load_cache(gui_data: &GuiData, item: &ProgressData) { let label_stage = gui_data.progress_window.label_stage.clone(); let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone(); let taskbar_state = gui_data.taskbar_state.clone(); progress_bar_current_stage.set_visible(false); taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE); let text = match item.sstage { CurrentStage::SameMusicCacheLoadingFingerprints | CurrentStage::SameMusicCacheLoadingTags => { flg!("progress_cache_loading") } CurrentStage::SameMusicCacheSavingFingerprints | CurrentStage::SameMusicCacheSavingTags => { flg!("progress_cache_saving") } CurrentStage::DuplicateCacheLoading => { flg!("progress_hash_cache_loading") } CurrentStage::DuplicateCacheSaving => { flg!("progress_hash_cache_saving") } CurrentStage::DuplicatePreHashCacheLoading => { flg!("progress_prehash_cache_loading") } CurrentStage::DuplicatePreHashCacheSaving => { flg!("progress_prehash_cache_saving") } CurrentStage::ExifRemoverCacheLoading | CurrentStage::ExifRemoverCacheSaving | CurrentStage::ExifRemoverExtractingTags | CurrentStage::CleaningExif => { panic!("Exif remover not implemented in gtk version") } _ => panic!("Invalid stage {:?}", item.sstage), }; label_stage.set_text(&text); } fn progress_collect_items(gui_data: &GuiData, item: &ProgressData, files: bool) { let label_stage = gui_data.progress_window.label_stage.clone(); let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone(); let taskbar_state = gui_data.taskbar_state.clone(); progress_bar_current_stage.set_visible(false); taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE); match item.sstage { CurrentStage::DuplicateScanningName => { label_stage.set_text(&flg!("progress_scanning_name", file_number_tm(item))); } CurrentStage::DuplicateScanningSizeName => { label_stage.set_text(&flg!("progress_scanning_size_name", file_number_tm(item))); } CurrentStage::DuplicateScanningSize => { label_stage.set_text(&flg!("progress_scanning_size", file_number_tm(item))); } _ => { if files { label_stage.set_text(&flg!("progress_scanning_general_file", file_number_tm(item))); } else { label_stage.set_text(&flg!("progress_scanning_empty_folders", folder_number = item.entries_checked)); } } } } fn progress_default(gui_data: &GuiData, item: &ProgressData) { let label_stage = gui_data.progress_window.label_stage.clone(); let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone(); let progress_bar_all_stages = gui_data.progress_window.progress_bar_all_stages.clone(); let taskbar_state = gui_data.taskbar_state.clone(); progress_bar_current_stage.set_visible(true); common_set_data(item, &progress_bar_all_stages, &progress_bar_current_stage, &taskbar_state); taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE); match item.sstage { CurrentStage::SameMusicReadingTags => { label_stage.set_text(&flg!("progress_scanning_music_tags", progress_ratio_tm(item))); } CurrentStage::SameMusicCalculatingFingerprints => { label_stage.set_text(&flg!("progress_scanning_music_content", progress_ratio_tm(item))); } CurrentStage::SameMusicComparingTags => { label_stage.set_text(&flg!("progress_scanning_music_tags_end", progress_ratio_tm(item))); } CurrentStage::SameMusicComparingFingerprints => { label_stage.set_text(&flg!("progress_scanning_music_content_end", progress_ratio_tm(item))); } CurrentStage::SimilarImagesCalculatingHashes => { label_stage.set_text(&flg!("progress_scanning_image", progress_ratio_tm(item))); } CurrentStage::SimilarImagesComparingHashes => { label_stage.set_text(&flg!("progress_comparing_image_hashes", progress_ratio_tm(item))); } CurrentStage::SimilarVideosCalculatingHashes => { label_stage.set_text(&flg!("progress_scanning_video", progress_ratio_tm(item))); } CurrentStage::SimilarVideosCreatingThumbnails => { label_stage.set_text(&flg!("progress_creating_video_thumbnails", progress_ratio_tm(item))); } CurrentStage::BrokenFilesChecking => { label_stage.set_text(&flg!("progress_scanning_broken_files", progress_ratio_tm(item))); } CurrentStage::BadExtensionsChecking => { label_stage.set_text(&flg!("progress_scanning_extension_of_files", progress_ratio_tm(item))); } CurrentStage::DuplicatePreHashing => { label_stage.set_text(&flg!("progress_analyzed_partial_hash", progress_ratio_tm(item))); } CurrentStage::DuplicateFullHashing => { label_stage.set_text(&flg!("progress_analyzed_full_hash", progress_ratio_tm(item))); } _ => unreachable!("Invalid stage {:?}", item.sstage), } } fn common_set_data(item: &ProgressData, progress_bar_all_stages: &ProgressBar, progress_bar_current_stage: &ProgressBar, taskbar_state: &Rc>) { let (current_items_checked, current_stage_items_to_check) = if item.bytes_to_check > 0 { (item.bytes_checked, item.bytes_to_check) } else { (item.entries_checked as u64, item.entries_to_check as u64) }; if item.entries_to_check != 0 { 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; let all_stages = all_stages.min(0.99); progress_bar_all_stages.set_fraction(all_stages); progress_bar_current_stage.set_fraction(current_items_checked as f64 / current_stage_items_to_check as f64); taskbar_state.borrow().set_progress_value( (item.current_stage_idx as u64) * current_stage_items_to_check + current_items_checked, current_stage_items_to_check * (item.max_stage_idx + 1) as u64, ); } else { let all_stages = (item.current_stage_idx as f64) / (item.max_stage_idx + 1) as f64; let all_stages = all_stages.min(0.99); progress_bar_all_stages.set_fraction(all_stages); progress_bar_current_stage.set_fraction(0f64); taskbar_state.borrow().set_progress_value(item.current_stage_idx as u64, 1 + item.max_stage_idx as u64); } } fn file_number_tm(item: &ProgressData) -> HashMap<&'static str, String> { generate_translation_hashmap(vec![("file_number", item.entries_checked.to_string())]) } fn progress_ratio_tm(item: &ProgressData) -> HashMap<&'static str, String> { let mut v = vec![("file_checked", item.entries_checked.to_string()), ("all_files", item.entries_to_check.to_string())]; if item.bytes_to_check != 0 { v.push(("data_checked", format_size(item.bytes_checked, BINARY))); v.push(("all_data", format_size(item.bytes_to_check, BINARY))); } generate_translation_hashmap(v) } ================================================ FILE: czkawka_gui/src/connect_things/connect_same_music_mode_changed.rs ================================================ use czkawka_core::common::model::CheckingMethod; use gtk4::prelude::*; use gtk4::{CheckButton, Widget}; use crate::gui_structs::gui_data::GuiData; use crate::help_combo_box::AUDIO_TYPE_CHECK_METHOD_COMBO_BOX; use crate::help_functions::scale_set_min_max_values; const MINIMUM_SECONDS: f64 = 0.5; const MAXIMUM_SECONDS: f64 = 180.0; const DEFAULT_SECONDS: f64 = 15.0; const MINIMUM_SIMILARITY: f64 = 0.0; const MAXIMUM_SIMILARITY: f64 = 10.0; const DEFAULT_SIMILARITY: f64 = 5.0; pub(crate) fn connect_same_music_change_mode(gui_data: &GuiData) { let check_button_music_title = gui_data.main_notebook.check_button_music_title.clone(); let check_button_music_approximate_comparison = gui_data.main_notebook.check_button_music_approximate_comparison.clone(); let check_button_music_bitrate = gui_data.main_notebook.check_button_music_bitrate.clone(); let check_button_music_artist = gui_data.main_notebook.check_button_music_artist.clone(); let check_button_music_genre = gui_data.main_notebook.check_button_music_genre.clone(); let check_button_music_length = gui_data.main_notebook.check_button_music_length.clone(); let check_button_music_year = gui_data.main_notebook.check_button_music_year.clone(); let buttons = [ check_button_music_title, check_button_music_approximate_comparison, check_button_music_bitrate, check_button_music_artist, check_button_music_genre, check_button_music_year, check_button_music_length, ]; let check_button_music_compare_only_in_title_group = gui_data.main_notebook.check_button_music_compare_only_in_title_group.clone(); let reversed_buttons = [check_button_music_compare_only_in_title_group]; let scale_seconds_same_music = gui_data.main_notebook.scale_seconds_same_music.clone(); let scale_similarity_same_music = gui_data.main_notebook.scale_similarity_same_music.clone(); let label_same_music_similarity = gui_data.main_notebook.label_same_music_similarity.clone(); let label_same_music_seconds = gui_data.main_notebook.label_same_music_seconds.clone(); scale_set_min_max_values(&scale_seconds_same_music, MINIMUM_SECONDS, MAXIMUM_SECONDS, DEFAULT_SECONDS, None); scale_set_min_max_values(&scale_similarity_same_music, MINIMUM_SIMILARITY, MAXIMUM_SIMILARITY, DEFAULT_SIMILARITY, None); let scales_and_labels = [ scale_seconds_same_music.into(), scale_similarity_same_music.into(), label_same_music_similarity.into(), label_same_music_seconds.into(), ]; let combo_box_audio_check_type = gui_data.main_notebook.combo_box_audio_check_type.clone(); let check_method_index = combo_box_audio_check_type.active().expect("Failed to get active item") as usize; let check_method = AUDIO_TYPE_CHECK_METHOD_COMBO_BOX[check_method_index].check_method; disable_enable_buttons(&buttons, &reversed_buttons, &scales_and_labels, check_method); combo_box_audio_check_type.connect_changed(move |combo_box_text| { if let Some(active) = combo_box_text.active() { let check_method = AUDIO_TYPE_CHECK_METHOD_COMBO_BOX[active as usize].check_method; disable_enable_buttons(&buttons, &reversed_buttons, &scales_and_labels, check_method); } }); } fn disable_enable_buttons(buttons: &[CheckButton; 7], reverse_buttons: &[CheckButton; 1], scales: &[Widget; 4], current_mode: CheckingMethod) { match current_mode { CheckingMethod::AudioTags => { buttons.iter().for_each(WidgetExt::show); reverse_buttons.iter().for_each(WidgetExt::hide); scales.iter().for_each(WidgetExt::hide); } CheckingMethod::AudioContent => { buttons.iter().for_each(WidgetExt::hide); reverse_buttons.iter().for_each(WidgetExt::show); scales.iter().for_each(WidgetExt::show); } _ => panic!(), } } ================================================ FILE: czkawka_gui/src/connect_things/connect_selection_of_directories.rs ================================================ use std::collections::HashSet; use std::path::PathBuf; #[cfg(target_family = "windows")] use czkawka_core::common::normalize_windows_path; use gdk4::{DragAction, FileList}; use gtk4::prelude::*; use gtk4::{DropTarget, FileChooserNative, Notebook, Orientation, ResponseType, TreeView, Window}; use crate::connect_things::file_chooser_helpers::extract_paths_from_file_chooser; use crate::flg; use crate::gui_structs::common_tree_view::TreeViewListStoreTrait; use crate::gui_structs::common_upper_tree_view::UpperTreeViewEnum; use crate::gui_structs::gui_data::GuiData; use crate::helpers::enums::{ColumnsExcludedDirectory, ColumnsIncludedDirectory}; use crate::helpers::list_store_operations::{append_row_to_list_store, check_if_value_is_in_list_store}; use crate::notebook_enums::{NotebookUpperEnum, to_notebook_upper_enum}; pub(crate) fn connect_selection_of_directories(gui_data: &GuiData) { let tree_view_included_directories = gui_data.upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::IncludedDirectories); let tree_view_excluded_directories = gui_data.upper_notebook.common_upper_tree_views.get_tree_view(UpperTreeViewEnum::ExcludedDirectories); // Add manually directory { let tree_view_included_directories = tree_view_included_directories.clone(); let window_main = gui_data.window_main.clone(); let buttons_manual_add_included_directory = gui_data.upper_notebook.buttons_manual_add_included_directory.clone(); buttons_manual_add_included_directory.connect_clicked(move |_| { add_manually_directories(&window_main, &tree_view_included_directories, false); }); } // Add manually excluded directory { let tree_view_excluded_directories = tree_view_excluded_directories.clone(); let window_main = gui_data.window_main.clone(); let buttons_manual_add_excluded_directory = gui_data.upper_notebook.buttons_manual_add_excluded_directory.clone(); buttons_manual_add_excluded_directory.connect_clicked(move |_| { add_manually_directories(&window_main, &tree_view_excluded_directories, true); }); } // Add included directory { let buttons_add_included_directory = gui_data.upper_notebook.buttons_add_included_directory.clone(); let file_dialog_include_exclude_folder_selection = gui_data.file_dialog_include_exclude_folder_selection.clone(); buttons_add_included_directory.connect_clicked(move |_| { file_dialog_include_exclude_folder_selection.set_visible(true); file_dialog_include_exclude_folder_selection.set_title(&flg!("include_folders_dialog_title")); }); } // Add excluded directory { let buttons_add_excluded_directory = gui_data.upper_notebook.buttons_add_excluded_directory.clone(); let file_dialog_include_exclude_folder_selection = gui_data.file_dialog_include_exclude_folder_selection.clone(); buttons_add_excluded_directory.connect_clicked(move |_| { file_dialog_include_exclude_folder_selection.set_visible(true); file_dialog_include_exclude_folder_selection.set_title(&flg!("exclude_folders_dialog_title")); }); } // Connect { let notebook_upper = gui_data.upper_notebook.notebook_upper.clone(); let tree_view_included_directories = tree_view_included_directories.clone(); let tree_view_excluded_directories = tree_view_excluded_directories.clone(); let file_dialog_include_exclude_folder_selection = gui_data.file_dialog_include_exclude_folder_selection.clone(); connect_file_dialog( &file_dialog_include_exclude_folder_selection, tree_view_included_directories, tree_view_excluded_directories, notebook_upper, ); } // Drag and drop { configure_directory_drop(tree_view_included_directories, false); configure_directory_drop(tree_view_excluded_directories, true); } // Remove Excluded Folder { let buttons_remove_excluded_directory = gui_data.upper_notebook.buttons_remove_excluded_directory.clone(); let tree_view_excluded_directories = tree_view_excluded_directories.clone(); buttons_remove_excluded_directory.connect_clicked(move |_| { remove_item_directory(&tree_view_excluded_directories); }); } // Remove Included Folder { let buttons_remove_included_directory = gui_data.upper_notebook.buttons_remove_included_directory.clone(); let tree_view_included_directories = tree_view_included_directories.clone(); buttons_remove_included_directory.connect_clicked(move |_| { remove_item_directory(&tree_view_included_directories); }); } } fn remove_item_directory(tree_view: &TreeView) { let list_store = tree_view.get_model(); let selection = tree_view.selection(); let (vec_tree_path, _tree_model) = selection.selected_rows(); for tree_path in vec_tree_path.iter().rev() { list_store.remove(&list_store.iter(tree_path).expect("Using invalid tree_path")); } } fn configure_directory_drop(tree_view: &TreeView, excluded_items: bool) { let tv = tree_view.clone(); let drop_target = DropTarget::builder().name("file-drop-target").actions(DragAction::COPY).build(); drop_target.set_types(&[FileList::static_type()]); drop_target.connect_drop(move |_, value, _, _| { if let Ok(file_list) = value.get::() { let mut folders: HashSet = HashSet::new(); for f in file_list.files() { if let Some(path) = f.path() { if path.is_dir() { folders.insert(path); } else if let Some(parent) = path.parent() && parent.is_dir() { folders.insert(parent.to_path_buf()); } } } add_directories(&tv, &folders.into_iter().collect(), excluded_items); } true }); tree_view.add_controller(drop_target); } fn connect_file_dialog(file_dialog_include_exclude_folder_selection: &FileChooserNative, include_tree_view: TreeView, exclude_tree_view: TreeView, notebook_upper: Notebook) { file_dialog_include_exclude_folder_selection.connect_response(move |file_chooser, response_type| { if response_type == ResponseType::Accept { let excluded_items; let tree_view = match to_notebook_upper_enum(notebook_upper.current_page().expect("Current page not set")) { NotebookUpperEnum::IncludedDirectories => { excluded_items = false; &include_tree_view } NotebookUpperEnum::ExcludedDirectories => { excluded_items = true; &exclude_tree_view } NotebookUpperEnum::ItemsConfiguration => panic!(), }; let folders = extract_paths_from_file_chooser(file_chooser); add_directories(tree_view, &folders, excluded_items); } }); } fn add_directories(tree_view: &TreeView, folders: &Vec, excluded_items: bool) { let list_store = tree_view.get_model(); if excluded_items { for file_entry in folders { let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &file_entry.to_string_lossy().to_string())]; append_row_to_list_store(&list_store, &values); } } else { for file_entry in folders { let values: [(u32, &dyn ToValue); 2] = [ (ColumnsIncludedDirectory::Path as u32, &file_entry.to_string_lossy().to_string()), (ColumnsIncludedDirectory::ReferenceButton as u32, &false), ]; append_row_to_list_store(&list_store, &values); } } } fn add_manually_directories(window_main: &Window, tree_view: &TreeView, excluded_items: bool) { let dialog = gtk4::Dialog::builder() .title(flg!("include_manually_directories_dialog_title")) .transient_for(window_main) .modal(true) .build(); dialog.set_default_size(300, 0); let entry: gtk4::Entry = gtk4::Entry::new(); let added_button = dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); let parent = added_button.parent().expect("Hack 1").parent().expect("Hack 2").downcast::().expect("Hack 3"); // TODO Hack, but not so ugly as before parent.set_orientation(Orientation::Vertical); parent.insert_child_after(&entry, None::<>k4::Widget>); dialog.set_visible(true); let tree_view = tree_view.clone(); dialog.connect_response(move |dialog, response_type| { if response_type == ResponseType::Ok { for text in entry.text().split(';') { let text = text.trim().to_string(); #[cfg(target_family = "windows")] let text = normalize_windows_path(text).to_string_lossy().to_string(); let mut text = text; remove_ending_slashes(&mut text); if !text.is_empty() { let list_store = tree_view.get_model(); if excluded_items { if !check_if_value_is_in_list_store(&list_store, ColumnsExcludedDirectory::Path as i32, &text) { let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &text)]; append_row_to_list_store(&list_store, &values); } } else if !check_if_value_is_in_list_store(&list_store, ColumnsIncludedDirectory::Path as i32, &text) { let values: [(u32, &dyn ToValue); 2] = [(ColumnsIncludedDirectory::Path as u32, &text), (ColumnsIncludedDirectory::ReferenceButton as u32, &false)]; append_row_to_list_store(&list_store, &values); } } } } dialog.close(); }); } fn remove_ending_slashes(original_string: &mut String) { let mut windows_disk_path: bool = false; let mut chars = original_string.chars(); if let Some(first_character) = chars.next() && first_character.is_alphabetic() && let Some(second_character) = chars.next() && second_character == ':' { windows_disk_path = true; original_string.push('/'); // In case of adding window path without ending slash e.g. C: instead C:/ or C:\ } while (original_string != "/" && (original_string.ends_with('/') || original_string.ends_with('\\'))) && (!windows_disk_path || original_string.len() > 3) { original_string.pop(); } } #[test] pub(crate) fn test_remove_ending_slashes() { let mut original = "/home/rafal".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/home/rafal"); let mut original = "/home/rafal/".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/home/rafal"); let mut original = "/home/rafal\\".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/home/rafal"); let mut original = "/home/rafal/////////".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/home/rafal"); let mut original = "/home/rafal/\\//////\\\\".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/home/rafal"); let mut original = "/home/rafal\\\\\\\\\\\\\\\\".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/home/rafal"); let mut original = "\\\\\\\\\\\\\\\\\\\\\\\\".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, ""); let mut original = "//////////".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "/"); let mut original = "C:/".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "C:/"); let mut original = "C:\\".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "C:\\"); let mut original = "C://////////".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "C:/"); let mut original = "C:/roman/function/".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "C:/roman/function"); let mut original = "C:/staszek/without".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "C:/staszek/without"); let mut original = "C:\\\\\\\\\\".to_string(); remove_ending_slashes(&mut original); assert_eq!(&original, "C:\\"); } ================================================ FILE: czkawka_gui/src/connect_things/connect_settings.rs ================================================ use std::collections::BTreeMap; use std::default::Default; use czkawka_core::common::cache::{load_cache_from_file_generalized_by_path, load_cache_from_file_generalized_by_size, save_cache_to_file_generalized}; use czkawka_core::common::config_cache_path::get_config_cache_path; use czkawka_core::common::model::HashType; use czkawka_core::helpers::messages::{MessageLimit, Messages}; use czkawka_core::re_exported::HashAlg; use czkawka_core::tools::duplicate::DuplicateEntry; use czkawka_core::tools::duplicate::core::get_duplicate_cache_file; use czkawka_core::tools::similar_images::core::get_similar_images_cache_file; use czkawka_core::tools::similar_videos::core::get_similar_videos_cache_file; use czkawka_core::tools::similar_videos::{DEFAULT_CROP_DETECT, DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION}; use gtk4::prelude::*; use gtk4::{Label, ResponseType, Window}; use image::imageops::FilterType; use log::error; use crate::flg; use crate::gtk_traits::DialogTraits; use crate::gui_structs::gui_data::GuiData; use crate::saving_loading::{load_configuration, reset_configuration, save_configuration}; pub(crate) fn connect_settings(gui_data: &GuiData) { // Connect scale { let label_restart_needed = gui_data.settings.label_restart_needed.clone(); gui_data.settings.scale_settings_number_of_threads.connect_value_changed(move |_| { if label_restart_needed.label().is_empty() { label_restart_needed.set_label(&flg!("settings_label_restart")); } }); } // Connect button settings { let button_settings = gui_data.header.button_settings.clone(); let window_settings = gui_data.settings.window_settings.clone(); button_settings.connect_clicked(move |_| { window_settings.set_visible(true); }); let window_settings = gui_data.settings.window_settings.clone(); window_settings.connect_close_request(move |window| { window.set_visible(false); glib::Propagation::Stop }); } // Connect save configuration button { let upper_notebook = gui_data.upper_notebook.clone(); let settings = gui_data.settings.clone(); let main_notebook = gui_data.main_notebook.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let button_settings_save_configuration = gui_data.settings.button_settings_save_configuration.clone(); button_settings_save_configuration.connect_clicked(move |_| { save_configuration(true, &upper_notebook, &main_notebook, &settings, &text_view_errors); }); } // Connect load configuration button { let upper_notebook = gui_data.upper_notebook.clone(); let settings = gui_data.settings.clone(); let main_notebook = gui_data.main_notebook.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let button_settings_load_configuration = gui_data.settings.button_settings_load_configuration.clone(); let scrolled_window_errors = gui_data.scrolled_window_errors.clone(); button_settings_load_configuration.connect_clicked(move |_| { load_configuration(true, &upper_notebook, &main_notebook, &settings, &text_view_errors, &scrolled_window_errors, None); }); } // Connect reset configuration button { let upper_notebook = gui_data.upper_notebook.clone(); let settings = gui_data.settings.clone(); let main_notebook = gui_data.main_notebook.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let button_settings_reset_configuration = gui_data.settings.button_settings_reset_configuration.clone(); button_settings_reset_configuration.connect_clicked(move |_| { reset_configuration(true, &upper_notebook, &main_notebook, &settings, &text_view_errors); }); } // Connect button for opening cache { let button_settings_open_cache_folder = gui_data.settings.button_settings_open_cache_folder.clone(); button_settings_open_cache_folder.connect_clicked(move |_| { if let Some(config_cache_path) = get_config_cache_path() { if let Err(e) = open::that(&config_cache_path.cache_folder) { error!("Failed to open config folder \"{}\", reason {e}", config_cache_path.cache_folder.to_string_lossy()); } } else { error!("Failed to get cache folder path"); } }); } // Connect button for opening settings { let button_settings_open_settings_folder = gui_data.settings.button_settings_open_settings_folder.clone(); button_settings_open_settings_folder.connect_clicked(move |_| { if let Some(config_cache_path) = get_config_cache_path() { if let Err(e) = open::that(&config_cache_path.config_folder) { error!("Failed to open config folder \"{}\", reason {e}", config_cache_path.config_folder.to_string_lossy()); } } else { error!("Failed to get settings folder path"); } }); } // Connect clear cache methods { { let button_settings_duplicates_clear_cache = gui_data.settings.button_settings_duplicates_clear_cache.clone(); let settings_window = gui_data.settings.window_settings.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let entry_settings_cache_file_minimal_size = gui_data.settings.entry_settings_cache_file_minimal_size.clone(); button_settings_duplicates_clear_cache.connect_clicked(move |_| { let dialog = create_clear_cache_dialog(&flg!("cache_clear_duplicates_title"), &settings_window); dialog.set_visible(true); let text_view_errors = text_view_errors.clone(); let entry_settings_cache_file_minimal_size = entry_settings_cache_file_minimal_size.clone(); dialog.connect_response(move |dialog, response_type| { if response_type == ResponseType::Ok { let mut messages: Messages = Messages::new(); for use_prehash in [true, false] { for type_of_hash in [HashType::Xxh3, HashType::Blake3, HashType::Crc32] { let file_name = get_duplicate_cache_file(type_of_hash, use_prehash); let (mut messages, loaded_items) = load_cache_from_file_generalized_by_size::(&file_name, true, &Default::default()); if let Some(cache_entries) = loaded_items { let mut hashmap_to_save: BTreeMap = Default::default(); for (_, vec_file_entry) in cache_entries { for file_entry in vec_file_entry { hashmap_to_save.insert(file_entry.path.to_string_lossy().to_string(), file_entry); } } let minimal_cache_size = entry_settings_cache_file_minimal_size.text().as_str().parse::().unwrap_or(2 * 1024 * 1024); let save_messages = save_cache_to_file_generalized(&file_name, &hashmap_to_save, false, minimal_cache_size); messages.extend_with_another_messages(save_messages); } } messages.messages.push(flg!("cache_properly_cleared")); text_view_errors.buffer().set_text(messages.create_messages_text(MessageLimit::NoLimit).as_str()); } } dialog.close(); }); }); } { let button_settings_similar_images_clear_cache = gui_data.settings.button_settings_similar_images_clear_cache.clone(); let settings_window = gui_data.settings.window_settings.clone(); let text_view_errors = gui_data.text_view_errors.clone(); button_settings_similar_images_clear_cache.connect_clicked(move |_| { let dialog = create_clear_cache_dialog(&flg!("cache_clear_similar_images_title"), &settings_window); dialog.set_visible(true); let text_view_errors = text_view_errors.clone(); dialog.connect_response(move |dialog, response_type| { if response_type == ResponseType::Ok { let mut messages: Messages = Messages::new(); for hash_size in [8, 16, 32, 64] { for image_filter in [ FilterType::Lanczos3, FilterType::CatmullRom, FilterType::Gaussian, FilterType::Nearest, FilterType::Triangle, ] { for hash_alg in [ HashAlg::Blockhash, HashAlg::Gradient, HashAlg::DoubleGradient, HashAlg::VertGradient, HashAlg::Mean, HashAlg::Median, ] { let file_name = get_similar_images_cache_file(hash_size, hash_alg, image_filter); let (mut messages, loaded_items) = load_cache_from_file_generalized_by_path::(&file_name, true, &Default::default()); if let Some(cache_entries) = loaded_items { let save_messages = save_cache_to_file_generalized(&file_name, &cache_entries, false, 0); messages.extend_with_another_messages(save_messages); } } } } messages.messages.push(flg!("cache_properly_cleared")); text_view_errors.buffer().set_text(messages.create_messages_text(MessageLimit::NoLimit).as_str()); } dialog.close(); }); }); } { let button_settings_similar_videos_clear_cache = gui_data.settings.button_settings_similar_videos_clear_cache.clone(); let settings_window = gui_data.settings.window_settings.clone(); let text_view_errors = gui_data.text_view_errors.clone(); button_settings_similar_videos_clear_cache.connect_clicked(move |_| { let dialog = create_clear_cache_dialog(&flg!("cache_clear_similar_videos_title"), &settings_window); dialog.set_visible(true); let text_view_errors = text_view_errors.clone(); dialog.connect_response(move |dialog, response_type| { if response_type == ResponseType::Ok { let file_name = get_similar_videos_cache_file(DEFAULT_SKIP_FORWARD_AMOUNT, DEFAULT_VID_HASH_DURATION, DEFAULT_CROP_DETECT); let (mut messages, loaded_items) = load_cache_from_file_generalized_by_path::(&file_name, true, &Default::default()); if let Some(cache_entries) = loaded_items { let save_messages = save_cache_to_file_generalized(&file_name, &cache_entries, false, 0); messages.extend_with_another_messages(save_messages); } messages.messages.push(flg!("cache_properly_cleared")); text_view_errors.buffer().set_text(messages.create_messages_text(MessageLimit::NoLimit).as_str()); } dialog.close(); }); }); } } } fn create_clear_cache_dialog(title_str: &str, window_settings: &Window) -> gtk4::Dialog { let dialog = gtk4::Dialog::builder().title(title_str).modal(true).transient_for(window_settings).build(); dialog.add_button(&flg!("general_ok_button"), ResponseType::Ok); dialog.add_button(&flg!("general_close_button"), ResponseType::Cancel); let label = Label::builder().label(flg!("cache_clear_message_label_1")).build(); let label2 = Label::builder().label(flg!("cache_clear_message_label_2")).build(); let label3 = Label::builder().label(flg!("cache_clear_message_label_3")).build(); let label4 = Label::builder().label(flg!("cache_clear_message_label_4")).build(); let internal_box = dialog.get_box_child(); internal_box.append(&label); internal_box.append(&label2); internal_box.append(&label3); internal_box.append(&label4); dialog } ================================================ FILE: czkawka_gui/src/connect_things/connect_show_hide_ui.rs ================================================ use gtk4::prelude::*; use crate::gui_structs::gui_data::GuiData; pub(crate) fn connect_show_hide_ui(gui_data: &GuiData) { let check_button_settings_show_text_view = gui_data.settings.check_button_settings_show_text_view.clone(); let buttons_show_errors = gui_data.bottom_buttons.buttons_show_errors.clone(); let scrolled_window_errors = gui_data.scrolled_window_errors.clone(); buttons_show_errors.connect_clicked(move |_| { if scrolled_window_errors.is_visible() { scrolled_window_errors.set_visible(false); check_button_settings_show_text_view.set_active(false); } else { scrolled_window_errors.set_visible(true); check_button_settings_show_text_view.set_active(true); } }); let buttons_show_upper_notebook = gui_data.bottom_buttons.buttons_show_upper_notebook.clone(); let notebook_upper = gui_data.upper_notebook.notebook_upper.clone(); buttons_show_upper_notebook.connect_clicked(move |_| { if notebook_upper.is_visible() { notebook_upper.set_visible(false); } else { notebook_upper.set_visible(true); } }); } ================================================ FILE: czkawka_gui/src/connect_things/connect_similar_image_size_change.rs ================================================ use czkawka_core::tools::similar_images::SIMILAR_VALUES; use czkawka_core::tools::similar_images::core::get_string_from_similarity; use gtk4::prelude::*; use crate::gui_structs::gui_data::GuiData; use crate::help_combo_box::IMAGES_HASH_SIZE_COMBO_BOX; pub(crate) fn connect_similar_image_size_change(gui_data: &GuiData) { let label_similar_images_minimal_similarity = gui_data.main_notebook.label_similar_images_minimal_similarity.clone(); label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[0][5], 8)); let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone(); let label_similar_images_minimal_similarity = gui_data.main_notebook.label_similar_images_minimal_similarity.clone(); let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone(); combo_box_image_hash_size.connect_changed(move |combo_box_image_hash_size| { let hash_size_index = combo_box_image_hash_size.active().expect("Failed to get active item") as usize; let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index]; let index = match hash_size { 8 => 0, 16 => 1, 32 => 2, 64 => 3, _ => panic!(), }; scale_similarity_similar_images.set_range(0_f64, SIMILAR_VALUES[index][5] as f64); scale_similarity_similar_images.set_fill_level(SIMILAR_VALUES[index][5] as f64); label_similar_images_minimal_similarity.set_text(&get_string_from_similarity(SIMILAR_VALUES[index][5], hash_size as u8)); }); } ================================================ FILE: czkawka_gui/src/connect_things/file_chooser_helpers.rs ================================================ use std::path::PathBuf; use gtk4::prelude::*; pub fn extract_paths_from_file_chooser(file_chooser: >k4::FileChooserNative) -> Vec { let mut folders: Vec = Vec::new(); let g_files = file_chooser.files(); for index in 0..g_files.n_items() { if let Some(file) = g_files.item(index) { let ss = file.clone().downcast::().expect("Failed to downcast to File"); if let Some(path_buf) = ss.path() { folders.push(path_buf); } } } folders } ================================================ FILE: czkawka_gui/src/connect_things/mod.rs ================================================ pub mod connect_about_buttons; pub mod connect_button_compare; pub mod connect_button_delete; pub mod connect_button_hardlink; pub mod connect_button_move; pub mod connect_button_save; pub mod connect_button_search; pub mod connect_button_select; pub mod connect_button_sort; pub mod connect_button_stop; pub mod connect_change_language; pub mod connect_duplicate_buttons; pub mod connect_header_buttons; pub mod connect_krokiet_info_dialog; pub mod connect_notebook_tabs; pub mod connect_popovers_select; pub mod connect_popovers_sort; pub mod connect_progress_window; pub mod connect_same_music_mode_changed; pub mod connect_selection_of_directories; pub mod connect_settings; pub mod connect_show_hide_ui; pub mod connect_similar_image_size_change; pub mod file_chooser_helpers; ================================================ FILE: czkawka_gui/src/gtk_traits.rs ================================================ use std::collections::VecDeque; use std::vec::Vec; use gtk4::prelude::{ComboBoxExtManual, *}; use gtk4::{Box as GtkBox, ComboBoxText, Dialog, Widget}; pub trait ComboBoxTraits { fn set_model_and_first(&self, models: I) where I: IntoIterator, S: AsRef; } impl ComboBoxTraits for ComboBoxText { fn set_model_and_first(&self, models: I) where I: IntoIterator, S: AsRef, { for item in models { self.append_text(item.as_ref()); } self.set_active(Some(0)); } } pub trait DialogTraits { fn get_box_child(&self) -> GtkBox; } impl DialogTraits for Dialog { fn get_box_child(&self) -> GtkBox { self.child().expect("Dialog has no child").downcast::().expect("Dialog child is not Box") } } #[allow(clippy::allow_attributes)] #[allow(dead_code)] pub trait WidgetTraits { fn get_all_direct_children(&self) -> Vec; fn get_all_widgets_of_type>(&self, recursive: bool) -> Vec; fn get_widget_of_type>(&self, recursive: bool) -> T; fn get_all_boxes(&self) -> Vec; fn debug_print_widget(&self, print_only_direct_children: bool); } impl> WidgetTraits for P { fn get_all_direct_children(&self) -> Vec { let mut vector = Vec::new(); if let Some(mut child) = self.first_child() { vector.push(child.clone()); loop { child = match child.next_sibling() { Some(t) => t, None => break, }; vector.push(child.clone()); } } vector } fn get_all_widgets_of_type>(&self, recursive: bool) -> Vec { let mut widgets_to_check = VecDeque::from([self.clone().upcast::()]); let mut found_widgets = Vec::new(); let mut is_root = true; while let Some(widget) = widgets_to_check.pop_front() { if (recursive || !is_root) && let Ok(specific_widget) = widget.clone().downcast::() { found_widgets.push(specific_widget); } if recursive || is_root { widgets_to_check.extend(widget.get_all_direct_children()); } is_root = false; } found_widgets } fn get_widget_of_type>(&self, recursive: bool) -> T { let mut widgets_to_check = VecDeque::from([self.clone().upcast::()]); let mut is_root = true; while let Some(widget) = widgets_to_check.pop_front() { if (recursive || !is_root) && let Ok(specific_widget) = widget.clone().downcast::() { return specific_widget; } if recursive || is_root { widgets_to_check.extend(widget.get_all_direct_children()); } is_root = false; } panic!("Widget doesn't have proper child of specified type"); } fn get_all_boxes(&self) -> Vec { let mut widgets_to_check = VecDeque::from([self.clone().upcast::()]); let mut boxes = Vec::new(); while let Some(widget) = widgets_to_check.pop_front() { if let Ok(bbox) = widget.clone().downcast::() { boxes.push(bbox); } widgets_to_check.extend(widget.get_all_direct_children()); } boxes } #[expect(clippy::print_stdout)] fn debug_print_widget(&self, print_only_direct_children: bool) { struct WidgetInfo { depth: usize, widget: Widget, } fn collect_widgets(widget: &Widget, depth: usize, print_only_direct_children: bool) -> Vec { let mut result = vec![WidgetInfo { depth, widget: widget.clone() }]; if !print_only_direct_children || depth == 0 { for child in widget.get_all_direct_children() { result.extend(collect_widgets(&child, depth + 1, print_only_direct_children)); } } result } let widget_infos = collect_widgets(&self.clone().upcast::(), 0, print_only_direct_children); println!("Widget hierarchy:"); for widget_info in widget_infos { let indent = " ".repeat(widget_info.depth); println!("{}{:?}", indent, widget_info.widget); } } } #[cfg(test)] mod test { use gtk4::prelude::BoxExt; use gtk4::{Image, Label, Orientation}; use super::*; #[gtk4::test] fn test_get_all_direct_children() { let obj = gtk4::Box::new(Orientation::Horizontal, 0); let obj2 = gtk4::Box::new(Orientation::Horizontal, 0); let obj3 = gtk4::Image::new(); let obj4 = gtk4::Image::new(); let obj5 = gtk4::Image::new(); obj.append(&obj2); obj.append(&obj3); obj2.append(&obj4); obj2.append(&obj5); assert_eq!(obj.get_all_direct_children().len(), 2); } #[gtk4::test] fn test_get_all_boxes() { let obj = gtk4::Box::new(Orientation::Horizontal, 0); let obj2 = gtk4::Box::new(Orientation::Horizontal, 0); let obj3 = gtk4::Image::new(); let obj4 = gtk4::Image::new(); let obj5 = gtk4::Image::new(); obj.append(&obj2); obj.append(&obj3); obj2.append(&obj4); obj2.append(&obj5); assert_eq!(obj.get_all_boxes().len(), 2); } #[gtk4::test] fn test_get_all_direct_children_empty() { let obj = gtk4::Box::new(Orientation::Horizontal, 0); assert_eq!(obj.get_all_direct_children().len(), 0); } #[gtk4::test] fn test_get_all_boxes_nested() { let root = gtk4::Box::new(Orientation::Horizontal, 0); let box1 = gtk4::Box::new(Orientation::Vertical, 0); let box2 = gtk4::Box::new(Orientation::Horizontal, 0); let box3 = gtk4::Box::new(Orientation::Vertical, 0); root.append(&box1); box1.append(&box2); box2.append(&box3); assert_eq!(root.get_all_boxes().len(), 4); } #[gtk4::test] fn test_get_all_boxes_with_mixed_widgets() { let root = gtk4::Box::new(Orientation::Horizontal, 0); let box1 = gtk4::Box::new(Orientation::Vertical, 0); let label = gtk4::Label::new(Some("Test")); let image = gtk4::Image::new(); let box2 = gtk4::Box::new(Orientation::Horizontal, 0); root.append(&box1); root.append(&label); root.append(&image); box1.append(&box2); assert_eq!(root.get_all_boxes().len(), 3); } #[gtk4::test] fn test_combo_box_set_model_and_first() { let combo = gtk4::ComboBoxText::new(); combo.set_model_and_first(["Option 1", "Option 2", "Option 3"]); assert_eq!(combo.active(), Some(0)); assert_eq!(combo.active_text().unwrap(), "Option 1"); } #[gtk4::test] fn test_dialog_get_box_child() { let dialog = gtk4::Dialog::new(); let result = dialog.get_box_child(); assert_eq!(result.spacing(), 0); } #[gtk4::test] #[should_panic(expected = "Widget doesn't have proper child of specified type")] fn test_get_custom_label_panic() { let container = gtk4::Box::new(Orientation::Horizontal, 0); let image = gtk4::Image::new(); container.append(&image); container.get_widget_of_type::